sairaj
sairaj

Reputation: 413

how to auto refresh the idToken when using firebase auth?

I am using firebase for authentication in my Next.js app and also I have an express server that serves a REST API, which has a middleware that uses firebase-admin to verify idToken that is sent from my app, to pass the authenticated routes

Currently

The idToken generated by firebase lasts for one hour and if the client is still on my app and hits any route that needs idToken and if the idToken is expired then the server just throws an error as unauthenticated, which is pretty good work, but this is not desired, I know my user is in there and just idToken is expired

Question

How do I refresh my idToken of a user if it has expired, without having to do a full refresh in the browser to get new idToken

Some Code

AuthContext.tsx

/* eslint-disable no-unused-vars */
import { useRouter } from 'next/router'
import nookies from 'nookies'
import { createContext, useContext, useEffect, useState } from 'react'
import { axios } from '../config/axios'
import firebase from '../config/firebase'
import { AuthUser } from '../types'
import { BaseUser } from '../types/user'
import { getProvider } from '../utils/oAuthProviders'

type AuthContextType = {
  user: AuthUser | null
  login: (email: string, password: string) => Promise<any>
  signup: (email: string, password: string) => Promise<any>
  logout: () => Promise<any>
  oAuthLogin: (provider: string) => Promise<any>
}

const AuthContext = createContext<AuthContextType>({} as AuthContextType)
export const useAuth = () => useContext(AuthContext)

const fromPaths = ['/login', '/signup']

const formatUser = (user: BaseUser, idToken: string): AuthUser => {
  return {
    ...user,
    idToken,
  }
}

export const AuthContextProvider = ({ children }: { children: React.ReactNode }) => {
  const [user, setUser] = useState<AuthUser | null>(null)
  const [loading, setLoading] = useState(true)
  const router = useRouter()
  console.log(user)

  useEffect(() => {
    const unsub = firebase.auth().onIdTokenChanged((user) => {
      if (user) {
        user
          .getIdToken()
          .then(async (idToken) => {
            try {
              const userResp = await axios.get('/user/me', {
                headers: {
                  Authorization: `Bearer ${idToken}`,
                },
              })
              nookies.set(undefined, 'idk', idToken, { path: '/' })
              const {
                data: { userFullDetials },
              } = userResp
              setUser(formatUser(userFullDetials, idToken))
              setLoading(false)
              if (fromPaths.includes(router.pathname)) {
                router.push('/home')
              }
            } catch (err) {
              console.log(err)
              setUser(null)
              setLoading(false)
            }
          })
          .catch((err) => {
            console.log(err.message)
            setUser(null)
            setLoading(false)
          })
      } else {
        setLoading(false)
        setUser(null)
      }
    })
    return () => unsub()
  }, [router])

  const login = (email: string, password: string) => {
    return firebase.auth().signInWithEmailAndPassword(email, password)
  }

  const signup = (email: string, password: string) => {
    return firebase.auth().createUserWithEmailAndPassword(email, password)
  }

  const oAuthLogin = (provider: string) => {
    return firebase.auth().signInWithPopup(getProvider(provider))
  }

  const logout = async () => {
    setUser(null)
    await firebase.auth().signOut()
  }

  const returnObj = {
    user,
    login,
    signup,
    logout,
    oAuthLogin,
  }

  return (
    <AuthContext.Provider value={returnObj}>
      {loading ? (
        <div className="flex items-center justify-center w-full h-screen bg-gray-100">
          <h1 className="text-indigo-600 text-8xl">S2Media</h1>
        </div>
      ) : (
        children
      )}
    </AuthContext.Provider>
  )
}

// auth.ts
// Auth Middleware in express

import { NextFunction, Request, Response } from 'express'
import fbadmin from 'firebase-admin'
import { DecodedIdToken } from '../types/index'

export default async (req: Request, res: Response, next: NextFunction) => {
  const authorization = req.header('Authorization')
  if (!authorization || !authorization.startsWith('Bearer')) {
    return res.status(401).json({
      status: 401,
      message: 'authorization denied',
    })
  }

  const idToken = authorization.split(' ')[1]
  if (!idToken) {
    return res.status(401).json({
      status: 401,
      message: 'authorization denied',
    })
  }

  try {
    const decodedToken = await fbadmin.auth().verifyIdToken(idToken)
    req.user = decodedToken as DecodedIdToken
    return next()
  } catch (err) {
    console.log(err.message)
    return res.status(401).json({
      status: 401,
      message: 'authorization denied',
    })
  }
}


Upvotes: 5

Views: 6476

Answers (1)

Dharmaraj
Dharmaraj

Reputation: 50930

The Firebase SDK does that for you. Whenever you call user.getIdToken() it will return a valid token for sure. If the existing token has expired, it will refresh and return a new token. You can use onIdTokenChanged()and which will trigger whenever a token is refreshed and store it in your state.

However, I don't see any cons in using getIdToken() method whenever you are making an API request to server. You won't have to deal with IdToken observer and get valid token always.

const makeAPIRequest = async () => {
  // get token before making API request
  const token = await user.getIdToken()

  // pass the token in request headers
}

Right now your code makes a request to server to get user's information whenever a token refreshes and that may be redundant.

Upvotes: 10

Related Questions