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 with Magic authentication #11810

Merged
merged 15 commits into from
Apr 22, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
2 changes: 2 additions & 0 deletions examples/magic/.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
MAGIC_PUBLISHABLE_KEY=
MAGIC_SECRET_KEY=
3 changes: 3 additions & 0 deletions examples/magic/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.env

.now
62 changes: 62 additions & 0 deletions examples/magic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# 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. You can use any db you want and add it [here](/lib/user.js).

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.

## Configure Magic

Login to the [Magic Dashboard](https://dashboard.magic.link/) and add new application to get your keys. Don't forget to put keys in `.env` (look for `.env.template` for example) and upload them as secrets to [ZEIT Now](https://zeit.co/now).

```
now secrets add @magic-publishable-key pk_test_*********
```

```
now secrets add @magic-secret-key sk_test_*********
```

![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)

## Deploy your own

Deploy the example using [ZEIT Now](https://zeit.co/now):

[![Deploy with ZEIT Now](https://zeit.co/button)](https://zeit.co/new/project?template=https://github.com/zeit/next.js/tree/canary/examples/magic)

## How to use

### Using `create-next-app`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Timer Should we remove this title too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lfades @Timer following up on this!~


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 magic magic-app
# or
yarn create next-app --example magic magic-app
```

### Download manually

Download the example [or clone the repo](https://github.com/zeit/next.js):

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

Install it and run:

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

seanli marked this conversation as resolved.
Show resolved Hide resolved
Deploy it to the cloud with [ZEIT Now](https://zeit.co/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
56 changes: 56 additions & 0 deletions examples/magic/components/form.js
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
67 changes: 67 additions & 0 deletions examples/magic/components/header.js
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
38 changes: 38 additions & 0 deletions examples/magic/components/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Head from 'next/head'
import Header from './header'

const Layout = props => (
<>
<Head>
<title>Magic</title>
</Head>

<Header />

<main>
<div className="container">{props.children}</div>
</main>

<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;
}
`}</style>
</>
)

export default Layout
39 changes: 39 additions & 0 deletions examples/magic/lib/auth-cookies.js
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]
}
31 changes: 31 additions & 0 deletions examples/magic/lib/hooks.js
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
}
14 changes: 14 additions & 0 deletions examples/magic/lib/iron.js
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)
}
18 changes: 18 additions & 0 deletions examples/magic/lib/magic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { findUser } from './user'

const { Magic } = require('@magic-sdk/admin')
const magic = new Magic(process.env.MAGIC_SECRET_KEY)

const MagicStrategy = require('passport-magic').Strategy

export const strategy = new MagicStrategy(async function(user, done) {
const userMetadata = await magic.users.getMetadataByIssuer(user.issuer)
// In real application, if existing user doesn't exist, create new user based on email
findUser({ email: userMetadata.email, issuer: userMetadata.issuer })
.then(user => {
done(null, user)
})
.catch(error => {
done(error)
})
})
14 changes: 14 additions & 0 deletions examples/magic/lib/user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// import crypto from 'crypto'

/**
* User methods. The example doesn't contain a DB, but for real applications you must use a
* db here, such as MongoDB, Fauna, SQL, etc.
*/

export async function findUser({ email, issuer }) {
// Here you should lookup for the user in your DB and compare the email:
//
// const user = await DB.findUser(...)

return { email, issuer, createdAt: Date.now() }
}
16 changes: 16 additions & 0 deletions examples/magic/next.config.js
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,
},
})
12 changes: 12 additions & 0 deletions examples/magic/now.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"env": {
"MAGIC_PUBLISHABLE_KEY": "@magic-publishable-key",
"MAGIC_SECRET_KEY": "@magic-secret-key"
},
"build": {
"env": {
"MAGIC_PUBLISHABLE_KEY": "@magic-publishable-key",
"MAGIC_SECRET_KEY": "@magic-secret-key"
}
}
}
Loading