Simple Firebase authentication for all Next.js rendering strategies.
Demo • Alternatives • Getting Started • API • Config • Types • Upgrading to Firebase 9 • Examples • Troubleshooting • Contributing
This package makes it simple to get the authenticated Firebase user and ID token during both client-side and server-side rendering (SSR).
We treat the Firebase JS SDK as the source of truth for auth status. When the user signs in, we call an endpoint to generate a refresh token and store the user info, ID token, and refresh token in cookies. Future requests to SSR pages receive the user info and ID token from cookies, refreshing the ID token as needed. When the user logs out, we unset the cookies.
See a live demo of the example app.
Depending on your app's needs, other approaches might work better for you.
If your app only uses static pages or doesn't need the Firebase user for SSR, use the Firebase JS SDK directly to load the user on the client side.
- Pros: It's simpler and removes this package as a dependency.
- Cons: You will not have access to the Firebase user when you use
getServerSideProps
.
If your app needs the Firebase user for SSR (but does not need the ID token server side), you could consider one of these approaches:
- On the client, set a JavaScript cookie with the Firebase user information once the Firebase JS SDK loads.
- Pros: You won't need login/logout API endpoints. You can structure the authed user data however you'd like.
- Cons: The cookie will be unsigned and accessible to other JavaScript, making this approach less secure. You won't always have access to the Firebase ID token server side, so you won't be able to access other Firebase services. (Note that you can set the ID token in the cookie, but it will expire after an hour and be invalid for future server-side-rendered pages.)
- Use Firebase's session cookies.
- Pros: It removes this package as a dependency.
- Cons: You won't have access to the Firebase ID token server side, so you won't be able to access other Firebase services. You'll need to implement the logic for verifying the session and managing the session state.
If your app needs a generalized authentication solution—not specifically Firebase authentication—you could consider using NextAuth.js. NextAuth.js does not use Firebase authentication but supports a wide variety of identity providers, including Google. Read more here about the differences between next-firebase-auth
and NextAuth.js to see which works best for your needs.
This package will likely be helpful if you expect to use both static pages and SSR or if you need access to Firebase ID tokens server side.
A quick note on what this package does not do:
- It does not provide authentication UI. Consider firebaseui-web or build your own.
- It does not extend Firebase functionality beyond providing universal access to the authed user. Use the Firebase admin SDK and Firebase JS SDK for any other needs.
Install:
yarn add next-firebase-auth@canary
or npm i next-firebase-auth@canary
⚠️ If you're using Firebase JS SDK v8 or below, usenext-firebase-auth@^0.15.0
.
Make sure peer dependencies are also installed:
yarn add firebase firebase-admin next react react-dom
Create a module to initialize next-firebase-auth
.
See config documentation for details
// ./initAuth.js
import { init } from 'next-firebase-auth'
const initAuth = () => {
init({
authPageURL: '/auth',
appPageURL: '/',
loginAPIEndpoint: '/api/login',
logoutAPIEndpoint: '/api/logout',
onLoginRequestError: (err) => {
console.error(err)
},
onLogoutRequestError: (err) => {
console.error(err)
},
firebaseAuthEmulatorHost: 'localhost:9099',
firebaseAdminInitConfig: {
credential: {
projectId: 'my-example-app-id',
clientEmail: '[email protected]',
// The private key must not be accessible on the client side.
privateKey: process.env.FIREBASE_PRIVATE_KEY,
},
databaseURL: 'https://my-example-app.firebaseio.com',
},
// Use application default credentials (takes precedence over firebaseAdminInitConfig if set)
// useFirebaseAdminDefaultCredential: true,
firebaseClientInitConfig: {
apiKey: 'MyExampleAppAPIKey123', // required
authDomain: 'my-example-app.firebaseapp.com',
databaseURL: 'https://my-example-app.firebaseio.com',
projectId: 'my-example-app-id',
},
cookies: {
name: 'ExampleApp', // required
// Keys are required unless you set `signed` to `false`.
// The keys cannot be accessible on the client side.
keys: [
process.env.COOKIE_SECRET_CURRENT,
process.env.COOKIE_SECRET_PREVIOUS,
],
httpOnly: true,
maxAge: 12 * 60 * 60 * 24 * 1000, // twelve days
overwrite: true,
path: '/',
sameSite: 'strict',
secure: true, // set this to false in local (non-HTTPS) development
signed: true,
},
onVerifyTokenError: (err) => {
console.error(err)
},
onTokenRefreshError: (err) => {
console.error(err)
},
})
}
export default initAuth
Set the private environment variables FIREBASE_PRIVATE_KEY
, COOKIE_SECRET_CURRENT
, and COOKIE_SECRET_PREVIOUS
in .env.local
. If you have enabled the Firebase Authentication Emulator, you will also need to set the FIREBASE_AUTH_EMULATOR_HOST
environment variable.
Initialize next-firebase-auth
in _app.js
:
// ./pages/_app.js
import initAuth from '../initAuth' // the module you created above
initAuth()
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default MyApp
Create login and logout API endpoints that set auth cookies:
// ./pages/api/login
import { setAuthCookies } from 'next-firebase-auth'
import initAuth from '../../initAuth' // the module you created above
initAuth()
const handler = async (req, res) => {
try {
await setAuthCookies(req, res)
} catch (e) {
return res.status(500).json({ error: 'Unexpected error.' })
}
return res.status(200).json({ success: true })
}
export default handler
// ./pages/api/logout
import { unsetAuthCookies } from 'next-firebase-auth'
import initAuth from '../../initAuth' // the module you created above
initAuth()
const handler = async (req, res) => {
try {
await unsetAuthCookies(req, res)
} catch (e) {
return res.status(500).json({ error: 'Unexpected error.' })
}
return res.status(200).json({ success: true })
}
export default handler
Finally, use the authenticated user in a page:
// ./pages/demo
import React from 'react'
import {
useAuthUser,
withAuthUser,
withAuthUserTokenSSR,
} from 'next-firebase-auth'
const Demo = () => {
const AuthUser = useAuthUser()
return (
<div>
<p>Your email is {AuthUser.email ? AuthUser.email : 'unknown'}.</p>
</div>
)
}
// Note that this is a higher-order function.
export const getServerSideProps = withAuthUserTokenSSR()()
export default withAuthUser()(Demo)
- init
- withAuthUser
- withAuthUserTokenSSR
- withAuthUserSSR
- useAuthUser
- setAuthCookies
- unsetAuthCookies
- verifyIdToken
- getUserFromCookies
- AuthAction
Initializes next-firebase-auth
, taking a config object. Must be called before calling any other method.
A higher-order function to provide the AuthUser
context to a component. Use this with any Next.js page that will access the authed user via the useAuthUser
hook. Optionally, it can client-side redirect based on the user's auth status.
It accepts the following options:
Option | Description | Default |
---|---|---|
whenAuthed |
The action to take if the user is authenticated. One of AuthAction.RENDER or AuthAction.REDIRECT_TO_APP . |
AuthAction.RENDER |
whenAuthedBeforeRedirect |
The action to take while waiting for the browser to redirect. Relevant when the user is authenticated and whenAuthed is set to AuthAction.REDIRECT_TO_APP. One of: AuthAction.RENDER or AuthAction.SHOW_LOADER or AuthAction.RETURN_NULL . |
AuthAction.RETURN_NULL |
whenUnauthedBeforeInit |
The action to take if the user is not authenticated but the Firebase client JS SDK has not yet initialized. One of: AuthAction.RENDER , AuthAction.REDIRECT_TO_LOGIN , AuthAction.SHOW_LOADER . |
AuthAction.RENDER |
whenUnauthedAfterInit |
The action to take if the user is not authenticated and the Firebase client JS SDK has already initialized. One of: AuthAction.RENDER , AuthAction.REDIRECT_TO_LOGIN . |
AuthAction.RENDER |
appPageURL |
The redirect destination URL when we should redirect to the app. A PageURL. | config.appPageURL |
authPageURL |
The redirect destination URL when we should redirect to the login page. A PageURL. | config.authPageURL |
LoaderComponent |
The component to render when the user is unauthed and whenUnauthedBeforeInit is set to AuthAction.SHOW_LOADER . |
null |
For example, this page will redirect to the login page if the user is not authenticated:
import { withAuthUser, AuthAction } from 'next-firebase-auth'
const DemoPage = () => <div>My demo page</div>
export default withAuthUser({
whenUnauthedAfterInit: AuthAction.REDIRECT_TO_LOGIN,
authPageURL: '/my-login-page/',
})(DemoPage)
Here's an example of a login page that shows a loader until Firebase is initialized, then redirects to the app if the user is already logged in:
import { withAuthUser, AuthAction } from 'next-firebase-auth'
const MyLoader = () => <div>Loading...</div>
const LoginPage = () => <div>My login page</div>
export default withAuthUser({
whenAuthed: AuthAction.REDIRECT_TO_APP,
whenUnauthedBeforeInit: AuthAction.SHOW_LOADER,
whenUnauthedAfterInit: AuthAction.RENDER,
LoaderComponent: MyLoader,
})(LoginPage)
For TypeScript usage, take a look here.
A higher-order function that wraps a Next.js pages's getServerSideProps
function to provide the AuthUser
context during server-side rendering. Optionally, it can server-side redirect based on the user's auth status. A wrapped function is optional; if provided, it will be called with a context
object that contains an AuthUser
property.
It accepts the following options:
Option | Description | Default |
---|---|---|
whenAuthed |
The action to take if the user is authenticated. Either AuthAction.RENDER or AuthAction.REDIRECT_TO_APP . |
AuthAction.RENDER |
whenUnauthed |
The action to take if the user is not authenticated. Either AuthAction.RENDER or AuthAction.REDIRECT_TO_LOGIN . |
AuthAction.RENDER |
appPageURL |
The redirect destination URL when we should redirect to the app. A PageURL. | config.appPageURL |
authPageURL |
The redirect destination URL when we should redirect to the login page. A PageURL. | config.authPageURL |
For example, this page will SSR for authenticated users, fetching props using their Firebase ID token, and will server-side redirect to the login page if the user is not authenticated:
import {
useAuthUser,
withAuthUser,
withAuthUserTokenSSR,
} from 'next-firebase-auth'
const DemoPage = ({ thing }) => <div>The thing is: {thing}</div>
export const getServerSideProps = withAuthUserTokenSSR({
whenUnauthed: AuthAction.REDIRECT_TO_LOGIN,
})(async ({ AuthUser }) => {
// Optionally, get other props.
const token = await AuthUser.getIdToken()
const response = await fetch('/api/my-endpoint', {
method: 'GET',
headers: {
Authorization: token,
},
})
const data = await response.json()
return {
props: {
thing: data.thing,
},
}
})
export default withAuthUser()(DemoPage)
Behaves nearly identically to withAuthUserTokenSSR
, with one key difference: it does not validate an ID token. Instead, it simply uses the AuthUser
data from a cookie. Consequently:
- It does not provide an ID token on the server side. The
AuthUser
provided via context will resolve to null when you callAuthUser.getIdToken()
. - It does not need to make a network request to refresh an expired ID token, so it will, on average, be faster than
withAuthUserTokenSSR
. - It does not check for token revocation. If you need verification that the user's credentials haven't been revoked, you should always use
withAuthUserTokenSSR
.
cookies.signed
is set to false
. Doing so is a potential security risk, because the authed user cookie values could be modified by the client.
This takes the same options as withAuthUserTokenSSR
.
A hook that returns the current AuthUser
. To use this, the Next.js page must be wrapped in withAuthUser
. If the user is not authenticated, useAuthUser
will return an AuthUser
instance with a null id
.
For example:
import { useAuthUser, withAuthUser } from 'next-firebase-auth'
const Demo = () => {
const AuthUser = useAuthUser()
return (
<div>
<p>Your email is {AuthUser.email ? AuthUser.email : 'unknown'}.</p>
</div>
)
}
export default withAuthUser()(Demo)
Sets cookies to store the authenticated user's info. Call this from your "login" API endpoint.
Cookies are managed with cookies
. See the config for cookie options.
The req
argument should be an IncomingMessage
/ Next.js request object. The res
argument should be a ServerResponse
/ Next.js response object. It requires that the Authorization
request header be set to the Firebase user ID token, which this package handles automatically.
This can only be called on the server side.
Unsets (expires) the auth cookies. Call this from your "logout" API endpoint.
The req
argument should be an IncomingMessage
/ Next.js request object. The res
argument should be a ServerResponse
/ Next.js response object.
This can only be called on the server side.
Verifies a Firebase ID token and resolves to an AuthUser
instance. This serves a similar purpose as Firebase admin SDK's verifyIdToken.
Added in v1
Verifies and returns the AuthUser
from auth cookies. This is an alternative to verifyIdToken
, which verifies the user from an ID token.
In general, we recommend that API endpoints use an ID token rather than cookies to identify the user, which avoids some potential CSRF vulnerabilities. However, this method will be helpful for endpoints must rely exclusively on cookie values to identify the user.
This can only be called on the server side.
See this example for more information on using this in a standalone backend environment outside of Next.js.
The options argument can include:
Object
– an IncomingMessage
/ Next.js request object
A request object whose cookie
header value will be used to verify a user. Either the req
value or authCookieValue
are required.
Boolean
Whether or not the returned user should include a Firebase ID token. Defaults to true. When true, the behavior follows that of withAuthUserTokenSSR
; when false, it follows that of withAuthUserSSR
. Read more about the distinction in the docs for withAuthUserSSR.
String
As an alternative to providing the req
object, you can directly provide the auth cookie value to use. For example, if your auth cookie is named MyAuth
, you would provide the value of the cookie MyAuth.AuthUser
(if includeToken
is false) or MyAuth.AuthUserTokens
(if includeToken
is true).
Either the req
value or authCookieValue
are required.
String
The value of the auth cookie's signature, if using signed cookies. For example, if your auth cookie is named MyAuth
, you would provide the value of the cookie MyAuth.AuthUser.sig
(if includeToken
is false) or MyAuth.AuthUserTokens.sig
(if includeToken
is true).
An object that defines rendering/redirecting options for withAuthUser
and withAuthUserTokenSSR
. See AuthAction.
See an example config here. Provide the config when you call init
.
String|Function|Object
– a PageURL
The default URL to navigate to when withAuthUser
or withAuthUserTokenSSR
need to redirect to login. Optional unless using the AuthAction.REDIRECT_TO_LOGIN
auth action.
String|Function|Object
– a PageURL
The default URL to navigate to when withAuthUser
or withAuthUserTokenSSR
need to redirect to the app. Optional unless using the AuthAction.REDIRECT_TO_APP
auth action.
String
The API endpoint this module will call when the auth state changes for an authenticated Firebase user.
String
The API endpoint this module will call when the auth state changes for an unauthenticated Firebase user.
Function
(optional)
A handler called if the login API endpoint returns a non-200 response. If a handler is not defined, this library will throw on any non-200 responses.
Not used or allowed if a custom tokenChangedHandler
is set.
Function
(optional)
A handler called if the logout API endpoint returns a non-200 response. If a handler is not defined, this library will throw on any non-200 responses.
Not used or allowed if a custom tokenChangedHandler
is set.
Function
A callback that runs when the auth state changes for a particular user. Use this if you want to customize how your client-side app calls your login/logout API endpoints (for example, to use a custom fetcher or add custom headers). tokenChangedHandler
receives an AuthUser
as an argument and is called when the user's ID token changes, similarly to Firebase's onIdTokenChanged
event.
If this callback is specified, user is responsible for:
- Calling their login/logout endpoints depending on the user's auth state.
- Passing the user's ID token in the Authorization header
- Ensuring it allows the request to set cookies.
See the default handler for guidance.
String
The host and port for the local Firebase Auth Emulator. If this value is set, the auth emulator will be initialized with the provided host and port.
Must match the value of the FIREBASE_AUTH_EMULATOR_HOST
environment variable, e.g., localhost:9099
.
Object
Configuration passed to firebase-admin
's initializeApp
. It should contain a credential
property (a plain object) and a databaseURL
property. Required unless you initialize firebase-admin
yourself before initializing next-firebase-auth
.
The firebaseAdminInitConfig.credential.privateKey
cannot be defined on the client side and should live in a secret environment variable.
ℹ️ Using Vercel? See adding a private key to Vercel for guidance.
Boolean
When true, firebase-admin
will implicitly find your hosting environment service account during initializeApp
. This is applicable for both Firebase, and Google Cloud Platform, and recommended over adding service account key to the code via file path or direct value.
Note: To setup firebase-admin
, either firebaseAdminInitConfig
or useFirebaseAdminDefaultCredential
must be provided. Using the default credentials will override values passed to firebaseAdminInitConfig.credential
if both are presented.
Object
Configuration passed to the Firebase JS SDK's initializeApp
. The firebaseClientInitConfig.apiKey
value is always required. Other properties are required unless you initialize the firebase
app yourself before initializing next-firebase-auth
(or, less commonly, if you're running next-firebase-auth
solely on the server side).
Object
Settings used for auth cookies. We use cookies
to manage cookies.
Properties include:
name
: Used as a base for cookie names: ifname
is set to "MyExample", cookies will be namedMyExample.AuthUser
andMyExample.AuthUserTokens
(plusMyExample.AuthUser.sig
andMyExample.AuthUserTokens.sig
if cookies are signed). Required.keys
: An array of strings that will be used to sign cookies; for instance,['xD$WVv3qrP3ywY', '2x6#msoUeNhVHr']
. As these strings are secrets, provide them via secret environment variables, such as[ process.env.COOKIE_SECRET_CURRENT, process.env.COOKIE_SECRET_PREVIOUS ]
. Thekeys
array is passed to the Keygrip constructor as described in thecookies
package. Required unlesssigned
is set tofalse
.- All options for
cookies.set
.
The keys
value cannot be defined on the client side and should live in a secret environment variable.
For security, the maxAge
value must be two weeks or less. Note that maxAge
is defined in milliseconds.
Note: The cookies' expirations will be extended automatically when the user loads the Firebase JS SDK.
The Firebase JS SDK is the source of truth for authentication, so if the cookies expire but the user is still authed with Firebase, the cookies will be automatically set again when the user loads the Firebase JS SDK—but the user will not be authed during SSR on that first request.
Function
(optional)
Error handler that will be called if there's an unexpected error while verifying the user's ID token server side. It will receive a Firebase auth error.
This library will not throw when it cannot verify an ID token. Instead, it will provide an unauthenticated user to the app. It will typically handle common auth-related errors such as auth/id-token-expired
and auth/user-disabled
without throwing. See #366 and #174 for additional background.
Function
(optional)
Error handler that will be called if there's an unexpected error while refreshing the user's ID token server side.
This library will not throw when it cannot refresh an ID token. Instead, it will provide an unauthenticated user to the app. See #366 and #174 for additional background.
Defines actions to take depending on a user's auth status, using the following constants:
AuthAction.RENDER
: render the child component
AuthAction.SHOW_LOADER
: show a loader component
AuthAction.RETURN_NULL
: return null instead of any component
AuthAction.REDIRECT_TO_LOGIN
: redirect to the login page
AuthAction.REDIRECT_TO_APP
: redirect to the app
The auth user object is used across server-side and client-side contexts. This is a normalized representation of a Firebase user.
id - String|null
The Firebase user's ID, or null if the user is not authenticated.
email - String|null
The Firebase user's email address, or null if the user has no email address.
emailVerified - Boolean
Whether the user's email address is verified.
phoneNumber - String|null
Added in v0.13.1
The Firebase user's phone number, or null if the user has no phone number.
displayName - String|null
Added in v0.13.1
The Firebase user's display name, or null if the user has no display name.
photoURL - String|null
Added in v0.13.1
The Firebase user's photo URL, or null if the user has no photo URL.
claims - Object
Added in v0.13.0
getIdToken - Function => Promise<String|null>
An async function that resolves to a valid Firebase ID token string, or null if no valid token is available.
clientInitialized - Boolean
Whether the Firebase JS SDK has initialized. If true
, we are no longer using any user info from server-side props.
firebaseUser - FirebaseUser
|null
The user from the Firebase JS SDK, if it has been initialized. Otherwise, null.
signOut - Function => Promise<void>
A method that calls Firebase's signOut
if the Firebase JS SDK has been initialized. If the SDK has not been initialized, this method is a no-op.
String|Function|Object
Used in appPageURL
and authPageURL
in the config and higher-order components, the PageURL defines a redirect destination URL or path.
It can be a string: /my-url/here/
Or an object:
{
destination: '/my-url/here/', // Required string: the URL destination of a redirect
basePath: true, // Optional boolean (defaults to true): whether to use the Next.js base path.
}
Or a function that receives { ctx, AuthUser }
and returns a string or RedirectObject:
const redirect = ({ ctx, AuthUser }) => {
// any custom logic here
return `/my-url/here/?username=${AuthUser.displayName}`
}
The ctx
is the Next.js context value if server side, or undefined if client side.
Firebase 9 has a new API surface designed to facilitate tree-shaking (removal of unused code) to make your web app as small and fast as possible.
If you were previously using version 7 of 8 of the SDK you can easily upgrade by following the official guide.
Here is an example of how the migration might look in your app:
-import firebase from 'firebase/app'
-import 'firebase/firestore'
+import { getApp } from 'firebase/app'
+import { getFirestore, collection, onSnapshot } from 'firebase/firestore'
import { useEffect } from 'react'
const Artists = () => {
const [artists, setArtists] = useState(artists)
useEffect(() => {
- return firebase.firestore()
- .collection('artists')
- .onSnapshot((snap) => {
+ return onSnapshot(collection(getFirestore(getApp()), 'artists'), (snap) => {
if (!snap) {
return
}
setArtists(snap.docs.map((doc) => ({ ...doc.data(), key: doc.id })))
})
}, [])
return (
<div>
{artists.map((artist) => (
<div>{artist.name}</div>
))}
</div>
)
}
- Adding a private key to Vercel
- Using the Firebase Apps
- Getting the user in an API route
- Getting the user in a standalone backend environment
- TypeScript
- Dynamic Redirects
- Testing and Mocking with Jest
There are various ways to add your Firebase private key as an environment variable to Vercel.
Vercel console
In the Vercel console, add the private key in double quotes (screenshot here).
Then, use the private key in your next-firebase-auth
config, in the firebaseAdminInitConfig.credential.privateKey
property:
privateKey: process.env.FIREBASE_PRIVATE_KEY
Vercel CLI
Via the Vercel CLI, add the private key with double quotes:
vercel secrets add firebase-private-key '"my-key-here"'
Then, use JSON.parse
in the firebaseAdminInitConfig.credential.privateKey
property:
privateKey: process.env.FIREBASE_PRIVATE_KEY
? JSON.parse(process.env.FIREBASE_PRIVATE_KEY)
: undefined
Alternative formatting
Others have taken different approaches to deal with escaped newline characters in the private key; for example, by using string replacement. This discussion includes other approaches: discussion #95
To use the Firebase admin package or Firebase JS SDK elsewhere in your app, simply import them as you normally would.
For example:
import { getApp } from 'firebase/app'
import { getFirestore, collection, onSnapshot } from 'firebase/firestore'
import { useEffect } from 'react'
const Artists = () => {
const [artists, setArtists] = useState(artists)
useEffect(() => {
return onSnapshot(collection(getFirestore(getApp()), 'artists'), (snap) => {
if (!snap) {
return
}
setArtists(snap.docs.map((doc) => ({ ...doc.data(), key: doc.id })))
})
}, [])
return (
<div>
{artists.map((artist) => (
<div>{artist.name}</div>
))}
</div>
)
}
Or for the admin app:
import { getAuth } from 'firebase-admin/auth'
import { init } from 'next-firebase-auth'
// Make sure NFA is initialized for any API routes, too.
init({
// ... config
})
const myCode = () => {
const auth = getAuth()
// ...
}
As a convenience, next-firebase-auth
initializes the default Firebase admin app and default Firebase JS SDK app if they haven't already been initialized. However, if you prefer, you can choose to initialize Firebase yourself prior to initializing next-firebase-auth
.
You can easily get the user in an API route by using verifyIdToken or getUserFromCookies. The demo app has an example API route.
Your app might need to authenticate the user in another server-side environment that's separate from your Next.js app, such as an API service or another server-side rendered stack. This is straightforward with next-firebase-auth
.
To do so:
- Install dependencies
yarn add next-firebase-auth firebase-admin
- Other peer dependencies are not required
- Ensure your environment supports
fetch
- Next.js ships with a global
fetch
polyfill, but your environment might not have it. - If
fetch
is not defined in your backend, add a polyfill usingnode-fetch
: documentation here
- Next.js ships with a global
- Initialize
next-firebase-auth
as you normally would- Ensure your Firebase admin and cookies settings exactly match the settings you're using in Next.js or elsewhere.
- All set! Use
verifyIdToken
orgetUserFromCookies
as needed.
A small example:
// my-api-module.js
import { init, getUserFromCookies } from 'next-firebase-auth'
// Adding `fetch` to a server environment that doesn't have it:
// https://github.com/node-fetch/node-fetch#providing-global-access
import './fetch-polyfill'
init({
// ... config
})
const myApiHandler = ({ req }) => {
const user = await getUserFromCookies({
req,
includeToken: true,
})
// ... other logic
return {
// whatever
}
}
export default myApiHandler
When using withAuthUser
with TypeScript, use TypeScript Generics. For example:
// /pages/demo.tsx
import { VFC } from 'react'
import { Loading } from 'components/Loading/Loading'
import { AuthAction, withAuthUser } from 'next-firebase-auth'
type DemoDataType = {
name: string
}
const Demo: VFC<DemoDataType> = ({ name }) => {
return <div>Hello {name}!</div>
}
export default withAuthUser<DemoDataType>({ // <--- Ensure that the type is provided
whenUnauthedBeforeInit: AuthAction.SHOW_LOADER,
whenUnauthedAfterInit: AuthAction.REDIRECT_TO_LOGIN,
LoaderComponent: Loading,
})(Demo)
For a full example with server-side data fetching, see the TypeScript demo page in the example app.
This package makes it easy to redirect to a login page or app page depending on whether a user is logged in. The destination URLs can also be dynamic: the PageURL can be a function that returns the URL at runtime.
The example app uses this to set a post-login destination URL:
// ./utils/initAuth.js
import { init } from 'next-firebase-auth'
import absoluteUrl from 'next-absolute-url'
const initAuth = () => init({
// This demonstrates setting a dynamic destination URL when
// redirecting from app pages. Alternatively, you can simply
// specify `authPageURL: '/auth-ssr'`.
authPageURL: ({ ctx }) => {
const isServerSide = typeof window === 'undefined'
const origin = isServerSide
? absoluteUrl(ctx.req).origin
: window.location.origin
const destPath =
typeof window === 'undefined' ? ctx.resolvedUrl : window.location.href
const destURL = new URL(destPath, origin)
return `auth-ssr?destination=${encodeURIComponent(destURL)}`
},
// This demonstrates setting a dynamic destination URL when
// redirecting from auth pages. Alternatively, you can simply
// specify `appPageURL: '/'`.
appPageURL: ({ ctx }) => {
const isServerSide = typeof window === 'undefined'
const origin = isServerSide
? absoluteUrl(ctx.req).origin
: window.location.origin
const params = isServerSide
? new URL(ctx.req.url, origin).searchParams
: new URLSearchParams(window.location.search)
const destinationParamVal = params.get('destination')
? decodeURIComponent(params.get('destination'))
: undefined
// By default, go to the index page if the destination URL
// is invalid or unspecified.
let destURL = '/'
if (destinationParamVal) {
// Verify the redirect URL host is allowed.
// https://owasp.org/www-project-web-security-testing-guide/v41/4-Web_Application_Security_Testing/11-Client_Side_Testing/04-Testing_for_Client_Side_URL_Redirect
const allowedHosts = ['localhost:3000', 'nfa-example.vercel.app']
const allowed =
allowedHosts.indexOf(new URL(destinationParamVal).host) > -1
if (allowed) {
destURL = destinationParamVal
} else {
// eslint-disable-next-line no-console
console.warn(
`Redirect destination host must be one of ${allowedHosts.join(
', '
)}.`
)
}
}
return destURL
},
// ... other config
}
export default initAuth
To test components wrapped with functions from next-firebase-auth
, you will likely want to mock the next-firebase-auth
library. This can be achieved using the manual mocks feature of Jest.
It can be helpful to define the default mock behavior of next-firebase-auth
across your tests. To do so, stub out the module in a top-level __mocks__
folder (alongside the node_modules
in your application):
├── __mocks__
│ └── next-firebase-auth
│ └── index.js
├── node_modules
│ └── ... all your deps
├── src
│ └── ... all your source code
In index.js
, export a mock of next-firebase-auth
:
const { AuthAction } = require('next-firebase-auth')
const NFAMock = jest.createMockFromModule('next-firebase-auth')
module.exports = {
...NFAMock,
// Customize any mocks as needed.
init: jest.fn(),
// For example, in tests, this will automatically render the child component of
// `withAuthUser`.
withAuthUser: jest.fn(() => (wrappedComponent) => wrappedComponent),
useAuthUser: jest.fn(() => ({
// ... you could return a default AuthUser here
}),
AuthAction,
}
See our implementation of this in our tab-web repository for a more robust example.
You will also likely want to have a utility to mock the AuthUser
object that is passed around via the hooks and higher-order functions in next-firebase-auth
. You might put this in a utils
folder in your app.
// Create a mock FirebaseUser instance with the fields that you use.
const mockFirebaseUser = {
displayName: 'Banana Manana',
// ... other fields from firebaseUser that you may use
}
/**
* Build and return a dummy AuthUser instance to use in tests.
*
* @arg {boolean} isLoggedIn - Pass `false` to mimic a logged out user.
* @returns {AuthUserContext} - A mocked AuthUser instance, with 'serialize' added.
*/
const getMockAuthUser = (isLoggedIn = true) => ({
id: isLoggedIn ? 'abcd1234' : null,
email: isLoggedIn ? '[email protected]' : null,
emailVerified: isLoggedIn,
getIdToken: jest.fn(async () => (isLoggedIn ? 'i_am_a_token' : null)),
clientInitialized: isLoggedIn,
firebaseUser: isLoggedIn ? mockFirebaseUser : null,
signOut: jest.fn(),
serialize: jest.fn(() => 'serialized_auth_user'),
})
export default getMockAuthUser
Now, you can use and customize the mock behavior in your tests.
If you're modifying higher-order functions, components being tested need to be required
inside a beforeEach
function or each test case. This is because mocking next-firebase-auth
has to happen before your component is imported, because the call to the next-firebase-auth
function is part of the default export of your component (e.g., export default withAuthUser()(MyComponent)
).
Given the following component:
import React from 'react'
import { useAuthUser, withAuthUser } from 'next-firebase-auth'
function UserDisplayName() {
const AuthUser = useAuthUser()
const { displayName = 'anonymous' } = AuthUser.firebaseUser
return <span>{displayName}</span>
}
export default withAuthUser()(UserDisplayName)
you can write a test suite like this:
import { render, screen } from '@testing-library/react'
// Import the functions that the component module calls, which allows jest to mock them
// in the context of this test run. This allows you to manipulate the return value of each
// function within this test suite.
import { useAuthUser, withAuthUser } from 'next-firebase-auth'
// Import your mock AuthUser generator
import getMockAuthUser from '../../utils/test-utils/get-mock-auth-user'
// Mock all of `next-firebase-auth`. This is *not* necessary if you set up manual mocks,
// because Jest will automatically mock the module in every test.
jest.mock('next-firebase-auth')
describe('UserDisplayName', () => {
// Create a placeholder for your component that you want to test
let UserDisplayName
beforeEach(() => {
// Mock the functions that your component uses, and import your component before each test.
useAuthUser.mockReturnValue(getMockAuthUser())
withAuthUser.mockImplementation(() => (wrappedComponent) => wrappedComponent))
UserDisplayName = require('./').default
})
afterAll(() => {
// Reset the mocks so that they don't bleed into the next test suite.
jest.resetAllMocks()
})
it('renders the logged in user\'s display name', () => {
// The default value for the mocked implementation of `withAuthUser` is a fully logged in and verified
// user. Rendering your component directly with the setup above will result in a "logged in" user being
// passed to your component.
render(<UserDisplayName />)
expect(screen.queryByTest(getMockAuthUser().firebaseUser.displayName)).toBeInTheDocument()
})
it('renders "anonymous" when user is not logged in', () => {
// If you want to test a "logged out" state, then you can mock the function again inside any test,
// passing a falsy value to `getMockAuthUser`, which will return a logged out AuthUser object.
useAuthUser.mockReturnValue(getMockAuthUser(false))
render(<Header />)
expect(screen.getByText('anonymous')).toBeInTheDocument()
})
})
When using TypeScript for your test files, you will have to cast the mocked functions to get access to the mockImplementation
and mockReturnValue
methods. If we were to rewrite the above example in TS, it might look something like this:
import type { ComponentType } from 'react'
import { render, screen } from '@testing-library/react'
// Import the functions that the component module calls, which allows jest to mock them
// in the context of this test run. This allows you to manipulate the return value of each
// function within this test suite.
import { useAuthUser, withAuthUser } from 'next-firebase-auth'
// Import your mock AuthUser generator
import getMockAuthUser from '../../utils/test-utils/get-mock-auth-user'
// Mock all of `next-firebase-auth`. This is *not* necessary if you set up manual mocks,
// because Jest will automatically mock the module
jest.mock('next-firebase-auth')
describe('UserDisplayName', () => {
// Create a placeholder for your component that you want to test
let UserDisplayName: ComponentType
beforeEach(() => {
// Mock the functions that your component uses, and import your component before each test.
(useAuthUser as jest.Mock).mockReturnValue(getMockAuthUser())
(withAuthUser as jest.Mock).mockImplementation(() => (wrappedComponent: ComponentType) => wrappedComponent: ComponentType))
UserDisplayName = require('./').default as ComponentType
})
afterAll(() => {
// Reset the mocks so that they don't bleed into the next test suite.
jest.resetAllMocks()
})
it('renders the logged in user\'s display name', () => {
// The default value for the mocked implementation of `withAuthUser` is a fully logged in and verified
// user. Rendering your component directly with the setup above will result in a "logged in" user being
// passed to your component.
render(<UserDisplayName />)
expect(screen.getByText(getMockAuthUser().firebaseUser.displayName)).toBeInTheDocument()
})
it('renders "anonymous" when user is not logged in', () => {
// If you want to test a "logged out" state, then you can mock the function again inside any test,
// passing a falsy value to `getMockAuthUser`, which will return a logged out AuthUser object.
(useAuthUser as jest.Mock).mockReturnValue(getMockAuthUser(false))
render(<Header />)
expect(screen.getByText('anonymous')).toBeInTheDocument()
})
})
Stuck? Search discussions or open your own Q&A discussion describing what you've already tried.
We expect certain sensitive config values to be falsy on the client side (see the config validation code). This is a precaution to make sure developers aren't accidentally bundling something like their Firebase private key with client JS.
To fix this, ensure the config setting is undefined
on the client side by logging it to your browser console. You can use Next's .env
support to set server-only variables. Never use the NEXT_PUBLIC*
prefix for any secret values.
This package will call a Google endpoint when it needs to refresh a token server side. You're seeing an error from that request.
To fix this, confirm that your firebaseAdminInitConfig.credential.clientEmail
is correct. It should be the email paired with your Firebase private key.
If that doesn't help, try inspecting the custom token to manually validate the values and structure. Some people encounter this problem when their server time is incorrect.
Server-side auth is not working. The user and token are always null when using withAuthUserTokenSSR
, but client-side auth works.
If auth is working on the client side but not on the server side, the auth cookies are most likely not set.
To fix this, confirm the auth cookies are set in your browser's dev tools. If they're not set, please check that the secure
, sameSite
, and path
options passed in the next-firebase-auth
config make sense for your environment. For example, if you're testing on non-HTTPS localhost, make sure secure
is false.
In addition, please double-check your server logs for any errors to ensure the Firebase admin app is initializing properly.
Often, this is caused by an incorrect email in Firebase credentials. Please verify that the email is correct and is from the same Firebase account as your private key, or try generating a new key: https://firebase.google.com/docs/admin/setup
You can try setting up your credentials in the example app to be sure your app code isn't a problem.
In local development, try clearing data/cookies for localhost
in case you previously signed in with another Firebase account and still have auth cookies signed by another private key.
You can also try disabling the Firebase Authentication Emulator. Remove firebaseAuthEmulatorHost
from your config and remove FIREBASE_AUTH_EMULATOR_HOST
from your .env
file.
See adding a private key to Vercel and this discussion on private key formatting.
We expect some apps will need some features that are not currently available:
- Supporting custom session logic: Currently, this package doesn't allow using a custom cookie or session module. Some developers may need this flexibility to, for example, keep auth user data in server-side session storage.
- Setting a single auth cookie: This package currently sets more than one cookie to store authentication state. It's not currently possible to use a single cookie with a customized name: #190
We'd love to hear your feedback on these or other features. Please feel free to open a discussion!
We welcome contributions! Please see contributing docs to get started.