I am using aws amplify and I know that the tokens get automatically refreshed when needed and that that is done behind the scenes.
What I need to do is change a custom attribute on the user in the cognito user pool via a Lambda backend process. This I can do, and it is working. However, the web client user never sees this new custom attribute and I am thinking the only way they can see it is if the token gets refreshed since the value is stored within the JWT token.
The correct solution as of 2021 is to call:
await Auth.currentAuthenticatedUser({bypassCache: true})
Here is how you can update tokens on demand (forcefully)
import { Auth } from 'aws-amplify';
try {
const cognitoUser = await Auth.currentAuthenticatedUser();
const currentSession = await Auth.currentSession();
cognitoUser.refreshSession(currentSession.refreshToken, (err, session) => {
console.log('session', err, session);
const { idToken, refreshToken, accessToken } = session;
// do whatever you want to do now :)
});
} catch (e) {
console.log('Unable to refresh Token', e);
}
Like it's said here:
https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-with-identity-providers.html
The access token and ID token are good for 1 hour. With Amplify you can get the info about the session using currentSession or currentUserInfo in Auth class to be able to retrieve information about tokens.
Undocumented, but you can use the refreshSession method on the User. Your next call to currentAuthenticatedUser and currentSession will have updated profile attributes (and groups)
User = Auth.currentAuthenticatedUser()
Session = Auth.currentSession()
User.refreshSession(Session.refreshToken)
#andreialecu wrote the correct answer. For full code to get the JWT:
static async amplifyRefresh() {
try {
const currentUser = await Auth.currentAuthenticatedUser({ bypassCache: true })
const currentSession = await Auth.currentSession()
const jwt = currentSession.getIdToken().getJwtToken()
// do what you want
} catch (error) {
console.log("error refreshing token: ", error)
throw error
}
}
Related
Okay, I have the bad feeling that I'm missing a key concept in what I'm doing. Hope someone can help me out with a hint.
I'm using Nuxt and Vuex Store Modules. Every fetch a Module Action does is wrapped in a helper Function (saveFetch) that I imported to decrease repetitive code, like this:
export const actions = {
async sampleAction(context, data){
...
await saveFetch(context, 'POST', '/pages', data)
...
}
}
The helper simple checks if the users accessToken is still valid, refreshes it if not and then sends the request:
export const saveFetch = async (context, method = 'POST', path, data) => {
const accessTokenExpiry = context.rootGetters['auth/getAccessTokenExpiry']
let accessToken = context.rootGetters['auth/getAccessToken']
// If the client provides an accessToken and the accessToken is expired,
// refresh the token before making the "real" fetch.
if (accessToken && accessTokenExpiry < new Date() && path !== '/auth/refresh-token') {
if (process.client) {
// Works fine
await window.$nuxt.$store.dispatch('auth/refreshToken')
} else {
// This is where the trouble starts
await context.dispatch('auth/refreshToken', null, { root: true })
}
accessToken = rootGetters['auth/getAccessToken']
}
return fetch(path, {
method,
headers: { ... },
body: JSON.stringify(data),
}
}
If the accessToken is expired the helper function dispatches a Vuex Action to refresh it. This works well on the client side, but not if that process happens on the server side.
The Problem that's coming up on the server side is, that the user has to provide a refreshToken to get a refreshed accessToken from the API. This refreshToken is stored as a HttpOnly Cookie in the Client. When logging the Nuxt request details on the API side of things I noticed, that Nuxt is not sending that cookie.
My current workaround looks like this:
export const actions = {
async refreshToken(context){
...
let refreshToken
if (process?.server && this?.app?.context?.req?.headers?.cookie) {
const parsedCookies = cookie.parse(
this.app.context.req.headers.cookie
)
refreshToken = parsedCookies?.refreshToken
}
const response = await saveFetch(context, 'POST', '/auth/refresh-token', {
refreshToken,
})
...
}
...
}
If on server side, access the req object, get the cookies from it and send the refreshToken Cookie Content in the requests body.
This looks clearly bad to me and I would love to get some feedback on how to do this better. Did I maybe miss some key concepts that would help me not get into this problem in the first place?
I'm trying to figure out how to access the accessToken, refreshToken, and idToken that I receive back from aws-amplify using the Auth library.
example in docs: https://aws.github.io/aws-amplify/media/authentication_guide.html
example of my usage:
const user = await Auth.signIn(email, password);
user has a bunch of properties that are inaccessible including everything I need. In the docs, it's unclear how to get to these properties because the examples all log the result. Any ideas?
Auth.currentSession().then(res=>{
let accessToken = res.getAccessToken()
let jwt = accessToken.getJwtToken()
//You can print them to see the full objects
console.log(`myAccessToken: ${JSON.stringify(accessToken)}`)
console.log(`myJwt: ${jwt}`)
})
Auth.currentSession() will return a CognitoUserSession containing accessToken, idToken, and refreshToken.
The CognitoUserSession is actually the following: CognitoUserSession {idToken: CognitoIdToken, refreshToken: CognitoRefreshToken, accessToken: CognitoAccessToken, clockDrift: 0}
Accessing pairs within that object can be achieved through straightforward dot notation at this point.
Example: Retrieve the accessToken and log to console
Auth.currentSession().then(data => console.log(data.accessToken));
The result will be a CognitoAccessToken in the form CognitoAccessToken { jwtToken: '', payload: ''}
If you just want the jwtToken within the CognitoAccessToken, it's just dot notation all the way down (with log to console example):
Auth.currentSession().then(data => console.log(data.accessToken.jwtToken));
Note: This method also refreshes the current session if needed (reference).
I believe you can do
Auth.currentCredentials(credentials => {
const tokens = Auth.essentialCredentials(credentials);
})
where essentialCredentials will return all of the tokens
Hope this helps.
Angular 9, getting JWT token from current session :
import Auth from '#aws-amplify/auth';
Auth.currentSession().then(data => console.log("JWT", data.getAccessToken().getJwtToken()));
For those in search of the AWSCredentials:
const checkCognitoUserSession = async () => {
const getAwsCredentials = await Auth.currentCredentials();
const awsCredentials = await Auth.essentialCredentials(getAwsCredentials);
// accessKeyId, secretAccessKey, sessionToken post login
return { awsCredentials };
};
Retrieve current session using aws-amplify
Auth.currentSession() returns a CognitoUserSession object which contains JWT accessToken, idToken, and refreshToken.
Auth.currentSession()
.then((data) => console.log(data))
.catch((err) => console.log(err));
aws-amplify Docs currentSession
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'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.
I'm trying to update a user's attribute right after authentication.
Auth works fine and I'm able to retrieve user attributes with it.
But the problem is var cognitoUser = getUserPool().getCurrentUser(); returns null. How do I retrieve the current user so that I am able to update the attribute but without refreshing the browser?
Perhaps another question would be, how do I use the accessToken to run functions on the current user without refreshing the browser?
var cognitoUser = getUserPool().getCurrentUser();
if (cognitoUser != null) {
cognitoUser.getSession(function(err, session) {
if ( err ) {
console.log(err);
return;
}else if( session.isValid() ){
updateUserAttribute( cognitoUser, 'custom:attr', attr )
}
});
}else{
console.log('Cognito User is null');
return;
}
getUserPool().getCurrentUser() looks for the user on the local storage, if it returns null is because the local storage do not have an user already set.
To setup the user on the local storage I use an instance of CognitoAuth that makes the job.
This solution is using Cognito Hosted UI.
On the callback url that is returned by Cognito UI:
Then, if you call getUserPool().getCurrentUser() this will not be null.
import { CognitoAuth } from "amazon-cognito-auth-js"
const authData = {
UserPoolId: "us-east-1_xxxxxx",
ClientId: "xxxxxxx",
RedirectUriSignIn: "https://examole.com/login",
RedirectUriSignOut: "https://example.com/logout",
AppWebDomain: "example.com",
TokenScopesArray: ["email"]
}
const auth = new CognitoAuth(authData)
auth.userhandler = {
onSuccess: function(result) {
//you can do something here
},
onFailure: function(err) {
// do somethig if fail
}
}
//get the current URL with the Hash that contain Cognito Tokens tokens
const curUrl = window.location.href
//This parse the hash and set the user on the local storage.
auth.parseCognitoWebResponse(curUrl)
Then, if you call getUserPool().getCurrentUser() this will not be null.
var cognitoUser = getUserPool().getCurrentUser();
That should return the last authenticated user, if the user pool is initialized correctly. Upon a successful authentication, we save the user keyed on the client id to local storage. Whenever you call getCurrentUser, it will retrieve that particular last authenticated user from local storage. The tokens are keyed on that user and client id. They are also saved to local storage after a successful authentication. Accessing the access token should be just:
cognitoUser.getSignInUserSession().getAccessToken().getJwtToken())
and you can use the token directly with the operations exposed in the CognitoIdentityServiceProvider client.
This is a fairly old question, so you may have moved on by now, but I think you need to create the user pool, using your userpoolId and clientID. I don't think you can just call getUserPool() without those being known and/or somehow available in memory. For example:
function makeUserPool () {
var poolData = {
UserPoolId : "your user pool id here",
ClientId : "you client id here"
};
return new AWSCognito.CognitoIdentityServiceProvider.CognitoUserPool(poolData);
}
var userPool = makeUserPool();
var currAWSUser = userPool.getCurrentUser();