Use Firebase callablle function context auth with other Google APIs - firebase

I have a callable function in which I want to access the Sheets API, but not with the service account, I want to impersonate the user. The problem is that I don't want to send the client an authorization URL. The user has already signed into the firebase app with google on the client-side and has consented permission to all the scopes I need (including the Sheets API scope), so I was wondering if it's possible to use the auth object in the context parameter of a callable function to authenticate with other APIs on behalf of the user.
function exportSpreadsheets(data, context)
{
const {google} = require('googleapis');
//How do I create an OAuth2 object I can use to access the API
//without having to send the user an authentication link?
//Maybe using the data in context.auth.token?
const auth = new google.auth.OAuth2();
const sheets = google.sheets({version: 'v4', auth});
sheets.spreadsheets.create()
.then(x =>
{
console.log(x);
});
}
I tried the above code and it doesn't work. I'm struggling a bit to understand all the OAuth2 process.
Thanks!

I figured it out!
The correct way to do this is to send the client's access token as a parameter to the function and use it to access the API on behalf of the user. Like this:
function exportSpreadsheet(data, context)
{
const oauth = new google.auth.OAuth2({
clientId: '<your-apps-client-id>',
clientSecret: '<your-apps-client-secret>',
});
oauth.setCredentials(data);
const sheets = google.sheets({version: 'v4', auth: oauth});
//Use the API
Also, you have to send the provider's access token (which in this case is Google), not Firebase's access token.

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.

Authenticate *from* Firebase Cloud Functions to custom Node Server

I'm having troubles to authenticate from Firebase Cloud Functions to a custom Node server.
Background:
I use a custom node + express server to receive calls from authenticated clients coming from our website. I can successfully verify their tokens using something like this:
const bearerHeader = req.headers['authorization'];
const bearerToken = bearerHeader.split(' ')[1];
await admin.auth().verifyIdToken( bearerToken );
What I need now:
I need to call one of this endpoints but from a Cloud Function (not from an authenticated web client).
So, on the cloud function I'm calling:
const admin = require('firebase-admin');
admin.initializeApp(); // regular initialization
const token = await admin.app().options.credential?.getAccessToken();
const config = { headers: { Authorization: `Bearer ${token.id_token}` } };
await axios.post(url, body, config);
The problem:
The token.id_token field is missing from getAccessToken():
// token:
{
access_token: 'ya29. ... ... ',
expires_in: 3599,
token_type: 'Bearer'
}
When locally run with other credentials (my user credentials when running firebase functions:shell, for example, or when setting GOOGLE_APPLICATION_CREDENTIALS to a service account credentials file) I do get an extra property id_token that I can use to authenticate. But when deployed and run on the Cloud Function, id_token property is empty.
Any ideas?
Thanks in advance.
PS: an extra problem... the local test with a service account do include id_token, but when authenticating to the server getAccessToken() fails with:
Firebase ID token has incorrect "aud" (audience) claim. Expected "<project-id>" but got "<some-hash>.apps.googleusercontent.com".
Maybe both problems solve the same way. But the server does work properly to verify user tokens coming from a website.
EDIT:
I had to exchange the access_token for an id_token, for which I followed something similar to this, with the projectId as audience.
Now the problem is about the issuer (iss) instead of the audience (aud):
Firebase ID token has incorrect "iss" (issuer) claim. Expected "https://securetoken.google.com/<project id>" but got "https://accounts.google.com".
I guess I could verify the token on the server using the same library, but then my web clients would have the same issuer problem

Can you use IAP to log in to Firebase?

I have an angular app that is protected with Identity Aware Proxy (IAP). I am trying to add Firebase to this app in order to use firestore for a component using AngularFire. I don't want to make the user log in twice, so I thought about using IAP to authenticate with Firebase.
I've tried:
Letting GCP do its magic and see if the user is automatically inserted the Firebase Auth module - it isn't.
I've tried using the token you get from IAP in the GCP_IAAP_AUTH_TOKEN cookie with the signInWithCustomToken method - doesn't work, invalid token.
I've tried using getRedirectResult after logging in through IAP to if it's injected there - it isn't.
I've spent days trying to get this to work, I've had a colleague look at it as well, but it just doesn't seem possible. Now, as a last resort, I'm writing here to see if someone knows if it's even possible.
If not, I will have to suggest to the team to switch auth method and get rid of IAP, but I'd rather keep it.
More info:
Environment: NodeJS 10 on App Engine Flexible
Angular version: 7.x.x
AngularFire version: 5.2.3
Notes: I do not have a backend, because I want to use this component standalone and at most with a couple of Cloud Functions if need be. I am trying to use Firestore as a "backend".
I managed to authenticate on Firebase automatically using the id token from the authentication made for Cloud IAP.
I just needed to use Google API Client Library for JavaScript
1) Add the Google JS library to your page i.e. in
<script src="https://apis.google.com/js/platform.js"></script>
2) Load the OAuth2 library, gapi.auth2
gapi.load('client:auth2', callback)
gapi.auth2.init()
3) Grab the id token from GoogleAuth:
const auth = gapi.auth2.getAuthInstance()
const token = auth.currentUser.get().getAuthResponse().id_token;
4) Pass the token to GoogleAuthProvider's credential
const credential = firebase.auth.GoogleAuthProvider.credential(token);
5) Authenticate on Firebase using the credential
firebase.auth().signInAndRetrieveDataWithCredential(credential)
Putting everything together on an Angular component, this is what I have (including a sign out method)
import { Component, isDevMode, OnInit } from '#angular/core';
import { AngularFireAuth } from '#angular/fire/auth';
import { Router } from '#angular/router';
import * as firebase from 'firebase/app';
// TODO: move this all to some global state logic
#Component({
selector: 'app-sign-in-page',
templateUrl: './sign-in-page.component.html',
styleUrls: ['./sign-in-page.component.scss']
})
export class SignInPageComponent implements OnInit {
GoogleAuth?: gapi.auth2.GoogleAuth = null;
constructor(public auth: AngularFireAuth, private router: Router) { }
async ngOnInit(): Promise<void> {
// The build is restricted by Cloud IAP on non-local environments. Google
// API Client is used to take the id token from IAP's authentication and
// auto authenticate Firebase.
//
// GAPI auth: https://developers.google.com/identity/sign-in/web/reference#gapiauth2authorizeparams-callback
// GoogleAuthProvider: https://firebase.google.com/docs/reference/js/firebase.auth.GoogleAuthProvider
if (isDevMode()) return;
await this.loadGapiAuth();
this.GoogleAuth = gapi.auth2.getAuthInstance();
// Prevents a reauthentication and a redirect from `/signout` to `/dashboard` route
if (this.GoogleAuth && this.router.url === "/signin") {
const token = this.GoogleAuth.currentUser.get().getAuthResponse().id_token;
const credential = firebase.auth.GoogleAuthProvider.credential(token);
this.auth.onAuthStateChanged((user) => {
if (user) this.router.navigate(["/dashboard"]);
});
this.auth.signInAndRetrieveDataWithCredential(credential)
}
}
// Sign in button, which calls this method, should only be displayed for local
// environment where Cloud IAP isn't setup
login() {
this.auth.useDeviceLanguage();
const provider = new firebase.auth.GoogleAuthProvider();
provider.addScope("profile");
provider.addScope("email");
this.auth.signInWithRedirect(provider);
}
logout() {
this.auth.signOut();
if (this.GoogleAuth) {
// It isn't a real sign out, since there's no way yet to sign out user from IAP
// https://issuetracker.google.com/issues/69698275
// Clearing the cookie does not change the fact that the user is still
// logged into Google Accounts. When the user goes to your website again,
// opens a new tab, etc. The user is still authenticated with Google and
// therefore is still authenticated with Google IAP.
window.location.href = "/?gcp-iap-mode=CLEAR_LOGIN_COOKIE"
}
}
private async loadGapiAuth() {
await new Promise((resolve) => gapi.load('client:auth2', resolve));
await new Promise((resolve) => gapi.auth2.init(GAPI_CONFIG).then(resolve));
}
}
given the nature of IAP and Firebase, it seems not to be possible. The workaround could be just as mentioned in previous comments, to implement a custom provider, but you should mint your own token. Then maybe, re-thinking your solution if maybe this is the best way to achieve your goals.
I'm not experienced with Google Identity Aware Product, but my expectation is that you'll have to implement a custom provider for Firebase Authentication. The key part that you're missing now is a server-side code that take the information from the IAP token and mints a valid Firebase token from that. You then pass that token back to the client, which can use it to sign in with signInWithCustomToken.

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()

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);
});
}

Resources