Do Callable Cloud Functions Ensure a Valid Token when Called - firebase

I am calling a callable cloud function from a Javascript frontend and when calling Firebase in the past I would chain firebase.auth().currentUser.getIdToken(... before my call to the backend to ensure I had a valid token. Now that I am switching to callable cloud functions, I am wondering if this token refresh check is embedded in the callable itself or if I have to still check that my token is valid.

When calling a method returned by the callable builder, like const myFunc = httpsCallable(funcName); myFunc(/* data */);, only the current ID token is attached. If the token has not yet expired, it is not forcibly refreshed.
At least for the JavaScript SDK, this is seen in the source code of packages/functions/src/service.ts and packages/functions/src/context.ts:
// in packages/functions/src/service.ts
const context = await functionsInstance.contextProvider.getContext();
if (context.authToken) {
headers['Authorization'] = 'Bearer ' + context.authToken;
}
// in packages/functions/src/context.ts
async getAuthToken(): Promise<string | undefined> {
if (!this.auth) {
return undefined;
}
try {
const token = await this.auth.getToken();
return token?.accessToken;
} catch (e) {
// If there's any error when trying to get the auth token, leave it off.
return undefined;
}
}
This essentially leads to the following decisions:
If Firebase Authentication isn't loaded, return undefined (don't attach any tokens).
If the no one is logged in, return null (don't attach any tokens).
If the token has expired, attempt to get a fresh one (and then attach it to the request).
If the token can't be obtained, return undefined (don't attach any tokens).
If the token has not expired, return the access token (and then attach it to the request).
Even though token expiry is handled by the SDK, you can still forcibly freshen up the token by using getIdToken(/* forciblyRefresh: */ true) before calling the function. The Cloud Functions SDK will call the Admin SDK to verify whatever token is sent as soon as the request is received regardless.
Aside from that, you can further enhance the security of your Cloud Function by enforcing a cutoff on how long ago the user signed into their account for privileged actions like account deletion, changing service account details and so on. This is done using the auth_time claim inside the access token's data or the authTime property on the id token object.

Related

Handle Firebase client-side token and access to protected pages

I'm using Firebase auth to login with Facebook, Google and email/pass. Basically, everything runs client-side, I make a call to Firebase and I receive an object containing an access token (that is a JWT ID Token), a customer id and its email. When I get this object, I put it into a persistent store (local storage, I know it's bad) and I perform an API call to one of my sveltekit endpoint that will in turn make another API call to a backend API (written in Go) to get all the user informations: firstname, lastname, phone, address, stats etc. To give a little bit of context, below is a diagram to illustrate what's happening when a user sign-in using Facebook.
Up to now, I just put the Firebase object into a store and just check if the information are there to allow access to a particular page. This check is done in the +layout.svelte page of the directory containing the page to be protected. It looks like something like this:
onMount(() => {
// redirect if not authenticated
if (browser && !$authStore?.uid) goto(`/auth/sign-in`);
});
It's probably not a good thing especially since my store persists in the local storage and therefore is prone to some javascript manipulation.
From my understanding, there's at least 2 things that could be better implemented but I may be wrong:
Set the access token in an httponly cookie straight after receiving it from Firebase. This would avoid storing the access token (that is a JWT) in the local storage and It could be used to authorize access or not to some protected pages...
I receive the Firebase authentication object on client-side buthttp only cookie can only be created from server side. I thought about sending the object as a payload to a POST sveltekit endpoint (like /routes/api/auth/token/+server.js) and set the cookie from here but apparently cookies is not available in a sveltekit endpoint.
So, how and where should I set this cookie ? I see that cookies is available in the load function of a +layout.server.js file, as well as in the handle function of a hooks.server.js file, but I don't see the logic here.
Populate locals.userwith the authenticated user once I've performed a call to my backend. Well, here, it's not obvious to me because I think point 1) would be enough to manage access to protected pages, but I see that a check of locals.user is something I've seen elsewhere.
I tried to set locals.user in the sveltekit endpoint that is making the API call to the backend API:
// /routes/api/users/[uid]/+server.js
import { json } from "#sveltejs/kit";
import axios from "axios";
import { GO_API_GATEWAY_BASE_URL } from "$env/static/private";
export async function GET({ request, locals, params }) {
try {
const config = {
method: "get",
baseURL: GO_API_GATEWAY_BASE_URL,
url: `/users/${params.uid}`,
headers: {
Authorization: `Bearer ${uidToken}`, // <-- the Firebase ID Token
},
withCredentials: true,
};
const res = await axios(config);
// set locals
locals.user = json(res.data); // <--- DOESN'T SEEM TO WORK
return json(res.data);
} catch (error) {...}
}
...but in the +layout.server.js page that I've created I see nothing:
// routes/app/protected_pages/+layout.server.js
import { redirect } from "#sveltejs/kit";
export function load({ locals }) {
console.log(locals); // <----- contains an empty object: {}
if (!locals.user) throw redirect(302, "/auth/sign-in");
}
Thank you so much for your help

