How to fix the "Malformed auth code" when trying to refreshToken on the second attempt?

11,825

Solution 1

Finally I achieved refreshing the access token as times as needed. The problem was a misconception of how it works the Google Api.

The first time to update the token, it is needed to call this endpoint with these parameters and setting as {{refreshToken}} the value obtained from the response of the Consent Screen call (serverAuthCode)

https://oauth2.googleapis.com/token?code={{refreshToken}}&client_id={{googleClientId}}&client_secret={{googleClientSecret}}&grant_type=authorization_code

After the first refresh, any update to the token needs to be call to this other endpoint by setting as {{tokenUpdated}} the attribute {{refresh_token}} obtained from the response of the first call.

https://oauth2.googleapis.com/token?refresh_token={{tokenUpdated}}&client_id={{googleClientId}}&client_secret={{googleClientSecret}}&grant_type=refresh_token

Here I show you an example of my AuthenticationService

import { Injectable} from '@angular/core';
import { Router } from '@angular/router';
import { GooglePlus } from '@ionic-native/google-plus/ngx';

@Injectable({
  providedIn: 'root'
})
export class AuthenticationService {


static AUTH_INFO_GOOGLE = 'auth-info-google';
static CLIENT_ID = 'XXXXX-XXXX.apps.googleusercontent.com';
static CLIENT_SECRET = 'SecretPasswordClientId';


public authenticationState = new BehaviorSubject(false);

  constructor(
    private router: Router,
    private googlePlus: GooglePlus) {

  }

public isAuthenticated() {
    return this.authenticationState.value;
}

public logout(): Promise<void> {
    this.authenticationState.next(false);   
    return this.googlePlus.disconnect()
    .then(msg => {
      console.log('User logged out: ' + msg);
    }, err => {
      console.log('User already disconected');
    }); 
}

/**
* Performs the login
*/
public async login(): Promise<any> {
    return this.openGoogleConsentScreen().then(async (user) => {
      console.log(' ServerAuth Code: ' + user.serverAuthCode);
      user.updated = false;
      await this.setData(AuthenticationService.AUTH_INFO_GOOGLE, JSON.stringify(user));
      this.authenticationState.next(true);
      // Do more staff after successfully login
    }, err => {
        this.authenticationState.next(false);
        console.log('An error ocurred in the login process: ' + err);
        console.log(err);
    });

}

  /**
   * Gets the Authentication Token
   */
public async getAuthenticationToken(): Promise<string> {
      return this.getAuthInfoGoogle()
        .then(auth => {
          if (this.isTokenExpired(auth)) {
            return this.refreshToken(auth);
          } else {
            return 'Bearer ' + auth.accessToken;
          }
        });
}



private async openGoogleConsentScreen(): Promise<any> {
  return this.googlePlus.login({
    // optional, space-separated list of scopes, If not included or empty, defaults to `profile` and `email`.
    'scopes': 'profile email openid',
    'webClientId': AuthenticationService.CLIENT_ID,
    'offline': true
  });
}

private isTokenExpired(auth: any): Boolean {
    const expiresIn = auth.expires - (Date.now() / 1000);
     const extraSeconds = 60 * 59 + 1;
    // const extraSeconds = 0;
    const newExpiration = expiresIn - extraSeconds;
     console.log('Token expires in ' + newExpiration + ' seconds. Added ' + extraSeconds + ' seconds for debugging purpouses');
    return newExpiration < 0;
}

private async refreshToken(auth: any): Promise<any> {
      console.log('The authentication token has expired. Calling for renewing');
      if (auth.updated) {
        auth = await this.requestGoogleRefreshToken(auth.serverAuthCode, auth.userId, auth.email);
      } else {
        auth = await this.requestGoogleAuthorizationCode(auth.serverAuthCode, auth.userId, auth.email);
      }
      await this.setData(AuthenticationService.AUTH_INFO_GOOGLE, JSON.stringify(auth));
      return 'Bearer ' + auth.accessToken;
}


private getAuthInfoGoogle(): Promise<any> {
    return this.getData(AuthenticationService.AUTH_INFO_GOOGLE)
    .then(oauthInfo => {
      return JSON.parse(oauthInfo);
    }, err => {
      this.clearStorage();
      throw err;
    });
}

private async requestGoogleAuthorizationCode(serverAuthCode: string, userId: string, email: string): Promise<any> {
    let headers = new HttpHeaders();
    headers = headers.set('Content-Type', 'application/x-www-form-urlencoded');
    let params: HttpParams = new HttpParams();
    params = params.set('code', serverAuthCode);
    params = params.set('client_id', AuthenticationService.CLIENT_ID);
    params = params.set('client_secret', AuthenticationService.CLIENT_SECRET);
    params = params.set('grant_type', 'authorization_code');
    const options = {
      headers: headers,
      params: params
    };
    const url = 'https://oauth2.googleapis.com/token';
    const renewalTokenRequestPromise: Promise<any> = this.http.post(url, {}, options).toPromise()
      .then((response: any) => {
        const auth: any = {};
        auth.accessToken = response.access_token;
        console.log('RefreshToken: ' + response.refresh_token);
        auth.serverAuthCode = response.refresh_token;
        auth.expires = Date.now() / 1000 + response.expires_in;
        auth.userId = userId;
        auth.email = email;
        auth.updated = true;
        return auth;
      }, (error) => {
        console.error('Error renewing the authorization code: ' + JSON.stringify(error));
        return {};
      });
    return await renewalTokenRequestPromise;
}

private async requestGoogleRefreshToken(serverAuthCode: string, userId: string, email: string): Promise<any> {
    let headers = new HttpHeaders();
    headers = headers.set('Content-Type', 'application/x-www-form-urlencoded');
    let params: HttpParams = new HttpParams();
    params = params.set('refresh_token', serverAuthCode);
    params = params.set('client_id', AuthenticationService.CLIENT_ID);
    params = params.set('client_secret', AuthenticationService.CLIENT_SECRET);
    params = params.set('grant_type', 'refresh_token');
    const options = {
      headers: headers,
      params: params
    };
    const url = 'https://oauth2.googleapis.com/token';
    const renewalTokenRequestPromise: Promise<any> = this.http.post(url, {}, options).toPromise()
      .then((response: any) => {
        const auth: any = {};
        auth.accessToken = response.access_token;
        console.log('RefreshToken: ' + serverAuthCode);
        auth.serverAuthCode = serverAuthCode;
        auth.expires = Date.now() / 1000 + response.expires_in;
        auth.userId = userId;
        auth.email = email;
        auth.updated = true;
        return auth;
      }, (error) => {
        console.error('Error renewing refresh token: ' + JSON.stringify(error));
        return {};
      });
    return await renewalTokenRequestPromise;
}

private setData(key: string, value: any): Promise<any> {
    console.log('Store the value at key entry in the DDBB, Cookies, LocalStorage, etc')
}

private getData(key: string): Promise<string> {
    console.log('Retrieve the value from the key entry from DDBB, Cookies, LocalStorage, etc')
}

private clearStorage(): Promise<string> {
    console.log('Remove entries from DDBB, Cookies, LocalStorage, etc related to authentication')
}



}

