Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add example: Firebase authentication + SSR #1

Closed
wants to merge 52 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
b85cb89
Clone with-firebase-authentication example
kmjennison Jul 1, 2020
e200662
Change package.json name
kmjennison Jul 1, 2020
6eed448
Add custom token and refresh token logic
kmjennison Jul 2, 2020
3d313a2
Add cookies middleware
kmjennison Jul 2, 2020
decf70f
Add base 64 encoding to cookies middleware
kmjennison Jul 2, 2020
99d7bf2
Enable absolute imports
kmjennison Jul 2, 2020
7a463e8
WIP - set a session cookie on login
kmjennison Jul 2, 2020
e0cea06
Add TODO
kmjennison Jul 2, 2020
8d53eb3
Remove whitespace
kmjennison Jul 9, 2020
404a5e7
WIP - withAuthUser
kmjennison Jul 9, 2020
d546e25
Merge branch 'canary' into kevin/with-firebase-authentication-ssr
kmjennison Jul 9, 2020
f5a3021
Add a HOC-type pattern for auth getServerSideProps
kmjennison Jul 9, 2020
d31259c
Create withAuthComponent
kmjennison Jul 9, 2020
b5b25ce
Move createAuthUser to its own module
kmjennison Jul 9, 2020
a764394
Add TODOs
kmjennison Jul 9, 2020
d43ee28
Remove outdated HOC
kmjennison Jul 9, 2020
16637ae
Add lodash
kmjennison Jul 9, 2020
d53d3d3
Add yarn.lock
kmjennison Jul 9, 2020
ff80e2c
Add TODO
kmjennison Jul 9, 2020
154ffb3
Create useAuthUser hook
kmjennison Jul 9, 2020
79e0800
Tweak createAuthUser
kmjennison Jul 9, 2020
ee6c602
Fix bugs in token refreshing
kmjennison Jul 10, 2020
7f1627f
Add data fetching
kmjennison Jul 10, 2020
0947b13
Add "getIdToken" method to AuthUser and create a separate serializabl…
kmjennison Jul 13, 2020
8eb4b80
Handle unauthed users
kmjennison Jul 13, 2020
282e800
Use the Firebase JS SDK data for AuthUser once it's available
kmjennison Jul 13, 2020
e9d6715
Update documentation link
kmjennison Jul 13, 2020
747ecf1
Break out the Firebase cookie management into its own hook
kmjennison Jul 13, 2020
7993500
Add logout
kmjennison Jul 13, 2020
bf06d35
Remove js-cookie
kmjennison Jul 13, 2020
edb5048
Update favorite foods
kmjennison Jul 13, 2020
9e45f2d
Use constant for cookie name
kmjennison Jul 14, 2020
835718b
Add "clientInitialized" field to AuthUser
kmjennison Jul 14, 2020
b362969
Absolute import
kmjennison Jul 14, 2020
2ed0d8d
Add redirect if the user is not authed
kmjennison Jul 14, 2020
4e1d948
Redirect to the app when the user is authed
kmjennison Jul 14, 2020
10ba891
Have auth component sometimes wait to render children
kmjennison Jul 14, 2020
0dfb638
Reenable authorization for endpoint
kmjennison Jul 14, 2020
3212198
Include the user token when fetching data from client
kmjennison Jul 14, 2020
cc6b6ce
Add another example page
kmjennison Jul 14, 2020
74edafa
Use the host to construct the API endpoint
kmjennison Jul 14, 2020
4b53f3b
Use HTTPS unless on localhost
kmjennison Jul 14, 2020
c894426
Merge branch 'canary' into kevin/with-firebase-authentication-ssr
kmjennison Jul 14, 2020
7670313
Update comments
kmjennison Jul 15, 2020
220e427
Update README.md
kmjennison Jul 15, 2020
0a5f3ff
Update README.md
kmjennison Jul 15, 2020
ca31d49
Update README.md
kmjennison Jul 15, 2020
fd84b3a
Update README.md
kmjennison Jul 15, 2020
731de56
Update README.md
kmjennison Jul 15, 2020
3eff896
Update README.md
kmjennison Jul 15, 2020
791630d
Update README.md
kmjennison Jul 15, 2020
43fe206
Add to .gitignore
kmjennison Jul 15, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions examples/with-firebase-authentication-ssr/.env.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Update these with your Firebase app's values.
[email protected]
NEXT_PUBLIC_FIREBASE_PUBLIC_API_KEY=MyExampleAppAPIKey123
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=my-example-app.firebaseapp.com
NEXT_PUBLIC_FIREBASE_DATABASE_URL=https://my-example-app.firebaseio.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=my-example-app-id

