Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cognito session refresh does not refresh Google access token #3619

Closed
tcchau opened this issue Jul 10, 2019 · 17 comments
Closed

Cognito session refresh does not refresh Google access token #3619

tcchau opened this issue Jul 10, 2019 · 17 comments
Labels
Auth Related to Auth components/category Cognito Related to cognito issues documentation Related to documentation feature requests

Comments

@tcchau
Copy link

tcchau commented Jul 10, 2019

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?

@tcchau
Copy link
Author

tcchau commented Jul 11, 2019

I have also now updated my code to use Auth.federatedSignIn( { provider: 'Google' } ) per the latest guidance from AWS Amplify. The results are the same: a new set of Cognito User Pool access and ID tokens are obtained by Amplify, but the custom attribute that holds the mapped Google access token remains unchanged. This is confirmed from inspecting the currentAuthethenticatedUser as well as looking in the Cognito User Pool using management console.

AWS Support team offers this as a workaround: prompt the user to login again... unusable for us because the user experience will be atrocious.

@manueliglesias manueliglesias added Auth Related to Auth components/category Cognito Related to cognito issues documentation Related to documentation feature requests labels Jul 11, 2019
@manueliglesias
Copy link
Contributor

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.

@tcchau
Copy link
Author

tcchau commented Jul 16, 2019

@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

@tcchau tcchau closed this as completed Jul 16, 2019
@syang
Copy link

syang commented Aug 3, 2020

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

@manueliglesias I wonder if User Pool can provide a mechanism for its user to achieve this. I knew this requires User Pool to send the request to Google with an offline parameter.

  • because of this, to accomplish refreshing my Google access token, I actually have to install the google-api-javascript-client

    • once the Cognito hosted UI has authenticated through Google and you get back your Cognito tokens, you need to then initialize the gapi client and "associate" it to the current Google authenticated session.
    • the thread in this issue google/google-api-javascript-client#401 helped out in this regard, in particular this comment: google/google-api-javascript-client#401 (comment)
    • thereafter, you can use the gapi.client..authorize() call again, to get a new, refreshed Google access token, after the current one has expried
  • 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

This basically makes User Pool in the whole solution as a sub-optimal choice. In other words, we want to use User Pool in the solution because of its other features; but the limit of not able to get refresh-token in the custom attribute made it really hard to solve some specific problem.

Not sure if Cognito team has any reason not supporting it.

@jmkmay
Copy link

jmkmay commented Oct 20, 2020

@tcchau I know this has been closed for quite a while, but did you ever figure out a solution here?

@tcchau
Copy link
Author

tcchau commented Oct 22, 2020

@tcchau I know this has been closed for quite a while, but did you ever figure out a solution here?

@jmkmay The solution I settled on was the one mentioned in my comment above.

Basically, since I need the Google API client to access other Google services anyway, I didn't mind using it to manage automatically refreshing my Google tokens as well.

@bredele
Copy link

bredele commented Mar 5, 2021

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.

@manueliglesias I guess this is still not clear in the documentation. Lots of confusing stuff in there. If I add an identity pool to my auth configuration, will it refresh google/Facebook tokens?

@tcchau
Copy link
Author

tcchau commented Mar 5, 2021

Hi, @bredele:

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

I had meant to use this approach originally because it was mentioned in a forum at some point. I never did get this to work properly at the time. This was quite some time ago, but I think either the refresh token couldn't be mapped to an attribute, or if it did map, it never got updated but stayed at the value that was initially set.

Again, this was quite some time ago, and as I found a usable workaround, I never re-visited this.

C.

@Sher-V
Copy link

Sher-V commented Jul 19, 2021

Hi, @tcchau

@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

Can you make your solution a bit more clear, please.
What do you mean by "associate" gapi client to the current Google authenticated session?
I did not really get the idea how you authorize your user using gapi.auth2.authorize an each time again and again without signing in again. Won't it open a new Google Popup?

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

@tcchau
Copy link
Author

tcchau commented 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.

@guentherwieser
Copy link

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.

@rickiesmooth
Copy link

@tcchau is there a similar approach on the server? I couldn't find a similar login method for the nodejs client and there are no cookies and localStorage but maybe withSSRContext could give the necessary info.

@tcchau
Copy link
Author

tcchau commented Apr 20, 2022

@rickiesmooth Not knowing more about your requirements, I can't say much more, but... I've discovered that the attribute mapping between OIDC providers and Cognito works much more reliably now. In my previous comments, I mention that mapping Google's access token and refresh tokens doesn't seem like a viable approach because it doesn't seem like they are updated over subsequent auth events. That does not appear to be the case anymore. I am using this method to access the OIDC's access tokens in another project I'm working on now and it's fine. Can't comment on the refresh token as I'm not using it.

What this means for you, is that you should be able to send along the access token from Google to your server, so that your server can make use of it to access Google APIs! Hope that helps.

@rickiesmooth
Copy link

rickiesmooth commented Apr 21, 2022

@tcchau that's exactly what I want to do! Access Google API's on the Cognito user behalf. Currently I'm using the Google Identity provider (not OpenID Connect (OIDC)).

Forgive my ignorance, but does that mean the access tokens will never be expired when mapped between the Google Provider and Cognito? Also, I tried mapping the access token in the console, but I don't see the option to map the access token user pool attribute.

@kraman-luna
Copy link

@manueliglesias can you help answer the question from @syang? Is there a way to have the UserPool pass in access_type=offline param when calling Google's /o/oauth2/v2/auth API? Looks like that is the only way to make the refresh token flow work, please let me know if there is an alternative.

@Robbie-Cook
Copy link

Robbie-Cook commented Jun 27, 2022

Hey guys, there are some Amplify docs on this now: https://docs.amplify.aws/lib/auth/advanced/q/platform/js/#facebook-sign-in-react-native---expo:~:text=Google%20sign%2Din,68 , which could help when combined with #3619 (comment)

@gsavvidis96
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Auth Related to Auth components/category Cognito Related to cognito issues documentation Related to documentation feature requests
Projects
None yet
Development

No branches or pull requests