Cognito User Pool: How to refresh Access Token using Refresh Token
Refreshing a session with the amazon-cognito-identity-js browser SDK; it mostly does it for you, and unless you're doing something unusual you won't need to handle the refresh token directly. Here's what you need to know:
Assume you have instantiated the user pool like this:
const userPool = new AmazonCognitoIdentity.CognitoUserPool({
UserPoolId: USER_POOL_ID,
ClientId: USER_POOL_CLIENT_ID
});
To find the last username authenticated, you would do this:
const cognitoUser = cognitoUserPool.getCurrentUser();
If it finds one, cognitoUser will be non-null, and you can do this, which will refresh your tokens behind the scenes if needed:
cognitoUser.getSession(function(err, data) {
if (err) {
// Prompt the user to reauthenticate by hand...
} else {
const cognitoUserSession = data;
const yourIdToken = cognitoUserSession.getIdToken().jwtToken;
const yourAccessToken = cognitoUserSession.getAccessToken().jwtToken;
}
});
If you don't want these tokens persisted in local storage, you can:
cognitoUser.signOut();
The way it works is, after a successful authentication, the browser will store your JWT tokens, including that refresh token. It stores these in local storage in your browser by default, though you can provide your own storage object if you want. By default, the refresh token is valid for 30d, but it's a property (RefreshTokenValidity) of your UserPoolClient, which you can change. When you do the above, getSession() will first see whether the tokens you have in storage exist and are still valid; if not, it will try to use whatever refreshToken it finds there to authenticate you into a new session.
The documentation http://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-with-identity-providers.html indicates that the iOS and Android SDKs will do this for you, though I have not used those so can't vouch for that.
The JavaScript SDK handles refreshing of the tokens internally. When you call getSession
to get tokens, in the absence of any valid cached access and id tokens the SDK uses the refresh token to get new access and id tokens. It invokes the user authentication, requiring user to provide username and password, only when the refresh token is also expired.
If you're in a situation where the Cognito Javascript SDK isn't going to work for your purposes, you can still see how it handles the refresh process in the SDK source:
You can see in refreshSession
that the Cognito InitiateAuth endpoint is called with REFRESH_TOKEN_AUTH
set for the AuthFlow
value, and an object passed in as the AuthParameters
value.
That object will need to be configured to suit the needs of your User Pool. Specifically, you may have to pass in your SECRET_HASH
if your targeted App client id has an associated App client secret. User Pool Client Apps created for use with the Javascript SDK currently can't contain a client secret, and thus a SECRET_HASH
isn't required to connect with them.
Another caveat that might throw you for a loop is if your User Pool is set to remember devices, and you don't pass in the DEVICE_KEY
along with your REFRESH_TOKEN
. The Cognito API currently returns an "Invalid Refresh Token" error if you are passing in the RefreshToken
without also passing in your DeviceKey
. This error is returned even if you are passing in a valid RefreshToken
. The thread linked above illuminates that, though I do hope AWS updates their error handling to be less cryptic in the future.
As discussed in that thread, if you are using AdminInitiateAuth along with ADMIN_NO_SRP_AUTH
, your successful authentication response payload does not currently contain NewDeviceMetadata
; which means you won't have any DeviceKey
to pass in as you attempt to refresh your tokens.
My app calls for implementation in Python, so here's an example that worked for me:
def refresh_token(self, username, refresh_token):
try:
return client.initiate_auth(
ClientId=self.client_id,
AuthFlow='REFRESH_TOKEN_AUTH',
AuthParameters={
'REFRESH_TOKEN': refresh_token,
'SECRET_HASH': self.get_secret_hash(username)
# Note that SECRET_HASH is missing from JSDK
# Note also that DEVICE_KEY is missing from my example
}
)
except botocore.exceptions.ClientError as e:
return e.response