# Your Firebase private key.
FIREBASE_PRIVATE_KEY=some-key-here

# Secrets used to encrypt cookies.
SESSION_SECRET_CURRENT=someSecretValue
SESSION_SECRET_PREVIOUS=anotherSecretValue
3 changes: 3 additions & 0 deletions examples/with-firebase-authentication-ssr/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
!/yarn.lock
.env.production
.vercel
75 changes: 75 additions & 0 deletions examples/with-firebase-authentication-ssr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Example: Firebase authentication with SSR

This example demonstrates server-side rendering with Firebase authentication, providing server-side access to a valid Firebase ID token.

## Before you use this example

Depending on your app's requirements, other approaches may be better.

**If you don't need SSR:** use [with-firebase-authentication](../with-firebase-authentication) to fetch data from the client side. It's less complicated, and your app will have a quicker initial page load.

**If you don't need server-side access to a Firebase ID token:** consider using [Firebase's session cookies](https://firebase.google.com/docs/auth/admin/manage-cookies). It's less complicated and will likely be quicker to render server-side. However, *you will not be able to access other Firebase services* with the session cookie.

## How it works

On login, we create a custom Firebase token, [fetch an ID token and refresh token](https://firebase.google.com/docs/reference/rest/auth/#section-verify-custom-token), and store the ID and refresh tokens in a cookie. On future requests, we verify the ID token server-side; if it's expired, we [use the refresh token](https://firebase.google.com/docs/reference/rest/auth/#section-refresh-token) to get a new one.

The authed user is provided as an isomorphic `AuthUser` object. During SSR and client-side rendering prior to initializing the Firebase JS SDK, `AuthUser` relies on the ID token from the cookie. After the Firebase JS SDK initializes, it relies on the Firebase JS SDK.

The Firebase JS SDK auth state remains the source of truth. On auth state change, we set/unset the auth cookie.

## How to use

### Using `create-next-app`

Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) or [npx](https://github.com/zkat/npx#readme) to bootstrap the example:

```bash
npx create-next-app --example with-firebase-authentication with-firebase-authentication-ssr
# or
yarn create next-app --example with-firebase-authentication with-firebase-authentication-ssr
```

### Download manually

Download the example:

```bash
curl https://codeload.github.com/vercel/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-firebase-authentication-ssr
cd with-firebase-authentication-ssr
```

## Configuration

Set up Firebase:

- Create a project at the [Firebase console](https://console.firebase.google.com/).
- Copy the contents of `.env.local.example` into a new file called `.env.local`
- Get your account credentials from the Firebase console at _Project settings > Service accounts_, where you can click on _Generate new private key_ and download the credentials as a json file. It will contain keys such as `project_id`, `client_email` and `client_id`. Set them as environment variables in the `.env.local` file at the root of this project.
- Get your authentication credentials from the Firebase console under _Project settings > General> Your apps_ Add a new web app if you don't already have one. Under _Firebase SDK snippet_ choose _Config_ to get the configuration as JSON. It will include keys like `apiKey`, `authDomain` and `databaseUrl`. Set the appropriate environment variables in the `.env.local` file at the root of this project.
- Go to **Develop**, click on **Authentication** and in the **Sign-in method** tab enable authentication for the app.

Install it and run:

```bash
npm install
npm run dev
# or
yarn
yarn dev
```

## Deploy to Vercel

Set environment variables in `.env.production`.

Deploy it to the cloud with [Vercel](https://vercel.com/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).

After deploying, copy the deployment URL and navigate to your Firebase project's Authentication tab. Scroll down in the page to "Authorized domains" and add that URL to the list.

## Recommended for production
* Set `secure` and `sameSite` options in `cookies.js`
* Ensure the session secrets in `.env` are unique, sufficiently random, and out of source control

## Future improvements
* Currently, we use client-side redirects to redirect unauthenticated users to the login page. When Next.js supports redirects in `getServerSideProps` (see [RFC](https://github.com/vercel/next.js/discussions/14890)), we should change this.
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/* globals window */
import { useEffect, useState } from 'react'
import StyledFirebaseAuth from 'react-firebaseui/StyledFirebaseAuth'
import firebase from 'firebase/app'
import 'firebase/auth'
import initFirebase from '../utils/auth/initFirebase'

// Init the Firebase app.
initFirebase()

const firebaseAuthConfig = {
signInFlow: 'popup',
// Auth providers
// https://github.com/firebase/firebaseui-web#configure-oauth-providers
signInOptions: [
{
provider: firebase.auth.EmailAuthProvider.PROVIDER_ID,
requireDisplayName: false,
},
],
signInSuccessUrl: '/',
credentialHelper: 'none',
callbacks: {
// https://github.com/firebase/firebaseui-web#signinsuccesswithauthresultauthresult-redirecturl
signInSuccessWithAuthResult: () => {
// Don't automatically redirect. We handle redirecting based on
// auth state in withAuthComponent.js.
return false
},
},
}

const FirebaseAuth = () => {
// Do not SSR FirebaseUI, because it is not supported.
// https://github.com/firebase/firebaseui-web/issues/213
const [renderAuth, setRenderAuth] = useState(false)
useEffect(() => {
if (typeof window !== 'undefined') {
setRenderAuth(true)
}
}, [])
return (
<div>
{renderAuth ? (
<StyledFirebaseAuth
uiConfig={firebaseAuthConfig}
firebaseAuth={firebase.auth()}
/>
) : null}
</div>
)
}

export default FirebaseAuth
5 changes: 5 additions & 0 deletions examples/with-firebase-authentication-ssr/jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"compilerOptions": {
"baseUrl": "."
}
}
20 changes: 20 additions & 0 deletions examples/with-firebase-authentication-ssr/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "with-firebase-authentication-ssr",
"version": "1.0.0",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"cookies": "0.8.0",
"firebase": "^7.15.5",
"firebase-admin": "^8.12.1",
"lodash": "4.17.19",
"next": "latest",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-firebaseui": "4.1.0",
"swr": "0.2.3"
}
}
16 changes: 16 additions & 0 deletions examples/with-firebase-authentication-ssr/pages/api/getFood.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { verifyIdToken } from 'utils/auth/firebaseAdmin'
const favoriteFoods = ['donuts', 'apples', 'pancakes', 'kale']

const getFood = async (req, res) => {
const token = req.headers.token
try {
await verifyIdToken(token)
return res.status(200).json({
food: favoriteFoods[Math.floor(Math.random() * favoriteFoods.length)],
})
} catch (error) {
return res.status(401).json({ error: 'Unauthorized.' })
}
}

export default getFood
32 changes: 32 additions & 0 deletions examples/with-firebase-authentication-ssr/pages/api/login.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import cookiesMiddleware from 'utils/middleware/cookies'
import { getCustomIdAndRefreshTokens } from 'utils/auth/firebaseAdmin'
import { AUTH_COOKIE_NAME } from 'utils/constants'

const handler = async (req, res) => {
if (!(req.headers && req.headers.authorization)) {
return res.status(400).json({ error: 'Missing Authorization header value' })
}

// This should be the original Firebase ID token from
// the Firebase JS SDK.
const token = req.headers.authorization

// Get a custom ID token and refresh token, given a valid
// Firebase ID token.
const { idToken, refreshToken } = await getCustomIdAndRefreshTokens(token)

// Store the ID and refresh tokens in a cookie. This
// cookie will be available to future requests to pages,
// providing a valid Firebase ID token for server-side rendering.
req.cookie.set(
AUTH_COOKIE_NAME,
JSON.stringify({
idToken,
refreshToken,
})
)

return res.status(200).json({ status: true })
}

export default cookiesMiddleware(handler)
11 changes: 11 additions & 0 deletions examples/with-firebase-authentication-ssr/pages/api/logout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import cookiesMiddleware from 'utils/middleware/cookies'
import { AUTH_COOKIE_NAME } from 'utils/constants'

const handler = async (req, res) => {
// An undefined value will delete the cookie.
// https://github.com/pillarjs/cookies#cookiesset-name--value---options--
req.cookie.set(AUTH_COOKIE_NAME, undefined)
res.status(200).json({ status: true })
}

export default cookiesMiddleware(handler)
15 changes: 15 additions & 0 deletions examples/with-firebase-authentication-ssr/pages/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import FirebaseAuth from '../components/FirebaseAuth'
import withAuthComponent from 'utils/auth/withAuthComponent'

const Auth = () => {
return (
<div>
<p>Sign in</p>
<div>
<FirebaseAuth />
</div>
</div>
)
}

export default withAuthComponent({ redirectIfAuthed: true })(Auth)
37 changes: 37 additions & 0 deletions examples/with-firebase-authentication-ssr/pages/example-2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Link from 'next/link'
import useAuthUser from 'utils/auth/useAuthUser'
import withAuthComponent from 'utils/auth/withAuthComponent'
import withAuthServerSideProps from 'utils/auth/withAuthServerSideProps'

const ExampleTwo = (props) => {
const AuthUser = useAuthUser()
return (
<div>
<p>
This page is does not require user auth, so it won't redirect to the
login page if you are not signed in.
</p>
<p>
If you remove getServerSideProps from this page, the page will be static
and load the authed user on the client side.
</p>
{AuthUser.id ? (
<p>You're signed in. Email: {AuthUser.email}</p>
) : (
<p>
You are not signed in.{' '}
<Link href={'/auth'}>
<a>Sign in</a>
</Link>
</p>
)}
<Link href={'/'}>
<a>Home</a>
</Link>
</div>
)
}

export const getServerSideProps = withAuthServerSideProps()()

export default withAuthComponent({ authRequired: false })(ExampleTwo)
16 changes: 16 additions & 0 deletions examples/with-firebase-authentication-ssr/pages/example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Link from 'next/link'

const Example = (props) => {
return (
<div>
<p>
This page is static. It does not fetch any data or use the authed user.
</p>
<Link href={'/'}>
<a>Home</a>
</Link>
</div>
)
}

export default Example
89 changes: 89 additions & 0 deletions examples/with-firebase-authentication-ssr/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import useSWR from 'swr'
import Link from 'next/link'
import useAuthUser from 'utils/auth/useAuthUser'
import withAuthComponent from 'utils/auth/withAuthComponent'
import withAuthServerSideProps from 'utils/auth/withAuthServerSideProps'
import logout from 'utils/auth/logout'

const getAbsoluteURL = (url, req = null) => {
let host
if (req) {
host = req.headers.host
} else {
if (typeof window === undefined) {
throw new Error(
'The "req" parameter must be provided if on the server side.'
)
}
host = window.location.host
}
const isLocalhost = host.indexOf('localhost') === 0
const protocol = isLocalhost ? 'http' : 'https'
return `${protocol}://${host}${url}`
}

const fetcher = async (url, token) => {
const res = await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json', token },
credentials: 'same-origin',
})
const resJSON = await res.json()
if (!res.ok) {
throw new Error(`Error fetching data: ${JSON.stringify(resJSON)}`)
}
return resJSON
}

const endpoint = '/api/getFood'

const Index = (props) => {
const AuthUser = useAuthUser()
const initialData = props.data
const fetchWithToken = async (url) => {
const token = await AuthUser.getIdToken()
return fetcher(getAbsoluteURL(url), token)
}
const { data } = useSWR(endpoint, fetchWithToken, { initialData })

return (
<div>
<div>
<p>You're signed in. Email: {AuthUser.email}</p>
{data ? <p>Your favorite food is {data.food}.</p> : <p>Loading...</p>}
<p
style={{
display: 'inline-block',
color: 'blue',
textDecoration: 'underline',
cursor: 'pointer',
}}
onClick={async () => await logout()}
>
Log out
</p>
</div>
<div>
<Link href={'/example'}>
<a>Example static page</a>
</Link>
</div>
<div>
<Link href={'/example-2'}>
<a>Example with optional auth</a>
</Link>
</div>
</div>
)
}

export const getServerSideProps = withAuthServerSideProps({
authRequired: true,
})(async (ctx) => {
const { AuthUser, req } = ctx
const token = await AuthUser.getIdToken()
const data = await fetcher(getAbsoluteURL(endpoint, req), token)
return { data: data }
})

export default withAuthComponent({ authRequired: true })(Index)
Loading