'invalid_grant exchanging authorization code for access and refresh tokens

My application is using OAuth to access the Youtube Data API. My OAuth callback is written in node and uses the OAuth2Client class from the "googleapis" npm package to exchange the authorization code for the access and refresh tokens.

Everything was working fine up to last week until suddenly I started getting the "invalid_grant" response during the authorization code exchange. I have tried everything to resolve this and am running out of ideas. My callback executes as a cloud function so I don't think that it would be out of sync with NTP.

My OAuth consent screen is in "Testing" mode and my email address is included in the test users. The odd thing is that even though the authorization code exchange fails, my Google account's "Third-party apps with account access" section lists my application as if the handshake succeeded.

Is there a limit to how many refresh tokens can be minted for my application? I am testing my implementation of incremental authorization so I have been going through the OAuth flow often.

Edit

I've included my code for generating the auth URL and exchanging the authorization code below. The invalid_grant occurs during the call to "oauth2.getToken"

async startFlow(scopes: string[], state: string): Promise<AuthFlow> {
        const codes = await oauth2.generateCodeVerifierAsync();
        const href = oauth2.generateAuthUrl({
            scope: scopes,
            state,
            access_type: 'offline',
            include_granted_scopes: true,
            prompt: 'consent',
            code_challenge_method: CodeChallengeMethod.S256,
            code_challenge: codes.codeChallenge
        });
        return { href, code_verifier: codes.codeVerifier };
    }

    async finishFlow(code: string, verifier: string): Promise<Tokens> {
        const tokens = await oauth2.getToken({ code, codeVerifier: verifier })
        return {
            refresh_token: tokens.tokens.refresh_token!,
            access_token: tokens.tokens.access_token!,
            expires_in: tokens.tokens.expiry_date!,
            token_type: 'Bearer',
            scopes: tokens.tokens.scope!.split(' ')
        };
    }

"oauth2" is an instance of OAuth2Client from "google-auth-library". I initialize it here:

export const oauth2 = new google.auth.OAuth2({
    clientId: YT_CLIENT_ID,
    clientSecret: YT_CLIENT_SECRET,
    redirectUri: `${APP_URI}/oauth`
});

Looking at the logs, the only out of the ordinary thing I notice is that the application/x-www-form-urlencoded body looks slightly different than the example https://developers.google.com/identity/protocols/oauth2/web-server#exchange-authorization-code

The POST request to "https://oauth2.googleapis.com/token" ends up looking like this:

code=4%2F0AX4XfWiKHVnsavUH7en0TywjPJVRyJ9aGN-JR8CAAcAG7dT-THxyWQNcxd769nzaHLUb8Q&client_id=XXXXXXXXXX-XXXXXXXXXXXXXXX.apps.googleusercontent.com&client_secret=XXXXXX-XXXXXXXXXXXXXXX-XX_XXX&redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth&grant_type=authorization_code&code_verifier=KjOBmr4D9ISLPSE4claEBWr3UN-bKdPHZa8BBcQvcmajfr9RhWrgt7G429PLEpsP7oGzFGnBICu3HgWaHPsLhMkGBuQ2GmHHiB4OpY2F0rJ06wkpCjV2cCTDdpfRY~Ej

Notice that the "/" characters are not percent-encoded in the official example, but they are in my requests. Could this actually be the issue? I don't see how the official google auth library would have an issue this large.



Solution 1:[1]

The most common cause for the invalid_grant error is your refresh token expiring.

If you check oauth2#expiration you will see the following

A Google Cloud Platform project with an OAuth consent screen configured for an external user type and a publishing status of "Testing" is issued a refresh token expiring in 7 days.

Once you set your project to production your refresh tokens will stop expiring.

enter image description here

Is there a limit to how many refresh tokens can be minted for my application?

No but you have a limit of 100 test users.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 DaImTo