Skip to content

Instantly share code, notes, and snippets.

@steveruizok
Last active March 14, 2023 19:20
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save steveruizok/75c8990ceb576e02cd3f7d1854569a2b to your computer and use it in GitHub Desktop.
Save steveruizok/75c8990ceb576e02cd3f7d1854569a2b to your computer and use it in GitHub Desktop.
Next.js SSR firebase auth
NEXT_PUBLIC_FIREBASE_PUBLIC_API_KEY=your_firebase_email
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your_firebase_auth_domain
NEXT_PUBLIC_FIREBASE_DATABASE_URL=your_firebase_database_url
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your_project_id
NEXT_PUBLIC_BASE_API_URL=http://localhost:3000
NEXT_PUBLIC_SERVICE_ACCOUNT=your_config_json_converted_to_base64
NEXT_PUBLIC_COOKIE_NAME=auth
// /lib/auth-client.ts
import router from "next/router"
import firebase from "./firebase"
async function clearUserToken() {
var path = "/api/logout"
var url = process.env.NEXT_PUBLIC_BASE_API_URL + path
return fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
})
}
async function postUserToken(token: string) {
var path = "/api/login"
var url = process.env.NEXT_PUBLIC_BASE_API_URL + path
var data = { token }
return fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
})
}
// API
export async function login() {
const provider = new firebase.auth.GoogleAuthProvider()
const auth = await firebase.auth().signInWithPopup(provider)
const token = await auth.user.getIdToken()
await postUserToken(token)
await firebase.auth().signOut()
router.reload()
}
export async function logout() {
await firebase.auth().signOut()
await clearUserToken()
router.reload()
}
// /lib/auth-server.ts
import { GetServerSidePropsContext } from "next"
import { parseCookies } from "nookies"
import pick from "lodash/pick"
import admin from "./firebase-admin"
import * as Types from "types"
async function verifyCookie(
cookie: string
): Promise<{
authenticated: boolean
user: Types.User
}> {
if (!admin) return null
let user: any = undefined
let authenticated: boolean = false
await admin
.auth()
.verifySessionCookie(cookie, true /** checkRevoked */)
.then((decodedClaims: { [key: string]: any }) => {
authenticated = true
user = pick(decodedClaims, "name", "email", "picture", "uid")
})
.catch(() => {
authenticated = false
})
return {
authenticated,
user,
}
}
// Public API
export function redirectToAuthPage(context: GetServerSidePropsContext) {
context.res.writeHead(303, { Location: "/auth" })
context.res.end()
return null
}
export function redirectToUserPage(context: GetServerSidePropsContext) {
context.res.writeHead(303, { Location: "/user" })
context.res.end()
return null
}
export async function getCurrentUser(
context?: GetServerSidePropsContext
): Promise<Types.AuthState> {
const cookies = parseCookies(context)
const result = {
user: null,
authenticated: false,
error: "",
}
if (!cookies[process.env.NEXT_PUBLIC_COOKIE_NAME]) {
result.error = "No cookie."
return result
}
const authentication = await verifyCookie(
cookies[process.env.NEXT_PUBLIC_COOKIE_NAME]
)
if (!authentication) {
result.error = "Could not verify cookie."
return result
}
const { user = null, authenticated = false } = authentication
result.user = user
result.authenticated = authenticated
return result
}
// /pages/auth.tsx
import { GetServerSidePropsContext, GetServerSidePropsResult } from "next"
import { getCurrentUser, redirectToUserPage } from "../lib/auth-server"
import { login, logout } from "../lib/auth-client"
import * as Types from "types"
export default function Auth({ error }: Types.AuthState) {
return (
<div>
<h1>Auth</h1>
<button onClick={login}>Log In</button>
{error && (
<>
<h2>Error:</h2>
<p>{error}</p>
</>
)}
</div>
)
}
export async function getServerSideProps(
context: GetServerSidePropsContext
): Promise<GetServerSidePropsResult<Types.AuthState>> {
const authState = await getCurrentUser(context)
if (authState.user) redirectToUserPage(context)
return {
props: authState,
}
}
// /lib/firebase-admin.ts
import atob from "atob"
import admin from "firebase-admin"
var serviceAccount = JSON.parse(atob(process.env.NEXT_PUBLIC_SERVICE_ACCOUNT))
if (!admin.apps.length) {
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,
})
}
export default admin
// /lib/firebase.ts
import firebase from "firebase/app"
import "firebase/firestore"
import "firebase/auth"
const config = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_PUBLIC_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
}
if (!firebase.apps.length) {
firebase.initializeApp(config)
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.NONE)
}
export default firebase
// /lib/firestore.ts
import firebase from "./firebase"
export default firebase.firestore()
// /pages/api/login.tsx
import { serialize } from "cookie"
import { NextApiResponse, NextApiRequest } from "next"
import admin from "../../lib/firebase-admin"
const SESSION_DURATION_IN_DAYS = 5
export default async function login(req: NextApiRequest, res: NextApiResponse) {
const expiresIn = SESSION_DURATION_IN_DAYS * (24 * 60 * 60 * 1000)
if (req.method === "POST") {
var idToken = req.body.token.toString()
const decodedIdToken = await admin.auth().verifyIdToken(idToken)
const cookie = await admin
.auth()
.createSessionCookie(idToken, { expiresIn })
if (!cookie) {
res.status(401).send({ response: "Invalid authentication" })
return
}
if (new Date().getTime() / 1000 - decodedIdToken.auth_time > 5 * 60) {
res.status(401).send({ response: "Recent sign in required!" })
return
}
const options = {
maxAge: expiresIn,
httpOnly: true,
secure: process.env.NEXT_PUBLIC_SECURE_COOKIE === "true",
path: "/",
}
res.setHeader(
"Set-Cookie",
serialize(process.env.NEXT_PUBLIC_COOKIE_NAME, cookie, options)
)
res.send({ response: "Logged in." })
} else {
res.status(400)
res.send({ response: "You need to post to this endpoint." })
}
}
export const config = {
api: {
externalResolver: true,
},
}
// /pages/api/logout.tsx
import { NextApiResponse, NextApiRequest } from "next"
import admin from "../../lib/firebase-admin"
export default async function logout(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === "POST") {
const cookie = req.cookies[process.env.NEXT_PUBLIC_COOKIE_NAME]
const decodedClaims = await admin.auth().verifySessionCookie(cookie)
await admin.auth().revokeRefreshTokens(decodedClaims.sub)
res.status(200)
res.send({ response: "Logged out" })
} else {
res.status(400)
res.send({ response: "You need to post to this endpoint." })
}
}
export const config = {
api: {
externalResolver: true,
},
}
export interface User {
name: string
uid: string
email: string
picture: string
}
export interface AuthState {
user: User | null
authenticated: boolean
error: string
}
// /pages/user.tsx
import { GetServerSidePropsContext, GetServerSidePropsResult } from "next"
import { getCurrentUser, redirectToAuthPage } from "../lib/auth-server"
import { logout } from "../lib/auth-client"
import Link from "next/link"
import * as Types from "types"
export default function User({ user }: Types.AuthState) {
return (
<div>
<h1>User</h1>
<img src={user.picture} />
<pre>{JSON.stringify(user, null, " ")}</pre>
<button onClick={logout}>Logout</button>
</div>
)
}
export async function getServerSideProps(
context: GetServerSidePropsContext
): Promise<GetServerSidePropsResult<Types.AuthState>> {
const authState = await getCurrentUser(context)
if (!authState.authenticated) redirectToAuthPage(context)
return {
props: authState,
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment