How to refresh a Firebase Session Cookie? (auth/session-cookie-expired) - firebase

My application uses a server-side Firebase session cookie. Everything from sign in to sign out is implemented with their official way in mind. However, if I don't use the application for some time, the session cookie will expire eventually. I have two options:
sign out the user and redirect to the sign in page
silently re-authenticate the user by refreshing the session cookie on the server
No questions about option 1. How would I implement option 2, if possible, with Firebase though?
The only thing I have is the expired session cookie on the client and server. At the moment, my server has a middleware which would return an authentication error:
export default async (
req,
res,
firebaseAdmin
) => {
const { session } = req.cookies;
if (!session) {
return null;
}
const checkRevoked = true;
return await firebaseAdmin
.auth()
.verifySessionCookie(session, checkRevoked)
.then(decodedClaims => {
return firebaseAdmin.auth().getUser(decodedClaims.uid);
})
.catch(error => {
throw new AuthenticationError(error.message);
});
};
Is there any way I can use the auth/session-cookie-expired error message to re-authenticate the user silently? Perhaps I shouldn't do this in the first place -- because of security issues -- and just go with option 1.

I think what you can do is the following:
Inactive sessions: If the user doesn't visit your site after some time and the session cookie expires, then you have to sign in the user again. This is the more secure option.
Active sessions: You can refresh the session once in a while (active window). If the cookie is set to expire in 2 weeks. You can refresh once a day and mint a new cookie for 2 weeks (extending the session effectively). One way to do that is to validate the cookie JWT and then sign in the user (using same uid) with custom token (using REST API or client Node.js) and then mint a new session cookie JWT with that ID token and give it a new 2 week lifetime.

Related

Resending Firebase auth verification emails when the user's email is no longer accessible

So far in my project, I have set up a basic user management system. However, I'm unable to figure out a way to resend verification link after the user registers.
For example: When the user signs up, createUserWithEmailAndPassword is called which returns a promise. The promise is now resolved using then (or await) to which sendEmailVerification is called. This is all fine.
Note: The above flow is what I currently have implemented to for user management on the client side with Firebase Auth.
However, what if the user happens to delete this email or for whatever reason has no access to it at all. I want to be able to resend the link.
This uses Firebase Admin SDK on the backend and is an example of how to generate the verification email on the server-side. However, it appears that it is used in conjunction with account creation. In addition, it appears that Firebase Auth follows the same set of restrictions.
Not too sure where to go next and was wondering if there are any suitable workarounds.
Thanks.
Add a link to the login page to resend the verification email.
Then, trigger something along these lines:
sendEmailVerification() async {
await FirebaseAuth.instance.currentUser?.sendEmailVerification();
}
Another option is to check during the login process whether the user verified the email. If not, resend it. Along these lines:
signInWithEmailAndPassword(
String email,
String password,
) async {
try {
final credential = await FirebaseAuth.instance
.signInWithEmailAndPassword(email: email, password: password);
if (credential.user!.emailVerified == false) {
await _sendEmailVerification();
return ... // not verified, but email sent
}
return ... // success
} on FirebaseAuthException catch (e) {
return ... // error
} catch (e) {
return ... // error
}
}
The problem described here I think is as follows ( I am facing it as well):
=> some new User enters his Email and Password to create an account
=> we call createUserWithEmailAndPassword(email, password) => account can be found in firebase console under "Authentication" => in the app Auth.currentUser is NOT NULL. This is because acc. to documentatoin of "createUserWithEmailAndPassword" we read:
"#remarks
On successful creation of the user account, this user will also be signed in to your application."
=> Then we call sendEmailVerification(Auth.currentUser) - everything works, User Auth.currentUser gets his Email with verification link.
BUT. What if he does not click this link (maybe it went to spam)? He searcehs it, time passes, he maybe switches off the PC or closes the App. And after some time tries again: opens the App, tries to register again...
=> as he enters again the same E-mail he entered when he tried to register for the first time, our createUserWithEmailAndPassword() will give an error (because the User with such E-mail, even though it is not verified, is already in the database) => Auth.currentUser will be NULL(!!!)
=> if now you try to "REsend the verification E-Mail" with sendEmailVerification(Auth.currentUser) - it will not send anything. Because the user is null.
So, the way out of this situation is to generate verficication link based on the E-mail only and send it somehow to the User via E-mail: https://firebase.google.com/docs/auth/admin/email-action-links#java_2

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.

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 is there any way to sign out both platform web and mobile app?

Can I let the user sign out both web and mobile app simultaneously so that once signed out from web app, no longer be able to login to mobile app?
Theres no way to logout someone on different devices but you have a few options, just requires putting some elements together.
You could revoke the users refresh tokens which would mean when the token expires and the sdk goes to refresh it couldn't and would log out the user.
// Revoke all refresh tokens for a specified user for whatever reason.
// Retrieve the timestamp of the revocation, in seconds since the epoch.
admin.auth().revokeRefreshTokens(uid)
.then(() => {
return admin.auth().getUser(uid);
})
.then((userRecord) => {
return new Date(userRecord.tokensValidAfterTime).getTime() / 1000;
})
.then((timestamp) => {
console.log("Tokens revoked at: ", timestamp);
});
You could also put a flag in the firebase realtime database and then when they reopen the app on their other devices, if logged in they could read the flag and do the logout on the client. Also if they had the web/mobile app open you could log them out in that way if they are listening for that flag at all times.
var logoutRef = firebase.database().ref('userLogoutRef/' + userUid);
logoutRef.on('value', function(snapshot) {
if (snapshot.val() === true) {
firebase.auth().signOut()
}
});
You would just want to make sure you remove this flag on a subsequent login so a user can login.

Resources