How to always get latest Firebase Auth Token - firebase

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.

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

Do Callable Cloud Functions Ensure a Valid Token when Called

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.

How to reconcile Firebase Auth token refreshing with Server-Side Rendering

We're using Firebase in a Next.js app at work. I'm new to both, but did my best to read up on both. My problem is more with Firebase, not so much with Next.js. Here's the context:
In the client app, I make some calls to our API, passing a JWT (the ID token) in an Authorization header. The API calls admin.auth().verifyIdToken to check that the ID token is fresh enough. This works fine, since I am more or less guaranteed that the ID token gets refreshed regularly (through the use of onIDTokenChanged (doc link)
Now I want to be able to Server-Side Render my app pages. In order to do that, I store the ID token in a cookie readable by the server. But from here on, I have no guarantee that the ID token will be fresh enough next time the user loads the app through a full page load.
I cannot find a server-side equivalent of onIDTokenChanged.
This blog post mentions a google API endpoint to refresh a token. I could hit it from the server and give it a refresh token, but it feels like I'm stepping out of the Firebase realm completely and I'm worried maintaining an ad-hoc system will be a burden.
So my question is, how do people usually reconcile Firebase auth with SSR? Am I missing something?
Thank you!
I've had that same problem recently, and I solved by handling it myself. I created a very simple page responsible for forcing firebase token refresh, and redirecting user back to the requested page. It's something like this:
On the server-side, check for token exp value after extracting it from cookies (If you're using firebase-admin on that server, it will probably tell you as an error after verifying it)
// Could be a handler like this
const handleTokenCookie = (context) => {
try {
const token = parseTokenFromCookie(context.req.headers.cookie)
await verifyToken(token)
} catch (err) {
if (err.name === 'TokenExpired') {
// If expired, user will be redirected to /refresh page, which will force a client-side
// token refresh, and then redirect user back to the desired page
const encodedPath = encodeURIComponent(context.req.url)
context.res.writeHead(302, {
// Note that encoding avoids URI problems, and `req.url` will also
// keep any query params intact
Location: `/refresh?redirect=${encodedPath}`
})
context.res.end()
} else {
// Other authorization errors...
}
}
}
This handler can be used on the /pages, like this
// /pages/any-page.js
export async function getServerSideProps (context) {
const token = await handleTokenCookie(context)
if (!token) {
// Token is invalid! User is being redirected to /refresh page
return {}
}
// Your code...
}
Now you need to create a simple /refresh page, responsible for forcing firebase token refresh on client-side, and after both token and cookie are updated, it should redirect user back to the desired page.
// /pages/refresh.js
const Refresh = () => {
// This hook is something like https://github.com/vercel/next.js/blob/canary/examples/with-firebase-authentication/utils/auth/useUser.js
const { user } = useUser()
React.useEffect(function forceTokenRefresh () {
// You should also handle the case where currentUser is still being loaded
currentUser
.getIdToken(true) // true will force token refresh
.then(() => {
// Updates user cookie
setUserCookie(currentUser)
// Redirect back to where it was
const decodedPath = window.decodeURIComponent(Router.query.redirect)
Router.replace(decodedPath)
})
.catch(() => {
// If any error happens on refresh, redirect to home
Router.replace('/')
})
}, [currentUser])
return (
// Show a simple loading while refreshing token?
<LoadingComponent />
)
}
export default Refresh
Of course it will delay the user's first request if the token is expired, but it ensures a valid token without forcing user to login again.

Custom provider claims in `additionalUserInfo.profile` are not available via firebase admin?

I'm following firebase/identity toolkit docs for a SAML identity provider. Upon successful login, the redirect result contains attributes derived from the provider:
provider = new firebase.auth.SAMLAuthProvider('saml.test-provider');
firebase.auth().signInWithRedirect(provider);
...
firebase.auth().getRedirectResult().then(function(result) {
if (result.credential) {
console.log(result.additionalUserInfo.profile) // Custom provider claims, e.g., {"provider-foo":"bar"}
}
}
From the docs, the same values are also available via
result.user.getIdTokenResult().idTokenResult.claims.firebase.sign_in_attributes
firebase.sign_in_attributes
These same attributes don't seem to be stored anywhere accessible in the firebase_admin SDK:
from firebase_admin import auth
user = auth.get_user(uid)
print(user.custom_claims) # => None ... *provider* claims aren't here
print(user.provider_data[0]) # => has `federatedId` and some others, but still no custom provider claims
Is there any way to get this info in the admin SDK? Any way to tell if it's even saved by Firebase? Do I need to capture it in firestore (and wouldn't that be risky since the user could fake claims coming from the provider?)
Thanks!
the additional SAML attributes are only persisted in the token claims accessible via:
result.user.getIdTokenResult().idTokenResult.claims.firebase.sign_in_attributes
They are not stored on the user record. Identity Platform/Firebase Auth does not persist additional user info in storage for privacy reasons.
However, you can always store the claims you need on the record via the Admin SDK.
You would send the ID token to your server, verify it, parse the claims you need and set them on the user record.
Here is sample via the Node.js Admin SDK.
app.post('/saveUserClaims', (req, res) => {
// Get the ID token passed.
const idToken = req.body.idToken;
admin.auth().verifyIdToken(idToken)
.then(function(decodedToken) {
const uid = decodedToken.uid;
// ...
const samlClaims = decodedToken.firebase.sign_in_attributes;
// You would filter the claims as there could be too many.
// You can also save these claims in your database, etc.
return admin.auth().setCustomUserClaims(uid, samlClaims)
.then(() => {
res.status(200).end();
});
}).catch(function(error) {
// Handle error
});
});
That said, in general there is no need to save these claims as they will always be available in the ID token and you can access them from your security rules or when you pass the ID token to your server for validation. This is a better way to do this as you don't run into synchronization issue where your DB is out of sync with the user's attributes.

Firebase Web Admin

First of all, I am using nodejs for the backend. I use firebase hosting and firebase functions to deploy an express() app.
What I am trying to achieve is to make an admin website, which is connected to Firebase. so I have a route /admin/ like this:
adminApp.get("/", (request, response) => {
return response.redirect("/admin/login");
});
Here I basically want to check if a current user is logged in - or not.
I know firebase supports client side authentication using:
firebase.auth().onAuthStateChanged(user => {
if (user) {
} else {
}
});
And using
function login() {
var userEmail = document.getElementById("email").value;
var userPass = document.getElementById("password").value;
firebase.auth().signInWithEmailAndPassword(userEmail, userPass).catch(function(error) {
var errorCode = error.code;
var errorMessage = error.message;
if (error) {
document.getElementById('loginError').innerHTML = `Error signing in to firebase`;
}
});
}
However image this case:
Someone (not an admin) is visiting /admin/some_secret_website/ which he obviously does not have access to.
If I rely on client side authentication, it first loads the entire website and the scripts and then notices - hey I am not authenticated, let me redirect to /login. By then however anyone knows the source code of an admin page.
I'd rather have something like:
adminApp.get("/admin/some_secret_website", (request, response) => {
if (request.user) {
// user is authenticated we can check if the user is an admin and give access to the admin page
}
});
I know that you can get the user's token and validate that token using the AdminSDK, but the token must be send by the client code, meaning the website was already loaded.
I came across Authorized HTTPS Endpoint by firebase, but it only allows a middleware when using a bearer token.
Does anybody know how I can maintain a server side user object to not even return admin html to the browser but only allow access to admins?
Like Doug indicated, the way your admin website/webapp would function with Firebase Cloud Functions (which is effectively a Nodejs server) is that you get the request, then use the headers token to authenticate them against Firebase Auth. See this answer for a code snippet on this.
In your case, I'm thinking you would create a custom claim for an "administrator" group and use that to determine whether to send a pug templated page as a response upon authentication. As far as Authorization, your db rules will determine what said user can CRUD.

Resources