Skip to content

Commit

Permalink
Add example: with-firebase-authentication-serverless (#10078)
Browse files Browse the repository at this point in the history
* Start from existing example

* Upgrade some dependencies

* Use dotenv

* Remove custom server

* Add serverless Firebase auth

* Add TODOs

* Update project name

* Fix build script

* Remove server middleware from client JS bundle

* Add logout functionality

* Redirect to auth page on logout

* Remove TODO

* Add comments about the cookie-session approach

* Remove the sessions folder

* Add comments for eslint

* Remove unused files

* Clarify comment

* Update README.md

* Rename variable for clarity

* Update README.md

* Change some comments

* Add more to gitignore

* Remove the bundle analyzer

* Move server-side auth user logic from _app.js to a HOC to support static HTML rendering

Co-authored-by: Joe Haddad <timer150@gmail.com>
  • Loading branch information
kmjennison and Timer committed Jan 20, 2020
1 parent 04f1dd5 commit 34f1aef
Show file tree
Hide file tree
Showing 24 changed files with 866 additions and 0 deletions.
22 changes: 22 additions & 0 deletions examples/with-firebase-authentication-serverless/.env
@@ -0,0 +1,22 @@

# For variables you need accessible at build time, add the variable to
# next.config.js. For secret values in local development, add the variable
# to .env.local, outside of source control.

# Update these with your Firebase app's values.
FIREBASE_AUTH_DOMAIN=my-example-app.firebaseapp.com
FIREBASE_CLIENT_EMAIL=my-example-app-email@example.com
FIREBASE_DATABASE_URL=https://my-example-app.firebaseio.com
FIREBASE_PROJECT_ID=my-example-app-id
FIREBASE_PUBLIC_API_KEY=MyExampleAppAPIKey123

# Create another file in this directory named ".env.local", which you
# should not include in source control. In .env.local, set these secret
# environment variables:

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

# Secrets used by cookie-session.
# SESSION_SECRET_CURRENT=someSecretValue
# SESSION_SECRET_PREVIOUS=anotherSecretValue
28 changes: 28 additions & 0 deletions examples/with-firebase-authentication-serverless/.gitignore
@@ -0,0 +1,28 @@
# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
.env.*local

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Local dotenv files. We're following the file structure used in
# create-react-app and documented in the Ruby dotenv:
# https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
.env.*local
51 changes: 51 additions & 0 deletions examples/with-firebase-authentication-serverless/README.md
@@ -0,0 +1,51 @@
# Example: Firebase authentication with a serverless API

## How to use

### Using `create-next-app`

Execute [`create-next-app`](https://github.com/zeit/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-serverless with-firebase-authentication-serverless-app
# or
yarn create next-app --example with-firebase-authentication-serverless with-firebase-authentication-serverless-app
```

### Download manually

Download the example:

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

Set up Firebase:

- Create a project at the [Firebase console](https://console.firebase.google.com/).
- 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` 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` file at the root of this project.
- Set the environment variables `SESSION_SECRET_CURRENT` and `SESSION_SECRET_PREVIOUS` in the `.env` file. (These are used by [`cookie-session`](https://github.com/expressjs/cookie-session/#secret).]

Install it and run:

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

Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download))

```bash
now
```

After `now` successfully deploys, a URL will for your site will be displayed. Copy that 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.

## The idea behind the example

This example includes Firebase authentication and serverless [API routes](https://nextjs.org/docs/api-routes/introduction). On login, the app calls `/api/login`, which stores the user's info (their decoded Firebase token) in a cookie so that it's available server-side in `getInitialProps`. On logout, the app calls `/api/logout` to destroy the cookie.
@@ -0,0 +1,46 @@
/* globals window */
import React, { 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',
}

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
36 changes: 36 additions & 0 deletions examples/with-firebase-authentication-serverless/env.js
@@ -0,0 +1,36 @@
// Responsible for setting environment variables.
// Note: this isn't strictly required for this example – you can
// inline your Firebase config or set environment variables howevever
// else you wish – but it's a convenient way to make sure the private
// key doesn't end up in source control.

const fs = require('fs')

const { NODE_ENV } = process.env
if (!NODE_ENV) {
throw new Error(
'The NODE_ENV environment variable is required but was not specified.'
)
}

// Set env vars from appropiate `.env` files. We're following the
// file structure used in create-react-app and documented in the
// Ruby dotenv. See:
// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
const dotEnvPath = './.env'
const dotEnvFiles = [
`${dotEnvPath}.${NODE_ENV}.local`,
`${dotEnvPath}.${NODE_ENV}`,
// Don't include `.env.local` for the test environment.
NODE_ENV !== 'test' && `${dotEnvPath}.local`,
dotEnvPath,
].filter(Boolean)

dotEnvFiles.forEach(dotenvFile => {
if (fs.existsSync(dotenvFile)) {
// eslint-disable-next-line global-require
require('dotenv').config({
path: dotenvFile,
})
}
})
12 changes: 12 additions & 0 deletions examples/with-firebase-authentication-serverless/next.config.js
@@ -0,0 +1,12 @@
require('./env.js')

module.exports = {
// Public, build-time env vars.
// https://nextjs.org/docs#build-time-configuration
env: {
FIREBASE_AUTH_DOMAIN: process.env.FIREBASE_AUTH_DOMAIN,
FIREBASE_DATABASE_URL: process.env.FIREBASE_DATABASE_URL,
FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID,
FIREBASE_PUBLIC_API_KEY: process.env.FIREBASE_PUBLIC_API_KEY,
},
}
23 changes: 23 additions & 0 deletions examples/with-firebase-authentication-serverless/package.json
@@ -0,0 +1,23 @@
{
"name": "with-firebase-auth-serverless",
"version": "1.0.0",
"scripts": {
"dev": "NODE_ENV=development next dev",
"build": "NODE_ENV=production next build",
"start": "NODE_ENV=production next start"
},
"dependencies": {
"cookie-session": "1.4.0",
"dotenv": "8.2.0",
"firebase": "^7.6.1",
"firebase-admin": "^8.9.0",
"isomorphic-unfetch": "^3.0.0",
"lodash": "4.17.15",
"next": "latest",
"prop-types": "15.7.2",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-firebaseui": "4.0.0"
},
"devDependencies": {}
}
@@ -0,0 +1,55 @@
/* eslint react/no-danger: 0 */
import React from 'react'
import PropTypes from 'prop-types'
import { get } from 'lodash/object'
import Document, { Html, Head, Main, NextScript } from 'next/document'

class CustomDocument extends Document {
render() {
// Store initial props from request data that we need to use again on
// the client. See:
// https://github.com/zeit/next.js/issues/3043#issuecomment-334521241
// https://github.com/zeit/next.js/issues/2252#issuecomment-353992669
// Alternatively, you could use a store, like Redux.
const { AuthUserInfo } = this.props
return (
<Html>
<Head>
<script
id="__MY_AUTH_USER_INFO"
type="application/json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(AuthUserInfo, null, 2),
}}
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}

CustomDocument.getInitialProps = async ctx => {
// Get the AuthUserInfo object. This is set if the server-rendered page
// is wrapped in the `withAuthUser` higher-order component.
const AuthUserInfo = get(ctx, 'myCustomData.AuthUserInfo', null)

const initialProps = await Document.getInitialProps(ctx)
return { ...initialProps, AuthUserInfo }
}

CustomDocument.propTypes = {
AuthUserInfo: PropTypes.shape({
AuthUser: PropTypes.shape({
id: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
emailVerified: PropTypes.bool.isRequired,
}),
token: PropTypes.string,
}).isRequired,
}

export default CustomDocument
@@ -0,0 +1,37 @@
import commonMiddleware from '../../utils/middleware/commonMiddleware'
import { verifyIdToken } from '../../utils/auth/firebaseAdmin'

const handler = (req, res) => {
if (!req.body) {
return res.status(400)
}

const { token } = req.body

// Here, we decode the user's Firebase token and store it in a cookie. Use
// express-session (or similar) to store the session data server-side.
// An alternative approach is to use Firebase's `createSessionCookie`. See:
// https://firebase.google.com/docs/auth/admin/manage-cookies
// Firebase docs:
// "This is a low overhead operation. The public certificates are initially
// queried and cached until they expire. Session cookie verification can be
// done with the cached public certificates without any additional network
// requests."
// However, in a serverless environment, we shouldn't rely on caching, so
// it's possible Firebase's `verifySessionCookie` will make frequent network
// requests in a serverless context.
return verifyIdToken(token)
.then(decodedToken => {
req.session.decodedToken = decodedToken
req.session.token = token
return decodedToken
})
.then(decodedToken => {
return res.status(200).json({ status: true, decodedToken })
})
.catch(error => {
return res.status(500).json({ error })
})
}

export default commonMiddleware(handler)
@@ -0,0 +1,10 @@
import commonMiddleware from '../../utils/middleware/commonMiddleware'

const handler = (req, res) => {
// Destroy the session.
// https://github.com/expressjs/cookie-session#destroying-a-session
req.session = null
res.status(200).json({ status: true })
}

export default commonMiddleware(handler)
17 changes: 17 additions & 0 deletions examples/with-firebase-authentication-serverless/pages/auth.js
@@ -0,0 +1,17 @@
import React from 'react'
import FirebaseAuth from '../components/FirebaseAuth'

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

Auth.propTypes = {}

export default Auth
24 changes: 24 additions & 0 deletions examples/with-firebase-authentication-serverless/pages/example.js
@@ -0,0 +1,24 @@
import React from 'react'
import Link from 'next/link'

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

Example.displayName = 'Example'

Example.propTypes = {}

Example.defaultProps = {}

export default Example

0 comments on commit 34f1aef

Please sign in to comment.