question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Cognito session refresh does not refresh Google access token

See original GitHub issue

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?

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:17 (1 by maintainers)

github_iconTop GitHub Comments

5reactions
tcchaucommented, Jul 19, 2021

@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.

4reactions
gsavvidis96commented, Sep 9, 2022

@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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Using the refresh token - Amazon Cognito - AWS Documentation
You can use the refresh token to retrieve new ID and access tokens. By default, the refresh token ... The session cookies do...
Read more >
How to use the refresh token with Cognito
Cognito tokens. When a client logs in to a Cognito user pool they get 3 tokens: a refresh_token , an id_token , and...
Read more >
Cognito User Pool: How to refresh Access Token using ...
This answer is correct! I updated the HTTP response to reflect the fact that it doesn't return a new refresh token. Refreshing a...
Read more >
Using the refresh token - Amazon Cognito
You can use the refresh token to retrieve new ID and access tokens. By default, the refresh token ... The session cookies do...
Read more >
How to refresh AWS Cognito user pool tokens for SSO - Medium
Refresh Token is for refreshing the above two tokens. The ID and access tokens are valid only for an hour but refresh token...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found