Google Identity Service - How Obtain Profile / Email Information From Authenticated user - javascript

I'm porting over some existing js code authenticating with the google cloud platform (as they are migrating to a new set of libraries).
(migration guide: https://developers.google.com/identity/oauth2/web/guides/migration-to-gis)
I'm struggling with getting a hold of the player's profile (to obtain their email).
The old approach would be along the lines of this (but as it says, it is now deprecated - I've been reading the new docs but it mostly surrounds getting authorized/authenticate and not the follow on from that):
https://developers.google.com/identity/sign-in/web/people
e.g.
var profile = auth2.currentUser.get().getBasicProfile();
var email = profile.getEmail();
In my new code I've have the access token, via the new approach:
client_id: vm.clientId,
scope: SCOPE,
callback: (tokenResponse) => {
if (tokenResponse && tokenResponse.access_token) {
access_token = tokenResponse.access_token;
// HERE??? HOW DO I GET THE PROFILE?
}
}
})
(largely taken from https://developers.google.com/identity/oauth2/web/guides/use-token-model)
I've seen this mentioned elsewhere but it doesn't work in my situation at least:
gapi.client.oauth2.userinfo.get().execute(function (resp) {
console.log(resp);
})
(How to get profile information from Google Identity Services?)
I've read via the migration guide: 'Instead, use direct references to credential sub-fields in the new JWT CredentialResponse object to work with user profile data.' but don't know how to get this Credentialresponse?
(https://developers.google.com/identity/gsi/web/guides/migration#token_response)

Adding a function like this works in my situation:
function getUserProfileData(accessToken) {
let promise = new Promise(function(resolve, reject) {
let request = new XMLHttpRequest();
const url = `https://www.googleapis.com/oauth2/v3/userinfo`;
request.addEventListener("loadend", function() {
const response = JSON.parse(this.responseText);
if (this.status === 200) {
resolve(response);
} else {
reject(this, response);
}
});
request.open("GET", url, true);
request.setRequestHeader('Authorization', `Bearer ${accessToken}`);
request.send();
});
promise.then(function(response) {
var email = response.email;
}, function(errorMessage) {
// TODO: print error
});
}
This returns a json that contains the following properties:
email
email_verified
family_name
given_name
hd
name
picture
sub

You can follow these steps that I had to undertake to achieve the entire flow using Javascript.
First create a button that will hold the HTML element:
<button id="btnGoogleSignIn" style="border:none;background: none;"> </button>
You can then use the below script and associated functions in which I am getting the JWT token from Google and then decoding it to get the required information out like email address etc. Note that I am calling the onSignInGSI as callback on the button initialization.
<script>
function decodeJwtResponseFromGoogleAPI(token) {
let base64Url = token.split('.')[1]
let base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
let jsonPayload =
decodeURIComponent(atob(base64).split('').map(function (c) {
return '%' + ('00' +
c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(jsonPayload)
}
function onSignInGSI(response) {
//console.log(response)
responsePayload = decodeJwtResponseFromGoogleAPI(response.credential);
console.log("ID: " + responsePayload.sub);
console.log('Full Name: ' + responsePayload.name);
console.log('Given Name: ' + responsePayload.given_name);
console.log('Family Name: ' + responsePayload.family_name);
console.log("Image URL: " + responsePayload.picture);
console.log("Email: " + responsePayload.email);
}
window.onload = function () {
google.accounts.id.initialize({
client_id: client_id,
context: 'signin',
callback: onSignInGSI
});
google.accounts.id.prompt();
google.accounts.id.renderButton(document.getElementById("btnGoogleSignIn"),
{
type: "standard",
text: "signin_with",
logo_alignment: "left",
width: 375
});
};
</script>
<script src="https://accounts.google.com/gsi/client" async defer></script>

Related

getIdForEmail() not a function in Google Drive API Permissions

I need to change the permission of every uploaded file. But when I try to add this code,
printPermissionIdForEmail(email) {
var request = gapi.client.drive.permissions.getIdForEmail({
'email': email,
});
request.execute(function(resp) {
return ('ID: ' + resp.id);
});
}
I got an error of getIdForEmail is not a function.
gapi.client.init, gapi.auth2.getAuthInstance(),
are working. But why gapi.client.drive.permissions.getIdForEmail is not working? There is something I need to do? in Google Developers Page? in my Code?
getIdForEmail is a method only available in Google Drive v2.
With V3 you are going to have to go after it in another manner.
Do a files.list with the q parameter. In the q parameter supply the user whos permissions you wish to change. You can see here how to use search This would find all the files where someuser is the owner.
'someuser#gmail.com' in owners
Then you will get a list of file resources you can then check the permissions on each file using permissions.list and use that to change the ones you need.
I am not a JavaScript developer but I found this in the documentation it shows how to use search to list files.
/**
* Print files.
*/
function listFiles() {
gapi.client.drive.files.list({
'q': "'someuser#gmail.com' in owners",
'fields': "*"
}).then(function(response) {
appendPre('Files:');
var files = response.result.files;
if (files && files.length > 0) {
for (var i = 0; i < files.length; i++) {
var file = files[i];
appendPre(file.name + ' (' + file.id + ')');
}
} else {
appendPre('No files found.');
}
});
}
Update:
I just spotted this. About.get Gets information about the user, the user's Drive, and system capabilities
{
"user": {
"kind": "drive#user",
"displayName": "Linda Lawton",
"photoLink": "xxxx",
"me": true,
"permissionId": "060305882255734372",
"emailAddress": "xxxx#gmail.com"
}
}
Could that be the same permissionId you were looking for?
The method I use is based on the OAuth2 library published on script.google.com. This is written for Google Apps Script with domain-wide delegation. The key here is building a valid url and option for UrlFetchApp.fetch(url, options), then parsing the result to find the ID number.
function getIdForEmailv3(userEmail) {
var service = getService(userEmail);
if (service.hasAccess()) {
Logger.log('getIdForEmailv3(%s) has access', userEmail);
var url = 'https://www.googleapis.com/drive/v3/about' + '?fields=user/permissionId'
var options = {
'method': 'get',
'contentType': 'application/json',
'headers': { Authorization: 'Bearer ' + service.getAccessToken() },
'muteHttpExceptions': true
};
var response = UrlFetchApp.fetch(url, options);
var resultString = JSON.stringify(response.getContentText());
var regex = new RegExp(/\d+/g);
var id = regex.exec(resultString)[0];
Logger.log('getIdForEmailv3 returned %s for %s', id, userEmail);
return id
} else {
Logger.log('getIdForEmailv3 getLastError: %s', service.getLastError());
Logger.log('getIdForEmailv3 returned %s for %s', 0, userEmail);
return 0;
}
}
Regex idea from: Easiest way to get file ID from URL on Google Apps Script
Fields format from comment on solution: How to add 'field' property to a Google Drive API v3 call in JavaScript?

Google Classroom API 401 Error

I am trying to create a Google Classroom course using the Google Classroom API and a service account. I am currently experimenting using JavaScript and I have everything set up and working to get a list of course. I set up a JWT and request an authentication token which I receive.
{"access_token":"----ACCESS TOKEN HERE----------","token_type":"Bearer","expires_in":3600}
When I use this to retrieve a user's course list (via GET) there is no problem. I receive back a proper response with a list of courses which I then display in a table.
When I try to use the same process to try to create a course (via POST), I get a 401 error:
{
"error": {
"code": 401,
"message": "The request does not have valid authentication credentials.",
"status": "UNAUTHENTICATED"
}
}
This is the code I use to authenticate:
function authenticate(callback) {
function b64EncodeUnicode(str) {
str = JSON.stringify(str);
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
return String.fromCharCode('0x' + p1);
}));
}
// constuct the JWT
var jwtHeader = {
"alg":"RS256",
"typ":"JWT"
}
jwtHeader = JSON.stringify(jwtHeader);
//construct the Claim
var jwtClaim = {
"iss":"psclassroomsync#psclassroomsync.iam.gserviceaccount.com",
"scope":"https://www.googleapis.com/auth/classroom.courses https://www.googleapis.com/auth/classroom.rosters",
"sub":"myemail#address.com", //this is an admin account I shouldn't really need this but tried with and without it
"aud":"https://www.googleapis.com/oauth2/v4/token",
"exp":(Math.round(new Date().getTime()/1000) + 60 * 10),
"iat":Math.round(new Date().getTime()/1000)
}
jwtClaim = JSON.stringify(jwtClaim);
//construct the signature
var key="-----BEGIN PRIVATE KEY-----Removed-----END PRIVATE KEY-----\n";
var jwtSign = b64EncodeUnicode(jwtSign);
var sJWT = KJUR.jws.JWS.sign("RS256", jwtHeader, jwtClaim, key);
var jwt = jwtHeader + "." + jwtClaim + "." + sJWT;
//request Token
var grantType = "urn:ietf:params:oauth:grant-type:jwt-bearer";
var tokenRequest = "grant_type=" + grantType + "&assertion=" + sJWT;
var postURL = "https://www.googleapis.com/oauth2/v4/token"
request = $j.ajax({
url: postURL,
type: "post",
data: tokenRequest,
success: callback
});
}
This is the code I use to GET the course list. (this works)
$j("#getClasses").click(function(event){
function getClasses(callback){
authenticate(function(data){
console.log(JSON.stringify(data));
var access_token = data["access_token"];
var apiUrl = 'https://classroom.googleapis.com/v1/courses'
var myData = 'teacherId=~(teacheremail)&access_token='+access_token;
var files = $j.ajax({
url: apiUrl,
type: "get",
data: myData,
success: function (data) {
var retreivedClasses = JSON.stringify(data);
for(var i = 0; i < data['courses'].length; i++){
nextObject = data['courses'];
$j('#classListTable').append('<tr><td>' + nextObject[i]['name'] + '</td><td>' + nextObject[i]['courseState'] + '</td><td>' + nextObject[i]['enrollmentCode'] + '</td></tr>');
}
//$j('#classList').text(retreivedClasses);
}
});
});
}
getClasses();
});
This is the code that I use to create a course via POST. I've hard coded a few of the variables for testing but still gives the 401 error.
$j("#createClass").click(function(event){
function createClass(callback){
authenticate(function(data){
console.log(JSON.stringify(data));
var access_token = data["access_token"];
var tokenInfo = $j.ajax({
url: 'https://www.googleapis.com/oauth2/v3/tokeninfo',
type: 'get',
data: "access_token="+access_token
});
var apiUrl = 'https://classroom.googleapis.com/v1/courses'
var myData = 'access_token='+access_token + '&ownerId=myemail#address.com&name=myClass'
console.log(myData);
var newGoogleClassroom = $j.ajax({
url: apiUrl,
type: "post",
data: myData,
success: function (data) {
var apiResponse = JSON.stringify(data);
$j('#classCreated').text(apiResponse);
}
});
});
};
createClass();
});
Finally, this is what I get when I get the token info. It looks fine to me i.e. proper scopes: (but I am new at this)
{
"azp": "removed",
"aud": "removed",
"scope": "https://www.googleapis.com/auth/classroom.courses https://www.googleapis.com/auth/classroom
.rosters",
"exp": "1474512198",
"expires_in": "3600",
"access_type": "offline"
}
I'd be grateful for any help.
Doug
P.S. I get the security implications of this code. It is in a secure environment for experimentation only. It won't see the light of day.
Based from this forum which is also receiving a 401 error, try to revoke the old oauth. As stated in this related thread, the 401 Unauthorized error you experienced may be related to OAuth 2.0 Authorization using the OAuth 2.0 client ID.
Suggested action: Refresh the access token using the long-lived refresh token. If this fails, direct through the OAuth flow.

Twitter request from Parse Cloud Code fails with "Could not authenticate you."

{"errors":[{"code":32,"message":"Could not authenticate you."}]}
This is what I get when trying to perform a GET users/show request to Twitter. Some background:
User is authenthicated in my Android app through ParseTwitterUtils;
From Android, I call a parse.com Cloud Code function passing in the user token and token secret (looks like bad practice, but for now I'd just like to see this work);
From Cloud Code, I format the auth header using this github library. This is needed as explained here.
You can see some of my code below. Android launch code:
HashMap<String, Object> params = new HashMap<>();
params.put("twitterId", ParseTwitterUtils.getTwitter().getUserId());
params.put("authToken", ParseTwitterUtils.getTwitter().getAuthToken());
params.put("authTokenSecret", ParseTwitterUtils.getTwitter().getAuthTokenSecret());
ParseCloud.callFunctionInBackground("fetchPictureFromTwitter", params, ... );
Cloud code main function:
Parse.Cloud.define("fetchPictureFromTwitter", function(request, response) {
var twitterId = request.params.twitterId;
var authToken = request.params.authToken;
var authTokenSecret = request.params.authTokenSecret;
var url = "https://api.twitter.com/1.1/users/show.json";
Parse.Cloud.httpRequest({
url: url,
followRedirects: true,
headers: {
"Authorization": getOAuthSignature(url,authToken,authTokenSecret)
},
params: {
user_id: twitterId
}
}).then(...)
And lastly here's getOAuthSignature, the function used to sign the request (I took this from the example page in the github link):
var getOAuthSignature = function(url, authToken, authTokenSecret) {
var nonce = OAuth.nonce(32);
var ts = Math.floor(new Date().getTime() / 1000);
var timestamp = ts.toString();
var consumerKey = <MY-APP-CONSUMER-KEY>
var consumerSecret = <MY-APP-CONSUMER-SECRET>
var accessor = {
"consumerSecret": consumerSecret,
"tokenSecret": authTokenSecret
};
var params = {
"oauth_version": "1.0",
"oauth_consumer_key": consumerKey,
"oauth_token": authToken,
"oauth_timestamp": timestamp,
"oauth_nonce": nonce,
"oauth_signature_method": "HMAC-SHA1"
};
var message = {
"method": "GET",
"action": url,
"parameters": params
};
OAuth.SignatureMethod.sign(message, accessor);
var normPar = OAuth.SignatureMethod.normalizeParameters(message.parameters);
var baseString = OAuth.SignatureMethod.getBaseString(message);
var sig = OAuth.getParameter(message.parameters, "oauth_signature") + "=";
var encodedSig = OAuth.percentEncode(sig);
return 'OAuth oauth_consumer_key="'+consumerKey+'", oauth_nonce=' + nonce + ', oauth_signature=' + encodedSig + ', oauth_signature_method="HMAC-SHA1", oauth_timestamp=' + timestamp + ',oauth_token="'+authToken+'", oauth_version="1.0"'
};
What could be wrong? I've spent two days on the matter now and I don't know what to do anymore.
The issue here was that user_id="<user-id> has to be encoded in the request header as well as all the other oauth_* parameters. So I had to change this section:
var params = {
"user_id": twitterId, // <- add here
"oauth_version": "1.0",
"oauth_consumer_key": consumerKey,
"oauth_token": authToken,
"oauth_timestamp": timestamp,
"oauth_nonce": nonce,
"oauth_signature_method": "HMAC-SHA1"
};
And I'm passing the userId from the outer function, like getOAuthSignature(url,twitterId,authToken,authTokenSecret).
As for passing auth token data from device to cloud, this is probably not needed because you can find all the authentication info in the authData field of any ParseUser (as long as it is linked with twitter or Facebook).

What is the right way to send JSON to clients?

I want to develop an app for Pebble. This app is going to tell you how long it takes from one place you set in options to another one taking in account traffic jams and stuff.
To achieve this I need to make a page that will return JSON. Pebble retrieves information using code like that:
var cityName = 'London';
var URL = 'http://api.openweathermap.org/data/2.5/weather?q=' + cityName;
ajax(
{
url: URL,
type: 'json'
},
function(data) {
// Success!
console.log('Successfully fetched weather data!');
},
function(error) {
// Failure!
console.log('Failed fetching weather data: ' + error);
}
);
I created a small page with a js script that gets needed information from Yandex API:
var route;
ymaps.ready(init);
var myMap;
function init(){
function getParameterByName(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search);
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
var time = 0;
var home = getParameterByName("h");
var work = getParameterByName("w");
ymaps.route([home, work],{avoidTrafficJams: true}).then(
function (router) {
route=router;
time = ((route.getTime())/60).toFixed(2);
var info = new Object;
info["home"] = home;
info["work"] = work;
info["time"] = ~~time+"m"+~~((time%1)*60)+"s";
JSON.stringify(info);
},
function (error) {
alert('Возникла ошибка: ' + error.message);
}
);
}
As you can see I can get a JSON string in the end. But how do I send it to clients when a request with right parameters is made?
I ended up using phantomjs and executing this js script on my php page.

Fitbit OAuth API request, invalid signature

I am trying to make an API request to fitbit, using the oauth debugger from fitbit (https://dev.fitbit.com/apps/oauthtutorialpage) i am trying to figure out what i am doing wrong. I have added comments to my code below to help you understand what i am trying to achieve. What i am quite sure of is that i am either signing my request wrong, or using the wrong data to sign it. This is echoed by the API response.
I know there are more fitbit api questions here on stackoverflow, however did not find my answer there.
Is there anyone with more experience in Oauth signatures that knows what i could be doing wrong? Or could help me find a different approach to this?
var request = require('request');
var crypto = require('crypto');
var params = {
'oauth_consumer_key' : 'key12345',
'oauth_nonce' : Math.random().toString(36).substring(3), //random string
'oauth_signature_method' : 'HMAC-SHA1',
'oauth_timestamp' : Date.now().toString().substring(0,10), //timestamp with the same length as in the tutorial
'oauth_version' : '1.0'
}
var oauth_consumer_secret = 'secret123';
var post_string = 'POST&https://api.fitbit.com/oauth/request_token';
for(var key in params){
post_string += '&' + key + '=' + params[key];
}
/*At this point we have made a post string that we have to hash with hmac-sha1
the post string looks like this:
POST&https://api.fitbit.com/oauth/request_token&oauth_consumer_key=key12345&oauth_nonce=az6r8cqlzyqfr&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1439147378&oauth_version=1.0
The post_string from the tutorial looks like this:
POST&%2Foauth%2Frequest_token&oauth_consumer_key%3D%26oauth_nonce%3D%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1439145944%26oauth_version%3D1.0
*/
var hmac = crypto.createHmac('sha1', oauth_consumer_secret + "&");
// The tutorial page shows me the signature was 'signed with secret&'. I have tried with and without the & at the end, but without luck.
hmac.setEncoding('base64'); //i'm not sure if this is correct
hmac.write(post_string);
hmac.end();
var hash = hmac.read();
//and finally adding the hash to the parameters.
params.oauth_signature = hash;
//now, making the request with an authorization header.
var header='';
for (var key in params){
if(header.length === 0){
header = ' OAuth ' + key + '="' + params[key] + '"';
}
else{
header += ', ' + key + '="' + params[key] + '"';
}
}
/*
At this point the header parameter looks like this
OAuth oauth_consumer_key="key12345", oauth_nonce="jnr97ppvjs2lnmi", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1439148049", oauth_version="1.0", oauth_signature="random_signature"
The tutorial tells me to use the headers:
OAuth oauth_consumer_key="key12345", oauth_nonce="jnr97ppvjs2lnmi", oauth_signature="different_signature", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1439145944", oauth_version="1.0"
*/
var headers ={
'Authorization' : header
}
var url="https://api.fitbit.com/oauth/request_token";
var requestTimeout = 5000;
var opts = {
url: url,
timeout: requestTimeout,
headers : headers
}
request(opts, function (err, res, body) {
if (err) {
console.dir(err);
return;
}
var statusCode = res.statusCode;
if(res.statusCode === 200){
console.log(body);
}
else{
console.log("http-error-code: " + res.statusCode);
console.log(body);
}
})
/*
The response:
http-error-code: 401
{"errors":[{"errorType":"oauth","fieldName":"oauth_signature","message":"Invalid signature: 9fXI85C7GvZqMyW1AK1EkOSWZCY="}],"success":false}
*/
To get access token and secret use Grant (you can test FitBit in the playground).
Once you have access token and secret use Purest to make subsequent request to the FitBit API.
Here is an example on how to get the user's profile:
var Purest = require('purest')
var fitbit = new Purest({provider:'fitbit',
key:'[CONSUMER_KEY]', secret:'[CONSUMER_SECRET]'})
fitbit.get('user/-/profile', {
oauth:{token:'[ACCESS_TOKEN]', secret:'[ACCESS_SECRET]'}
}, function (err, res, body) {})
Alternatively you can use request for that:
var request = require('request')
request.get('https://api.fitbit.com/1/user/-/profile.json', {
oauth:{
consumer_key:'..',
consumer_secret:'..',
token:'..',
token_secret:'..'
}
}, function (err, res, body) {})
In short - don't try to implement the web server OAuth flow by yourself - use Grant, then use either Purest or request, just keep in mind that you don't have to pass all of the OAuth parameters by yourself, just pass the credentials.

Categories