Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add example: with-firebase-authentication-serverless (#10078)
* 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
1 parent
04f1dd5
commit 34f1aef
Showing
24 changed files
with
866 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
28
examples/with-firebase-authentication-serverless/.gitignore
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
51
examples/with-firebase-authentication-serverless/README.md
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
46 changes: 46 additions & 0 deletions
46
examples/with-firebase-authentication-serverless/components/FirebaseAuth.js
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
12
examples/with-firebase-authentication-serverless/next.config.js
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
23
examples/with-firebase-authentication-serverless/package.json
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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": {} | ||
} |
55 changes: 55 additions & 0 deletions
55
examples/with-firebase-authentication-serverless/pages/_document.js
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
37 changes: 37 additions & 0 deletions
37
examples/with-firebase-authentication-serverless/pages/api/login.js
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
10 changes: 10 additions & 0 deletions
10
examples/with-firebase-authentication-serverless/pages/api/logout.js
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
17
examples/with-firebase-authentication-serverless/pages/auth.js
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
24
examples/with-firebase-authentication-serverless/pages/example.js
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Oops, something went wrong.