Trying to authenticate a service account with firebase-admin from a Cloud Scheduler call? Error: Firebase ID token has incorrect "iss" (issuer) claim - firebase

I'm trying to authenticate an API call made to my server (on Cloud Run) from a Cloud Scheduler cron job.
I'm trying to use a service account to this.
Note: this is all happening inside the same Project.
References:
https://cloud.google.com/scheduler/docs/http-target-auth
This is what I'm doing:
STEP 1 - Create the service account
I went to https://console.cloud.google.com/apis/credentials and created a new service account. I've assigned the role as owner.
STEP 2 - Create the cron job.
I went to https://console.cloud.google.com/cloudscheduler to create the cron job like I always do.
In the service account field I've put my service account e-mail. In the Audience field, I've put my project id because at some point I got an error saying that it was expecting it to be the name of my project id.
This was the error:
Firebase ID token has incorrect "aud" (audience) claim. Expected "PROJECT_ID"
STEP 3 - Running the job and identify decoding the token:
This is the code on my server:
import * as admin from "firebase-admin";
admin.initializeApp({
credential: admin.credential.cert(
// THIS IS THE DEFAULT FIREBASE-ADMIN SERVICE ACCOUNT
// THAT IS AUTOMATICALLY CREATED BY FIREBASE
SERVICE_ACCOUNT as admin.ServiceAccount
)});
// THIS IS THE CODE THAT IS INSIDE MY SERVER TRYING TO VERIFY THE SERVICE ACCOUNT
try {
const authHeader = req.headers.authorization;
console.log(`authHeader: ${authHeader}`);
if (authHeader) {
const idToken = authHeader.split(" ")[1]; // GETS THE USER ID TOKEN
const decodedToken = await admin.auth().verifyIdToken(idToken);
console.log(`decodedToken: ${decodedToken}`);
}
}
And this is the error I'm currently getting:
Firebase ID token has incorrect "iss" (issuer) claim. Expected "https://securetoken.google.com/"my-project-id" but got "https://accounts.google.com". Make sure the ID token comes from the same Firebase project as the service account used to authenticate this SDK. See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to retrieve an ID token.
Is there anything wrong with the way I'm doing it? Should I not be using firebase-admin to this?
Should I be using google-auth-library to verify the token?
https://github.com/googleapis/google-auth-library-nodejs#verifying-id-tokens
https://developers.google.com/identity/sign-in/web/backend-auth

After a morning in hell trying to debug this, here is what I've found.
It seems that firebase-admin's admin.auth().verifyIdToken() only works for tokens generated from the firebase SDK.
I got it to work by using the google-auth-library directly.
I did the following.
NOTE: The rest of the code is the same (using the same service account as described in the question):
import { OAuth2Client } from "google-auth-library";
export const apiExpressRouteHandler: RequestHandler = async (req, res) => {
try {
const PROJECT_ID = process.env.PROJECT_ID;
const authHeader = req.headers.authorization;
if (authHeader) {
const client = new OAuth2Client(PROJECT_ID);
const ticket = await client.verifyIdToken({
idToken: authHeader.split(" ")[1],
audience: PROJECT_ID
});
// LOGGING ticket PROPERTIES
console.log(`userId: ${JSON.stringify(ticket.getUserId())}`);
console.log(`payload: ${JSON.stringify(ticket.getPayload())}`);
console.log(`envelope: ${JSON.stringify(ticket.getEnvelope())}`);
console.log(`attributes: ${JSON.stringify(ticket.getAttributes())}`);
}
// REST OF THE CODE
}
}
catch(err) {
// ...
}
I'm not sure if the PROJECT_ID is necessary to initialize the client with new OAuth2Client(PROJECT_ID); but it is working like this.

Related

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

Programmatically add Google Analytics to Firebase project

