Cognito session refresh does not refresh Google access token
See original GitHub issueDescribe the bug Per https://aws-amplify.github.io/docs/js/authentication#react-components we expect that when the Cognito user session is refreshed, that the associated Google access token from a login using Google would also be refreshed. However it is not.
To Reproduce Steps to reproduce the behavior:
- Login user using hosted UI for social sign on, Google in our case.
- Use cognitoAuthClient.parseCognitoWebResponse( currentUrl ) to process the code provided as a query parameter in the redirect URI.
- Cognito User Pool user is signed in per normal.
- We use attribute mapping in the Cognito User Pool configuration to map the Google access token to an attribute we then get as payload in the idToken that is returned from Cognito Auth. With this token we can then access the user’s Google resources.
- At this point, we have both valid Cognito User Pool credentials in the form of idToken and accessToken and refreshToken, as well as a Google-vended access token.
- After an hour, our app detects that the Cognito Auth session has expired and automatically uses the refreshToken to obtain a new set of Cognito Auth credentials.
- The Google access token, however, remains unchanged/unrefreshed, and using it will generate an ‘unauthenticated’ error response from Google.
Expected behavior We expected that the Google access token would also be refreshed, per the AWS Amplify documentation.
Screenshots N/A
Desktop (please complete the following information):
- OS: Mac OS 10.14.5
- Browser Chrome
- Version 75.0.3770.100
Smartphone (please complete the following information): N/A
Additional context
- We are using aws-amplify v1.1.29
- We are using Cognito User Pool to manage our user accounts.
- We have set up the User Pool to allow Google as an identity provider.
- We are using Cognito hosted UI to facilitate the Google sign in.
- We use amazon-cognito-auth-js’s parseCognitoWebResponse to process the redirect URL with query parameter.
Sample code Code to open hosted UI:
function openHostedUI( provider: Provider, callbackPath: string ) {
const domain = cognitoConfiguration.hostedUICognitoDomain;
const clientId = cognitoConfiguration.clientId;
const callback = `${ document.location.protocol }//${ document.location.host }${ callbackPath }`;
const type = 'code';
const hostedUIUrl = `https://${ domain }/authorize?response_type=${ type }&client_id=${ clientId }&redirect_uri=${ callback }&identity_provider=${ provider }`;
document.location.assign( hostedUIUrl );
}
Code to process hosted UI callback:
export function processHostedUICallback( callbackPath: string ): Promise<{}> {
return new Promise( ( resolve, reject ) => {
const callbackUrlRoot = `${ document.location.protocol }//${ document.location.host }`;
const signInCallback = `${ callbackUrlRoot }${ callbackPath }`;
const signOutCallback = `${ callbackUrlRoot }${ cognitoConfiguration.hostedUISignOutCallbackPath }`;
const params = {
ClientId: cognitoConfiguration.clientId,
UserPoolId: cognitoConfiguration.userPool,
AppWebDomain: cognitoConfiguration.hostedUICognitoDomain,
TokenScopesArray: [
'phone',
'email',
'openid',
'aws.cognito.signin.user.admin',
'profile',
],
RedirectUriSignIn: signInCallback,
RedirectUriSignOut: signOutCallback,
ResponseType: 'code',
// Intentionally left commented - we want to use the default local storage
// rather than cookie storage
// Storage,
};
const cognitoAuthClient = new CognitoAuth( params );
cognitoAuthClient.userhandler = {
// user signed in
onSuccess: ( result ) => {
return resolve( result );
},
onFailure: ( error ) => {
return reject( error );
},
};
const currentUrl = document.location.href;
cognitoAuthClient.parseCognitoWebResponse( currentUrl );
} );
}
Code to refresh Cognito tokens:
return Auth.currentSession()
.then( ( session ) => {
// we don't really need the session, that was just used
// to refresh the session if necessary, which is done
// automatically by AWS Auth
return Auth.currentAuthenticatedUser()
} )
You can turn on the debug mode to provide more info for us by setting window.LOG_LEVEL = ‘DEBUG’; in your app. Initial login:
[DEBUG] 33:47.345 AuthClass - Getting current session
11:33:47.874 ConsoleLogger.js:111 [DEBUG] 33:47.874 AuthClass - Getting the session from this user: CognitoUser {username: "Google_<removed_for_privacy>", pool: CognitoUserPool, Session: null, client: Client, signInUserSession: CognitoUserSession, …}
11:33:47.874 ConsoleLogger.js:111 [DEBUG] 33:47.874 AuthClass - Succeed to get the user session CognitoUserSession {idToken: CognitoIdToken, refreshToken: CognitoRefreshToken, accessToken: CognitoAccessToken, clockDrift: 0}
11:33:47.877 ConsoleLogger.js:101 [DEBUG] 33:47.877 AuthClass - getting current authenticted user
11:33:47.878 ConsoleLogger.js:101 [DEBUG] 33:47.878 AuthClass - cannot load federated user from auth storage
11:33:47.878 ConsoleLogger.js:101 [DEBUG] 33:47.878 AuthClass - get current authenticated userpool user
Tokens being refreshed:
11:30:37.257 ConsoleLogger.js:101 [DEBUG] 30:37.257 AuthClass - Getting current session
11:30:37.493 ConsoleLogger.js:111 [DEBUG] 30:37.493 AuthClass - Getting the session from this user: CognitoUser {username: "Google_<removed_for_privacy>", pool: CognitoUserPool, Session: null, client: Client, signInUserSession: CognitoUserSession, …}
11:30:37.494 ConsoleLogger.js:111 [DEBUG] 30:37.494 AuthClass - Succeed to get the user session CognitoUserSession {idToken: CognitoIdToken, refreshToken: CognitoRefreshToken, accessToken: CognitoAccessToken, clockDrift: 3}
11:30:37.495 ConsoleLogger.js:101 [DEBUG] 30:37.495 AuthClass - getting current authenticted user
11:30:37.496 ConsoleLogger.js:101 [DEBUG] 30:37.496 AuthClass - cannot load federated user from auth storage
11:30:37.496 ConsoleLogger.js:101 [DEBUG] 30:37.496 AuthClass - get current authenticated userpool user
11:30:41.040 UserManagementHelpers.js:160 User <removed_for_privacy>@gmail.com session reactivated
Error from Google once the access token has expired:
curl -X GET "https://classroom.googleapis.com/v1/courses?alt=json" -H"Authorization: Bearer <access_token>"
{
"error": {
"code": 401,
"message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
"status": "UNAUTHENTICATED"
}
}
Final Comment
I don’t know if AuthClass - cannot load federated user from auth storage
is significant. I find it strange that I have to use attribute mapping to access the Google access tokens. Maybe I’m not setting up Cognito correctly and I should be able to get the federated user information through some other means, and because it’s missing, the refresh is not working?
Issue Analytics
- State:
- Created 4 years ago
- Comments:17 (1 by maintainers)
Top GitHub Comments
@Sher-V
I tried to express my answer in general terms because your particular situation will almost certainly require very specific code. But I’ll try to outline in a different way.
Recap…
Prerequisites: You’re using Cognito User Pool to manage your user accounts, and have configured Google as an OpenID IDP properly, and you’ve also configured Cognito hosted authentication to facilitate sign-in using Google.
Once the user has authenticated using the hosted authentication mechanism, your frontend will have received two things: all the Cognito tokens necessary for accessing the AWS API, and also the access token from Google for accessing the Google API. These are stored in a combination of localstorage and cookies.
The problem is that the Google access token will not be automatically refreshed, and you do not have programmatic access to the Google refresh token in a clean way; you can try to reverse-engineer the localstorage or cookies, but that approach is going to be very brittle.
The Google API Javascript client, however, promises to automatically refresh access tokens so you won’t even have to worry about it. So the trick is, instantiate the gapi object, then associate the instance with your already-authenticated Google account – recall the user has already signed in using their Google account.
This makes a globally available object named ‘gapi’.
The gapi client will reach out to the Google auth servers to validate the Google tokens stored in localstorage and cookies and will understand that the user associated with the Google ID has already authenticated. They will not be prompted to login again!
Hope that gives you enough of an idea what we’re trying to do here.
But how can I do it with the new Google Identity API since the one in the example is deprecated. There is no login_hint there.