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>
This commit is contained in:
parent
04f1dd52b9
commit
34f1aefa4a
24 changed files with 866 additions and 0 deletions
22
examples/with-firebase-authentication-serverless/.env
Normal file
22
examples/with-firebase-authentication-serverless/.env
Normal file
|
@ -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
examples/with-firebase-authentication-serverless/.gitignore
vendored
Normal file
28
examples/with-firebase-authentication-serverless/.gitignore
vendored
Normal file
|
@ -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
examples/with-firebase-authentication-serverless/README.md
Normal file
51
examples/with-firebase-authentication-serverless/README.md
Normal file
|
@ -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
examples/with-firebase-authentication-serverless/env.js
Normal file
36
examples/with-firebase-authentication-serverless/env.js
Normal file
|
@ -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,
|
||||
})
|
||||
}
|
||||
})
|
|
@ -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,
|
||||
},
|
||||
}
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
114
examples/with-firebase-authentication-serverless/pages/index.js
Normal file
114
examples/with-firebase-authentication-serverless/pages/index.js
Normal file
|
@ -0,0 +1,114 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { get } from 'lodash/object'
|
||||
import Link from 'next/link'
|
||||
import Router from 'next/router'
|
||||
import withAuthUser from '../utils/pageWrappers/withAuthUser'
|
||||
import withAuthUserInfo from '../utils/pageWrappers/withAuthUserInfo'
|
||||
import logout from '../utils/auth/logout'
|
||||
|
||||
const Index = props => {
|
||||
const { AuthUserInfo, data } = props
|
||||
const AuthUser = get(AuthUserInfo, 'AuthUser', null)
|
||||
const { favoriteFood } = data
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Hi there!</p>
|
||||
{!AuthUser ? (
|
||||
<p>
|
||||
You are not signed in.{' '}
|
||||
<Link href={'/auth'}>
|
||||
<a>Sign in</a>
|
||||
</Link>
|
||||
</p>
|
||||
) : (
|
||||
<div>
|
||||
<p>You're signed in. Email: {AuthUser.email}</p>
|
||||
<p
|
||||
style={{
|
||||
display: 'inlinelock',
|
||||
color: 'blue',
|
||||
textDecoration: 'underline',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await logout()
|
||||
Router.push('/auth')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Log out
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Link href={'/example'}>
|
||||
<a>Another example page</a>
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<div>Your favorite food is {favoriteFood}.</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Just an example.
|
||||
const mockFetchData = async userId => ({
|
||||
user: {
|
||||
...(userId && {
|
||||
id: userId,
|
||||
}),
|
||||
},
|
||||
favoriteFood: 'pizza',
|
||||
})
|
||||
|
||||
Index.getInitialProps = async ctx => {
|
||||
// Get the AuthUserInfo object. This is set in `withAuthUser.js`.
|
||||
// The AuthUserInfo object is available on both the server and client.
|
||||
const AuthUserInfo = get(ctx, 'myCustomData.AuthUserInfo', null)
|
||||
const AuthUser = get(AuthUserInfo, 'AuthUser', null)
|
||||
|
||||
// You can also get the token (e.g., to authorize a request when fetching data)
|
||||
// const AuthUserToken = get(AuthUserInfo, 'token', null)
|
||||
|
||||
// You can fetch data here.
|
||||
const data = await mockFetchData(get(AuthUser, 'id'))
|
||||
|
||||
return {
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
Index.displayName = 'Index'
|
||||
|
||||
Index.propTypes = {
|
||||
AuthUserInfo: PropTypes.shape({
|
||||
AuthUser: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
email: PropTypes.string.isRequired,
|
||||
emailVerified: PropTypes.bool.isRequired,
|
||||
}),
|
||||
token: PropTypes.string,
|
||||
}),
|
||||
data: PropTypes.shape({
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
}).isRequired,
|
||||
favoriteFood: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
Index.defaultProps = {
|
||||
AuthUserInfo: null,
|
||||
}
|
||||
|
||||
// Use `withAuthUser` to get the authed user server-side, which
|
||||
// disables static rendering.
|
||||
// Use `withAuthUserInfo` to include the authed user as a prop
|
||||
// to your component.
|
||||
export default withAuthUser(withAuthUserInfo(Index))
|
|
@ -0,0 +1,24 @@
|
|||
import * as admin from 'firebase-admin'
|
||||
|
||||
export const verifyIdToken = token => {
|
||||
const firebasePrivateKey = process.env.FIREBASE_PRIVATE_KEY
|
||||
|
||||
if (!admin.apps.length) {
|
||||
admin.initializeApp({
|
||||
credential: admin.credential.cert({
|
||||
projectId: process.env.FIREBASE_PROJECT_ID,
|
||||
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
|
||||
// https://stackoverflow.com/a/41044630/1332513
|
||||
privateKey: firebasePrivateKey.replace(/\\n/g, '\n'),
|
||||
}),
|
||||
databaseURL: process.env.FIREBASE_DATABASE_URL,
|
||||
})
|
||||
}
|
||||
|
||||
return admin
|
||||
.auth()
|
||||
.verifyIdToken(token)
|
||||
.catch(error => {
|
||||
throw error
|
||||
})
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// From:
|
||||
// https://github.com/zeit/next.js/blob/canary/examples/with-firebase-authentication/pages/index.js
|
||||
|
||||
import fetch from 'isomorphic-unfetch'
|
||||
|
||||
export const setSession = user => {
|
||||
// Log in.
|
||||
if (user) {
|
||||
return user.getIdToken().then(token => {
|
||||
return fetch('/api/login', {
|
||||
method: 'POST',
|
||||
// eslint-disable-next-line no-undef
|
||||
headers: new Headers({ 'Content-Type': 'application/json' }),
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ token }),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Log out.
|
||||
return fetch('/api/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import firebase from 'firebase/app'
|
||||
import 'firebase/auth'
|
||||
import initFirebase from './initFirebase'
|
||||
import { setSession } from './firebaseSessionHandler'
|
||||
import { createAuthUserInfo } from './user'
|
||||
|
||||
initFirebase()
|
||||
|
||||
// https://benmcmahen.com/using-firebase-with-react-hooks/
|
||||
|
||||
// Defaults to empty AuthUserInfo object.
|
||||
export const AuthUserInfoContext = React.createContext(createAuthUserInfo())
|
||||
|
||||
export const useAuthUserInfo = () => {
|
||||
return React.useContext(AuthUserInfoContext)
|
||||
}
|
||||
|
||||
// Returns a Firebase JS SDK user object.
|
||||
export const useFirebaseAuth = () => {
|
||||
const [state, setState] = useState(() => {
|
||||
const user = firebase.auth().currentUser
|
||||
return {
|
||||
initializing: !user,
|
||||
user,
|
||||
}
|
||||
})
|
||||
|
||||
function onChange(user) {
|
||||
setState({ initializing: false, user })
|
||||
|
||||
// Call server to update session.
|
||||
setSession(user)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Listen for auth state changes.
|
||||
const unsubscribe = firebase.auth().onAuthStateChanged(onChange)
|
||||
|
||||
// Unsubscribe to the listener when unmounting.
|
||||
return () => unsubscribe()
|
||||
}, [])
|
||||
|
||||
return state
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import firebase from 'firebase/app'
|
||||
import 'firebase/auth'
|
||||
|
||||
const config = {
|
||||
apiKey: process.env.FIREBASE_PUBLIC_API_KEY,
|
||||
authDomain: process.env.FIREBASE_AUTH_DOMAIN,
|
||||
databaseURL: process.env.FIREBASE_DATABASE_URL,
|
||||
projectId: process.env.FIREBASE_PROJECT_ID,
|
||||
}
|
||||
|
||||
export default () => {
|
||||
if (!firebase.apps.length) {
|
||||
firebase.initializeApp(config)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/* globals window */
|
||||
import firebase from 'firebase/app'
|
||||
import 'firebase/auth'
|
||||
|
||||
export default async () => {
|
||||
return firebase
|
||||
.auth()
|
||||
.signOut()
|
||||
.then(() => {
|
||||
// Sign-out successful.
|
||||
if (typeof window !== 'undefined') {
|
||||
// Remove the server-side rendered user data element. See:
|
||||
// https://github.com/zeit/next.js/issues/2252#issuecomment-353992669
|
||||
try {
|
||||
const elem = window.document.getElementById('__MY_AUTH_USER_INFO')
|
||||
elem.parentNode.removeChild(elem)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e)
|
||||
return false
|
||||
})
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import { get, has } from 'lodash/object'
|
||||
|
||||
/**
|
||||
* Take the user object from Firebase (from either the Firebase admin SDK or
|
||||
* or the client-side Firebase JS SDK) and return a consistent AuthUser object.
|
||||
* @param {Object} firebaseUser - A decoded Firebase user token or JS SDK
|
||||
* Firebase user object.
|
||||
* @return {Object|null} AuthUser - The user object.
|
||||
* @return {String} AuthUser.id - The user's ID
|
||||
* @return {String} AuthUser.email - The user's email
|
||||
* @return {Boolean} AuthUser.emailVerified - Whether the user has verified their email
|
||||
*/
|
||||
export const createAuthUser = firebaseUser => {
|
||||
if (!firebaseUser || !firebaseUser.uid) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
id: get(firebaseUser, 'uid'),
|
||||
email: get(firebaseUser, 'email'),
|
||||
emailVerified: has(firebaseUser, 'emailVerified')
|
||||
? get(firebaseUser, 'emailVerified') // Firebase JS SDK
|
||||
: get(firebaseUser, 'email_verified'), // Firebase admin SDK
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an object with an AuthUser object and AuthUserToken value.
|
||||
* @param {Object} firebaseUser - A decoded Firebase user token or JS SDK
|
||||
* Firebase user object.
|
||||
* @param {String} firebaseToken - A Firebase auth token string.
|
||||
* @return {Object|null} AuthUserInfo - The auth user info object.
|
||||
* @return {String} AuthUserInfo.AuthUser - An AuthUser object (see
|
||||
* `createAuthUser` above).
|
||||
* @return {String} AuthUser.token - The user's encoded Firebase token.
|
||||
*/
|
||||
export const createAuthUserInfo = ({
|
||||
firebaseUser = null,
|
||||
token = null,
|
||||
} = {}) => {
|
||||
return {
|
||||
AuthUser: createAuthUser(firebaseUser),
|
||||
token,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import cookieSession from './cookieSession'
|
||||
import cookieSessionRefresh from './cookieSessionRefresh'
|
||||
|
||||
// Load environment variables.
|
||||
require('../../env')
|
||||
|
||||
export default handler => cookieSession(cookieSessionRefresh(handler))
|
|
@ -0,0 +1,41 @@
|
|||
import cookieSession from 'cookie-session'
|
||||
|
||||
export const addSession = (req, res) => {
|
||||
// Ensure that session secrets are set.
|
||||
if (
|
||||
!(process.env.SESSION_SECRET_CURRENT && process.env.SESSION_SECRET_PREVIOUS)
|
||||
) {
|
||||
throw new Error(
|
||||
'Session secrets must be set as env vars `SESSION_SECRET_CURRENT` and `SESSION_SECRET_PREVIOUS`.'
|
||||
)
|
||||
}
|
||||
|
||||
// An array is useful for rotating secrets without invalidating old sessions.
|
||||
// The first will be used to sign cookies, and the rest to validate them.
|
||||
// https://github.com/expressjs/cookie-session#keys
|
||||
const sessionSecrets = [
|
||||
process.env.SESSION_SECRET_CURRENT,
|
||||
process.env.SESSION_SECRET_PREVIOUS,
|
||||
]
|
||||
|
||||
// Example:
|
||||
// https://github.com/billymoon/micro-cookie-session
|
||||
const includeSession = cookieSession({
|
||||
keys: sessionSecrets,
|
||||
// TODO: set other options, such as "secure", "sameSite", etc.
|
||||
// https://github.com/expressjs/cookie-session#cookie-options
|
||||
maxAge: 604800000, // week
|
||||
httpOnly: true,
|
||||
overwrite: true,
|
||||
})
|
||||
includeSession(req, res, () => {})
|
||||
}
|
||||
|
||||
export default handler => (req, res) => {
|
||||
try {
|
||||
addSession(req, res)
|
||||
} catch (e) {
|
||||
return res.status(500).json({ error: 'Could not get user session.' })
|
||||
}
|
||||
return handler(req, res)
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
// Update a value in the cookie so that the set-cookie will be sent.
|
||||
// Only changes every minute so that it's not sent with every request.
|
||||
// https://github.com/expressjs/cookie-session#extending-the-session-expiration
|
||||
export default handler => (req, res) => {
|
||||
if (req.session) {
|
||||
req.session.nowInMinutes = Math.floor(Date.now() / 60e3)
|
||||
}
|
||||
handler(req, res)
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/* eslint react/jsx-props-no-spreading: 0 */
|
||||
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { get, set } from 'lodash/object'
|
||||
import { AuthUserInfoContext, useFirebaseAuth } from '../auth/hooks'
|
||||
import { createAuthUser, createAuthUserInfo } from '../auth/user'
|
||||
|
||||
// Gets the authenticated user from the Firebase JS SDK, when client-side,
|
||||
// or from the request object, when server-side. Add the AuthUserInfo to
|
||||
// context.
|
||||
export default ComposedComponent => {
|
||||
const WithAuthUserComp = props => {
|
||||
const { AuthUserInfo, ...otherProps } = props
|
||||
|
||||
// We'll use the authed user from client-side auth (Firebase JS SDK)
|
||||
// when available. On the server side, we'll use the authed user from
|
||||
// the session. This allows us to server-render while also using Firebase's
|
||||
// client-side auth functionality.
|
||||
const { user: firebaseUser } = useFirebaseAuth()
|
||||
const AuthUserFromClient = createAuthUser(firebaseUser)
|
||||
const { AuthUser: AuthUserFromSession, token } = AuthUserInfo
|
||||
const AuthUser = AuthUserFromClient || AuthUserFromSession || null
|
||||
|
||||
return (
|
||||
<AuthUserInfoContext.Provider value={{ AuthUser, token }}>
|
||||
<ComposedComponent {...otherProps} />
|
||||
</AuthUserInfoContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
WithAuthUserComp.getInitialProps = async ctx => {
|
||||
const { req, res } = ctx
|
||||
|
||||
// Get the AuthUserInfo object.
|
||||
let AuthUserInfo
|
||||
if (typeof window === 'undefined') {
|
||||
// If server-side, get AuthUserInfo from the session in the request.
|
||||
// Don't include server middleware in the client JS bundle. See:
|
||||
// https://arunoda.me/blog/ssr-and-server-only-modules
|
||||
const { addSession } = require('../middleware/cookieSession')
|
||||
addSession(req, res)
|
||||
AuthUserInfo = createAuthUserInfo({
|
||||
firebaseUser: get(req, 'session.decodedToken', null),
|
||||
token: get(req, 'session.token', null),
|
||||
})
|
||||
} else {
|
||||
// If client-side, get AuthUserInfo from stored data. We store it
|
||||
// in _document.js. See:
|
||||
// https://github.com/zeit/next.js/issues/2252#issuecomment-353992669
|
||||
try {
|
||||
const jsonData = JSON.parse(
|
||||
window.document.getElementById('__MY_AUTH_USER_INFO').textContent
|
||||
)
|
||||
if (jsonData) {
|
||||
AuthUserInfo = jsonData
|
||||
} else {
|
||||
// Use the default (unauthed) user info if there's no data.
|
||||
AuthUserInfo = createAuthUserInfo()
|
||||
}
|
||||
} catch (e) {
|
||||
// If there's some error, use the default (unauthed) user info.
|
||||
AuthUserInfo = createAuthUserInfo()
|
||||
}
|
||||
}
|
||||
|
||||
// Explicitly add the user to a custom prop in the getInitialProps
|
||||
// context for ease of use in child components.
|
||||
set(ctx, 'myCustomData.AuthUserInfo', AuthUserInfo)
|
||||
|
||||
// Evaluate the composed component's getInitialProps().
|
||||
let composedInitialProps = {}
|
||||
if (ComposedComponent.getInitialProps) {
|
||||
composedInitialProps = await ComposedComponent.getInitialProps(ctx)
|
||||
}
|
||||
|
||||
return {
|
||||
...composedInitialProps,
|
||||
AuthUserInfo,
|
||||
}
|
||||
}
|
||||
|
||||
WithAuthUserComp.displayName = `WithAuthUser(${ComposedComponent.displayName})`
|
||||
|
||||
WithAuthUserComp.propTypes = {
|
||||
AuthUserInfo: PropTypes.shape({
|
||||
AuthUser: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
email: PropTypes.string.isRequired,
|
||||
emailVerified: PropTypes.bool.isRequired,
|
||||
}),
|
||||
token: PropTypes.string,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
WithAuthUserComp.defaultProps = {}
|
||||
|
||||
return WithAuthUserComp
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/* eslint react/jsx-props-no-spreading: 0 */
|
||||
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { get } from 'lodash/object'
|
||||
import { AuthUserInfoContext } from '../auth/hooks'
|
||||
|
||||
// Provides an AuthUserInfo prop to the composed component.
|
||||
export default ComposedComponent => {
|
||||
const WithAuthUserInfoComp = props => {
|
||||
const { AuthUserInfo: AuthUserInfoFromSession, ...otherProps } = props
|
||||
return (
|
||||
<AuthUserInfoContext.Consumer>
|
||||
{AuthUserInfo => (
|
||||
<ComposedComponent
|
||||
{...otherProps}
|
||||
AuthUserInfo={AuthUserInfo || AuthUserInfoFromSession}
|
||||
/>
|
||||
)}
|
||||
</AuthUserInfoContext.Consumer>
|
||||
)
|
||||
}
|
||||
|
||||
WithAuthUserInfoComp.getInitialProps = async ctx => {
|
||||
const AuthUserInfo = get(ctx, 'myCustomData.AuthUserInfo', null)
|
||||
|
||||
// Evaluate the composed component's getInitialProps().
|
||||
let composedInitialProps = {}
|
||||
if (ComposedComponent.getInitialProps) {
|
||||
composedInitialProps = await ComposedComponent.getInitialProps(ctx)
|
||||
}
|
||||
|
||||
return {
|
||||
...composedInitialProps,
|
||||
AuthUserInfo,
|
||||
}
|
||||
}
|
||||
|
||||
WithAuthUserInfoComp.displayName = `WithAuthUserInfo(${ComposedComponent.displayName})`
|
||||
|
||||
WithAuthUserInfoComp.propTypes = {
|
||||
AuthUserInfo: PropTypes.shape({
|
||||
AuthUser: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
email: PropTypes.string.isRequired,
|
||||
emailVerified: PropTypes.bool.isRequired,
|
||||
}),
|
||||
token: PropTypes.string,
|
||||
}),
|
||||
}
|
||||
|
||||
WithAuthUserInfoComp.defaultProps = {}
|
||||
|
||||
return WithAuthUserInfoComp
|
||||
}
|
Loading…
Reference in a new issue