I wish to automate all of the steps involved in setting up a new Firebase project without any user interaction. I've accomplished most of the steps via either the gCloud CLI, Firebase CLI or the GoogleApis NodeJS library.
Authentication has been done via the CLI tools or via service accounts.
The only thing I haven't been able to do so far is adding Google Analytics to the newly created Firebase project. I have found this Google Api which should accomplish this, but I'm having problems authenticating the request.
How would I authenticate a request to this API without any user interaction? The API is not available via the CLI tools, so my best guess would be to use a service account with the owner IAM-role, but the request keeps failing.
My steps so far have been:
Ensuring that the management API is enabled
Add a service account to the GCloud project with owner privileges
Download the service account
Run the following code
import { google } from 'googleapis';
import * as fetch from 'node-fetch';
async function addGoogleAnalytics {
const token = await getJWTAcessToken();
await addAnalyticsFetch(token);
};
async function addAnalyticsFetch(accessToken) {
const url = `https://firebase.googleapis.com/v1beta1/projects/<my-project-id>:addGoogleAnalytics`;
const fetchResult = await fetch(url, {
method: 'POST',
headers: { Authorization: `Bearer ${accessToken}` },
json: true,
body: JSON.stringify({ analyticsAccountId: '<my-analytics-account-id>' }),
});
const fetchResultText = await fetchResult.text();
console.log('Fetch result: ', fetchResultText);
}
function getJWTAcessToken() {
const SCOPES = ['https://www.googleapis.com/auth/cloud-platform'];
const key = require('../../serviceAccount.json');
return new Promise((resolve, reject) => {
const jwtClient = new google.auth.JWT(key.client_email, null, key.private_key, SCOPES, null);
jwtClient.authorize((err, tokens) => {
if (err) {
reject(err);
return;
}
resolve(tokens.access_token);
});
});
}
The result of the API call is a simple 403 - "The caller does not have permission".
I've also attempted this using the GoogleApis NodeJS library with similar results.
If being a project owner doesn't give enough privileges, how do I permit this service account to perform this API call? I have a suspicion that I'm failing because the service account is in no way associated with the Google Analytics account, but the documentation doesn't list that as a requirement. It is also not listed as a step in Google's own guide.
It turns out that the above code is 100 % valid. The problem was indeed that the service account had enough privileges to edit the Firebase-project, but it had no authorization to create a new property for the Google Analytics account.
After giving the service account edit privileges for the Google Analytics account, the connection between Firebase and Google Analytics was successfully established. This process can be automated via this API.

Error on firebase admin nodejs Permission iam.serviceAccounts.signBlob is required

im using this tutorial:
https://firebase.google.com/docs/auth/admin/create-custom-tokens#using_a_service_account_id
to create a node.js function (deployed to google cloud functions) to authenticate my users. the function is super simple:
const admin = require('firebase-admin');
admin.initializeApp({
serviceAccountId: 'authenticator#igibo-b0b27.iam.gserviceaccount.com'
});
exports.authenticate = (req, res) => {
let pass;
let uid;
if (req.query) {
if (req.query.v == 3) {
pass = req.query.p;
uid = req.query.u;
}
admin.auth().createCustomToken(uid)
.then(function(customToken) {
res.status(200).send(customToken);
return customToken;
})
.catch(function(error) {
console.error("Error creating custom token:" + JSON.stringify(error));
res.status(400).send(error);
});
} else {
console.error("EMPTY to authentication");
res.end();
}
};
but im getting this annoying error:
{"code":"auth/insufficient-permission","message":"Permission iam.serviceAccounts.signBlob is required to perform this operation on service account projects/-/serviceAccounts/authenticator#igibo-b0b27.iam.gserviceaccount.com.; Please refer to https://firebase.google.com/docs/auth/admin/create-custom-tokens for more details on how to use and troubleshoot this feature."}
in the very same tutorial it says i must go to IAM and adjust some roles for the service account WHICH I DID but still getting this error.
this is a absolutelly simple task and shouldn't being such a hassle...
what i am forgetting? the id is correct! the role is correct! the code is correct!
what is wrong?
Firebase mentions about this error on its docs:
https://firebase.google.com/docs/auth/admin/create-custom-tokens#failed_to_determine_service_account
You must initialize your app correctly through a JSON config file.
A simple fix would be:
Go to
https://console.cloud.google.com/iam-admin/iam?project=PROJECT_NAME
Edit your default service account.
Add the role Service Account
Token Creator
In a few minutes your project will be able to create signed tokens.

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

Resources