How to handle access_token expiry and google signin status in Google Token Model? - google-signin

For google sign in, as google deprecated the platform.js library so we are going to use the google identity service library, and get the access_token using the Token Model.
const client = google.accounts.oauth2.initTokenClient({
client_id: 'YOUR_GOOGLE_CLIENT_ID',
scope: 'https://www.googleapis.com/auth/calendar.readonly',
callback: (response) => {
...
},
});
To trigger the UX flow, the client.requestAccessToken(); is called on each click of the Google signin button.
One thing I didn't understand is how to handle the access_token expiry -
As per the doc -
By design, access tokens have a short lifetime. If the access token expires prior to the end of the user's session, obtain a new token by calling requestAccessToken() from a user-driven event such as a button press.
Q.1 Does the above mean we need to call this function explicitly from the callback function? Or how would we know the access_token is expired?
Also with platform.js, we had an option to check if the user is already logged in google -
GoogleAuth = gapi.auth2.getAuthInstance();
currentUser = GoogleAuth.currentUser.get();
if (currentUser && currentUser.isSignedIn()) {
// user is already loggedin in google
} else {
googleAuthPromise = GoogleAuth.signIn();
googleAuthPromise.then(function (user) {
// now user is loggedin in google
});
}
Q.2 But with the new library, these methods are not available, how can we achieve that if a user is already logged in to google (by not involving any custom session or cookie variables)?

Related

Can I get a Google user identify from their access_token?

