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.
You are using signin. Signin is open id connect and returns an id token. Id tokes contain very few claims. Gender is not one of them.
The only way to get access to the full user profile info is to go though the people api as you have mentioned.
You can use the try me to see it working and generate the sample for you.
<script src="https://apis.google.com/js/api.js"></script>
<script>
/**
* Sample JavaScript code for people.people.get
* See instructions for running APIs Explorer code samples locally:
* https://developers.google.com/explorer-help/code-samples#javascript
*/
function authenticate() {
return gapi.auth2.getAuthInstance()
.signIn({scope: "https://www.googleapis.com/auth/contacts https://www.googleapis.com/auth/contacts.readonly https://www.googleapis.com/auth/directory.readonly https://www.googleapis.com/auth/user.addresses.read https://www.googleapis.com/auth/user.birthday.read https://www.googleapis.com/auth/user.emails.read https://www.googleapis.com/auth/user.gender.read https://www.googleapis.com/auth/user.organization.read https://www.googleapis.com/auth/user.phonenumbers.read https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile"})
.then(function() { console.log("Sign-in successful"); },
function(err) { console.error("Error signing in", err); });
}
function loadClient() {
gapi.client.setApiKey("YOUR_API_KEY");
return gapi.client.load("https://people.googleapis.com/$discovery/rest?version=v1")
.then(function() { console.log("GAPI client loaded for API"); },
function(err) { console.error("Error loading GAPI client for API", err); });
}
// Make sure the client is loaded and sign-in is complete before calling this method.
function execute() {
return gapi.client.people.people.get({
"resourceName": "people/me",
"personFields": "genders"
})
.then(function(response) {
// Handle the results here (response.result has the parsed body).
console.log("Response", response);
},
function(err) { console.error("Execute error", err); });
}
gapi.load("client:auth2", function() {
gapi.auth2.init({client_id: "YOUR_CLIENT_ID"});
});
</script>
<button onclick="authenticate().then(loadClient)">authorize and load</button>
<button onclick="execute()">execute</button>
The issue that you are then going to have is the above sample uses Oauth2 and not open id connect (signin) It needs an access token to work. If you check your code I belive that the signin does return an access token. Your job then is to feed the access token to the code above so that you dont have to go though the authorization process again.
So far i have not found anyone able to link the new signin system with the old oauth2 system. If you get it to work i would love to see it.
Html
To call this api you need an access_token. a google access token is not a jwt. it is not the id_token
GET https://people.googleapis.com/v1/people/me?personFields=genders&key=[YOUR_API_KEY] HTTP/1.1
Authorization: Bearer [YOUR_ACCESS_TOKEN]
Accept: application/json
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:
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);
}
});
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.
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
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);
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.
If you are using NestJS with typescript, this worked for me
#Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor(configService: ConfigService) {
super({
clientID: configService.get('GOOGLE_CLIENT_ID'),
clientSecret: configService.get('GOOGLE_SECRET'),
callbackURL: configService.get('GOOGLE_REDIRECT_URL'),
scope: [
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/plus.login',
'https://www.googleapis.com/auth/user.birthday.read',
'https://www.googleapis.com/auth/user.phonenumbers.read',
'https://www.googleapis.com/auth/user.gender.read',
],
});
}
async validate(
accessToken: string,
refreshToken: string,
profile: any,
done: VerifyCallback,
): Promise<any> {
const { name, emails, photos, sub, birthday, phoneNumber, gender } =
profile;
const user = {
sub,
email: emails[0].value,
firstName: name.givenName,
lastName: name.familyName,
picture: photos[0].value,
dob: birthday,
phoneNumber,
gender,
refreshToken,
accessToken,
};
done(null, user);
}
}
Then add GoogleStrategy to your provider. Of course, don't forget your keys in your .env file.
I have a paid developer account with Google. I have verified my People API is enabled and that I am using the correct API Key and Client ID. When I try the Google Login Start, I get this error in the console for reason.result.error...
{
code: 401,
message: "The request does not have valid authentication credentials.",
status: "UNAUTHENTICATED"
}
I verified that the API key and ClientID are correct. I start off with a method like this for the login button...
didClickGoogleLogin = () => {
// 1. Load the JavaScript client library.
gapi.load('client', this.googleLoginStart);
}
Then it calls the googleLoginStart method...
googleLoginStart = () => {
// 2. Initialize the JavaScript client library.
gapi.client.init({
'apiKey': <API KEY HERE>,
// clientId and scope are optional if auth is not required.
'clientId': <CLIENT ID HERE>,
'scope': 'profile',
}).then(function() {
// 3. Initialize and make the API request.
return gapi.client.request({
'path': 'https://people.googleapis.com/v1/people/me?personFields=metadata,names,emailAddresses',
})
}).then((response) => {
console.log(response.result);
const googleID = response.result.metadata.sources[0].id;
const googleEmail = response.result.emailAddresses[0].value;
this.updateUser(googleID, googleEmail);
}, function(reason) {
console.log("reason: ", reason);
});
}
This was working for me in another web app I wrote a few months ago, but it's not working there now either.
Any ideas why I'm getting an invalid creds error when the API is enabled, the API Key & Client ID are correct?
I am attempting to verify my pact.json that has been generated by my consumer. However for verifying I need to include AWS4 credentials in order to be able to get a response from my provider. I am attempting to do this using customProviderHeaders. I am using the library AWS4(https://github.com/mhart/aws4) to generate the token. Below is my code:
const aws4 = require('aws4');
const path = require('path');
import { before, beforeEach, describe, it } from 'mocha';
const {
Verifier
} = require('../../../node_modules/#pact-foundation/pact');
function getToken() {
const opts: any = {
method: 'GET',
region: 'us-east-2',
service: 'execute-api',
path: '/qa/api/',
host: '123456789.execute-api.us-east-2.amazonaws.com',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
};
aws4.sign(opts, {accessKeyId: '$AWSACCESSKEY', secretAccessKey: '$AWSSECRETKEY'});
return opts.headers;
}
describe('Pact Verification', () => {
it('should validate the watchlist expectations', () => {
let headers = getToken();
let authToken = headers.Authorization;
let date = headers[`X-Amz-Date`];
let opts = {
provider: 'DealerBlock',
providerBaseUrl: 'https://3ua1cprd53.execute-api.us-east-2.amazonaws.com',
pactUrls: [path.resolve(process.cwd(), 'src/test/pact/path_to_my_json')],
customProviderHeaders: [`Authorization: ${authToken}`, `X-Amz-Date: ${date}`]
};
return new Verifier().verifyProvider(opts)
.then(output => {
console.log('STARTED');
console.log(opts.pactUrls);
console.log('Pact Verification Complete');
console.log(output);
});
});
});
The function getToken() generates a new token and I then grab the token and date and insert them into my request using the customer provider headers.
I see the following:
INFO: Replacing header 'Authorization: ' with 'Authorization: AWS4-HMAC-SHA256 Credential=AKIAJ5FTCODVMSUTEST/2018908/us-east-2/execute-api/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=ceea9aac0303769da58357cb37cb849cb0bbfc13ff0a25cea977385368531349'
INFO: Replacing header 'X-Amz-Date: ' with 'X-Amz-Date: 20180528T184202Z'
However I get the following error:
Actual: {"message":"The request signature we calculated does not match the signature you provided.
Check your AWS Secret Access Key and signing method. Consult the service documentation for details."}
Am I using the customProviderHeaders in the correct manner? Or does anyone have any suggestions as to what I should do differently? I am able to send a request using the same credentials via Postman so not sure whats going on here.
Thanks!
It looks OK to my eyes.
Could it be that you're not interpolating the variables (that also appear not to be defined anywhere) in the following statement:
aws4.sign(opts, {accessKeyId: '$AWSACCESSKEY', secretAccessKey: '$AWSSECRETKEY'});
Was able to get this working when I passed in headers of: 'Content-Type': 'application/x-www-form-urlencoded' via customProviderHeaders.
Even though this header was listed in my consumer generated json contract, the pact provider did not seem to see it.
I want to test a a cloud function that creates users.
In normal cases, inside the browser i generate an idToken and i send it to server via headers: Authorization : Bearer etcIdToken
But I want to test this function without the browser. In my mocha tests i have:
before(done => {
firebase = require firebase.. -- this is suppose to be like the browser lib.
admin = require admin..
idToken = null;
uid = "AY8HrgYIeuQswolbLl53pjdJw8b2";
admin.auth()
.createCustomToken(uid) -- admin creates a customToken
.then(customToken => {
return firebase.auth() -- this is like browser code. customToken get's passed to the browser.
.signInWithCustomToken(customToken) -- browser signs in.
.then(signedInUser => firebase.auth() -- now i want to get an idToken. But this gives me an error.
.currentUser.getIdToken())
})
.then(idToken_ => {
idToken = idToken_
done();
})
.catch(err => done(err));
})
The error i'm getting is:
firebase.auth(...).currentUser.getIdToken is not a function - getting the idToken like this works on client - and is documented here.
I tried directly with signedInUser.getIdToken(). Same problem:
signedInUser.getIdToken is not a function - not documented. just a test.
I think this is because firebase object is not intended for node.js use like i'm doing here. When signing in - stuff get's saved in browser local storage - and maybe this is why.
But the question still remains. How can i get an idToken inside node.js in order to be able to test:
return chai.request(myFunctions.manageUsers)
.post("/create")
.set("Authorization", "Bearer " + idToken) --- i need the idToken here - like would be if i'm getting it from the browser.
.send({
displayName: "jony",
email: "jony#gmail.com",
password: "123456"
})
am I approaching this wrong? I know that if i can get the idToken it will work. Do i rely need the browser for this? Thanks :)
From Exchange custom token for an ID and refresh token, you can transform a custom token to an id token with the api. Hence, you just have to generate a custom token first from the uid, then transform it in a custom token. Here is my sample:
const admin = require('firebase-admin');
const config = require('config');
const rp = require('request-promise');
module.exports.getIdToken = async uid => {
const customToken = await admin.auth().createCustomToken(uid)
const res = await rp({
url: `https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken?key=${config.get('firebase.apiKey')}`,
method: 'POST',
body: {
token: customToken,
returnSecureToken: true
},
json: true,
});
return res.idToken;
};
L. Meyer's Answer Worked for me.
But, the rp npm package is deprecated and is no longer used.
Here is the modified working code using axios.
const axios = require('axios').default;
const admin = require('firebase-admin');
const FIREBASE_API_KEY = 'YOUR_API_KEY_FROM_FIREBASE_CONSOLE';
const createIdTokenfromCustomToken = async uid => {
try {
const customToken = await admin.auth().createCustomToken(uid);
const res = await axios({
url: `https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken?key=${FIREBASE_API_KEY}`,
method: 'post',
data: {
token: customToken,
returnSecureToken: true
},
json: true,
});
return res.data.idToken;
} catch (e) {
console.log(e);
}
}
curl 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=<FIREBASE_KEY>' -H 'Content-Type: application/json'--data-binary '{"email": "test#test.com","password":"test","returnSecureToken":true}'
If this curl doesn't run, try running the same thing on Postman. It works!
I inherited a Windows 8 application that is written with XAML. So in C# when I make this call
user = await MobileServices.MobileService
.LoginAsync(MobileServiceAuthenticationProvider.MicrosoftAccount);
(This is for Azure Mobile Services)
The user object is ONLY giving me the Token and the MicrosoftAccount:..............
In order to get to authenticate people, I need to be able to see WHO is requesting access...
I looking at articles like below, but I seem to be missing something? Is this javascript in the article something I would have to write in Node.js?
Example article:
http://blogs.msdn.com/b/carlosfigueira/archive/2013/12/12/expanded-login-scopes-in-azure-mobile-services.aspx
Currently to be able to get more information about the logged in user, you need to make a second call to the service to retrieve the user info. You don't really need to ask for additional login scopes (the topic of the post you mentioned) to retrieve the user name, since that is given by default for all the providers.
This post should have the code you need to write in the server side (node.js) to get more information about the logged in user. The TL;DR version is given below:
On the server side: add this custom API (I'll call it "userInfo"; set the permission of GET to "user", and all others to admin):
exports.get = function(request, response) {
var user = request.user;
user.getIdentities({
success: function(identities) {
var accessToken = identities.microsoft.accessToken;
var url = 'https://apis.live.net/v5.0/me/?method=GET&access_token=' + accessToken;
var requestCallback = function (err, resp, body) {
if (err || resp.statusCode !== 200) {
console.error('Error sending data to the provider: ', err);
response.send(statusCodes.INTERNAL_SERVER_ERROR, body);
} else {
try {
var userData = JSON.parse(body);
response.send(200, userData);
} catch (ex) {
console.error('Error parsing response from the provider API: ', ex);
response.send(statusCodes.INTERNAL_SERVER_ERROR, ex);
}
}
}
var req = require('request');
var reqOptions = {
uri: url,
headers: { Accept: "application/json" }
};
req(reqOptions, requestCallback);
}
});
}
On the client side, after a successful login, call that API:
user = await MobileServices.MobileService
.LoginAsync(MobileServiceAuthenticationProvider.MicrosoftAccount);
var userInfo = await MobileServices.MobileService.InvokeApiAsync(
"userInfo", HttpMethod.Get, null);
userInfo will contain a JObject with the user information. There is an open feature request to make this better at http://feedback.azure.com/forums/216254-mobile-services/suggestions/5211616-ability-to-intercept-the-login-response.