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

Google Sign-In: GoogleIdTokenVerifier.verify returns null for a valid id token #2343

Open
idrisadetunmbi opened this issue Jul 22, 2023 · 14 comments
Labels
priority: p3 Desirable enhancement or fix. May not be included in next release. type: docs Improvement to the documentation for an API.

Comments

@idrisadetunmbi
Copy link

idrisadetunmbi commented Jul 22, 2023

Environment details

  1. OS type and version: Mac OS Ventura 13.3.1
  2. Java version: openjdk 17.0.6 2023-01-17 LTS
  3. version(s): 1.33.0

Description

I am integrating the Google sign-in and authenticating the id token on the backend as described here. The documentation for the GoogleIdTokenVerifier states to specify the client ids of apps (the mobile apps I believe) when instantiating the class, but when I call GoogleIdTokenVerifier.verify on the idToken (returned from the flow with the mobile app), it returns null (meaning the idToken is invalid). Specifying the web client id (which is for the server) however, works. Can this be clarified, please? Which client ids are meant to be specified when instantiating the class?

Steps to reproduce

Same steps as described here

Code example

val verifier = GoogleIdTokenVerifier.Builder(NetHttpTransport(), GsonFactory())
  .setAudience(/* mobile client ids */)
  .build()
verifier.verify(/* idToken from android */) // returns null instead of the GoogleIdToken instance

Stack trace

NA

External references such as API reference guides

https://developers.google.com/identity/sign-in/android/backend-auth

Any additional information below

NA

@diegomarquezp diegomarquezp self-assigned this Jul 24, 2023
@diegomarquezp diegomarquezp added type: question Request for information or clarification. Not an issue. priority: p2 Moderately-important priority. Fix may not be included in next release. labels Jul 24, 2023
@diegomarquezp diegomarquezp removed their assignment Jul 24, 2023
@diegomarquezp
Copy link
Contributor

Hi @idrisadetunmbi, thanks for bringing this up.
I believe this may be a good suit for @googleapis/googleapis-auth

@clundin25
Copy link

There is a slight difference in the documentation here and the code.

The documentation says one of the returned audience values is in the response. The code expects ALL the audience values are expected.

So to answer your question, the audience should be set to the audience specified by the server.

@diegomarquezp diegomarquezp added type: docs Improvement to the documentation for an API. and removed type: question Request for information or clarification. Not an issue. labels Jul 25, 2023
@idrisadetunmbi
Copy link
Author

@clundin25, thanks for your answer, but I still don't get what the correct usage should be. I decoded a JWT retrieved from the client (through the Google Sign-in Android SDK) and the audience is actually a string and not a list which means the code only checks if that single string audience is in one of the "trustedClientIds".

My expectation of the flow is that the verifier takes the list of trusted mobile client ids, and when it is verifying an id token, it checks that the aud (of the id token is one of them), but the aud that seems to be in the id token is the server's web client id and not the mobile client id.

@clundin25
Copy link

You want to verify the audience from the server is what you expect it to be.

@DaTigerYT
Copy link

DaTigerYT commented Aug 5, 2023

Ping! I came here to report the same thing.

And... I'm also confused about what to do here... I can confirm that my client ID is the Android client ID (and not the Web) for the backend/server side. Also, I manually de-coded the ID token and can confirm that the aud, iss, exp should be valid, it's only the signature that I can't be sure is valid or not - but I'm not sure why it wouldn't be.

Here's my code:

SignInCredential credential = oneTapClient.getSignInCredentialFromIntent(result.getData());
String unverifiedIdToken = credential.getGoogleIdToken(); // a JWT token

