[FIXED] How could I obtain the user's gender and birthday after they sign in with their Google account?

Issue

I have followed the example in Display the Sign In With Google button to get a Google sign in button working in my Angular application:

<div id="g_id_onload"
   class="mt-3"
   data-client_id="XXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.apps.googleusercontent.com"
   data-login_uri="http://localhost:1337/login/google"
   data-auto_prompt="false">
</div>
<div class="g_id_signin"
   data-width="250"
   data-type="standard"
   data-size="large"
   data-theme="outline"
   data-text="continue_with"
   data-shape="rectangular"
   data-logo_alignment="center">
</div>

Once the user signs in, I verify and decode the JWT token provided by Google in my Express server using jsonwebtoken:

app.post('/login/google', express.urlencoded(), async(request, response, next) => {
    try {
        console.log(`${request.method} ${request.url} was called.`);
        let token: string = request.body.credential;
        let body: Response = await fetch('https://www.googleapis.com/oauth2/v1/certs', { method: 'GET', headers: { Accept: 'application/json' }});
        let json: any = await body.json();
        let certificates: string[] = Object.keys(json).map(key => json[key]);
        let decoded: any;
        let lastError: any;
        certificates.every(certificate => {
            try {
                decoded = jwt.verify(token, certificate, { algorithms: ['RS256'], ignoreExpiration: false });
            }
            catch (error) { 
                lastError = error;
            }
            return !decoded;
        });
        if (!decoded)
            throw lastError;
    }
    catch (error) {
        next(error);
    }
});

The problem is that the decoded token does not contain the user’s gender or birthday information. How can I obtain this data?

I have just recently tried manually appending the https://www.googleapis.com/auth/user.birthday.read and https://www.googleapis.com/auth/user.gender.read scopes to my application’s OAuth Consent Screen found at https://console.cloud.google.com/apis/credentials/consent/edit, but I don’t see the user being prompted to provide this data to my application when it runs. I tried deleting permissions to my application from my account at accounts.google.com (under the Third-Party Access section) as well in hopes that it might prompt for these extra pieces of data. I am not sure at this point how to go about getting this extra data because I can’t seem to find a good documentation piece on how to achieve this. Also, I wanted to add that my test account’s Gender and Birthday information is set to be Private in https://myaccount.google.com/personal-info. I was wondering if its possible to fetch these private scopes somehow.

So, just to be clear, when I try to sign in I still only get the following prompt, which makes me believe that something is wrong and its not actually requesting the scope for birthday and gender from the user:

Confirm you want to sign in to [Application Name] with [User’s Name].

To create your account, Google will share your name, email address,
and profile picture with [Application Name].

I also tried going on https://developers.google.com/oauthplayground/ and I pasted this in for Input your own scopes: https://www.googleapis.com/auth/userinfo.email,https://www.googleapis.com/auth/userinfo.profile,https://www.googleapis.com/auth/user.birthday.read,https://www.googleapis.com/auth/user.gender.read. I then hit the Authorize API button, logged in, granted access to these scopes (was prompted correctly on the playground), performed the token exchange, then I tried to List possible operations and under the People API, I called the get people endpoint, and modified the URI to https://people.googleapis.com/v1/people/me as per the documentation. This endpoint seems to work to fetch the data I need, but now I can’t seem to wrap my head around what authorization parameters to use for this endpoint from the data I get back from the POST to my Express server. I have also tried enabling the People API from Enabled APIs & services.

Solution

I finally managed to get it working with the help of this guide.

I had to scrap the idea of using the Google sign in button because it does not seem to allow extended scopes such as birthday and gender (well, not if they’re private anyways – if anyone finds a way of doing it with the sign in button, please post an answer). Luckily, their OAuth API does support extended scopes. As such, I’ve implemented my own Google sign in button using the googleapis package.

There are a few steps to this:

  1. Use the googleapis package to generate a URI to present to the user that will ask them to consent to gender and birthday access.

For example:

