Stack Overflow Asked by maudulus on December 25, 2021
I am integrating a Meteor application with Google’s One Tap. Attempting to use Meteor’s loginWithGoogle
in order to get the user to save to Meteor Accounts (built into Meteor.js). The complexity of this is that
One-Tap library is not meant to authorize the user (i.e. produce
Access Token), only to authenticate the user
Thus, what I’ve had to do is authenticate the user using Google Api, or gapi
to retrieve the necessary access_token
and id_token
. Props to this post.
What I’ve got so far is as follows:
HTML
<div data-prompt_parent_id="g_id_onload" style={{ position: "absolute", top: "5em", right: "1em" }} id="g_id_onload"></div>
CLENT SIDE
google.accounts.id.initialize({
prompt_parent_id: "g_id_onload",
client_id: "42424242-example42.apps.googleusercontent.com",
auto_select: false,
callback: handleCredentialResponse
});
const handleCredentialResponse = async oneTapResponse => {
// see the SERVER SIDE code, which is where validation of One Tap response happens
Meteor.call("verifyOneTap", oneTapResponse.credential, oneTapResponse.clientId, (error, result) => {
if (error) {
console.log(error);
}
if (result) {
// Initialize the JavaScript client library.
gapi.load("auth2", function() {
// Ready. Make a call to gapi.auth2.init or some other API
gapi.auth2.authorize(
{
client_id: oneTapResponse.clientId,
scope: "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
response_type: "code token id_token",
prompt: "none",
// this is the actual email address of user, [email protected], passed back from the server where we validated the One Tap event...
login_hint: result.email
},
function(result, error) {
if (error) {
// An error happened.
console.log(error);
return;
}
//these are the authentication tokens taht are so difficult to capture...
let theAccessToken = result.access_token;
let theIdToken = result.id_token;
//*********************************
//this is the part that doesn't work
//trying to get it to create the account without another Google prompt...
Meteor.loginWithGoogle({ accessToken: theAccessToken, idToken: theIdToken, prompt: "none" }, function(err, res) {
if (err) {
console.log(err)
}
});
//*********************************
}
);
});
}
});
};
google.accounts.id.prompt(notification => {
//this just tells you when things go wrong...
console.log(notification);
});
SERVER SIDE
const { OAuth2Client } = require("google-auth-library");
const clientOA2 = new OAuth2Client("42424242-example42.apps.googleusercontent.com");
// the token and clientId are returned from One Tap in an object, are credential (token) and clientId (clientId)
verifyOneTap: async (token, clientId) => {
const ticket = await clientOA2.verifyIdToken({
idToken: token,
audience: clientId // Specify the CLIENT_ID of the app that accesses the backend
// Or, if multiple clients access the backend:
//[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3]
});
const payload = await ticket.getPayload();
//perform validation here so you don't get hacked...
return payload;
// If request specified a G Suite domain:
// const domain = payload['hd'];
}
Tried writing this in different ways on the client/server, as well as considered ways to go around this and just signing up with Meteor’s Accounts.createUser, but it is less than ideal. What is wrong with the [options]
that I am passing to loginWithGoogle
? I would think accessToken
and idToken
were enough…
What happens is that on login, it does log me in through the first stage of Google One Tap, but then options that I threw into Meteor.loginWithGoogle
are somehow not being recognized:
this works (first step of One Step flow) =>
but then it asks for login again 😐
The documentation on loginWithGoogle
states that the format is typically:
Meteor.loginWith<ExternalService>([options], [callback])
and with regards to loginWithGoogle
:
options may also include Google’s additional URI parameters
Google’s Additional URI Parameters
Required:
client_id, nonce, response_type, redirect_uri, scope
Optional:
access_type, display, hd, include_granted_scopes, login_hint, prompt
Unfortunately, it is clearly not recognizing something in the [options]
that I am passing, otherwise it would save the user to MongoDB, which it isn’t doing.
Ok, found an answer - I'm working on something cleaner but this is the current fix - thank you jimmy knoot and methodx for some inspiration.
Note: everything else same as original question above.
CLIENT
// this is the callback from the Google One Tap `google.accounts.id.initialize` (see original Stack Overflow question above)
const handleCredentialResponse = async oneTapResponse => {
// see the SERVER SIDE code, which is where validation of One Tap response happens
Meteor.call("verifyOneTap", oneTapResponse.credential, oneTapResponse.clientId, (error, result) => {
if (error) {
console.log(error);
}
if (result) {
// Initialize the JavaScript client library.
gapi.load("auth2", function() {
// Ready. Make a call to gapi.auth2.init or some other API
gapi.auth2.authorize(
{
client_id: oneTapResponse.clientId,
scope: "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
response_type: "code token id_token",
prompt: "none",
// this is the actual email address of user, [email protected], passed back from the server where we validated the One Tap event...
login_hint: result.email
},
function(tokens, error) {
if (error) {
// An error happened.
console.log(error);
return;
}
//gapi returns tokens including accessToken and idToken...
Meteor.call("createOneTapUser", result, tokens, (error, stampedLoginToken) => {
if (error) {
console.log(error);
}
//this logs in with the token created earlier...should do whatever your normal google login functionality does...
//*********************************
// this is where you skip the Google login popup :)
Meteor.loginWithToken(stampedLoginToken);
});
//*********************************
}
);
});
}
});
};
SERVER
createOneTapUser: async (userDetails, accessDetails) => {
//just including details here for what part of a user object would look like from Meteor.loginWithGoogle > note especially resume > loginTokens
let oneTapUserObj = {
services: {
google: {
accessToken: accessDetails.access_token,
idToken: accessDetails.id_token,
scope: ["https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile", "openid"], // yours may be different...
expiresAt: accessDetails.expires_at,
id: userDetails.sub,
email: userDetails.email,
verified_email: userDetails.email_verified,
name: userDetails.name,
given_name: userDetails.given_name,
family_name: userDetails.family_name,
picture: userDetails.picture,
locale: "en"
},
resume: {
loginTokens: []
}
} //...whatever your user object normally looks like.
};
//manually inserting the user
Meteor.users.insert(oneTapUserObj);
let newOneTapUser = await Meteor.users.findOne({ "profile.email": userDetails.email });
// generates the login token that goes under user > services > resume > loginTokens...
let stampedLoginToken = Accounts._generateStampedLoginToken();
Accounts._insertLoginToken(newOneTapUser._id, stampedLoginToken);
//sets the social media image from the google account...you'll need to build your own...
userDetails.picture ? scrapeSocialMediaImage(newOneTapUser._id, userDetails.picture) : console.log("Google One Tap user " + newOneTapUser._id + " has no profile picture...");
return stampedLoginToken.token;
}
Answered by maudulus on December 25, 2021
Get help from others!
Recent Questions
Recent Answers
© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP