forked from vercel/next.js
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(examples): add Authsignal passwordless example (vercel#41079)
Lands vercel#39048 with lint fixes. Needed to open a new PR, because GitHub does not allow maintainers to edit organization forks. https://github.com/orgs/community/discussions/5634 Closes vercel#39048 ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) Co-authored-by: Chris Fisher <[email protected]>
- Loading branch information
Showing
23 changed files
with
476 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,3 @@ | ||
AUTHSIGNAL_SECRET= | ||
SESSION_TOKEN_SECRET= | ||
REDIRECT_URL=http://localhost:3000/api/finalize-login |
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 @@ | ||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
|
||
# dependencies | ||
/node_modules | ||
/.pnp | ||
.pnp.js | ||
|
||
# testing | ||
/coverage | ||
|
||
# next.js | ||
/.next/ | ||
/out/ | ||
|
||
# production | ||
/build | ||
|
||
# misc | ||
.DS_Store | ||
*.pem | ||
|
||
# debug | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
.pnpm-debug.log* | ||
|
||
# local env files | ||
.env*.local | ||
|
||
# vercel | ||
.vercel | ||
|
||
# typescript | ||
*.tsbuildinfo | ||
next-env.d.ts |
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,45 @@ | ||
# Authsignal Passwordless Login Example | ||
|
||
This example shows how to integrate Authsignal with Next.js in order to implement passwordless login using email magic links and server-side redirects. | ||
|
||
The login session is managed using cookies. Session data is encrypted using [@hapi/iron](https://hapi.dev/family/iron). | ||
|
||
A live version of this example can be found [here](https://authsignal-next-passwordless-example.vercel.app). | ||
|
||
## Deploy your own | ||
|
||
Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): | ||
|
||
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/authsignal-passwordless&project-name=authsignal-passwordless&repository-name=authsignal-passwordless) | ||
|
||
## How to use | ||
|
||
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example: | ||
|
||
```bash | ||
npx create-next-app --example authsignal-passwordless authsignal-passwordless-app | ||
# or | ||
yarn create next-app --example authsignal-passwordless authsignal-passwordless-app | ||
# or | ||
pnpm create next-app --example authsignal-passwordless authsignal-passwordless-app | ||
``` | ||
|
||
Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). | ||
|
||
## Configuration | ||
|
||
Log in to the [Authsignal Portal](https://portal.authsignal.com) and [enable email magic links for your tenant](https://portal.authsignal.com/organisations/tenants/authenticators). | ||
|
||
Copy the .env.local.example file to .env.local: | ||
|
||
``` | ||
cp .env.local.example .env.local | ||
``` | ||
|
||
Set `AUTHSIGNAL_SECRET` as your [Authsignal secret key](https://portal.authsignal.com/organisations/tenants/api). | ||
|
||
The `SESSION_TOKEN_SECRET` is used to encrypt the session cookie. Set it to a random string of 32 characters. | ||
|
||
## Notes | ||
|
||
To learn more about Authsignal take a look at the [API Documentation](https://docs.authsignal.com/). |
38 changes: 38 additions & 0 deletions
38
examples/authsignal/passwordless-login/components/dashboard.module.css
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,38 @@ | ||
.header { | ||
display: flex; | ||
justify-content: space-between; | ||
align-items: center; | ||
background-color: #1d1d1d; | ||
width: 100%; | ||
} | ||
|
||
.header button { | ||
cursor: pointer; | ||
font-size: 13px; | ||
font-weight: 500; | ||
line-height: 1; | ||
border: none; | ||
background: none; | ||
color: #fff; | ||
padding: 15px; | ||
transition: background-color 0.15s, color 0.15s; | ||
} | ||
|
||
.user { | ||
display: flex; | ||
justify-content: center; | ||
align-items: center; | ||
flex-direction: column; | ||
flex-grow: 1; | ||
} | ||
|
||
.label { | ||
font-size: 12px; | ||
margin-bottom: 5px; | ||
} | ||
|
||
.logo { | ||
font-size: 18px; | ||
margin: 15px; | ||
color: #fff; | ||
} |
35 changes: 35 additions & 0 deletions
35
examples/authsignal/passwordless-login/components/dashboard.tsx
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,35 @@ | ||
import { useRouter } from 'next/router' | ||
import { User } from '../lib' | ||
import styles from './dashboard.module.css' | ||
|
||
interface Props { | ||
user: User | ||
} | ||
|
||
export const Dashboard = ({ user }: Props) => { | ||
const router = useRouter() | ||
|
||
const logout = async () => { | ||
await fetch('/api/logout', { | ||
method: 'POST', | ||
credentials: 'same-origin', | ||
}) | ||
|
||
router.push('/') | ||
} | ||
|
||
return ( | ||
<> | ||
<header className={styles.header}> | ||
<div className={styles.logo}>My Example App</div> | ||
<button onClick={() => logout()}>Log out</button> | ||
</header> | ||
<div className={styles.user}> | ||
<div> | ||
<div className={styles.label}>Logged in as:</div> | ||
<div>{user.email}</div> | ||
</div> | ||
</div> | ||
</> | ||
) | ||
} |
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 @@ | ||
export * from './dashboard' | ||
export * from './layout' | ||
export * from './login' |
16 changes: 16 additions & 0 deletions
16
examples/authsignal/passwordless-login/components/layout.tsx
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 @@ | ||
import Head from 'next/head' | ||
|
||
type Props = { | ||
children: React.ReactNode | ||
} | ||
|
||
export const Layout = (props: Props) => ( | ||
<> | ||
<Head> | ||
<title>Authsignal Passwordless Example</title> | ||
<link rel="icon" href="/favicon.ico" /> | ||
</Head> | ||
|
||
{props.children} | ||
</> | ||
) |
57 changes: 57 additions & 0 deletions
57
examples/authsignal/passwordless-login/components/login.module.css
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 @@ | ||
.login { | ||
display: flex; | ||
flex-direction: column; | ||
align-items: center; | ||
justify-content: center; | ||
flex-grow: 1; | ||
} | ||
|
||
.login form { | ||
display: flex; | ||
flex-direction: column; | ||
min-width: 300px; | ||
} | ||
|
||
.login label { | ||
font-size: 12px; | ||
margin-bottom: 5px; | ||
color: #ababab; | ||
} | ||
|
||
.login input { | ||
outline: none; | ||
font-family: inherit; | ||
font-size: 13px; | ||
font-weight: 400; | ||
background-color: #fff; | ||
border-radius: 6px; | ||
color: #1d1d1d; | ||
border: 1px solid #e8e8e8; | ||
padding: 0 15px; | ||
margin: 0 0 15px 0; | ||
height: 40px; | ||
} | ||
|
||
.login button { | ||
cursor: pointer; | ||
font-size: 13px; | ||
font-weight: 500; | ||
line-height: 1; | ||
border-radius: 6px; | ||
border: none; | ||
background-color: #1d1d1d; | ||
color: #fff; | ||
padding: 0 15px; | ||
height: 40px; | ||
transition: background-color 0.15s, color 0.15s; | ||
} | ||
|
||
.login button:hover:not(:active) { | ||
background-color: #282828; | ||
} | ||
|
||
.title { | ||
font-size: 24px; | ||
margin-bottom: 30px; | ||
font-weight: 400; | ||
} |
12 changes: 12 additions & 0 deletions
12
examples/authsignal/passwordless-login/components/login.tsx
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 @@ | ||
import styles from './login.module.css' | ||
|
||
export const Login = () => ( | ||
<main className={styles.login}> | ||
<h1 className={styles.title}>My Example App</h1> | ||
<form method="POST" action="/api/login"> | ||
<label htmlFor="email">Email</label> | ||
<input id="email" type="email" name="email" required /> | ||
<button type="submit">Log in / Sign up</button> | ||
</form> | ||
</main> | ||
) |
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,5 @@ | ||
import { Authsignal } from '@authsignal/node' | ||
|
||
const secret = process.env.AUTHSIGNAL_SECRET! | ||
|
||
export const authsignal = new Authsignal({ secret }) |
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 Iron from '@hapi/iron' | ||
import { parse, serialize } from 'cookie' | ||
|
||
export const COOKIE_NAME = 'session_token' | ||
|
||
const TOKEN_SECRET = process.env.SESSION_TOKEN_SECRET! | ||
|
||
export async function createCookieForSession(user: User) { | ||
// Make login session valid for 8 hours | ||
const maxAge = 60 * 60 * 8 | ||
|
||
const expires = new Date() | ||
expires.setSeconds(expires.getSeconds() + maxAge) | ||
|
||
const sessionData: SessionData = { user, expiresAt: expires.toString() } | ||
|
||
const sessionToken = await Iron.seal(sessionData, TOKEN_SECRET, Iron.defaults) | ||
|
||
const cookie = serialize(COOKIE_NAME, sessionToken, { | ||
maxAge, | ||
expires, | ||
httpOnly: true, | ||
secure: process.env.NODE_ENV === 'production', | ||
path: '/', | ||
sameSite: 'lax', | ||
}) | ||
|
||
return cookie | ||
} | ||
|
||
export async function getSessionFromCookie(cookie: string | undefined) { | ||
const cookies = parse(cookie ?? '') | ||
|
||
const sessionToken = cookies[COOKIE_NAME] | ||
|
||
if (!sessionToken) { | ||
return undefined | ||
} | ||
|
||
const sessionData: SessionData = await Iron.unseal( | ||
sessionToken, | ||
TOKEN_SECRET, | ||
Iron.defaults | ||
) | ||
|
||
return sessionData | ||
} | ||
|
||
export interface SessionData { | ||
user: User | ||
expiresAt: string | ||
} | ||
|
||
export interface User { | ||
userId: string | ||
email?: string | ||
} |
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 @@ | ||
export * from './authsignal' | ||
export * from './cookies' |
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,6 @@ | ||
/** @type {import('next').NextConfig} */ | ||
const nextConfig = { | ||
reactStrictMode: true, | ||
} | ||
|
||
module.exports = nextConfig |
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 @@ | ||
{ | ||
"private": true, | ||
"scripts": { | ||
"dev": "next dev", | ||
"build": "next build", | ||
"start": "next start" | ||
}, | ||
"dependencies": { | ||
"@authsignal/node": "^0.0.29", | ||
"@hapi/iron": "^7.0.0", | ||
"cookie": "^0.5.0", | ||
"next": "latest", | ||
"react": "18.2.0", | ||
"react-dom": "18.2.0" | ||
}, | ||
"devDependencies": { | ||
"@types/cookie": "^0.5.1", | ||
"@types/node": "18.0.3", | ||
"@types/react": "18.0.15", | ||
"@types/react-dom": "18.0.6", | ||
"typescript": "4.7.4" | ||
} | ||
} |
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,6 @@ | ||
import type { AppProps } from 'next/app' | ||
import './globals.css' | ||
|
||
export default function MyApp({ Component, pageProps }: AppProps) { | ||
return <Component {...pageProps} /> | ||
} |
27 changes: 27 additions & 0 deletions
27
examples/authsignal/passwordless-login/pages/api/finalize-login.ts
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,27 @@ | ||
import { NextApiRequest, NextApiResponse } from 'next' | ||
import { authsignal, createCookieForSession } from '../../lib' | ||
|
||
// This route handles the redirect back from the Authsignal Prebuilt MFA page | ||
export default async function finalizeLogin( | ||
req: NextApiRequest, | ||
res: NextApiResponse | ||
) { | ||
// Only GET requests since we are handling redirects | ||
if (req.method !== 'GET') { | ||
return res.status(405).send({ message: 'Only GET requests allowed' }) | ||
} | ||
|
||
const token = req.query.token as string | ||
|
||
// This step uses your secret key to validate the token returned via the redirect | ||
// It makes an authenticated call to Authsignal to check if the magic link challenge succeeded | ||
const { success, user } = await authsignal.validateChallenge({ token }) | ||
|
||
if (success) { | ||
const cookie = await createCookieForSession(user) | ||
|
||
res.setHeader('Set-Cookie', cookie) | ||
} | ||
|
||
res.redirect('/') | ||
} |
Oops, something went wrong.