if (unverifiedIdToken != null) {
        GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new GsonFactory())
                .setAudience(Collections.singletonList(getString(R.string.android_client_id)))
                .build();
        
        try {
                GoogleIdToken idToken = verifier.verify(unverifiedIdToken); // null
                ...

Decoding the JWT token manually, I successfully managed to get a header and payload but I didn't get a signature... I'm not sure if that's expected behaviour or not tbh, in the documentation they mention that "The ID token is properly signed by Google" so I assumed I would be able to get the signature after decoding.

They also mentioned two public keys which I assume we were supposed to use at some point for the missing signature.

@DaTigerYT
Copy link

Well @idrisadetunmbi it's been 2 weeks now, did you find a solution to this?

@idrisadetunmbi
Copy link
Author

Well @idrisadetunmbi it's been 2 weeks now, did you find a solution to this?

Hi @DaTigerYT. For now, I just specified the web client id (meant for the server) as part of the client ids when initializing the verifier.

The problem could also be from the client and not the verifier, but I have not had the time to verify this.

@DaTigerYT
Copy link

DaTigerYT commented Aug 5, 2023

I don't do it often, but I just ran my code in debug mode and I noticed something pretty interesting... as shown in the screenshots below, after the verifier does GoogleIdToken.parse() it seems it swaps the aud and azp around, where the aud should be the android client ID I input but is instead the web client ID I input when building GoogleIdTokenRequestOptions.

image
image
image

@DaTigerYT
Copy link

DaTigerYT commented Aug 6, 2023

Finally got the thing to work, I should mention: .setAudience() seems to accept the web_client_id (for the front end) rather than the android_client_id (supposed to be for the backend), again this is highly likely because the aud and azp are mysteriously switched for some reason. Not only that, when using Arrays.asList() as long as one of the strings is the web client ID it will work but you can also just get rid of setAudience() together and it'll work.

For those in the future who may be reading this:

After that, I was faced with a android.os.NetworkOnMainThreadException error, and I can't be bothered to explain the reason so I'll spoonfed my working code instead...
Before:

GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(transport, jsonFactory)
    .setAudience(Collections.singleton(getString(R.string.web_client_id)))
    .build();

try {
    return verifier.verify(unverifiedIdToken);
    // Put code here to do after verification here

After:

ExecutorService executor = Executors.newSingleThreadExecutor();

Future<GoogleIdToken> future = executor.submit(() -> {
    GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new GsonFactory())
            .setAudience(Collections.singletonList("WEB_CLIENT_ID")) // or just get rid of the setAudience entirely for some reason
            .build();

    try {
        return verifier.verify(unverifiedIdToken);
    } catch (GeneralSecurityException | IOException e) {
        e.printStackTrace();
        return null;
    }
});

executor.execute(() -> {
    GoogleIdToken idToken = null;
    try {
        idToken = future.get(); // This will block until the task is done
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }

    // Put code here to do after verification here

    executor.shutdown(); // Shut down the executor after the task is done
});

Hopefully that helped someone... if only Google just updated their damn documentation haha

@idrisadetunmbi
Copy link
Author

Hopefully there isn't a security risk in the mix when not specifying the audience or specifying the web client id as one of the audience.

Another option is to use the verifier without the audience but check that the azp from the GoogleIdToken is in one of the trusted client ids

@clundin25
Copy link

Thanks for diving deep into this @DaTigerYT. We will take a closer look at this

@vlastami
Copy link

oh and by the way, if you put your client-id in a property, make sure you don't have it as a string there. just discovered that in my project. 😄

@DaTigerYT
Copy link

DaTigerYT commented Nov 18, 2023

Finally got the thing to work, I should mention: .setAudience() seems to accept the web_client_id (for the front end) rather than the android_client_id (supposed to be for the backend), again this is highly likely because the aud and azp are mysteriously switched for some reason. Not only that, when using Arrays.asList() as long as one of the strings is the web client ID it will work but you can also just get rid of setAudience() together and it'll work.

If anyone is currently experiencing the Error 400: redirect_uri_mismatch error when using oauth2 for access to Google APIs.

It's likely because the issue was fixed and thus you should now use your web_client_id (as opposed to the android_client_id) like originally intended.

@ideazh
Copy link

ideazh commented Nov 27, 2023

@DaTigerYT Thank you very much for your answer; it worked after I changed 'audience' to 'WEB_CLIENT_ID'.

@blakeli0 blakeli0 added priority: p3 Desirable enhancement or fix. May not be included in next release. and removed priority: p2 Moderately-important priority. Fix may not be included in next release. labels Jan 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
priority: p3 Desirable enhancement or fix. May not be included in next release. type: docs Improvement to the documentation for an API.
Projects
None yet
Development

No branches or pull requests

8 participants