app.get('/login/google/uri', async(request, response, next) => {
    try {
        console.log(`${request.method} ${request.url} was called.`);
        let client = new google.auth.OAuth2(
            'ClientID',
            'ClientSecret',
            `http://localhost:4200/login/google/redirect`
        );
        const scopes = [
            'https://www.googleapis.com/auth/userinfo.email',
            'https://www.googleapis.com/auth/userinfo.profile',
            'https://www.googleapis.com/auth/user.birthday.read',
            'https://www.googleapis.com/auth/user.gender.read'
        ];
        const authorizationUrl: string = client.generateAuthUrl({
            access_type: 'offline',
            scope: scopes,
            include_granted_scopes: false
        });
        response.status(200).send({ uri: authorizationUrl });
    }
    catch (error) {
        next(error);
    }
});
  1. Ensure that http://localhost:4200/login/google/redirect (or whatever redirect URI you use) is part of your OAuth 2.0 Client ID Credential’s Authorized redirect URIs in the console.
  2. Google will redirect to your redirect URI (http://localhost:4200/login/google/redirect) with a query parameter named code. For example: http://localhost:4200/login/google/redirect?code=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&scope=email%20profile%20https:%2F%2Fwww.googleapis.com%2Fauth%2Fuser.gender.read%20https:%2F%2Fwww.googleapis.com%2Fauth%2Fuser.birthday.read%20https:%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email%20https:%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile%20openid&authuser=0&prompt=consent
  3. Take the code (XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX) and exchange it for an access token.

For example:

let client = new google.auth.OAuth2(
    'ClientID',
    'ClientSecret',
    `http://localhost:4200/login/google/redirect`
);
let code: string = request.params.code;
let { tokens } = await client.getToken(code);
console.log(tokens.access_token);
  1. Use the access_token (it looks something like XXXX.XXXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX) when making requests to the People API and set it in the Authorization header as the bearer token.

For example:

curl "https://people.googleapis.com/v1/people/me?key=XXXXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXX&personFields=genders,birthdays" -H "Authorization: Bearer XXXX.XXXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

key is your API key from the console (you can create one and restrict it to the People API – if you don’t see the People API as a restriction option you might need to enable it from the Enabled APIs and services tab). I’m sure there is a more API friendly way of making this request in the googleapis package that you can explore, but I just wanted to highlight how it works with curl.

The response you will see should be like this:

{
    "resourceName": "people/XXXXXXXXXXXXXXXXXXXX",
    "etag": "XXXXXXXXXXXXXXXXXXXXX",
    "genders": [
        {
            "metadata": {
                "primary": true,
                "source": {
                    "type": "PROFILE",
                    "id": "XXXXXXXXXXXXXXXXXXXX"
                }
            },
            "value": "male",
            "formattedValue": "Male"
        }
    ],
    "birthdays": [
        {
            "metadata": {
                "primary": true,
                "source": {
                    "type": "ACCOUNT",
                    "id": "XXXXXXXXXXXXXXXXXXXX"
                }
            },
            "date": {
                "year": 1901,
                "month": 1,
                "day": 1
            }
        }
    ]
}

Edit: Just for completion, here is the API friendly way to do all of this.

First, generate this URI and redirect the user to it:

app.get('/login/google/uri', async(request, response, next) => {
    try {
        console.log(`${request.method} ${request.url} was called.`);
        let client = new googleapis.Auth.OAuth2Client(
            Globals.GoogleClientID,
            Globals.GoogleClientSecret,
            `${Globals.UIHost}/login`
        );
        const scopes = [
            'https://www.googleapis.com/auth/userinfo.email',
            'https://www.googleapis.com/auth/userinfo.profile',
            'https://www.googleapis.com/auth/user.birthday.read',
            'https://www.googleapis.com/auth/user.gender.read'
        ];
        const authorizationUrl: string = client.generateAuthUrl({
            access_type: 'offline',
            scope: scopes,
            include_granted_scopes: false
        });
        response.status(200).send({ uri: authorizationUrl });
    }
    catch (error) {
        next(error);
    }
});

Second, after the user has signed in and you get a code posted back to your redirect URI, parse the query param for the code and use it like how I am doing so in the following POST method on my server to get these extra user details for birthdays, genders, and emails:

app.post('/login/google', express.json(), async(request, response, next) => {
    try {
        console.log(`${request.method} ${request.url} was called.`);
        let client = new googleapis.Auth.OAuth2Client(
            Globals.GoogleClientID,
            Globals.GoogleClientSecret,
            `${Globals.UIHost}/login`
        );
        let code: string = request.body.code;
        let { tokens } = await client.getToken(code);
        let accessToken: string = tokens.access_token;
        client.setCredentials({ access_token: accessToken });
        let people = new googleapis.people_v1.People({});
        let result = await people.people.get({
            resourceName: 'people/me',
            personFields: 'emailAddresses,birthdays,genders',
            auth: client
        });
        console.log(result.data);
    }
    catch (error) {
        next(error);
    }
});

result.data should contain the information.

Answered By – Alexandru

Answer Checked By – Timothy Miller (Easybugfix Admin)

Leave a Reply

(*) Required, Your email will not be published