-
Notifications
You must be signed in to change notification settings - Fork 27.3k
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 Magic authentication (#11810)
* Add example with Magic and Passport.js * Tweaked wording on README * Fixed lint error * Fixed prettier error * Update examples/magic/README.md Removed Download manually section from README Co-Authored-By: Joe Haddad <[email protected]> * Removed dependency on passport and express + cleanup * Changed ZEIT brand to Vercel * Updated readme instructions and secrets * Renamed example * Changed db comment Co-authored-by: Joe Haddad <[email protected]> Co-authored-by: Luis Alvarez <[email protected]>
- Loading branch information
1 parent
7d2ff81
commit 91adb86
Showing
21 changed files
with
549 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,2 @@ | ||
MAGIC_PUBLISHABLE_KEY= | ||
MAGIC_SECRET_KEY= |
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,2 @@ | ||
.env | ||
.now |
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,53 @@ | ||
# Magic Example | ||
|
||
This example show how to use [Magic](https://magic.link) with Next.js. The example features cookie-based, passwordless authentication with email-based magic links. | ||
|
||
The example shows how to do a login and logout; and to get the user info using a hook with [SWR](https://swr.now.sh). | ||
|
||
A DB is not included. But you can add any DB you like!. | ||
|
||
The login cookie is `httpOnly`, meaning it can only be accessed by the API, and it's encrypted using [@hapi/iron](https://hapi.dev/family/iron) for more security. | ||
|
||
## Deploy your own | ||
|
||
Deploy the example using [Vercel Now](https://vercel.com/docs/now-cli#commands/overview/basic-usage): | ||
|
||
[![Deploy with Vercel Now](https://vercel.com/button)](https://vercel.com/new/project?template=https://github.com/zeit/next.js/tree/canary/examples/with-magic) | ||
|
||
## How to use | ||
|
||
### Using `create-next-app` | ||
|
||
Execute [`create-next-app`](https://github.com/zeit/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example: | ||
|
||
```bash | ||
npm init next-app --example with-magic with-magic-app | ||
# or | ||
yarn create next-app --example with-magic with-magic-app | ||
``` | ||
|
||
## Configuration | ||
|
||
Login to the [Magic Dashboard](https://dashboard.magic.link/) and get the keys of your application | ||
|
||
![Magic Dashboard](https://gblobscdn.gitbook.com/assets%2F-M1XNjqusnKyXZc7t7qQ%2F-M3HsSftOAghkNs-ttU3%2F-M3HsllfdwdDmeFXBK3U%2Fdashboard-pk.png?alt=media&token=4d6e7543-ae20-4355-951c-c6421b8f1b5f) | ||
|
||
Next, copy the `.env.example` file in this directory to .env (which will be ignored by Git): | ||
|
||
```bash | ||
cp .env.example .env | ||
``` | ||
|
||
Then set each variable on `.env`: | ||
|
||
- `MAGIC_PUBLISHABLE_KEY` should look like `pk_test_abc` or `pk_live_ABC` | ||
- `MAGIC_SECRET_KEY` should look like `sk_test_ABC` or `sk_live_ABC` | ||
|
||
To deploy on Vercel, you need to set the environment variables with **Now Secrets** using [Now CLI](https://vercel.com/download) ([Documentation](https://vercel.com/docs/now-cli#commands/secrets)). | ||
|
||
Install [Now CLI](https://vercel.com/download), log in to your account from the CLI, and run the following commands to add the environment variables. Replace `<MAGIC_PUBLISHABLE_KEY>` and `<MAGIC_SECRET_KEY>` with the corresponding strings in `.env`: | ||
|
||
```bash | ||
now secrets add next_example_magic_publishable_key <MAGIC_PUBLISHABLE_KEY> | ||
now secrets add next_example_magic_secret_key <MAGIC_SECRET_KEY> | ||
``` |
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,56 @@ | ||
const Form = ({ errorMessage, onSubmit }) => ( | ||
<form onSubmit={onSubmit}> | ||
<label> | ||
<span>Email</span> | ||
<input type="email" name="email" required /> | ||
</label> | ||
|
||
<div className="submit"> | ||
<button type="submit">Sign Up / Login</button> | ||
</div> | ||
|
||
{errorMessage && <p className="error">{errorMessage}</p>} | ||
|
||
<style jsx>{` | ||
form, | ||
label { | ||
display: flex; | ||
flex-flow: column; | ||
} | ||
label > span { | ||
font-weight: 600; | ||
} | ||
input { | ||
padding: 8px; | ||
margin: 0.3rem 0 1rem; | ||
border: 1px solid #ccc; | ||
border-radius: 4px; | ||
} | ||
.submit { | ||
display: flex; | ||
justify-content: flex-end; | ||
align-items: center; | ||
justify-content: space-between; | ||
} | ||
.submit > a { | ||
text-decoration: none; | ||
} | ||
.submit > button { | ||
padding: 0.5rem 1rem; | ||
cursor: pointer; | ||
background: #fff; | ||
border: 1px solid #ccc; | ||
border-radius: 4px; | ||
} | ||
.submit > button:hover { | ||
border-color: #888; | ||
} | ||
.error { | ||
color: brown; | ||
margin: 1rem 0 0; | ||
} | ||
`}</style> | ||
</form> | ||
) | ||
|
||
export default Form |
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,67 @@ | ||
import Link from 'next/link' | ||
import { useUser } from '../lib/hooks' | ||
|
||
const Header = () => { | ||
const user = useUser() | ||
|
||
return ( | ||
<header> | ||
<nav> | ||
<ul> | ||
<li> | ||
<Link href="/"> | ||
<a>Home</a> | ||
</Link> | ||
</li> | ||
{user ? ( | ||
<> | ||
<li> | ||
<Link href="/profile"> | ||
<a>Profile</a> | ||
</Link> | ||
</li> | ||
<li> | ||
<a href="/api/logout">Logout</a> | ||
</li> | ||
</> | ||
) : ( | ||
<li> | ||
<Link href="/login"> | ||
<a>Login</a> | ||
</Link> | ||
</li> | ||
)} | ||
</ul> | ||
</nav> | ||
<style jsx>{` | ||
nav { | ||
max-width: 42rem; | ||
margin: 0 auto; | ||
padding: 0.2rem 1.25rem; | ||
} | ||
ul { | ||
display: flex; | ||
list-style: none; | ||
margin-left: 0; | ||
padding-left: 0; | ||
} | ||
li { | ||
margin-right: 1rem; | ||
} | ||
li:first-child { | ||
margin-left: auto; | ||
} | ||
a { | ||
color: #fff; | ||
text-decoration: none; | ||
} | ||
header { | ||
color: #fff; | ||
background-color: #333; | ||
} | ||
`}</style> | ||
</header> | ||
) | ||
} | ||
|
||
export default Header |
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,57 @@ | ||
import Head from 'next/head' | ||
import Header from './header' | ||
|
||
const Layout = props => ( | ||
<> | ||
<Head> | ||
<title>Magic</title> | ||
<link rel="icon" href="/favicon.ico" /> | ||
</Head> | ||
|
||
<Header /> | ||
|
||
<main> | ||
<div className="container">{props.children}</div> | ||
</main> | ||
|
||
<footer> | ||
<a | ||
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app" | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
> | ||
Powered by <img src="/vercel.svg" alt="Vercel Logo" /> | ||
</a> | ||
</footer> | ||
|
||
<style jsx global>{` | ||
*, | ||
*::before, | ||
*::after { | ||
box-sizing: border-box; | ||
} | ||
body { | ||
margin: 0; | ||
color: #333; | ||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, | ||
'Helvetica Neue', Arial, Noto Sans, sans-serif, 'Apple Color Emoji', | ||
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; | ||
} | ||
.container { | ||
max-width: 42rem; | ||
margin: 0 auto; | ||
padding: 2rem 1.25rem; | ||
} | ||
footer { | ||
width: 100%; | ||
height: 100px; | ||
border-top: 1px solid #eaeaea; | ||
display: flex; | ||
justify-content: center; | ||
align-items: center; | ||
} | ||
`}</style> | ||
</> | ||
) | ||
|
||
export default Layout |
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,39 @@ | ||
import { serialize, parse } from 'cookie' | ||
|
||
const TOKEN_NAME = 'token' | ||
const MAX_AGE = 60 * 60 * 8 // 8 hours | ||
|
||
export function setTokenCookie(res, token) { | ||
const cookie = serialize(TOKEN_NAME, token, { | ||
maxAge: MAX_AGE, | ||
expires: new Date(Date.now() + MAX_AGE * 1000), | ||
httpOnly: true, | ||
secure: process.env.NODE_ENV === 'production', | ||
path: '/', | ||
sameSite: 'lax', | ||
}) | ||
res.setHeader('Set-Cookie', cookie) | ||
} | ||
|
||
export function removeTokenCookie(res) { | ||
const cookie = serialize(TOKEN_NAME, '', { | ||
maxAge: -1, | ||
path: '/', | ||
}) | ||
|
||
res.setHeader('Set-Cookie', cookie) | ||
} | ||
|
||
export function parseCookies(req) { | ||
// For API Routes we don't need to parse the cookies. | ||
if (req.cookies) return req.cookies | ||
|
||
// For pages we do need to parse the cookies. | ||
const cookie = req.headers?.cookie | ||
return parse(cookie || '') | ||
} | ||
|
||
export function getTokenCookie(req) { | ||
const cookies = parseCookies(req) | ||
return cookies[TOKEN_NAME] | ||
} |
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,31 @@ | ||
import { useEffect } from 'react' | ||
import Router from 'next/router' | ||
import useSWR from 'swr' | ||
|
||
const fetcher = url => | ||
fetch(url) | ||
.then(r => r.json()) | ||
.then(data => { | ||
return { user: data?.user || null } | ||
}) | ||
|
||
export function useUser({ redirectTo, redirectIfFound } = {}) { | ||
const { data, error } = useSWR('/api/user', fetcher) | ||
const user = data?.user | ||
const finished = Boolean(data) | ||
const hasUser = Boolean(user) | ||
|
||
useEffect(() => { | ||
if (!redirectTo || !finished) return | ||
if ( | ||
// If redirectTo is set, redirect if the user was not found. | ||
(redirectTo && !redirectIfFound && !hasUser) || | ||
// If redirectIfFound is also set, redirect if the user was found | ||
(redirectIfFound && hasUser) | ||
) { | ||
Router.push(redirectTo) | ||
} | ||
}, [redirectTo, redirectIfFound, finished, hasUser]) | ||
|
||
return error ? null : user | ||
} |
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,14 @@ | ||
import Iron from '@hapi/iron' | ||
import { getTokenCookie } from './auth-cookies' | ||
|
||
// Use an environment variable here instead of a hardcoded value for production | ||
const TOKEN_SECRET = 'this-is-a-secret-value-with-at-least-32-characters' | ||
|
||
export function encryptSession(session) { | ||
return Iron.seal(session, TOKEN_SECRET, Iron.defaults) | ||
} | ||
|
||
export async function getSession(req) { | ||
const token = getTokenCookie(req) | ||
return token && Iron.unseal(token, TOKEN_SECRET, Iron.defaults) | ||
} |
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,3 @@ | ||
const { Magic } = require('@magic-sdk/admin') | ||
|
||
export const magic = new Magic(process.env.MAGIC_SECRET_KEY) |
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,16 @@ | ||
const nextEnv = require('next-env') | ||
const dotenvLoad = require('dotenv-load') | ||
|
||
dotenvLoad() | ||
|
||
const withNextEnv = nextEnv() | ||
|
||
module.exports = withNextEnv({ | ||
env: { | ||
MAGIC_PUBLISHABLE_KEY: process.env.MAGIC_PUBLISHABLE_KEY, | ||
MAGIC_SECRET_KEY: process.env.MAGIC_SECRET_KEY, | ||
}, | ||
devIndicators: { | ||
autoPrerender: false, | ||
}, | ||
}) |
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 @@ | ||
{ | ||
"env": { | ||
"MAGIC_PUBLISHABLE_KEY": "@next_example_magic_publishable_key", | ||
"MAGIC_SECRET_KEY": "@next_example_magic_secret_key" | ||
}, | ||
"build": { | ||
"env": { | ||
"MAGIC_PUBLISHABLE_KEY": "@next_example_magic_publishable_key", | ||
"MAGIC_SECRET_KEY": "@next_example_magic_secret_key" | ||
} | ||
} | ||
} |
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,21 @@ | ||
{ | ||
"name": "with-magic", | ||
"scripts": { | ||
"dev": "next", | ||
"build": "next build", | ||
"start": "next start" | ||
}, | ||
"dependencies": { | ||
"@hapi/iron": "6.0.0", | ||
"@magic-sdk/admin": "1.0.0", | ||
"cookie": "0.4.0", | ||
"dotenv-load": "^2.0.0", | ||
"magic-sdk": "1.0.1", | ||
"next": "latest", | ||
"next-env": "^1.1.1", | ||
"react": "latest", | ||
"react-dom": "latest", | ||
"swr": "0.1.16" | ||
}, | ||
"license": "ISC" | ||
} |
Oops, something went wrong.