I'm trying to implement social login using Microsoft account in AWS Cognito User Pools.
I followed documentation and the solution mentioned in this thread:
https://forums.aws.amazon.com/thread.jspa?threadID=287376&tstart=0
My problem is with setting the issuer to allow multiple tenants.
This issuer works only for private accounts:
https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0
This issuer works only for accounts in our directory (tenant):
https://login.microsoftonline.com/AZURE_ACTIVE_DIRECTORY/v2.0
This issuer does not work at all. I get bad issuer error or bad request after sign in with Microsoft:
https://login.microsoftonline.com/common/v2.0
I need to have one oidc provider that will work for any Microsoft account (all tenants) is that even possible?
If I set issuer tenant to common in the AWS Cognito oidc config, then this starts the correct Microsoft flow, but I assume the check for issuer in Cognito fails because Microsoft always returns the specific tenant id inside the jwt token as part of the issuer.
Additional info from microsoft documentation I have checked:
https://learn.microsoft.com/de-de/azure/active-directory/develop/v2-protocols-oidc
https://learn.microsoft.com/de-de/azure/active-directory/develop/id-tokens
I am a colleague of Dragan and after a lot of trying we have found a solution in our team that actually works. Just to notice that we actually had access to premium AWS and Microsoft support, but they couldn't help us. The AWS Cognito Team is aware of the issue, but seems like it has no priority - since nearly a year there hasn't been any fix.
Explanation of the flow
We authenticate against microsoft using their javascript library msal in the frontend (no Cognito involved). We receive a JWT token and use this one to create a normal Cognito user in the user pool. The e-mail is read from the microsoft token and the password is autogenerated with a secure random (as long as possible). Additionally we send the microsoft token as custom user attribute. In PreSignUp Lambda we auto activate the user if the microsoft token is valid, so no password verify e-mail is sent to the user. Back in the frontend we use the amplify custom auth challenge signIn with the e-mail we have cached in the frontend. Now we go through DefineAuthChallenge and then CreateAuthChallenge. CreateAuthChallenge doesn't do anything as the microsoft token is our challenge and doesn't need to be created. Back in the frontend we call CustomChallenge containing sessionKey and microsoft token. We are now in VerifyChallenge Lambda where we verify the microsoft token itself using open source JWT libraries. The flow goes back through DefineAuthChallenge where we only allow one try. Finally the user receives the Cognito tokens from Cognito.
The following snippets are the full code snippets for the Lambdas. I had to remove some specific stuff from our project so hopefully didn't break anything while doing so. All files are the index.js and no additional files are needed for the Lambdas. You could for sure outsource some duplicated code, which we haven't done yet. The FE code is not included here.
PreSignUp Lambda
const jwksClient = require('jwks-rsa');
const jwt = require('jsonwebtoken');
const client = jwksClient({
jwksUri: 'https://login.microsoftonline.com/common/discovery/v2.0/keys'
});
const options = {
algorithms: ['RS256']
};
function getKey(header, callback) {
client.getSigningKey(header.kid, function (err, key) {
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}
const verifyMicrosoftToken = async (jwt, token, key) => {
if (!token) return {};
return new Promise((resolve, reject) =>
jwt.verify(token, key, options, (err, decoded) => err ? reject({}) :
resolve(decoded))
);
};
exports.handler = async (event) => {
const email = event.request.userAttributes.email.toLowerCase();
//verify microsoft and auto enable user
if (event.request.userAttributes['custom:msalIdtoken']) {
const token = await verifyMicrosoftToken(
jwt, event.request.userAttributes['custom:msalIdtoken'], getKey
);
const emailFromToken = token.email !== undefined ? token.email : token.preferred_username;
if (token && emailFromToken.toLowerCase() === email) {
event.response.autoConfirmUser = true;
event.response.autoVerifyEmail = true;
}
}
return event;
};
DefineAuthChallenge Lambda
exports.handler = (event, context, callback) => {
if (event.request.session &&
event.request.session.length > 0 &&
event.request.session.slice(-1)[0].challengeName === 'CUSTOM_CHALLENGE' &&
event.request.session.slice(-1)[0].challengeResult === true){
console.log("Session: ", event.request.session);
event.response.issueTokens = true;
event.response.failAuthentication = false;
} else {
event.response.failAuthentication = false;
event.response.issueTokens = false;
event.response.challengeName = 'CUSTOM_CHALLENGE';
}
// Return to Amazon Cognito
callback(null, event);
};
CreateChallenge Lambda
exports.handler = (event, context, callback) => {
if (event.request.challengeName === 'CUSTOM_CHALLENGE') {
event.response.publicChallengeParameters = {};
event.response.publicChallengeParameters.dummy = 'dummy';
event.response.privateChallengeParameters = {};
event.response.privateChallengeParameters.dummy = 'dummy';
event.response.challengeMetadata = 'MICROSOFT_JWT_CHALLENGE';
}
callback(null, event);
};
VerifyAuthChallenge Lambda
const AWS = require('aws-sdk');
const jwksClient = require('jwks-rsa');
const jwt = require('jsonwebtoken');
const client = jwksClient({
jwksUri: 'https://login.microsoftonline.com/common/discovery/v2.0/keys'
});
const options = {
algorithms: ['RS256']
};
function getKey(header, callback){
client.getSigningKey(header.kid, function(err, key) {
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}
exports.handler = (event, context, callback) => {
if(event.request.challengeAnswer){
jwt.verify(event.request.challengeAnswer, getKey, options, function(err, decoded) {
if(decoded){
const email = decoded.email !== undefined ? decoded.email : decoded.preferred_username;
if (email.toLowerCase() === event.request.userAttributes['email'].toLowerCase()) {
event.response.answerCorrect = true;
// it is necessary to add this group to user so in BE we can resolve microsoft provider
const cognitoIdentityServiceProvider = new AWS.CognitoIdentityServiceProvider();
var params = {
GroupName: "CUSTOM_MICROSOFT_AUTH",
UserPoolId: event.userPoolId,
Username: event.userName
};
cognitoIdentityServiceProvider.adminAddUserToGroup(params, function (err) {
if (err) {
console.log("Group cannot be added to the user: " + event.userName, err);
}
callback(null, event);
});
}
}
if(err){
console.log(err);
}
});
}else{
event.response.answerCorrect = false;
callback(null, event);
}
};
Frontend (Angular component)
ngOnInit() {
// after microsoft successful sign in we need to continue to cognito authentication
this.authMsalService.handleRedirectCallback((authError, response) => {
if (authError) {
this.showLoginError = true;
return;
}
this.signUpOrSignInWithMicrosoftToken(response.idToken.rawIdToken);
});
}
onSignInWithProvider(provider: string) {
this.cognitoService.clearAuthData();
if (provider === SINGLE_SIGN_ON_PROVIDER.MICROSOFT) {
this.authMsalService.loginRedirect({
scopes: ['user.read', 'email'],
});
} else {
const options: FederatedSignInOptions = {provider: CognitoHostedUIIdentityProvider[GeneralUtils.capitalize(provider)]};
this.socialSignIn(options);
}
}
private socialSignIn(options: any): void {
Auth.federatedSignIn(options).catch(() => {
this.showLoginError = true;
this.uiBlockerService.setIsUiBlocked(false);
});
}
private signUpOrSignInWithMicrosoftToken(microsoftIdToken: string) {
this.uiBlockerService.setIsUiBlocked(true);
const attributes = {};
const userName: string = this.authMsalService.getAccount().userName.toLowerCase();
attributes['email'] = userName;
attributes['custom:msalIdtoken'] = microsoftIdToken;
if (this.authMsalService.getAccount().idToken['family_name']) {
attributes['family_name'] = this.authMsalService.getAccount().idToken['family_name'];
}
if (this.authMsalService.getAccount().idToken['given_name']) {
attributes['given_name'] = this.authMsalService.getAccount().idToken['given_name'];
}
Auth.signUp({
username: userName,
password: SSOUtils.getSecureRandomString(20),
attributes: attributes
}).then(user => {
// register
// after successfully signup we need to continue with authentication so user is signed in automatically
this.authenticateWithMicrosoftToken(microsoftIdToken);
}).catch(error => {
// login
// if user is already registered we continue with sign in
if (error.code === 'UsernameExistsException') {
this.authenticateWithMicrosoftToken(microsoftIdToken);
}
this.uiBlockerService.setIsUiBlocked(false);
});
}
private authenticateWithMicrosoftToken(microsoftIdToken: string) {
const userName: string = this.authMsalService.getAccount().userName.toLowerCase();
Auth.signIn(userName).then(cognitoUser => {
// after sign in is started we need to continue with authentication and we sent microsft token
Auth.sendCustomChallengeAnswer(cognitoUser, microsoftIdToken);
});
}
Here are some links we used
https://aws.amazon.com/blogs/mobile/implementing-passwordless-email-authentication-with-amazon-cognito/
https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-challenge.html
https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-overview
PostScript
If you find any security relevant issue in this code, please contact me privately and our company will show some appreciation ($) depending on severity.
Root cause of the issue:
When we integrate Microsoft login via OIDC, we have a couple of options based on our requirement.
In the case where only the users with work or school accounts from Azure AD can sign in to the application then, we have to refer to https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration
Also in the case where any user who has a Microsoft account (work or school Azure AD accounts, OR personal - outlook, live, etc) can sign in to the application then, we have to refer to
https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
In those metadata files, we can see that the issuer is https://login.microsoftonline.com/{tenantid}/v2.0.
So basically, depending on the end user’s Azure AD tenant, the id_token issued by Azure AD will have a different value for issuer (iss) claim.
Which means the iss claim is dynamically change for each user.
Right now, that dynamic behavior isn’t being supported by Cognito.
In Cognito, under the OIDC Identity provider configurations, we have to specify the issuer manually and we can only specify one.
So Cognito can not properly validate the id_token issued by Azure AD. and it returns an error saying Bad id_token issuer.
Another Workaround:
There are identity providers which support this dynamic iss claim behavior of Azure AD. (Auth0, Azure AD B2C, etc). So we can select one of them and configure that to communicate with Microsoft (Azure AD) via OIDC. Then add that IDP as an OIDC identity provider in Cognito. Basically we place that IDP in between Cognito and Microsoft (Azure AD).
I avoided this (tenancy/issuer) problem by avoiding usage of the userpool, and directly interacting with the azure endpoints https://login.microsoftonline.com/common/oauth2/v2.0/authorize etc..
I still have to use the identitypool, to map to IAM role.
Understandably, this is more work than having the userpool handle token stuff, but this is the only way I found it to work with all azure ad accounts.
Related
I want to build a rest api using Aws Rest Api Gateway. This will be the new version of a already in production api (hosted on private servers).
On the current version of the api we use oauth2 with grant type password for authentication. This means that a client send his username and pass to a ".../access_token" endpoint from where it gets his token. With this token he can then call the other endpoints.
On the new api version I'm using the AWS Api Gateway with Authorizer. I want to provide acces to resources based on the username & passwords fields
I've created a user pool and added my users there. How do i get authenticated using only api endpoints?
I cannot use Oauth "client credentials" flow since is machine to machine and client secret will have to be exposed.
On Authorization code or Implicit grant i have to ask the user to login on AWS / custom ui and get redirected. So i cannot use these in an Api.
What i'm missing ?
I understand that you need to authenticate your users without using a browser. An idea would be to create a login endpoint, where users will give their username and password and get back a token. You should implement this endpoint yourself. From this question:
aws cognito-idp admin-initiate-auth --region {your-aws-region} --cli-input-json file://auth.json
Where auth.json is:
{
"UserPoolId": "{your-user-pool-id}",
"ClientId": "{your-client-id}",
"AuthFlow": "ADMIN_NO_SRP_AUTH",
"AuthParameters": {
"USERNAME": "admin#example.com",
"PASSWORD": "password123"
}
}
This will give access, id and refresh tokens (the same way as the authorization grant type) to your users. They should be able to use the access token to access resources and the refresh token against the Token endpoint to renew access tokens.
This isn't the common way to authenticate an API and may have some security implications.
I solved this issue by creating custom Lambda in NodeJS 16x with exposed URL, that does Basic Authentication on the Cognito side with stored app client id, user pool id, secret. I attach the code here, but you still need to create lambda layer with Cognito SDK, configure IAM yourself.
const AWS = require('aws-sdk');
const {
CognitoIdentityProviderClient,
AdminInitiateAuthCommand,
} = require("/opt/nodejs/node16/node_modules/#aws-sdk/client-cognito-identity-provider");
const client = new CognitoIdentityProviderClient({ region: "eu-central-1" });
exports.handler = async (event, context, callback) => {
let username = event.queryStringParameters.username;
let password = event.queryStringParameters.password;
let app_client_id = process.env.app_client_id;
let app_client_secret = process.env.app_client_secret;
let user_pool_id = process.env.user_pool_id;
let hash = await getHash(username, app_client_id, app_client_secret);
let auth = {
"UserPoolId": user_pool_id,
"ClientId": app_client_id,
"AuthFlow": "ADMIN_NO_SRP_AUTH",
"AuthParameters": {
"USERNAME": username,
"PASSWORD": password,
"SECRET_HASH": hash
}
};
let cognito_response = await requestToken(auth);
var lambda_response;
if (cognito_response.startsWith("Error:")){
lambda_response = {
statusCode: 401,
body: JSON.stringify(cognito_response) + "\n input: username = " + username + " password = " + password,
};
}
else {
lambda_response = {
statusCode: 200,
body: JSON.stringify("AccessToken = " + cognito_response),
};
}
return lambda_response;
};
async function getHash(username, app_client_id, app_client_secret){
const { createHmac } = await import('node:crypto');
let msg = new TextEncoder().encode(username+app_client_id);
let key = new TextEncoder().encode(app_client_secret);
const hash = createHmac('sha256', key) // TODO should be separate function
.update(msg)
.digest('base64');
return hash;
}
async function requestToken(auth) {
const command = new AdminInitiateAuthCommand(auth);
var authResponse;
try {
authResponse = await client.send(command);
} catch (error) {
return "Error: " + error;
}
return authResponse.AuthenticationResult.AccessToken;
}
I am using Congnito User Pool to perform API Gateway authorization which is working fine. Now, I am trying to add Instagram as one of the login provider, for that I created Custom Authentication Provider in Federated Identities and using code below:
var cognitoidentity = new AWS.CognitoIdentity({ apiVersion: '2014-06-30' });
var params = {
IdentityPoolId: 'us-east-1:7d99e750-.....',
Logins: {
'login.instagram': 'Access-Token-Returned-By-Instagram',
},
TokenDuration: 60
};
cognitoidentity.getOpenIdTokenForDeveloperIdentity(params, function (err, data) {
if (err) {
console.log(err, err.stack);
} else {
console.log(data);
var idParams = {
IdentityId: data['IdentityId'],
Logins: {
'cognito-identity.amazonaws.com': data['Token']
}
};
cognitoidentity.getCredentialsForIdentity(idParams, function (err2, data2) {
if (err2) console.log(err2, err2.stack); // an error occurred
else console.log(data2); // successful response
});
}
});
I am able to get accessToken and sessionToken, however, I am still unable to find a way to get idToken and accessToken which is required by API Gateway to authorize the incoming request.
I tried looking into SDK as well as AWS forum, but I am still unable to find a way to use custom federated identity provider to authorize API Gateway which use a cognito user pool.
I am going to take a whirl at this....
Do you have a cognito user?
No
import { CognitoUser, CognitoUserPool, AuthenticationDetails } from "amazon-cognito-identity-js";
let cognitoUser;
const userPool = new CognitoUserPool({
UserPoolId: config.USER_POOL.pool_Id, //your userpool id
ClientId: config.appClientId, //your appClient
});
const userData = {
Username: 'user name',
Pool: userPool
};
cognitoUser = new CognitoUser(userData);
/* Should now have a cognitoUser */
Authenticate your cognito user
const authenticationData = {
Username : payload.userName,
Password : payload.password,
};
const authenticationDetails = new AuthenticationDetails(authenticationData);
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: function (result) {
const accessToken = result.getAccessToken().getJwtToken();
/* Use the idToken for Logins Map when Federating User Pools with identity pools or when passing through an Authorization Header to an API Gateway Authorizer*/
const idToken = result.idToken.jwtToken;
/*
Do something with
idToken
accessToken
I would write them to a file or encrypt them and store them on the localStorage OR encrypt and save in REDUX store.
*/
},
onFailure: function(err) {
//handle error
},
});
The goal is to implement a social provider auth flow as described in User Pools App Integration and Federation.
One important thing that I want to satisfy, is to merge user pool accounts that have the same email address.
I am accomplishing that by calling adminLinkProviderForUser within the PreSignUp_ExternalProvider cognito lambda trigger.
So with this, everything works. The new social provided user is being registered and linked with the already existing Cognito (user+pass) user.
However, the authentication flow, from user's perspective doesn't complete. It fails at the last step where the callback uri (defined in cognito user pool) is being called:
error: invalid_request
error_description: Already found an entry for username Facebook_10155611263152353
But then, if the user retries the social auth flow, everything works, and would get session tokens that represent the original Cognito User Pool user (the one that already had that email).
Note that I'm testing the auth flow on an empty User Pool, zero user accounts.
For all the poor souls fighting with this issue still in 2020 the same way I did:
I have eventually fixed the issue by catching the "Already found an entry for username" in my client application and repeating the entire auth flow once more.
Luckily the error only gets fired on the initial external provider signup but not in the subsequent signins of the same user (cause it happens during signup trigger, duh).
I'm taking a wild guess, but here is what I think is happening:
In my case, the facebook provider was getting succesfully linked with
the pre-existing cognito email/password user. new Facebook userpool
entry linking to the email/password user was succesfully created.
Still, it seems
like cognito tried to register the fully isolated Facebook_id user
during the internal signup process (even though a link user entry with the same username was already created in the previous step). Since the "link user" with the
username Facebook_id was already existing, cognito threw an
"Already found an entry for username Facebook_id error" internal error.
This error has been repeatedly voiced over to the AWS developers since 2017 and there are even some responses of them working on it, but in 2020, it's still not fixed.
Yes, this is how it is currently setup. If you try to link users using PreSignUp trigger, the first time won't work. A better way to handle this(I think) would be to provide an option in your UI to link external accounts on sign-in. In the pre-signup trigger, search for a user with the same unique attribute (say email) and see if the sign up is from external provider. Then show a message such as email already exists. Login in & use this menu/option to link. Haven't tested this though.
To elaborate on #agent420's answer, this is what I am currently using (Typescript example).
When a social identity attempts to sign up and the email address already exists I catch this using the PreSignUp trigger and then return an error message to the user. Inside the app, on the user's profile page, there is an option to link an identity provider which calls the adminLinkProviderForUser API.
import {
Context,
CognitoUserPoolTriggerEvent,
CognitoUserPoolTriggerHandler,
} from 'aws-lambda';
import * as aws from 'aws-sdk';
import { noTryAsync } from 'no-try';
export const handle: CognitoUserPoolTriggerHandler = async (
event: CognitoUserPoolTriggerEvent,
context: Context,
callback: (err, event: CognitoUserPoolTriggerEvent) => void,
): Promise<any> => {
context.callbackWaitsForEmptyEventLoop = false;
const { email } = event.request.userAttributes;
// pre sign up with external provider
if (event.triggerSource === 'PreSignUp_ExternalProvider') {
// check if a user with the email address already exists
const sp = new aws.CognitoIdentityServiceProvider();
const { error } = await noTryAsync(() =>
sp
.adminGetUser({
UserPoolId: 'your-user-pool-id',
Username: email,
})
.promise(),
);
if (error && !(error instanceof aws.AWSError)) {
throw error;
} else if (error instanceof aws.AWSError && error.code !== 'UserNotFoundException') {
throw error;
}
}
callback(null, event);
};
I finally got this thing working in a non-weird way where users have to authorize twice or other things.
Process explained:
User tries to authenticate using an identity provider, for the first time => PreSignUp lambda kicks in and check if user exists via email
1a. If the user exists, it will throw an error, eg. CONFIRM_IDENTITY_LINK_token that I'm capturing on the client.
token is a base64 string with the username and identity id ("username:facebook_123456")
1b. If the username does not exist, I create a new user with a temporary password and throw an error FORCE_CHANGE_PASSWORD_token. Same token but I add the temporary password to this time.
In the client I have one callback route '/authorize' => this is the one you set up as a callback URL in Cognito, and 2 extra routes: '/confirm-password' and '/configure-password'.
In the /authorize route I'm capturing the errors and getting the attached tokens and redirect to the extra routes: 1a => /configure-password?token=token and 1b => /confirm-password?token=token
For "/confirm-password" I ask the user to confirm its current password in order to authorize linking with the provider, then use the token to log him in with the identity id as clientMetadata, eg "{"LINK_PROVIDER": "facebbok_12345678"}"
On login, I have a PostAuthentication lambda which checks for the "LINK_PROVIDER" in the clientMetadata, and links it to the user.
For "/configure-password" I parse the token and do a "shallow" login with the credentials from the token and identity id as client metadata (same as above) then prompt the user to configure a new password for his account.
I know it might seem a little bit restrictive but I find it better than to authorize twice.
Also, this does not create extra users for identities in the user pool.
Code examples:
PreSignUp lambda
export async function handler(event: PreSignUpTriggerEvent) {
try {
const { userPoolId, triggerSource, request, userName } = event
if (triggerSource === 'PreSignUp_ExternalProvider') {
// Check if user exists in cognito
let currentUser = await getUserByEmail(userPoolId, request.userAttributes.email)
if (currentUser) {
// User exists, thow error with identity id
const identity = Buffer.from(`${currentUser}:${userName}`).toString('base64')
throw new Error(`CONFIRM_USER_IDENTITY_${identity}`)
}
// Create new Cognito user with temp password
const tempPassword = generatePassword()
currentUser = await createNewUser(userPoolId, request.userAttributes, tempPassword)
// Throw error with token
const state = Buffer.from(`${currentUser}:${tempPassword}:${userName}`).toString('base64')
throw new Error(`FORCE_CHANGE_PASSWORD_${state}`)
}
return event
} catch (error) {
throw new Error(error)
}
}
PostAuthentication lambda
export async function handler(event: PostAuthenticationTriggerEvent) {
try {
const { userPoolId, request, userName } = event
if (request.clientMetadata?.LINK_IDENTITY) {
const identity = request.clientMetadata['LINK_IDENTITY']
// Link identity to user
await linkIdentityProvider(userPoolId, userName, identity)
}
return event
} catch (error) {
console.error(error)
throw new Error('Internal server error')
}
}
We faced the same issue and tried various hacks to get around it. As we started to use SignInWithApple, we couldn't handle it with the 'double turnaround' because Apple always wants the user to enter their email and password, not like Google, where the second time, everything works automatically. So the solution we ended up building was to store the Cognito/IdP ID (Google_1234, SignInWithApple_XXXX.XXX.XXX) in our database but still create a native Cognito user that isn't linked via Cognito.
The native user is created to make unlinking easier because first, we get rid of the data (IdP user-id) we store in our database and then the Cognito IdP user. The user can then proceed using the Native Cognito user. Then we have a middleware component in place that allows us to have JWT in the external IdP or Cognito native format and translates so we can use both versions. As long as the user uses an IdP/SSO, we reset the Native users' password to a very long random value and prevent resetting it, so they must use the IdP.
So whatever you are trying to do, prevent using the admin-link-provider-for-user command!
The same code in JavaScript getUser has been called instead of listUsers. It is also assumed that all users have their email id as their username.
const aws = require('aws-sdk');
exports.handler = async (event, context, callback) => {
console.log("event" + JSON.stringify(event));
const cognitoidentityserviceprovider = new aws.CognitoIdentityServiceProvider({apiVersion: '2016-04-18'});
const emailId = event.request.userAttributes.email
const userName = event.userName
const userPoolId = event.userPoolId
var params = {
UserPoolId: userPoolId,
Username: userName
};
var createUserParams = {
UserPoolId: userPoolId,
Username: emailId,
UserAttributes: [
{
Name: "email",
Value: emailId
},
],
TemporaryPassword: "xxxxxxxxx"
};
var googleUserNameSplitArr = userName.split("_");
var adminLinkUserParams = {
DestinationUser: {
ProviderAttributeName: 'UserName',
ProviderAttributeValue: emailId,
ProviderName: "Cognito"
},
SourceUser: {
ProviderAttributeName: "Cognito_Subject",
ProviderAttributeValue: googleUserNameSplitArr[1],
ProviderName: 'Google'
},
UserPoolId: userPoolId
};
var addUserToGroupParams = {
GroupName: "Student",
UserPoolId: userPoolId,
Username: emailId
};
if (userName.startsWith("Google_")) {
await cognitoidentityserviceprovider.adminGetUser(params, function (err, data) {
if (err) {
console.log("No user present")
console.log(err, err.stack);
cognitoidentityserviceprovider.adminCreateUser(createUserParams, function (err, data) {
if (err) console.log(err, err.stack);
else {
console.log("User Created ")
cognitoidentityserviceprovider.adminAddUserToGroup(addUserToGroupParams, function (err, data) {
if (err) console.log(err, err.stack);
else {
console.log("added user to group");
console.log(data);
}
});
cognitoidentityserviceprovider.adminLinkProviderForUser(adminLinkUserParams, function (err, data) {
if (err) console.log(err, err.stack);
else {
console.log("user linked");
console.log(data);
}
});
console.log(data);
}
});
} else {
console.log("user already present")
cognitoidentityserviceprovider.adminLinkProviderForUser(adminLinkUserParams, function (err, data) {
if (err) console.log(err, err.stack); // an error occurred
else {
console.log("userlinked since user already existed");
console.log(data);
}
});
console.log(data);
}
});
}
console.log("after the function custom");
callback(null, event);
};
This is a well know error. I handle it by retrying the request after this error and it will work. The error is because there is not way in the SDK to let it know to the pool that you already link the Federation Credentials to an user and it try to create a new user with those credentials
I wanted to have the feature of having a user seamlessly being able to login with one social provider (ex: Facebook) and then another one (Google).
I struggled with the retry process, especially with Google Login. At the signup process, if a user have several accounts, he will need to process twice the account selection.
What I ended up doing is just using Cognito for the client side code and token generation and have a lambda in the pre signup process mapping userIds with their email in a custom DB (Postgres or DynamoDB).
Then when a user query my API, based on their userId (whether it's a FacebookId or a cognito email userId, I am querying the DB to find the linked email and I am able to authenticate any users and their data like this.
Did this bug all of a sudden stop happening on 2/21/23? We didn't change anything but now this is no longer happening to users on their first time signing up. We also noticed that the UI for how Cognito is showing linked users is different - there is just 1 cognito account you're able to see in Cognito instead of multiple. You can still see the federated linked accounts in the identities property though
I tried with the aws-sdk-react-native module:
https://github.com/awslabs/aws-sdk-react-native
The configuration took some time, but thanks to this links I could i.e. list the topics:
https://github.com/awslabs/aws-sdk-react-native/issues/35
https://github.com/awslabs/aws-sdk-react-native/blob/master/SNS/IntegrationTests/SNSTests.js
The test includes a sample how to subscribe to get emails but not how to get notifications in the app. I don't know how to get the platformEndpoint, the PlatformApplicationArn and the deviceToken.
endPoint = sns.createPlatformEndpoint({
PlatformApplicationArn: '{APPLICATION_ARN}',
Token: '{DEVICE_TOKEN}'
})
...
var subscribeRequest= {
"Protocol":"application",
"TopicArn":topicARN,
"Endpoint":endPoint
}
try{
await AWSSNS.Subscribe(subscribeRequest);
}catch(e){
console.error(e);
shouldResolve = false;
return shouldResolve;
}
Are there any samples for this?
I'm also looking for an authentication sample.
Would it be easier to use firebase?
Thanks
I have used GCM over SNS to send notifications. Here are the steps I went through assuming you already set up GCM and added required libraries from AWS React Native SDK:
First create a SNS app from AWS:
Then you need to create Federated Identity through Cognito service of AWS. This is required for sending your device token from mobile app to AWS SNS app. Choose Manage Federated Identities
Then create your pool, don't forget to check Enable Access to unauthenticated identities
When you create the pool you will need to create IAM Roles for unauthenticated and authenticated roles of that pool. AWS will help you create new roles for that but you need to go to IAM Roles menu and attach AmazonSNSFullAccess to created roles, otherwise from the mobile app you won't able to send device token.
After doing these steps you will able send your device token using Amazon's React Native SDK. I have written a helper class for sending token to AWS SNS as suggested here:
class AWSUtility {
constructor() {
const region = "us-west-1"; //change it with your region
const IDENTITY_POOL_ID = "pool id created from Federated Identities"
AWSCognitoCredentials.initWithOptions({region, identity_pool_id: IDENTITY_POOL_ID});
AWSSNS.initWithOptions({region});
}
addTokenToAWSSNS(token, snsEndpointARN) {
const applicationArn = "change with SNS application Amazon resource name";
return Promise.try(() => {
if (!snsEndpointARN) {
return this.createPlatformEndpoint(token, applicationArn);
} else {
return AWSSNS.GetEndpointAttributes({EndpointArn: snsEndpointARN})
.then((result) => {
const {Attributes = {}} = result;
const {Token, Enabled} = Attributes;
const updateNeeded = Token !== token || Enabled !== 'true';
if (updateNeeded) {
return this.updateEndpoint(token).then(() => result.EndpointArn);
}
return snsEndpointARN;
})
.catch(() => {
this.createPlatformEndpoint(token, applicationArn)
});
}
});
}
updateEndpoint(snsEndpointARN, token) {
//AWS is returning error saying that it requires 6 params to update endpoint, if anyone has any idea about it let me know please
return AWSSNS.SetEndpointAttributes({EndpointArn: snsEndpointARN, Attributes: {Token: token, Enabled: true}});
}
createPlatformEndpoint(token, applicationArn) {
return AWSSNS.CreatePlatformEndpoint({Token: token, PlatformApplicationArn: applicationArn})
.then(result => result.EndpointArn)
.catch((error = {}) => {
console.log(error);
});
}
}
export default new AWSUtility();
I'm trying to figure out how to use the changePassword function of the AWS.CognitoIdentityServiceProvider.
I need to pass the following as params:
{
PreviousPassword: 'STRING_VALUE', /* required */
ProposedPassword: 'STRING_VALUE', /* required */
AccessToken: 'STRING_VALUE'
}
I use this inside a Lambda function, so how do I get hold of the access token? I have the cognitoIdentityPoolId and the cognitoIdentityId to use, but I can't understand which this access token is.
Because there is no admin version of changePassword, you must first authenticate as the user you are trying to impact, then you can call the changePassword routine. It took me a long time to figure this out and no other posts seem to cover the case where you are running a NodeJS lambda function with the admin calls and UserPools, where you want to support "admin" changing of a user password. My (currently working) code is below. Note I believe preventing the admin from changing the user password is a deliberate design decision made by AWS, so I am not sure how long the workaround below will continue to be valid...
const cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider();
// Accept a POST with a JSON structure containing the
// refresh token provided during the original user login,
// and an old and new password.
function changeUserPassword(event, context, callback) {
// Extract relevant JSON into a request dict (This is my own utility code)
let requiredFields = ['old_password','new_password','refresh_token'];
let request = Utils.extractJSON(event['body'], requiredFields);
if (request == false) {
Utils.errorResponse("Invalid JSON or missing required fields", context.awsRequestId, callback);
return; // Abort here
}
// This function can NOT be handled by admin APIs, so we need to
// authenticate the user (not the admin) and use that
// authentication instead.
let refreshToken = request['refresh_token']
// Authenticate as the user first, so we can call user version
// of the ChangePassword API
cognitoidentityserviceprovider.adminInitiateAuth({
AuthFlow: 'REFRESH_TOKEN',
ClientId: Config.ClientId,
UserPoolId: Config.UserPoolId,
AuthParameters: {
'REFRESH_TOKEN': refreshToken
},
ContextData: getContextData(event)
}, function(err, data) {
if(err){
Utils.errorResponse(err['message'], context.awsRequestId, callback);
return // Abort here
} else {
// Now authenticated as user, change the password
let accessToken = data['AuthenticationResult']['AccessToken'] // Returned from auth - diff signature than Authorization header
let oldPass = request['old_password']
let newPass = request['new_password']
let params = {
AccessToken: accessToken, /* required */
PreviousPassword: oldPass, /* required */
ProposedPassword: newPass /* required */
}
// At this point, really just a pass through
cognitoidentityserviceprovider.changePassword(params, function(err2, data2) {
if(err2){
let message = {
err_message: err2['message'],
access_token: accessToken
}
Utils.errorResponse(message, context.awsRequestId, callback);
} else {
let response = {
'success': 'OK',
'response_data': data2 // Always seems to be empty
}
callback(response)
}
});
}
});
}
As You are using the AWS Lambda you dont need to worry about the access token you can simply pass the username and password along with the poolID to the cognito function adminSetUserPassword().this function will update the password easily
const updateCognitoPassword = async(user_name, password) => {
try {
var changePasswordParams = {
Password: password,
Permanent: true,
Username: user_name.trim(),
UserPoolId: constants.user_pool_id
};
let data = await cognitoidentityserviceprovider.adminSetUserPassword(changePasswordParams).promise();
return data;
}
catch (err) {
throw new Error(err);
}
};
I would like to extend on some answers above with a solution that can be used inside the lambda function and also shows how to set the authentication required (using an AWS access key and secret access key.
This is a worked example of a "change password" function created as a lambda.
export async function change_password (event, context, callback) {
context.callbackWaitsForEmptyEventLoop = false;
try {
const { aws_cognito_id, newPassword } = JSON.parse(event.body)
const cognitoIdentityService = new AWS.CognitoIdentityServiceProvider({ apiVersion: '2016-04-18', region: '***AWS REGION GOES HERE***' });
const userPoolId = "***COGNITO USER POOL ID GOES HERE***";
const params = {
Password: newPassword,
Permanent: true,
Username: aws_cognito_id,
UserPoolId: userPoolId
};
AWS.config.region = '**AWS REGION**';
cognitoIdentityService.config.update({
accessKeyId: '***AWS ACCESS KEY***',
secretAccessKey: '***AWS SECRET ACCESS KEY***'
})
let result = await cognitoIdentityService.adminSetUserPassword(params).promise();
return generate_response(200, result)
} catch (err) {
return generate_error(500, err.message)
}
}
The identity pool id and identity id are Cognito federated identities concepts, while the ChangePassword API is a user pools one. They are two different services - think of user pools as an identity provider to your identity pool.
The short version is you can get the access token by signing in with a user in your user pool. Doing so returns an access token, id token, and refresh token. That being said, a common theme is to use the admin versions of the various user pool APIs on Lambda side, since you may not have user credentials there.