From bb200e585afe14a93379bbf46e1dc144d314352b Mon Sep 17 00:00:00 2001
From: Sean Li
Date: Fri, 10 Apr 2020 08:21:56 -0700
Subject: [PATCH 01/10] Add example with Magic and Passport.js
---
examples/magic/.env.template | 2 +
examples/magic/.gitignore | 3 ++
examples/magic/README.md | 61 ++++++++++++++++++++++++++
examples/magic/components/form.js | 58 +++++++++++++++++++++++++
examples/magic/components/header.js | 67 +++++++++++++++++++++++++++++
examples/magic/components/layout.js | 38 ++++++++++++++++
examples/magic/lib/auth-cookies.js | 39 +++++++++++++++++
examples/magic/lib/hooks.js | 31 +++++++++++++
examples/magic/lib/iron.js | 14 ++++++
examples/magic/lib/magic.js | 18 ++++++++
examples/magic/lib/user.js | 14 ++++++
examples/magic/next.config.js | 16 +++++++
examples/magic/now.json | 12 ++++++
examples/magic/package.json | 24 +++++++++++
examples/magic/pages/api/login.js | 40 +++++++++++++++++
examples/magic/pages/api/logout.js | 13 ++++++
examples/magic/pages/api/user.js | 9 ++++
examples/magic/pages/index.js | 36 ++++++++++++++++
examples/magic/pages/login.js | 63 +++++++++++++++++++++++++++
examples/magic/pages/profile.js | 15 +++++++
20 files changed, 573 insertions(+)
create mode 100644 examples/magic/.env.template
create mode 100644 examples/magic/.gitignore
create mode 100644 examples/magic/README.md
create mode 100644 examples/magic/components/form.js
create mode 100644 examples/magic/components/header.js
create mode 100644 examples/magic/components/layout.js
create mode 100644 examples/magic/lib/auth-cookies.js
create mode 100644 examples/magic/lib/hooks.js
create mode 100644 examples/magic/lib/iron.js
create mode 100644 examples/magic/lib/magic.js
create mode 100644 examples/magic/lib/user.js
create mode 100644 examples/magic/next.config.js
create mode 100644 examples/magic/now.json
create mode 100644 examples/magic/package.json
create mode 100644 examples/magic/pages/api/login.js
create mode 100644 examples/magic/pages/api/logout.js
create mode 100644 examples/magic/pages/api/user.js
create mode 100644 examples/magic/pages/index.js
create mode 100644 examples/magic/pages/login.js
create mode 100644 examples/magic/pages/profile.js
diff --git a/examples/magic/.env.template b/examples/magic/.env.template
new file mode 100644
index 0000000000000..3c4727b6476cf
--- /dev/null
+++ b/examples/magic/.env.template
@@ -0,0 +1,2 @@
+MAGIC_PUBLISHABLE_KEY=
+MAGIC_SECRET_KEY=
diff --git a/examples/magic/.gitignore b/examples/magic/.gitignore
new file mode 100644
index 0000000000000..3c97ab2f03f9d
--- /dev/null
+++ b/examples/magic/.gitignore
@@ -0,0 +1,3 @@
+.env
+
+.now
diff --git a/examples/magic/README.md b/examples/magic/README.md
new file mode 100644
index 0000000000000..881a088d1501d
--- /dev/null
+++ b/examples/magic/README.md
@@ -0,0 +1,61 @@
+# 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`
+
+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
+```
+
+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)).
diff --git a/examples/magic/components/form.js b/examples/magic/components/form.js
new file mode 100644
index 0000000000000..739530757061f
--- /dev/null
+++ b/examples/magic/components/form.js
@@ -0,0 +1,58 @@
+import Link from 'next/link'
+
+const Form = ({ errorMessage, onSubmit }) => (
+
+)
+
+export default Form
diff --git a/examples/magic/components/header.js b/examples/magic/components/header.js
new file mode 100644
index 0000000000000..0b605d656739a
--- /dev/null
+++ b/examples/magic/components/header.js
@@ -0,0 +1,67 @@
+import Link from 'next/link'
+import { useUser } from '../lib/hooks'
+
+const Header = () => {
+ const user = useUser()
+
+ return (
+
+ )
+}
+
+export default Header
diff --git a/examples/magic/components/layout.js b/examples/magic/components/layout.js
new file mode 100644
index 0000000000000..107d457280a25
--- /dev/null
+++ b/examples/magic/components/layout.js
@@ -0,0 +1,38 @@
+import Head from 'next/head'
+import Header from './header'
+
+const Layout = props => (
+ <>
+
+ Magic
+
+
+
+
+
+ {props.children}
+
+
+
+ >
+)
+
+export default Layout
diff --git a/examples/magic/lib/auth-cookies.js b/examples/magic/lib/auth-cookies.js
new file mode 100644
index 0000000000000..0ae93e7523988
--- /dev/null
+++ b/examples/magic/lib/auth-cookies.js
@@ -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]
+}
diff --git a/examples/magic/lib/hooks.js b/examples/magic/lib/hooks.js
new file mode 100644
index 0000000000000..645364466d3c0
--- /dev/null
+++ b/examples/magic/lib/hooks.js
@@ -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
+}
diff --git a/examples/magic/lib/iron.js b/examples/magic/lib/iron.js
new file mode 100644
index 0000000000000..977c4b110dd99
--- /dev/null
+++ b/examples/magic/lib/iron.js
@@ -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)
+}
diff --git a/examples/magic/lib/magic.js b/examples/magic/lib/magic.js
new file mode 100644
index 0000000000000..6a361e7d4bfbd
--- /dev/null
+++ b/examples/magic/lib/magic.js
@@ -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)
+ })
+})
diff --git a/examples/magic/lib/user.js b/examples/magic/lib/user.js
new file mode 100644
index 0000000000000..c5381b14d04a2
--- /dev/null
+++ b/examples/magic/lib/user.js
@@ -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() }
+}
diff --git a/examples/magic/next.config.js b/examples/magic/next.config.js
new file mode 100644
index 0000000000000..bbe65ee561f95
--- /dev/null
+++ b/examples/magic/next.config.js
@@ -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,
+ }
+})
diff --git a/examples/magic/now.json b/examples/magic/now.json
new file mode 100644
index 0000000000000..99836df89092d
--- /dev/null
+++ b/examples/magic/now.json
@@ -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"
+ }
+ }
+}
diff --git a/examples/magic/package.json b/examples/magic/package.json
new file mode 100644
index 0000000000000..5f77ab8c4b49f
--- /dev/null
+++ b/examples/magic/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "magic-next",
+ "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",
+ "express": "4.17.1",
+ "magic-sdk": "1.0.1",
+ "next": "latest",
+ "next-env": "^1.1.1",
+ "passport": "0.4.1",
+ "passport-magic": "1.0.0",
+ "react": "latest",
+ "react-dom": "latest",
+ "swr": "0.1.16"
+ },
+ "license": "ISC"
+}
diff --git a/examples/magic/pages/api/login.js b/examples/magic/pages/api/login.js
new file mode 100644
index 0000000000000..83217cd0fb06c
--- /dev/null
+++ b/examples/magic/pages/api/login.js
@@ -0,0 +1,40 @@
+import express from 'express'
+import passport from 'passport'
+import { strategy } from '../../lib/magic'
+import { encryptSession } from '../../lib/iron'
+import { setTokenCookie } from '../../lib/auth-cookies'
+
+const app = express()
+const authenticate = (method, req, res) =>
+ new Promise((resolve, reject) => {
+ passport.authenticate(method, { session: false }, (error, token) => {
+ if (error) {
+ reject(error)
+ } else {
+ resolve(token)
+ }
+ })(req, res)
+ })
+
+app.disable('x-powered-by')
+
+app.use(passport.initialize())
+
+passport.use(strategy)
+
+app.post('/api/login', async (req, res) => {
+ try {
+ const user = await authenticate('magic', req, res)
+ // session is the payload to save in the token, it may contain basic info about the user
+ const session = { ...user }
+ // The token is a string with the encrypted session
+ const token = await encryptSession(session)
+ setTokenCookie(res, token)
+ res.status(200).send({ done: true })
+ } catch (error) {
+ console.error(error)
+ res.status(401).send(error.message)
+ }
+})
+
+export default app
diff --git a/examples/magic/pages/api/logout.js b/examples/magic/pages/api/logout.js
new file mode 100644
index 0000000000000..1e514b2bc3428
--- /dev/null
+++ b/examples/magic/pages/api/logout.js
@@ -0,0 +1,13 @@
+import { removeTokenCookie } from '../../lib/auth-cookies'
+import { getSession } from '../../lib/iron'
+
+const { Magic } = require("@magic-sdk/admin")
+const magic = new Magic(process.env.MAGIC_SECRET_KEY)
+
+export default async function logout(req, res) {
+ const session = await getSession(req)
+ await magic.users.logoutByIssuer(session.issuer)
+ removeTokenCookie(res)
+ res.writeHead(302, { Location: '/' })
+ res.end()
+}
diff --git a/examples/magic/pages/api/user.js b/examples/magic/pages/api/user.js
new file mode 100644
index 0000000000000..dad216c76e9a3
--- /dev/null
+++ b/examples/magic/pages/api/user.js
@@ -0,0 +1,9 @@
+import { getSession } from '../../lib/iron'
+
+export default async function user(req, res) {
+ const session = await getSession(req)
+ // After getting the session you may want to fetch for the user instead
+ // of sending the session's payload directly, this example doesn't have a DB
+ // so it won't matter in this case
+ res.status(200).json({ user: session || null })
+}
diff --git a/examples/magic/pages/index.js b/examples/magic/pages/index.js
new file mode 100644
index 0000000000000..5797758b1f47f
--- /dev/null
+++ b/examples/magic/pages/index.js
@@ -0,0 +1,36 @@
+import { useUser } from '../lib/hooks'
+import Layout from '../components/layout'
+
+const Home = () => {
+ const user = useUser()
+
+ return (
+
+ Magic Example
+
+ Steps to test the example:
+
+
+ - Click Login and enter an email.
+ -
+ You'll be redirected to Home. Click on Profile, notice how your
+ session is being used through a token stored in a cookie.
+
+ -
+ Click Logout and try to go to Profile again. You'll get redirected to
+ Login.
+
+
+
+ {user && Currently logged in as:
{JSON.stringify(user, null, 2)}
}
+
+
+
+ )
+}
+
+export default Home
diff --git a/examples/magic/pages/login.js b/examples/magic/pages/login.js
new file mode 100644
index 0000000000000..2ddfe6ecd3bd0
--- /dev/null
+++ b/examples/magic/pages/login.js
@@ -0,0 +1,63 @@
+import { useState } from 'react'
+import Router from 'next/router'
+import { useUser } from '../lib/hooks'
+import Layout from '../components/layout'
+import Form from '../components/form'
+
+import { Magic } from 'magic-sdk'
+
+const Login = () => {
+ useUser({ redirectTo: '/', redirectIfFound: true })
+
+ const [errorMsg, setErrorMsg] = useState('')
+
+ async function handleSubmit(e) {
+ event.preventDefault()
+
+ if (errorMsg) setErrorMsg('')
+
+ const body = {
+ email: e.currentTarget.email.value
+ }
+
+ try {
+ const magic = new Magic(process.env.MAGIC_PUBLISHABLE_KEY)
+ const didToken = await magic.auth.loginWithMagicLink({ email: body.email })
+ const res = await fetch('/api/login', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': "Bearer " + didToken
+ },
+ body: JSON.stringify(body),
+ })
+ if (res.status === 200) {
+ Router.push('/')
+ } else {
+ throw new Error(await res.text())
+ }
+ } catch (error) {
+ console.error('An unexpected error happened occurred:', error)
+ setErrorMsg(error.message)
+ }
+ }
+
+ return (
+
+
+
+
+
+
+ )
+}
+
+export default Login
diff --git a/examples/magic/pages/profile.js b/examples/magic/pages/profile.js
new file mode 100644
index 0000000000000..b4822bcad500d
--- /dev/null
+++ b/examples/magic/pages/profile.js
@@ -0,0 +1,15 @@
+import { useUser } from '../lib/hooks'
+import Layout from '../components/layout'
+
+const Profile = () => {
+ const user = useUser({ redirectTo: '/login' })
+
+ return (
+
+ Profile
+ {user && Your session:
{JSON.stringify(user, null, 2)}
}
+
+ )
+}
+
+export default Profile
From c73d7dc05d9b30be2dc38223369e97ab729f394c Mon Sep 17 00:00:00 2001
From: Sean Li
Date: Fri, 10 Apr 2020 08:29:10 -0700
Subject: [PATCH 02/10] Tweaked wording on README
---
examples/magic/README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/examples/magic/README.md b/examples/magic/README.md
index 881a088d1501d..a01c533b1fddf 100644
--- a/examples/magic/README.md
+++ b/examples/magic/README.md
@@ -1,6 +1,6 @@
# 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.
+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).
From 81808008add11b07c32aa2a717bce89a16a362ff Mon Sep 17 00:00:00 2001
From: Sean Li
Date: Fri, 10 Apr 2020 08:37:46 -0700
Subject: [PATCH 03/10] Fixed lint error
---
examples/magic/components/form.js | 2 --
1 file changed, 2 deletions(-)
diff --git a/examples/magic/components/form.js b/examples/magic/components/form.js
index 739530757061f..112cae74af4a2 100644
--- a/examples/magic/components/form.js
+++ b/examples/magic/components/form.js
@@ -1,5 +1,3 @@
-import Link from 'next/link'
-
const Form = ({ errorMessage, onSubmit }) => (