├── .env.local.example ├── .gitignore ├── .prettierrc ├── README.md ├── auth.tsx ├── firebaseAdmin.ts ├── firebaseClient.ts ├── next-env.d.ts ├── package.json ├── pages ├── _app.tsx ├── authenticated.tsx ├── index.tsx └── login.tsx ├── public ├── favicon.ico └── vercel.svg ├── tsconfig.json ├── tsconfig.node.json └── yarn.lock /.env.local.example: -------------------------------------------------------------------------------- 1 | # Admin Credentials 2 | # Go to Firebase Console > open your project > click the gear icon > Project Settings > Service Accounts > click Node.js > Generate new private key 3 | 4 | PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nXXXXX\n-----END PRIVATE KEY-----\n" 5 | CLIENT_EMAIL="firebase-adminsdk-test@YOUR_PROJECT_ID.iam.gserviceaccount.com" 6 | PROJECT_ID="my-project-a48b8e" 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "printWidth": 80 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js + Firebase + getServerSideProps 2 | 3 | This project demonstrates how to implement authenticated server-side rendering with Next.js and Firebase Authentication. 4 | 5 | > Update November 19, 2020: A bug has been fixed where the Firebase tokens would expire after an hour without being refreshed. All tokens are now force refreshed every 10 minutes. 6 | 7 | > Update November 9, 2020: this repo has been updated to use the [redirect functionality](https://github.com/vercel/next.js/discussions/14890) introduced in `next@9.5.4` . Currently you must be on the `canary` release of Next for this approach to work ( `yarn add next@canary` ). 8 | 9 | ### Versions 10 | 11 | * `next@10` 12 | * `react@17` 13 | * `firebase@8` 14 | * `firebase-admin@9.4` 15 | * `nookies@2.5` 16 | 17 | ### Documentation 18 | 19 | Full walkthrough and documentation here: [Authenticated server-side rendering with Next.js and Firebase](https://colinhacks.com/essays/nextjs-firebase-authentication). 20 | 21 | ### How to use 22 | 23 | To run this example: 24 | 25 | * Clone the repo: `git clone git@github.com:vriad/next-firebase-ssr.git` 26 | * Navigate into directory: `cd next-firebase-ssr` 27 | * Install dependencies: `yarn` 28 | * Create a Firebase project if you haven't already. Make sure you go into the Authentication tab in the Console, go to "Sign-in method", and enable "Email/Password" 29 | * Add your Firebase client credentials to `firebaseClient.ts`. To get these, go to the Firebase Console > open your project > Gear Icon > Project Settings > General > Your apps > Firebase SDK Snippet > click the "Config" radio button > copy/paste. 30 | * Add your service account (Admin) credentials to the project. To do this, go to the Firebase Console > open your project > click the gear icon > Project Settings > Service Accounts > click Node.js > Generate new private key. Open the JSON file that downloads. Then create a copy of `.env.local.example` and rename it to `.env.local`. This is the file where you will put your secret Firebase credentials. Copy/paste the values from the private key JSON file over to the new `.env.local` file: `privateKey` becomes `PRIVATE_KEY`, `project_id` becomes `PROJECT_ID`, and `clientEmail` becomes `CLIENT_EMAIL`. 31 | * Run `yarn dev` 32 | * Go to `localhost:3000` 33 | -------------------------------------------------------------------------------- /auth.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext, createContext } from "react"; 2 | import nookies from "nookies"; 3 | import { firebaseClient } from "./firebaseClient"; 4 | 5 | const AuthContext = createContext<{ user: firebaseClient.User | null }>({ 6 | user: null, 7 | }); 8 | 9 | export function AuthProvider({ children }: any) { 10 | const [user, setUser] = useState(null); 11 | 12 | useEffect(() => { 13 | if (typeof window !== "undefined") { 14 | (window as any).nookies = nookies; 15 | } 16 | return firebaseClient.auth().onIdTokenChanged(async (user) => { 17 | console.log(`token changed!`); 18 | if (!user) { 19 | console.log(`no token found...`); 20 | setUser(null); 21 | nookies.destroy(null, "token"); 22 | nookies.set(null, "token", "", {path: '/'}); 23 | return; 24 | } 25 | 26 | console.log(`updating token...`); 27 | const token = await user.getIdToken(); 28 | setUser(user); 29 | nookies.destroy(null, "token"); 30 | nookies.set(null, "token", token, {path: '/'}); 31 | }); 32 | }, []); 33 | 34 | // force refresh the token every 10 minutes 35 | useEffect(() => { 36 | const handle = setInterval(async () => { 37 | console.log(`refreshing token...`); 38 | const user = firebaseClient.auth().currentUser; 39 | if (user) await user.getIdToken(true); 40 | }, 10 * 60 * 1000); 41 | return () => clearInterval(handle); 42 | }, []); 43 | 44 | return ( 45 | {children} 46 | ); 47 | } 48 | 49 | export const useAuth = () => { 50 | return useContext(AuthContext); 51 | }; 52 | -------------------------------------------------------------------------------- /firebaseAdmin.ts: -------------------------------------------------------------------------------- 1 | import * as firebaseAdmin from "firebase-admin"; 2 | 3 | const privateKey = process.env["PRIVATE_KEY"]; 4 | const clientEmail = process.env["CLIENT_EMAIL"]; 5 | const projectId = process.env["PROJECT_ID"]; 6 | 7 | if (!privateKey || !clientEmail || !projectId) { 8 | console.log( 9 | `Failed to load Firebase credentials. Follow the instructions in the README to set your Firebase credentials inside environment variables.` 10 | ); 11 | } 12 | 13 | if (!firebaseAdmin.apps.length) { 14 | firebaseAdmin.initializeApp({ 15 | credential: firebaseAdmin.credential.cert({ 16 | privateKey: privateKey, 17 | clientEmail, 18 | projectId, 19 | }), 20 | databaseURL: `https://${projectId}.firebaseio.com`, 21 | }); 22 | } 23 | 24 | export { firebaseAdmin }; 25 | -------------------------------------------------------------------------------- /firebaseClient.ts: -------------------------------------------------------------------------------- 1 | import firebaseClient from "firebase/app"; 2 | import "firebase/auth"; 3 | 4 | /* 5 | 6 | Copy/paste your *client-side* Firebase credentials below. 7 | 8 | To get these, go to the Firebase Console > open your project > Gear Icon > 9 | Project Settings > General > Your apps. If you haven't created a web app 10 | already, click the "" icon, name your app, and copy/paste the snippet. 11 | Otherwise, go to Firebase SDK Snippet > click the "Config" radio button > 12 | copy/paste. 13 | 14 | */ 15 | const CLIENT_CONFIG = { 16 | apiKey: "AIzaSyAoonOmu_H1Bksv7378GKcKdrExuj-On14", 17 | authDomain: "fir-nextjs-ssr.firebaseapp.com", 18 | databaseURL: "https://fir-nextjs-ssr.firebaseio.com", 19 | projectId: "fir-nextjs-ssr", 20 | storageBucket: "fir-nextjs-ssr.appspot.com", 21 | messagingSenderId: "364051821923", 22 | appId: "1:364051821923:web:658516ef4516511223cf56", 23 | }; 24 | 25 | if (typeof window !== "undefined" && !firebaseClient.apps.length) { 26 | firebaseClient.initializeApp(CLIENT_CONFIG); 27 | firebaseClient 28 | .auth() 29 | .setPersistence(firebaseClient.auth.Auth.Persistence.SESSION); 30 | (window as any).firebase = firebaseClient; 31 | } 32 | 33 | export { firebaseClient }; 34 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nicetry", 3 | "version": "0.1.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "export": "next export", 9 | "start": "next start", 10 | "codegen:dev": "nodemon -e ts --ignore generated -x ts-node --project tsconfig.node.json ./scripts/codegen_dev.ts", 11 | "codegen:staging": "ts-node --project tsconfig.node.json ./scripts/codegen_staging.ts", 12 | "codegen:prod": "ts-node --project tsconfig.node.json ./scripts/codegen_prod.ts", 13 | "migrate:dev": "nodemon -e ts --ignore generated -x ts-node --project tsconfig.node.json ./scripts/migrate_dev.ts", 14 | "migrate:staging": "ts-node --project tsconfig.node.json ./scripts/migrate_staging.ts", 15 | "migrate:prod": "ts-node --project tsconfig.node.json ./scripts/migrate_prod.ts" 16 | }, 17 | "dependencies": { 18 | "@types/react": "^16.9.56", 19 | "firebase": "^8.0.2", 20 | "firebase-admin": "^9.4.0", 21 | "next": "10", 22 | "nookies": "^2.5.0", 23 | "react": "^17.0.1", 24 | "react-dom": "^17.0.1", 25 | "typescript": "^4.0.5" 26 | }, 27 | "devDependencies": {} 28 | } -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app'; 2 | import { AuthProvider } from '../auth'; 3 | 4 | function MyApp({ Component, pageProps }: AppProps) { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | export default MyApp; 12 | -------------------------------------------------------------------------------- /pages/authenticated.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import nookies from "nookies"; 3 | import { useRouter } from 'next/router' 4 | import { firebaseAdmin } from "../firebaseAdmin"; 5 | import { firebaseClient } from "../firebaseClient"; 6 | 7 | import { InferGetServerSidePropsType, GetServerSidePropsContext } from "next"; 8 | 9 | export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { 10 | try { 11 | const cookies = nookies.get(ctx); 12 | console.log(JSON.stringify(cookies, null, 2)); 13 | const token = await firebaseAdmin.auth().verifyIdToken(cookies.token); 14 | const { uid, email } = token; 15 | 16 | // the user is authenticated! 17 | // FETCH STUFF HERE 18 | 19 | return { 20 | props: { message: `Your email is ${email} and your UID is ${uid}.` }, 21 | }; 22 | } catch (err) { 23 | // either the `token` cookie didn't exist 24 | // or token verification failed 25 | // either way: redirect to the login page 26 | // either the `token` cookie didn't exist 27 | // or token verification failed 28 | // either way: redirect to the login page 29 | return { 30 | redirect: { 31 | permanent: false, 32 | destination: "/login", 33 | }, 34 | // `as never` is required for correct type inference 35 | // by InferGetServerSidePropsType below 36 | props: {} as never, 37 | }; 38 | } 39 | }; 40 | 41 | function AuthenticatedPage( 42 | props: InferGetServerSidePropsType 43 | ) { 44 | const router = useRouter(); 45 | 46 | return ( 47 |
48 |

