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:
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...
}
/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.