How to always get latest Firebase Auth Token

Currently I am using this code to get the latest auth token for firebase, which I use in a header with Apollo or URQL to query something else to be validated...
async getToken(): Promise<any> {
return await new Promise((resolve: any, reject: any) => {
this.afa.onAuthStateChanged((user: any) => {
if (user) {
user.getIdToken(true).then((token: string) => {
resolve(token);
}, (e: any) => reject(e));
}
});
});
}
I am use getIdToken(true) to always make sure I get a valid token since the token expires after one hour and the custom claims could be updated at some point.
However, my code gets a new token every time, when really I only need to get a new token when the old one is expired, or there is new information in the token's custom claim.
Should I be using some for of onIdTokenChanged() ? Does firebase store all this automatically in the firebase localstoreage db (IndexedDB), or should I be using some form of localstorage and calculating the expiry time ?
Basically, what is the best way to minimize the number of refreshes to the token to speed up my app instead of getting a new token every time?
Thanks,
J
Unless you are using a custom solution with the REST API, the firebase client modules will automatically refresh the auth token with the refresh token when the old one expires.
As for updating the custom claims, you will have to communicate with the client app through some means such as a server response if you invoke a cloud function or a realtime database listener that the user is subscribed to if you are updating it based on 'external' conditions.

Firebase service account to generate authentication token for client-side use with Google Apps Script

