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

Assertion failure when access token reponse does not include refresh token #487

Open
lukehutch opened this issue Jan 8, 2023 · 11 comments

Comments

@lukehutch
Copy link

In oauth2_flows/auth_code.dart, the function obtainAccessCredentialsViaCodeExchange() sends an OAuth2 token request, obtaining an access token as JSON, of the form:

0: "access_token" -> "<elided>"
1: "expires_in" -> 3487
2: "scope" -> "openid https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email"
3: "token_type" -> "Bearer"
4: "id_token" -> "<elided>"

This is returned and stored in credentials in endpoints/google_endpoint.dart:230. Then an AutoRefreshingClient is instantiated using these credentials. That causes an assert failure in auth_http_utils.dart:98:

        assert(credentials.refreshToken != null),

The access token JSON does not include refresh_token (which would be saved in credentials as refreshToken by auth_code.dart:128).

This causes Google login to fail.

I don't know why the response does not include a refresh token in this case, but wouldn't it be better to still enable temporary login even if a refresh token is not provided by the server?

@lukehutch
Copy link
Author

I found this info here: https://stackoverflow.com/a/54572538/3950982

In order to get new refresh_token each time on authentication the type of OAuth 2.0 credentials created in the dashboard should be "Other". Also as mentioned above the access_type='offline' option should be used when generating the authURL.

When using credentials with type "Web application" no combination of prompt/approval_prompt variables will work - you will still get the refresh_token only on the first request.

I'm using the Android OAuth type in the developer console, so maybe this is why no refresh token is being sent?

Either way, the code should not fail here with an assertion failure, if the refresh token isn't sent under some circumstances.

@lukehutch
Copy link
Author

It looks like you need to add access_type='offline' prompt='consent' to always be sent a refresh token:

googleapis/google-api-python-client#213 (comment)

@lukehutch
Copy link
Author

lukehutch commented Jan 9, 2023

Also quoted in that bug report:

"Be sure to store the refresh token safely and permanently, because you can only obtain a refresh token the first time that you perform the code exchange flow."

Although another comment says consent has to be re-requested every 2 months or so.

Is the refresh token being stored somewhere for the user, to avoid requesting it again?

To get a new refresh token:

Users have to deauthorize your application from their security dashboard and you'll get a new refresh token the next time they authorize.

@lukehutch
Copy link
Author

However adding 'access_type': 'offline' to the request in obtainAccessCredentialsViaCodeExchange() gives a 500 server error...

@lukehutch
Copy link
Author

See also this, re forcing the user to re-authorize, in order to obtain another refresh token. Is this done automatically by the googleapis library?

https://stackoverflow.com/questions/8942340/get-refresh-token-google-api/10220362#10220362

@kevmoo
Copy link
Collaborator

kevmoo commented Jan 17, 2023

@lukehutch – which version of googleapis_auth are you using? What's in your pubspec.lock file?

@lukehutch
Copy link
Author

I was using serverpod_auth_server 0.9.21, which depends upon googleapis_auth ^1.3.0.

https://github.com/serverpod/serverpod/blob/main/modules/serverpod_auth/serverpod_auth_server/pubspec.yaml

Currently this means it's pulling in version 1.3.1, since that is the latest version:

  googleapis_auth:
    dependency: transitive
    description:
      name: googleapis_auth
      url: "https://pub.dartlang.org"
    source: hosted
    version: "1.3.1"

@kevmoo
Copy link
Collaborator

kevmoo commented Jan 18, 2023 via email

@lukehutch
Copy link
Author

As discussed on Twitter, this bug was triggered by (a slightly modified version of) the serverpod_auth example code, but I don't have this code anymore, because I had to move on to use Firebase Authentication. It should be relatively easy to try running this example code to replicate the problem.

https://docs.serverpod.dev/concepts/authentication
https://github.com/serverpod/serverpod/tree/main/modules/serverpod_auth
https://github.com/serverpod/serverpod/tree/main/examples/auth_example

@lukehutch
Copy link
Author

lukehutch commented Mar 17, 2023

@kevmoo I tried creating PR #524 to fix this bug, based on this suggestion but it didn't work -- what is the right way to force the user to consent to offline access?

Here's what's going on:

auth_code.dart, method obtainAccessCredentialsViaCodeExchange calls

  final jsonMap = await client.oauthTokenRequest(
    {
      'client_id': clientId.identifier,
      'client_secret': clientId.secret ?? '',
      'code': code,
      if (codeVerifier != null) 'code_verifier': codeVerifier,
      'grant_type': 'authorization_code',
      'redirect_uri': redirectUrl,
    },
  );

The response of this request does not include a refresh_token if the user has signed in before. Therefore, refreshToken ends up being null:

 final refreshToken = jsonMap['refresh_token'] as String?;

This is passed to AccessCredentials:

  return AccessCredentials(
    accessToken,
    refreshToken,
    scopes,
    idToken: idToken,
  );

These AccessCredentials are later passed to an AutoRefreshingClient, which causes the assert failure in auth_http_utils.dart:98:

        assert(credentials.refreshToken != null),

Possible solutions:

  1. The refresh token should be saved in SharedPreferences and reused if available, as long as the logged-in user matches the user for the saved refresh token; or
  2. The request should be made with the parameters prompt=consent&access_type=offline, to force consent on each login, which will deliver a new refresh token for each login. (I couldn't get this to work, though...)
  3. AutoRefreshingClient could simply give up on trying to refresh the token if refreshToken is null, instead of failing with an assertion error. (Preferably though it could just use the saved refresh token when none was returned, assuming the token is for the correct user -- see 1.)

@lukehutch
Copy link
Author

I found a fix/workaround: by using the GoogleSignIn library, and providing forceCodeForRefreshToken: true, I can set prompt=consent on the first auth request.

However, the googleapis.dart library still needs to do something smarter if a refresh token is not returned from a request, rather than throwing an assertion error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants