amplify-js: Cognito session refresh does not refresh Google access token

Describe 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:

  1. Login user using hosted UI for social sign on, Google in our case.
  2. Use cognitoAuthClient.parseCognitoWebResponse( currentUrl ) to process the code provided as a query parameter in the redirect URI.
  3. Cognito User Pool user is signed in per normal.
  4. 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.
  5. 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.
  6. 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.
  7. 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?

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 17 (1 by maintainers)

Most upvoted comments

@Sher-V

That would be very helpful if you could show a couple lines of code to get the idea how you manage all this.

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.

  1. Install the Google API Javascript client in your index.html
    <script
      type="text/javascript"
      src="https://maps.googleapis.com/maps/api/js?key=<YOUR_PUBLIC_GOOGLE_API_KEY>&libraries=<GOOGLE_LIBRARIES_YOU_USE>"
    ></script>

This makes a globally available object named ‘gapi’.

  1. Get the currently logged-in Cognito user’s Google ID, from the Cognito user’s session:
    const identities = cognitoUser.signInUserSession.idToken.payload.identities;
    const googleIdentity = identities.filter((identity) => {
      return identity.providerName === 'Google';
    });
    const googleUserId = googleIdentity[0].userId;
  1. Authenticate to Google again, but provide the Google ID as a login hint:
    const auth2Params = {
      client_id: getConfiguration('google').clientId,
      immediate: true,
      login_hint: googleUserId,
    };

    gapi.load('client:auth2', () => {
      gapi.auth2.init(auth2Params);
    });

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.

@Sher-V

That would be very helpful if you could show a couple lines of code to get the idea how you manage all this.

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.

  1. Install the Google API Javascript client in your index.html
    <script
      type="text/javascript"
      src="https://maps.googleapis.com/maps/api/js?key=<YOUR_PUBLIC_GOOGLE_API_KEY>&libraries=<GOOGLE_LIBRARIES_YOU_USE>"
    ></script>

This makes a globally available object named ‘gapi’.

  1. Get the currently logged-in Cognito user’s Google ID, from the Cognito user’s session:
    const identities = cognitoUser.signInUserSession.idToken.payload.identities;
    const googleIdentity = identities.filter((identity) => {
      return identity.providerName === 'Google';
    });
    const googleUserId = googleIdentity[0].userId;
  1. Authenticate to Google again, but provide the Google ID as a login hint:
    const auth2Params = {
      client_id: getConfiguration('google').clientId,
      immediate: true,
      login_hint: googleUserId,
    };

    gapi.load('client:auth2', () => {
      gapi.auth2.init(auth2Params);
    });

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.

Thx @tcchau this solved it for me, too!

Just a side note: it is MANDATORY to use the hosted UI, this is what I skipped even though it’s in the solution proposal, but can be easily overlooked (at least I did). It doesn’t work when you use Auth.federatedSignIn({provider: CognitoHostedUIIdentityProvider.Google}) directly.

We’re using this in a React app running inside an Electron as a desktop application. Using https://www.npmjs.com/package/gapi-script helps accessing the gapi object.

@manueliglesias Thanks. I had to go work on a different project the last couple days and didn’t have time to check back on this.

Here’s what I’ve found, and perhaps this will help others who have requirements to work with both AWS and Google services:

  • this could be another bug, not sure, but the refresh token is something I’ve already mapped into the User Pool custom attributes, but it never gets populated.
  • because of this, to accomplish refreshing my Google access token, I actually have to install the google-api-javascript-client
  • note that using this method of self-managing the Google access tokens means that you don’t even need to map the original access token from Google into a User Pool custom attribute. Merely use authorize to get an access token after authentication with Cognito has completed. This is, IMHO, preferable over using the attribute mapping which feels very kludgy

Hi @tcchau

Thank you for the very detailed report! 😃

This is a problem in the documentation. The docs should make clear mention of:

  1. The fact that automatic Facebook and Google tokens refresh is automatic only when using federation with identity pools (not with user pools).
  2. When using User Pools, the tokens that are automatically refreshed are the Cognito User Pools ones (Not the ones from the identity provider).

The suggestion that I have for you, is to capture google’s refresh token too in an attribute and use the Pre Token Generation Lambda Trigger to listen for TokenGeneration_RefreshTokens and refresh the google token using: How do I refresh the auth token, and how often should I do it?

I’ll make a note to fix the docs.