Solution 2

In my case it was pretty stupid: google api changes the auth code coding between requests.

Step 1 - During the first request to obtain tokens google returns quite normal, not encoded string as the code.

Step 2 - During second and N-th request to obtain tokens (if they were not revoked) google returns the auth code as url-encoded. In my case the killing change was '/' -> '%2F'.

Solution: Always URL-Decode the auth code before exchanging it for the access tokens!

Solution 3

u need to decode your code like this

    String code = "4%2F0AX************...";
    String decodedCode = "";
    try {
      decodedCode = java.net.URLDecoder.decode(code, StandardCharsets.UTF_8.name());
    } catch (UnsupportedEncodingException e) {
      //do nothing
    }

then use decodedCode as param

Share:
11,825
Ausiàs Armesto
Author by

Ausiàs Armesto

Updated on July 03, 2022

Comments

  • Ausiàs Armesto
    Ausiàs Armesto almost 2 years

    I'm developping an Android App with Angular and Cordova plugins and I want to integrate it with Google Authentication. I have installed the cordova-plugin-googleplus and I have successfully integrated into the application. When the user logs in, I get a response where I can get accessToken, profile user information and refreshToken.

    Now I want to implement a feature to refresh the token without disturbing the user with a new prompt screen every hour.

    I have managed to renew accessToken, but it only works the first time

    I have used these two ways:

    1. Sending a curl request with the following data
    curl -X POST \
      'https://oauth2.googleapis.com/token?code=XXXXXXXXXXXXXXXX&client_id=XXXXXXXXXXXXXXXX.apps.googleusercontent.com&client_secret=YYYYYYYYYYYY&grant_type=authorization_code' \
      -H 'Cache-Control: no-cache' \
      -H 'Content-Type: application/x-www-form-urlencoded'
    
    1. Implementing it on the server side using the Google API Client Library for Java and following mainly these code

    The point is that when the user logs in for the first time (using the cordova-plugin-googleplus), I receive a refreshToken with this format

    4/rgFU-hxw9QSbfdj3ppQ4sqDjK2Dr3m_YU_UMCqcveUgjIa3voawbN9TD6SVLShedTPveQeZWDdR-Sf1nFrss1hc
    

    If after a while I try to refresh the token in any of the above ways I get a successful response with a new accessToken, and a new refreshToken. And that new refreshToken has this other format

    1/FTSUyYTgU2AG8K-ZsgjVi6pExdmpZejXfoYIchp9KuhtdknEMd6uYCfqMOoX2f85J
    

    In the second attempt to renew the token, I replace the token with the one returned in the first request

    curl -X POST \
      'https://oauth2.googleapis.com/token?code=1/FTSUyYTgU2AG8K-ZsgjVi6pExdmpZejXfoYIchp9KuhtdknEMd6uYCfqMOoX2f85J&client_id=XXXXXXXXXXXXXXXX.apps.googleusercontent.com&client_secret=YYYYYYYYYYYY&grant_type=authorization_code' \
      -H 'Cache-Control: no-cache' \
      -H 'Content-Type: application/x-www-form-urlencoded'
    

    But this time, both ways (Curl and Java) I am getting the same error.

    {
      "error" : "invalid_grant",
      "error_description" : "Malformed auth code."
    }
    

    I read on this thread that was a problem to specify the clientId as an email, but I have not discovered how to solve it either because the first login it's done with the client id 'XXXXXXX.apps.googleusercontent.com' and if I set an email from the google accounts it says that is an "Unknown Oauth Client"

    I hope any can help me with this, as I'm stuck for several days