I am having difficulty using the FirebaseApp (a 3rd party API) to generate an authentication token that can be passed to a sidebar and used by the client to login and access my Firebase Database client-side.
I'm trying to use this tutorial but cannot get it working without using a database secret (which is being depreciated) in makeToken(). I'd prefer to use a service account as reflected in this tutorial. When I look at the difference between the tokens generated, the first 2 pieces separated by a '.' are identical, the last piece after the final '.' is different. The lengths are the same as well. eg:
//Example Generated by Database Secret: TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBv.ZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=.dGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2U=
//Example Generated by Service Account: TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBv.ZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=.IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbml=
I can generate the OAuth access token, pass it to FirebaseApp and generate an authentication token, but when it is passed client-side and I attempt to authenticate I get an error: Login Failed! Error: INVALID_TOKEN: Failed to validate MAC.
It seems like there is a lot of misinformation and conflicting information on how this should be done.
I have a getFirebaseService() function server-side that uses Apps Script OAuth2 Library to get an access token.
function getFirebaseService() {
return OAuth2.createService('Firebase')
// Set the endpoint URL.
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
// Set the private key and issuer.
.setPrivateKey(fb_PRIVATE_KEY) //Service account private key
.setIssuer(fb_SERVICE_EMAIL) //Service account email
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getScriptProperties())
// Set the scopes.
.setScope('https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/firebase.database');
}
I have a makeToken() function server-side that gets an authentication token from Firebase using the OAuth access token. I am able to use the service.getAccessToken() OAuth token server-side to access and store data. So that works, I guess my issue is creating a client auth token that's more restrictive.
function makeToken(){
var service = getFirebaseService();
if (service.hasAccess()) {
return FirebaseApp.getDatabaseByUrl(fb_URL, service.getAccessToken()) //Database Secret Works: "AAslhfi3MYACCESSTOKEN2930hf03ah4th8" but is being depreciated.
.createAuthToken(Session.getActiveUser().getEmail());
} else {
Logger.log("makeToken: " + service.getLastError());
}
}
Then client-side, from the sidebar, I try to authenticate with a custom auth token retrieved server-side from makeToken().
var userAuthToken;
google.script.run.withSuccessHandler(function (requestAuthToken) {
userAuthToken = authenticateClient(requestAuthToken)
}).makeToken();
function authenticateClient(userRequestToken) {
var ref = new Firebase(fb_URL);
ref.authWithCustomToken(userRequestToken, function (error, authData) {
if (error) {
console.log("FB Login Failed!", error); //Error below come from here.
}
else {
console.log("FB Login Succeeded!", authData);
}
});
return ref.authData.auth;
}
This results in Login Failed! Error: INVALID_TOKEN: Failed to validate MAC..
Edit: Is it possible FirebaseApp is incorrectly generating the JWT Authentication Token?
Edit2: I think the above edit is unlikely as I attempted to use the GSApp library and had the same issue. It only seems to want the depreciated database secret, not a service account OAuth.
Alright, so after a very long day I figured it out. I'm going to lay out what I ended up using for libraries and what the issue was (see the third library). The main problem was essentially that the tutorial was outdated and no a lot of people use Firebase in apps script.
OAuth2 (Server-side)
Link
I didn't have to change anything here! It was working fine and never an issue.
FirebaseApp (Server-side)
Link
This is a nice library and I stuck with it because it worked well (once I got it there). I had to make a change to my original code that came from the tutorial I mentioned. My code ended up like this and worked:
if (service.hasAccess()) {
return FirebaseApp.getDatabaseByUrl(fb_URL, service.getAccessToken()) //get OAuth Token
.createAuthToken(Session.getEffectiveUser().getEmail(), null, serviceAccount.client_email, serviceAccount.private_key);
//... Added the null, private key, and service email parameters.
Firebase (Client-side)
Link
Alright, so this is where my main issue was -- The tutorial I followed for client-side setup was old. I had to upgrade the code on my own to use the new 3.x version:
<script src="https://www.gstatic.com/firebasejs/5.8.2/firebase.js"></script>
// Initialize Firebase
var config = {
apiKey: "<Web API Key>",
authDomain: "<Project ID>.firebaseapp.com",
databaseURL: "https://<DB URL>.firebaseio.com/"
};
firebase.initializeApp(config);
With this firebase instance I was able to update my original authenticateClient() method:
function authenticateClient(userRequestToken) {
firebase.auth().signInWithCustomToken(userRequestToken).catch(function(error) {
// Handle Errors here.
console.error("authClient: ", error.code, error.message);
});
return {
uid: firebase.auth().currentUser.uid,
metadata: {
lastSignInTime: firebase.auth().currentUser.lastSignInTime
}
};
}
That's it! I now have a firebase instance with a signed in user via JWT Custom Token! I came across a few people with similar issues an I hope this helps.

How to test an onCall function as an authenticated user in the functions:shell

I want to test an onCall function via firebase functions:shell as an authenticated usr
I’ve tried many of the combinations of calls from https://firebase.google.com/docs/functions/local-emulator#invoke_https_functions
As well as downloading and setting the GOOGLE_APPLICATION_CREDENTIALS env var
My function looks like this:
exports.sendMessage = functions.https.onCall((data, context) => {
if (!context.auth) {
throw new functions.https.HttpsError('failed-precondition', 'The function must be called while authenticated.');
}
// Other logic
}
It works fine once deployed and hit from my ios app, but i can’t get it running in the shell.
The following command will actually hit the function:
sendMessage.post({headers: {'content-type' : 'application/json'},body: JSON.stringify({data: {'messageId':'test'} }) })
and returns
RESPONSE RECEIVED FROM FUNCTION: 400, {“error”:{“status”:“FAILED_PRECONDITION”,“message”:“The function must be called while authenticated.“}}
Which is correct but I want an authenticated user now. When I try to add auth like the docs recommend:
sendMessage('data', {auth:{uid:'USERUID'}}).post({headers: {'content-type' : 'application/json'},body: JSON.stringify({data: {'messageId':'test'} }) })
I end up getting ERROR SENDING REQUEST: Error: no auth mechanism defined
If I try following the Authorization headers on this page https://firebase.google.com/docs/functions/callable-reference like so:
sendMessage.post({headers: {'Authorization': 'Bearer USERUID', 'content-type' : 'application/json'},body: JSON.stringify({data: {'messageId':'test'} }) })
I get back:
RESPONSE RECEIVED FROM FUNCTION: 401, {"error":{"status":"UNAUTHENTICATED","message":"Unauthenticated"}}
How do you send the request as an authenticated user?
Related Links
How to test `functions.https.onCall` firebase cloud functions locally?
According to the documentation, the Authorization header requires an authentication id token, not a user id. You'll have to generate a token somehow and pass that to the function. The callable function will then validate that token and provide the user id to the function via context.auth.
It looks like you can use the Firebase Auth REST API to get one of these tokens. Or you can generate one in a client app that uses Firebase Auth client libraries, log it and copy its value, and use it until it expires (1 hour).

Where to find auth.token data, inside firebase objects

I am using signInWithCustomToken, after authentication I can not find where is stored my custom claims data which I have set in the server side(createCustomToken).
I can see them in firebase rules via auth.token, but how can I access them through firebase objects from within my javascript code.
The information in the token is not automatically available to your application code. But it is embedded in the token, so you can decode it yourself:
function parseJwt (token) {
var base64Url = token.split('.')[1];
var base64 = base64Url.replace('-', '+').replace('_', '/');
return JSON.parse(window.atob(base64));
};
var user = firebase.auth().currentUser
user.getToken().then(data => {
console.log(parseJwt(data));
});
The function to parse the JWT comes from this question: How to decode jwt token in javascript
You'll note that it doesn't verify that the ID token is valid. That seems fine to me in client-side code, since the information will be used by the user themselves anyway. But if you do want to verify the token, you'll have to use a more involved method.

Resources