Implementing Google OAuth in Firebase Cloud Functions.
Everything is working but I have a weird issue. Everything is working, but I don't know how to identify the user to save the tokens to their user object in Firestore.
Using the google API nodejs library, I create an authURL using the OAuth2 client, set the scopes etc, then redirect the user to it. This works great.
const {google} = require('googleapis');
const oauth2Client = new google.auth.OAuth2(
YOUR_CLIENT_ID,
YOUR_CLIENT_SECRET,
YOUR_REDIRECT_URL
);
const scopes = [
'https://www.googleapis.com/auth/calendar'
];
const authorizationUrl = oauth2Client.generateAuthUrl({
// 'online' (default) or 'offline' (gets refresh_token)
access_type: 'offline',
state: 'state_parameter_gets_passed_back',
scope: scopes,
// Enable incremental authorization. Recommended as a best practice.
include_granted_scopes: true
});
console.log('created an authorizationUrl: ' + authorizationUrl);
res.redirect(authorizationUrl);
I then have an https Cloud Function endpoint set as the redirect URL, waiting for the response.
When it comes I get the code and request the tokens. This also works great, until I get to saveUserToken. Who is the user? My Cloud Function is just listening to responses
exports.recieveGoogleCodeFromResponseURL = functions.https.onRequest(async (req, res) => {
const code = req.query.code;
console.log('got a code, it is:' + code);
const url = require('url');
if (req.query.code != null) {
let userCredential;
console.log('we have a code, trading it for tokens');
let { tokens } = await oauth2Client.getToken(code);
console.log({ tokens });
oauth2Client.setCredentials(tokens);
//THIS IS THE PROBLEM HERE, who is the user to save the tokens to?
saveUserToken(tokens, uid); //saves to Firestore
}
res.json({result: `Got a response from Google`, code: code, scope: req.query.scope});
});
The response looks like this:
{
access_token: "longtoken",
expiry_date: 166...,
refresh_token: "anothertoken",
scope: "https://www.googleapis.com/auth/calendar",
token_type: "Bearer"
}
From what I understand neither the access_token or refresh_token is JWT token I could decode to get user info.
All of the Firebase Cloud Functions examples I have read from Google say something like 'In production you would save this token to a secure persistent DB', which I can do with Firestore. I just can't figure out how to ID the user the callback and code belongs to.
All the code samples that show OAuth with other services (Instagram, LinkedIn, Twitch) either the results come with the user id, or their API allows you to query the service with just the access_token and get the user.
For example in this Login with Instagram example the response comes with the user Id.
Code here > https://github.com/firebase/functions-samples/blob/main/instagram-auth/functions/index.js
const oauth2 = instagramOAuth2Client();
const results = await oauth2.authorizationCode.getToken({
code: req.query.code,
redirect_uri: OAUTH_REDIRECT_URI,
});
functions.logger.log('Auth code exchange result received:', results);
// We have an Instagram access token and the user identity now.
const accessToken = results.access_token;
const instagramUserID = results.user.id;
In this OAuth example from LinkedIn, once again they pass the access token to a LinkedIn endpoint to identify the user. Code here https://github.com/firebase/functions-samples/blob/main/linkedin-auth/functions/index.js
const linkedin = Linkedin.init(results.access_token);
linkedin.people.me(async (error, userResults) => {
if (error) {
throw error;
}
functions.logger.log(
'Auth code exchange result received:',
userResults
);
// We have a LinkedIn access token and the user identity now.
const linkedInUserID = userResults.id;
I can use this Google library to validate an ID token, but I am not getting an ID token back from the OAuth process.
Feels like I am missing something simple. Is there a Google API I can pass an access_token to to identify the user?
The access_token returned by Google OAuth is not a JWT. It's an opaque string that is only meaningful to Google, and that you can use to identify the user with Google APIs.
You can use the Google People API to get information about the user. With the access_token you can query the Google People API and get information about the user
Sh_ghosa's answer sent me down a misleading rabbit hole so I'm providing this answer to protect your time that I've spent for you.
The following code example uses the NodeJS Google Cloud SDK and therefore assumes that the GoogleAuth module is provided in the language you're using.
// libraries (npm)
const {google} = require('googleapis');
const jwt = require('jsonwebtoken');
// business logic
const auth = new google.auth.GoogleAuth({
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
});
const authClient = await auth.getClient();
const accessToken = await authClient.getAccessToken();
const decodedJwt = jwt.decode(accessToken?.res?.data?.id_token);
const principal = decodedJwt?.email; // <:: the principal
console.log('Who am I? ', principal);
Found a solution.
Ask for additional scopes
At first I was just asking for Google Calendar permissions.
const scopes = 'https://www.googleapis.com/auth/calendar'
The trick is to ask for email, and profile as well,
const scopes = 'https://www.googleapis.com/auth/calendar email profile'
If you ask for these additional scopes, Google sends back an id_token with the access_token and the refresh_token
You can get the user email and other information from that id_token.
You can decode the token locally since it came from a secure https session with Google and you check that the state variable passed back matches the one your system generated.
function decodeIdTokenLocally(token){
//we split the id_token at the period ., and just decode the 2nd part
let secondPart = token.split('.')[1];
let localUserJsonString = atob(secondPart);
let localUser = JSON.parse(localUserJsonString);
return localUser
}
Quick note - Asking for more than 1 scope at once gives this ugly checkbox interface where users have to manually click the checkboxes of the scope you are asking for.
Better to have them Login With Google first, which grants the email and profile scopes, then ask for Calendar scope or additional scopes one at a time which will present the nice one click to accept interface.
I haven't tried #sh_gosha suggestion of sending the access_token to the Google People API but I think that would work as well, but it would add an additional API call.

firebase auth with MIcrosoft Graph (accessToken)

I am super hopeful someone can help me - I'm kind of stuck.
I'm happily using firebase auth with Microsoft AD. My AuthProvider is firebase.auth.OAuthProvider('microsoft.com').
When I call firebase.auth().signInWithPopup() with that provider, everything works GREAT. I can pick out the accessToken from the resulting UserCredential and access Microsoft Graph api's no problem (yay!).
Firebase persists and renews the authentication and my app gets the expected callback via onAuthStateChanged with the new firebase.User when the user returns to my SPA later (also yay!).
The bad news (where I'm stuck) is: how do I get the Microsoft Graph accessToken in this flow (e.g. when the user returns to my app later)? I don't want them to have to re-authenticate with another popup (yech).
Basically, how do I go from a valid firebase.User to a MS Graph accessToken when the user returns?
Thanks so so much for any help!
Firebase Auth only focuses on authentication only. They will return the OAuth access token on sign in success via UserCredential but will discard the Microsoft OAuth refresh token and not store any OAuth credential associated with the provider. So you have no way to get a new access token afterwards. If you have a good reason for Firebase Auth to manage OAuth access tokens, please file an official feature request.
UPDATE/answer: so it turns out to be simpler than I thought:
The basic idea is to authenticate (re-authenticate) using firebase and use the same clientID for silent microsoft authentication. However, you must supply a loginHint
parameter to the microsoft auth, even if you were previously authorized. loginHint can
be the email address for the firebase user...
In that scenario, the authentication is shared and you won't need to popup a second sign-in for the "microsoft half" of the process - the firebase auth works fine.
I ended up using microsoft's MSAL library (https://github.com/AzureAD/microsoft-authentication-library-for-js)... something like this:
const graphDebug = false;
const msalLogger = new Logger(msalLogCallback, { level: LogLevel.Error });
export async function graphClient(loginHint: string) {
const msal = new UserAgentApplication({
// gotcha: MUST set the redirectUri, otherwise get weird errors when msal
// tries to refresh an expired token.
auth: { clientId: CLIENT_ID, redirectUri: window.location.origin },
system: { logger: msalLogger },
// TODO: should we set cache location to session/cookie?
});
/**
* Create an authprovider for use in subsequent graph calls. Note that we use
* the `aquireTokenSilent` mechanism which works because firebase has already
* authenticated this user once, so we can share the single sign-on.
*
* In order for that to work, we must pass a `loginHint` with the user's
* email. Failure to do that is fatal.
*/
const authProvider: AuthProvider = callback => {
msal
.acquireTokenSilent({ scopes: SCOPES, loginHint })
.then(result => {
callback(null, result.accessToken);
})
.catch(err => callback(err, null));
};
const client = Client.init({
authProvider,
debugLogging: graphDebug,
});
return client;
}
When you are using signInWithPopup, the result object contains the credentials you are looking for.
firebase.auth().signInWithPopup(provider)
.then(function(result) {
// User is signed in.
// IdP data available in result.additionalUserInfo.profile.
// OAuth access token can also be retrieved:
// result.credential.accessToken
// OAuth ID token can also be retrieved:
// result.credential.idToken
})
.catch(function(error) {
// Handle error.
});
Hope this helps.
If you look deep enough you should find msal access token in firebase response under (firebaseAuth.currentUser as zzx).zzj()

Show multi-login popup when user is loggedin in multiple google account and bypass the login popup if user is loggedin in only one accout

I am working to add google sign-in flow on web app. Trying to figure out if we can achieve this functionality,
if user is logged-in in multiple google account, show the multi-login popup where user can choose the account.
if user is logged-in in only one google account and has previously signed into the application, bypass the login page and direct them to the application.
I thought auth2.signIn, it checks to see how many accounts are present, and based on this it achieve those above use-cases.
As further context, the javascript for the login page code is currently:
<script src="https://apis.google.com/js/platform.js?onload=init_google_signin" async defer></script>
<script>
function init_google_signin() {
var auhtInstance,
clientID = 'client_id';
auhtInstance = gapi.load('auth2', function () {
/**
* Retrieve the singleton for the GoogleAuth library and set up the
* client.
*/
auth2 = gapi.auth2.init({
client_id: clientID,
scope: 'email profile'
});
});
}
</script>
Further the google sign-in button click handler is written as,
function handleGoogleSignIn() {
var GoogleAuth,
currentUser,
googleAuthPromise;
GoogleAuth = gapi.auth2.getAuthInstance();
currentUser = GoogleAuth.currentUser.get();
if (currentUser && currentUser.isSignedIn()) {
// submit form with access_token and user info, service will redirect user to logged-in page
setGoogleFormValues(currentUser, currentUser.getBasicProfile());
} else {
googleAuthPromise = GoogleAuth.signIn();
googleAuthPromise.then(function (user) {
// submit form with access_token and user info, service will redirect user to logged-in page
setGoogleFormValues(currentUser, currentUser.getBasicProfile());
});
}
}
To Fix this, i tried,
when user logging-out from app, logout from google GoogleAuth.signOut();
when user clicks on google sign-in button, revoke access first then it will always prompt the login popup GoogleAuth.disconnect();
use prompt: 'select_account' while signin, so handleGoogleSignIn will look like,
function handleGoogleSignIn() {
var GoogleAuth,
currentUser,
googleAuthPromise;
GoogleAuth = gapi.auth2.getAuthInstance();
currentUser = GoogleAuth.currentUser.get();
googleAuthPromise = GoogleAuth.signIn(prompt: 'select_account');
googleAuthPromise.then(function (user) {
// submit form with access_token and user info, service will redirect user to logged-in page
setGoogleFormValues(currentUser, currentUser.getBasicProfile());
});
}
}
These all three fix always open the popup so it fixed the 1st use case but failed on 2nd use case.
I would really appreciate thoughts on how to achieve both use cases.
Many thanks!

Firebase auth - login user from app in website

We have an App that uses firebase auth. Now we would like to send the user from the app to our website in order for him to perform some actions.
Is it somehow possible to automatically login the user when he comes from the App? He has already provided his login credentials and we have the auth object from firebase. Can we pass a token or somthing to our website and have the firebase SDK on the site log him automatically?
The common way to do this right now is as follows:
After the user signs in in one website, get the ID token via currentUser.getIdToken()
Post the ID token to your backend to serve the destination website.
On your backend, using Admin SDK, get the ID token, verify it, check the auth_time to make sure it is a recent login to minimize any window of attack. If everything checks out, mint a custom token via admin sdk createCustomToken.
Return the custom token to the website.
On the client side, sign in with custom token: signInWithCustomToken and the same user is now signed in on the website.
I was able to do this a bit easier, without having my own backend server to create a custom token, as long as you are using Google auth. It might work for other providers but here's how I did it with Google auth on Android:
Android:
activity.startActivityForResult(
googleSignInClient.signInIntent,
object : ActivityWithResultListener.OnActivityResultListener {
override fun onActivityResult(resultCode: Int, data: Intent?) {
if (data != null) {
val credential = GoogleAuthProvider.getCredential(account.idToken!!, null)
idToken = account.idToken!!
// Then pass the token to your webview via a JS interface
}
)
Web:
const idTokenFromApp = AndroidBridge.getAuthToken(); // This is the idToken from the Android code above
if (idTokenFromApp) {
// from https://firebase.google.com/docs/auth/web/google-signin
// Build Firebase credential with the Google ID token.
// https://firebase.google.com/docs/reference/js/v8/firebase.auth.GoogleAuthProvider#static-credential
const credential = firebase.auth.GoogleAuthProvider.credential(idTokenFromApp);
// Sign in with credential from the Google user.
firebase.auth.signInWithCredential(credential).catch((error) => {
// Handle Errors here.
const errorCode = error.code;
const errorMessage = error.message;
logError(`Error logging in: ${errorCode} ${errorMessage} token: ${idTokenFromApp}`, error);
});
}

Logging in via Firebase Email/Password

I am trying to build a basic web application w/ user authentication via email/password registration using Firebase.
My setup right now includes a main.js file that consists of the following:
var dbRef = new Firebase('https://url.firebaseIO.com');
var authClient = new FirebaseAuthClient(dbRef, function(error, user) {
if (error) {
// an error occurred while attempting login
console.log(error);
} else if (user) {
// user authenticated with Firebase
console.log('User ID: ' + user.id + ', Provider: ' + user.provider);
} else {
// user is logged out
console.log('logged out!');
}
});
function next(){
window.location = 'index.html';
}
function test(){
authClient.login('password', {
email: email,
password: password,
rememberMe: true
},next());
// window.location = 'index.html';
}
I obtain email/password values from a form and login. That works. But as soon as I include a callback function to then redirect them to a new authenticated page, it no longer works. In fact, most of the time I get an "UNKOWN ERROR" response.
When I get to the next page, I am no longer logged in. If I remove the next() function and stay on the same page, it works - even if I then trigger the next function from the console. Is there a different way you are supposed to proceed to another page?
I'm pretty sure there is some sort of communication issue (possibly the login does not get a return before the page is switched?) because if I add a 1s timeout before the next function, it then works. But surely this is not best practice?
Thanks!
Per https://www.firebase.com/docs/security/simple-login-email-password.html, the authClient.login() method does not actually accept a callback, so the problem you're seeing is likely the result of navigating away from the current page before the callback is returned, as you suggested.
I would recommend doing the redirect in the callback you're passing during the instantiation of the auth client. (new FirebaseAuthClient(ref, callback)) and redirect if you detect a logged-in user. This callback will be invoked once upon instantiation with the current authentication state of the user, and then again any time the user's authentication state changes (such as on login or logout).

Resources