Last active
March 14, 2023 19:20
-
-
Save steveruizok/75c8990ceb576e02cd3f7d1854569a2b to your computer and use it in GitHub Desktop.
Next.js SSR firebase auth
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// /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() | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// /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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// /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, | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// /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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// /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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// /lib/firestore.ts | |
import firebase from "./firebase" | |
export default firebase.firestore() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// /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, | |
}, | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// /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, | |
}, | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export interface User { | |
name: string | |
uid: string | |
email: string | |
picture: string | |
} | |
export interface AuthState { | |
user: User | null | |
authenticated: boolean | |
error: string | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// /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