{props.message!}

49 | 61 |
62 | ); 63 | } 64 | 65 | export default AuthenticatedPage; 66 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useCallback } from 'react'; 2 | import Link from 'next/link'; 3 | import { useAuth } from '../auth'; 4 | 5 | export default () => { 6 | const { user } = useAuth(); 7 | 8 | return ( 9 |
10 |

{`User ID: ${user ? user.uid : 'no user signed in'}`}

11 | 12 |

13 | 14 | Go to authenticated route 15 | 16 |

17 |

18 | 19 | Login 20 | 21 |

22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /pages/login.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Link from 'next/link'; 3 | import { firebaseClient } from '../firebaseClient'; 4 | 5 | export default (_props: any) => { 6 | const [email, setEmail] = useState(''); 7 | const [pass, setPass] = useState(''); 8 | return ( 9 |
10 | 11 | Go back to home page 12 | 13 |
14 | setEmail(e.target.value)} 17 | placeholder={'Email'} 18 | /> 19 | setPass(e.target.value)} 23 | placeholder={'Password'} 24 | /> 25 | 35 | 43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toplustar/next-ssr/c3f407d4240bee5a7ece698d8f84876793f1bd81/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | // "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noImplicitAny": true, 14 | "noImplicitThis": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noImplicitReturns": true, 18 | "noEmit": true, 19 | "esModuleInterop": true, 20 | "module": "esnext", 21 | "moduleResolution": "node", 22 | "resolveJsonModule": true, 23 | "isolatedModules": true, 24 | "jsx": "preserve", 25 | "skipLibCheck": true 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx" 31 | ], 32 | "exclude": [ 33 | "node_modules" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "target": "es2017", 6 | "allowJs": true, 7 | }, 8 | } --------------------------------------------------------------------------------