Compare commits

...

20 Commits

Author SHA1 Message Date
Lluis Agusti
e8a9e8aeb6 fix(client): unit tests setup and providers error handling (#1992)
* test(client): initial Jest + RTL setup

* test(client): add tests for `getSession`

* test(client): document expect cases and fix regex

* test(client): small refactors

* chore(npm): re-generate package-lock.json

* test(client): initial test for `signIn`

* test(client): refactor session tests for consistency

* test(client): credentials/email signin scenarios

* test(client): finish sign-in tests

* chore(github): add test to ci

* test(client): refactor and extend use cases

* test(client): sign-out tests

* refactor(client): code review suggestions (1)

* test(client): add few more sign-in/sign-out cases

* test(client): broadcasting session events

* fix(client): handle fetch providers error
2021-06-01 17:12:13 +02:00
Balázs Orbán
1fb308a6f4 docs(adapter): correct npm install script 2021-06-01 00:44:07 +02:00
Paul van Dyk
613c303315 docs: fix spelling in docs (#2105)
`restriected` => `restricted`
2021-05-31 19:22:39 +02:00
Nico Domino
d24fe1cebb docs: add error + warning pages to sidebar (#2100) 2021-05-31 02:14:27 +02:00
Manten
885b02ca95 chore(dev): add property to decrypt JWT (#2095) 2021-05-31 01:07:46 +02:00
Balázs Orbán
f218697fd6 docs(adapter): remove unnecessary section from prisma 2021-05-30 23:22:00 +02:00
Balázs Orbán
dbead0ad85 docs(adapter): fix API mixup in legacy adapter 2021-05-30 23:17:57 +02:00
Nico Domino
704ded5310 docs(prisma): add prisma-legacy separate docs page (#2097) 2021-05-30 21:44:58 +02:00
Manten
25fbcb4648 docs(FAQ): fix typo (#2088) 2021-05-29 16:47:06 +02:00
Nico Domino
53a439b44b docs(firebase): update firebase usage and options (#2076)
* docs(firebase): update firebase usage and options

* docs(firebase): add firebase tips/warnings

Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-05-28 16:05:15 +02:00
Ben Orozco
16a2e37fd6 feat: Allow client to override scope (#2079)
* Ref[Signin]: Allow client to override scope

Allow client to override `scope` via query params

* Doc[Client]: Signin no longer overrides scope server-side
2021-05-28 10:07:42 +02:00
Colby Fayock
0392a8df9a docs(website): add Twitter Provider tutorial 2021-05-26 09:29:08 +02:00
Olav Fosse
a459b95c5b docs(website): fix typo (#2061) 2021-05-25 20:53:37 +02:00
Balázs Orbán
13df7eb81d docs: update urls to .vercel.app (#2039) 2021-05-25 00:35:57 +02:00
Kiran Paul
62f261209c docs(provider): improve authorize code example (#2046)
* Updated user fetch code as per review comments
2021-05-24 16:54:50 +02:00
Nico Domino
da43d0d896 docs(adapters): reorganise adapter docs for new pkg (#2051)
* docs(adapters): reorganise adapter docs for new pkg

* docs(adapters): fix link typos

* docs(adapters): add vercel.json redirects for new adapters URLs
2021-05-23 22:16:14 +02:00
Ben West
4b1271ba75 docs: Remove claim that new users do not have an ID (#1737)
I'm not sure when this changed, but it's no longer true. If the person logging in doesn't have a stored user account, the ID will be the provider_account_id
2021-05-22 13:47:48 +02:00
Marshall Bowers
d30da0170f fix(provider): make WorkOS domain configurable from signIn (#2038)
* Don't pass `domain` to the WorkOS provider

* Update docs

* Change `apiUrl` to `domain`
2021-05-22 13:40:48 +02:00
Nico Domino
887b2985fc docs(adapters): update copy regarding adapters (#2026)
* docs(adapters): update copy regarding adapters

* docs(adapters): add prisma schema page

* docs(adapters): add fauna schema/setup page

* docs(adapters): address PR comments

* Update www/docs/schemas/adapters.md

Co-authored-by: Lluis Agusti <hi@llu.lu>

* docs(adapters): update adapters.md

* docs(adapters): update adapters.md

Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-05-22 12:10:26 +02:00
Nico Domino
d2bbac1164 docs: explain where pageProps come from in Provider docs (#2016)
* docs: explain where pageProps come from in Provider docs

* chore: formatting

* docs(getting-started): add alternative client session handling methods

* docs(getting-started): update alternative client api docs
2021-05-22 11:30:38 +02:00
45 changed files with 34768 additions and 7770 deletions

39
.github/workflows/tests.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: Tests
on:
push:
branches:
- main
- beta
- next
pull_request:
branches:
- main
- beta
- next
jobs:
types:
name: "Types"
runs-on: ubuntu-latest
steps:
- name: Init
uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
- name: Install dependencies
uses: bahmutov/npm-install@v1
- name: Check types
run: npm run test:types
tests:
name: "Integration Tests"
runs-on: ubuntu-latest
steps:
- name: Init
uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
- name: Install dependencies
uses: bahmutov/npm-install@v1
- name: Run tests
run: npm test -- --ci

View File

@@ -1,27 +0,0 @@
name: Types
on:
push:
branches:
- main
- beta
- next
pull_request:
branches:
- main
- beta
- next
jobs:
lint-and-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 14
- name: Install dependencies
uses: bahmutov/npm-install@v1
- name: Check types
run: npm run test:types

View File

@@ -4,7 +4,7 @@
NEXTAUTH_URL=http://localhost:3000
# You can use `openssl rand -hex 32` or
# https://generate-secret.now.sh/32 to generate a secret.
# https://generate-secret.vercel.app/32 to generate a secret.
# Note: Changing a secret may invalidate existing sessions
# and/or verificaion tokens.
SECRET=

View File

@@ -4,6 +4,6 @@ import jwt from 'next-auth/jwt'
const secret = process.env.SECRET
export default async (req, res) => {
const token = await jwt.getToken({ req, secret })
const token = await jwt.getToken({ req, secret, encryption: true })
res.send(JSON.stringify(token, null, 2))
}

View File

@@ -18,5 +18,16 @@ module.exports = {
test: ["../src/server/pages/**"],
presets: ["preact"],
},
{
test: ["../src/**/*.test.js"],
presets: [
[
"@babel/preset-react",
{
runtime: "automatic",
},
],
],
},
],
}

2
config/jest-setup.js Normal file
View File

@@ -0,0 +1,2 @@
import "@testing-library/jest-dom"
import "whatwg-fetch"

8
config/jest.config.js Normal file
View File

@@ -0,0 +1,8 @@
module.exports = {
transform: {
"\\.js$": ["babel-jest", { configFile: "./config/babel.config.js" }],
},
roots: ["../src"],
setupFilesAfterEnv: ["./jest-setup.js"],
testMatch: ["**/*.test.js"],
}

40562
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -37,7 +37,7 @@
"watch": "npm run watch:js | npm run watch:css",
"watch:js": "babel --config-file ./config/babel.config.js --watch src --out-dir dist",
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir dist",
"test": "echo \"Write some tests...\"; npm run test:types",
"test": "jest --config ./config/jest.config.js",
"test:types": "dtslint types --onlyTestTsNext",
"prepublishOnly": "npm run build",
"lint": "eslint .",
@@ -90,15 +90,20 @@
"@babel/core": "^7.9.6",
"@babel/plugin-proposal-optional-catch-binding": "^7.14.2",
"@babel/plugin-transform-runtime": "^7.13.15",
"@babel/preset-env": "^7.14.2",
"@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.13.13",
"@semantic-release/commit-analyzer": "^8.0.1",
"@semantic-release/github": "^7.2.0",
"@semantic-release/npm": "7.0.8",
"@semantic-release/release-notes-generator": "^9.0.1",
"@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^13.1.9",
"@types/react": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"autoprefixer": "^9.7.6",
"babel-jest": "^26.6.3",
"babel-preset-preact": "^2.0.0",
"conventional-changelog-conventionalcommits": "4.4.0",
"cssnano": "^4.1.10",
@@ -108,16 +113,20 @@
"eslint-config-prettier": "^8.2.0",
"eslint-config-standard-with-typescript": "^19.0.1",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jest": "^24.3.6",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-standard": "^5.0.0",
"jest": "^26.6.3",
"msw": "^0.28.2",
"next": "^10.0.5",
"postcss-cli": "^7.1.1",
"postcss-nested": "^4.2.1",
"prettier": "^2.2.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"typescript": "^4.1.3"
"typescript": "^4.1.3",
"whatwg-fetch": "^3.6.2"
},
"prettier": {
"semi": false
@@ -144,7 +153,23 @@
"localStorage": "readonly",
"location": "readonly",
"fetch": "readonly"
}
},
"overrides": [
{
"files": [
"./**/*test.js"
],
"env": {
"jest/globals": true
},
"extends": [
"plugin:jest/recommended"
],
"plugins": [
"jest"
]
}
]
},
"release": {
"branches": [

View File

@@ -0,0 +1,87 @@
import { setupServer } from "msw/node"
import { rest } from "msw"
import { randomBytes } from "crypto"
export const mockSession = {
user: {
image: null,
name: "John",
email: "john@email.com",
},
expires: 123213139,
}
export const mockProviders = {
github: {
id: "github",
name: "Github",
type: "oauth",
signinUrl: "path/to/signin",
callbackUrl: "path/to/callback",
},
credentials: {
id: "credentials",
name: "Credentials",
type: "credentials",
authorize: null,
credentials: null,
},
email: {
id: "email",
type: "email",
name: "Email",
},
}
export const mockCSRFToken = {
csrfToken: randomBytes(32).toString("hex"),
}
export const mockGithubResponse = {
ok: true,
status: 200,
url: "https://path/to/github/url",
}
export const mockCredentialsResponse = {
ok: true,
status: 200,
url: "https://path/to/credentials/url",
}
export const mockEmailResponse = {
ok: true,
status: 200,
url: "https://path/to/email/url",
}
export const mockSignOutResponse = {
ok: true,
status: 200,
url: "https://path/to/signout/url",
}
export const server = setupServer(
rest.post("/api/auth/signout", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockSignOutResponse))
),
rest.get("/api/auth/session", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockSession))
),
rest.get("/api/auth/csrf", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockCSRFToken))
),
rest.get("/api/auth/providers", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockProviders))
),
rest.post("/api/auth/signin/github", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockGithubResponse))
),
rest.post("/api/auth/callback/credentials", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockCredentialsResponse))
),
rest.post("/api/auth/signin/email", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockEmailResponse))
),
rest.post("/api/auth/_log", (req, res, ctx) => res(ctx.status(200)))
)

View File

@@ -0,0 +1,97 @@
import { render, screen, waitFor } from "@testing-library/react"
import { rest } from "msw"
import { server, mockSession } from "./mocks"
import logger from "../../lib/logger"
import { useState, useEffect } from "react"
import { getSession } from ".."
import { getBroadcastEvents } from "./utils"
jest.mock("../../lib/logger", () => ({
__esModule: true,
default: {
warn: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
},
proxyLogger(logger) {
return logger
},
}))
beforeAll(() => server.listen())
beforeEach(() => {
// eslint-disable-next-line no-proto
jest.spyOn(window.localStorage.__proto__, "setItem")
})
afterEach(() => {
server.resetHandlers()
jest.restoreAllMocks()
})
afterAll(() => server.close())
test("if it can fetch the session, it should store it in `localStorage`", async () => {
render(<SessionFlow />)
// In the start, there is no session
const noSession = await screen.findByText("No session")
expect(noSession).toBeInTheDocument()
// After we fetched the session, it should have been rendered by `<SessionFlow />`
const session = await screen.findByText(new RegExp(mockSession.user.name))
expect(session).toBeInTheDocument()
const broadcastCalls = getBroadcastEvents()
const [broadcastedEvent] = broadcastCalls
expect(broadcastCalls).toHaveLength(1)
expect(broadcastCalls).toHaveLength(1)
expect(broadcastedEvent.eventName).toBe("nextauth.message")
expect(broadcastedEvent.value).toStrictEqual({
data: {
trigger: "getSession",
},
event: "session",
})
})
test("if there's an error fetching the session, it should log it", async () => {
server.use(
rest.get("/api/auth/session", (req, res, ctx) => {
return res(ctx.status(500), ctx.body("Server error"))
})
)
render(<SessionFlow />)
await waitFor(() => {
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toBeCalledWith(
"CLIENT_FETCH_ERROR",
"session",
new SyntaxError("Unexpected token S in JSON at position 0")
)
})
})
function SessionFlow() {
const [session, setSession] = useState(null)
useEffect(() => {
async function fetchUserSession() {
try {
const result = await getSession({})
setSession(result)
} catch (e) {
console.error(e)
}
}
fetchUserSession()
}, [])
if (session) {
return <pre>{JSON.stringify(session, null, 2)}</pre>
}
return <p>No session</p>
}

View File

@@ -0,0 +1,290 @@
import { useState } from "react"
import userEvent from "@testing-library/user-event"
import { render, screen, waitFor } from "@testing-library/react"
import logger from "../../lib/logger"
import {
server,
mockCredentialsResponse,
mockEmailResponse,
mockGithubResponse,
} from "./mocks"
import { signIn } from ".."
import { rest } from "msw"
const { location } = window
jest.mock("../../lib/logger", () => ({
__esModule: true,
default: {
warn: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
},
proxyLogger(logger) {
return logger
},
}))
beforeAll(() => {
server.listen()
delete window.location
window.location = {
...location,
replace: jest.fn(),
reload: jest.fn(),
}
})
beforeEach(() => {
jest.resetAllMocks()
server.resetHandlers()
})
afterAll(() => {
window.location = location
server.close()
})
const callbackUrl = "https://redirects/to"
test.each`
provider | type
${""} | ${"no"}
${"foo"} | ${"unknown"}
`(
"if $type provider, it redirects to the default sign-in page",
async ({ provider }) => {
render(<SignInFlow providerId={provider} callbackUrl={callbackUrl} />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(
`/api/auth/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
)
})
}
)
test.each`
provider | type
${""} | ${"no"}
${"foo"} | ${"unknown"}
`(
"if $type provider supplied and no callback URL, redirects using the current location",
async ({ provider }) => {
render(<SignInFlow providerId={provider} />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(
`/api/auth/signin?callbackUrl=${encodeURIComponent(
window.location.href
)}`
)
})
}
)
test.each`
provider | mockUrl
${`email`} | ${mockEmailResponse.url}
${`credentials`} | ${mockCredentialsResponse.url}
`(
"$provider provider redirects if `redirect` is `true`",
async ({ provider, mockUrl }) => {
render(<SignInFlow providerId={provider} redirect={true} />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(mockUrl)
})
}
)
test("redirection can't be stopped using an oauth provider", async () => {
render(
<SignInFlow
providerId="github"
callbackUrl={callbackUrl}
redirect={false}
/>
)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(mockGithubResponse.url)
})
})
test("redirection can be stopped using the 'credentials' provider", async () => {
render(
<SignInFlow
providerId="credentials"
callbackUrl={callbackUrl}
redirect={false}
/>
)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).not.toHaveBeenCalledWith(
mockCredentialsResponse.url
)
expect(screen.getByTestId("signin-result").textContent).not.toBe(
"no response"
)
})
// snapshot the expected return shape from `signIn`
expect(JSON.parse(screen.getByTestId("signin-result").textContent))
.toMatchInlineSnapshot(`
Object {
"error": null,
"ok": true,
"status": 200,
"url": "https://path/to/credentials/url",
}
`)
})
test("redirection can be stopped using the 'email' provider", async () => {
render(
<SignInFlow providerId="email" callbackUrl={callbackUrl} redirect={false} />
)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).not.toHaveBeenCalledWith(
mockEmailResponse.url
)
expect(screen.getByTestId("signin-result").textContent).not.toBe(
"no response"
)
})
// snapshot the expected return shape from `signIn` oauth
expect(JSON.parse(screen.getByTestId("signin-result").textContent))
.toMatchInlineSnapshot(`
Object {
"error": null,
"ok": true,
"status": 200,
"url": "https://path/to/email/url",
}
`)
})
test("if callback URL contains a hash we force a window reload when re-directing", async () => {
const mockUrlWithHash = "https://path/to/email/url#foo-bar-baz"
server.use(
rest.post("/api/auth/signin/email", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
...mockEmailResponse,
url: mockUrlWithHash,
})
)
})
)
render(<SignInFlow providerId="email" callbackUrl={mockUrlWithHash} />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(mockUrlWithHash)
// the browser will not refresh the page if the redirect URL contains a hash, hence we force it on the client, see #1289
expect(window.location.reload).toHaveBeenCalledTimes(1)
})
})
test("params are propagated to the signin URL when supplied", async () => {
let matchedParams = ""
const authParams = "foo=bar&bar=foo"
server.use(
rest.post("/api/auth/signin/github", (req, res, ctx) => {
matchedParams = req.url.search
return res(ctx.status(200), ctx.json(mockGithubResponse))
})
)
render(<SignInFlow providerId="github" authorizationParams={authParams} />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(matchedParams).toEqual(`?${authParams}`)
})
})
test("when it fails to fetch the providers, it redirected back to signin page", async () => {
const errorMsg = "Error when retrieving providers"
server.use(
rest.get("/api/auth/providers", (req, res, ctx) =>
res(ctx.status(500), ctx.json(errorMsg))
)
)
render(<SignInFlow providerId="github" />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledWith(`/api/auth/error`)
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toBeCalledWith(
"CLIENT_FETCH_ERROR",
"providers",
errorMsg
)
})
})
function SignInFlow({
providerId,
callbackUrl,
redirect = true,
authorizationParams = {},
}) {
const [response, setResponse] = useState(null)
async function handleSignIn() {
const result = await signIn(
providerId,
{
callbackUrl,
redirect,
},
authorizationParams
)
setResponse(result)
}
return (
<>
<p data-testid="signin-result">
{response ? JSON.stringify(response) : "no response"}
</p>
<button onClick={() => handleSignIn()}>Sign in</button>
</>
)
}

View File

@@ -0,0 +1,129 @@
import { useState } from "react"
import userEvent from "@testing-library/user-event"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockSignOutResponse } from "./mocks"
import { signOut } from ".."
import { rest } from "msw"
import { getBroadcastEvents } from "./utils"
const { location } = window
beforeAll(() => {
server.listen()
delete window.location
window.location = {
...location,
replace: jest.fn(),
reload: jest.fn(),
}
})
beforeEach(() => {
// eslint-disable-next-line no-proto
jest.spyOn(window.localStorage.__proto__, "setItem")
})
afterEach(() => {
jest.resetAllMocks()
server.resetHandlers()
})
afterAll(() => {
window.location = location
server.close()
})
const callbackUrl = "https://redirects/to"
test("by default it redirects to the current URL if the server did not provide one", async () => {
server.use(
rest.post("/api/auth/signout", (req, res, ctx) =>
res(ctx.status(200), ctx.json({ ...mockSignOutResponse, url: undefined }))
)
)
render(<SignOutFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(window.location.href)
})
})
test("it redirects to the URL allowed by the server", async () => {
render(<SignOutFlow callbackUrl={callbackUrl} />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(
mockSignOutResponse.url
)
})
})
test("if url contains a hash during redirection a page reload happens", async () => {
const mockUrlWithHash = "https://path/to/email/url#foo-bar-baz"
server.use(
rest.post("/api/auth/signout", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
...mockSignOutResponse,
url: mockUrlWithHash,
})
)
})
)
render(<SignOutFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.reload).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(mockUrlWithHash)
})
})
test("will broadcast the signout event to other tabs", async () => {
render(<SignOutFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
const broadcastCalls = getBroadcastEvents()
const [broadcastedEvent] = broadcastCalls
expect(broadcastCalls).toHaveLength(1)
expect(broadcastedEvent.eventName).toBe("nextauth.message")
expect(broadcastedEvent.value).toStrictEqual({
data: {
trigger: "signout",
},
event: "session",
})
})
})
function SignOutFlow({ callbackUrl, redirect = true }) {
const [response, setResponse] = useState(null)
async function setSignOutRes() {
const result = await signOut({ callbackUrl, redirect })
setResponse(result)
}
return (
<>
<p data-testid="signout-result">
{response ? JSON.stringify(response) : "no response"}
</p>
<button onClick={() => setSignOutRes()}>Sign out</button>
</>
)
}

View File

@@ -0,0 +1,8 @@
export function getBroadcastEvents() {
return window.localStorage.setItem.mock.calls
.filter((call) => call[0] === "nextauth.message")
.map(([eventName, value]) => {
const { timestamp, ...rest } = JSON.parse(value)
return { eventName, value: rest }
})
}

View File

@@ -8,9 +8,15 @@
//
// We use HTTP POST requests with CSRF Tokens to protect against CSRF attacks.
import { useState, useEffect, useContext, createContext, createElement } from 'react'
import _logger, { proxyLogger } from '../lib/logger'
import parseUrl from '../lib/parse-url'
import {
useState,
useEffect,
useContext,
createContext,
createElement,
} from "react"
import _logger, { proxyLogger } from "../lib/logger"
import parseUrl from "../lib/parse-url"
// This behaviour mirrors the default behaviour for getting the site name that
// happens server side in server/index.js
@@ -22,8 +28,14 @@ import parseUrl from '../lib/parse-url'
const __NEXTAUTH = {
baseUrl: parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
basePath: parseUrl(process.env.NEXTAUTH_URL).basePath,
baseUrlServer: parseUrl(process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
basePathServer: parseUrl(process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL).basePath,
baseUrlServer: parseUrl(
process.env.NEXTAUTH_URL_INTERNAL ||
process.env.NEXTAUTH_URL ||
process.env.VERCEL_URL
).baseUrl,
basePathServer: parseUrl(
process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL
).basePath,
keepAlive: 0,
clientMaxAge: 0,
// Properties starting with _ are used for tracking internal app state
@@ -31,7 +43,7 @@ const __NEXTAUTH = {
_clientSyncTimer: null,
_eventListenersAdded: false,
_clientSession: undefined,
_getSession: () => {}
_getSession: () => {},
}
const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
@@ -39,7 +51,7 @@ const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
const broadcast = BroadcastChannel()
// Add event listners on load
if (typeof window !== 'undefined' && !__NEXTAUTH._eventListenersAdded) {
if (typeof window !== "undefined" && !__NEXTAUTH._eventListenersAdded) {
__NEXTAUTH._eventListenersAdded = true
// Listen for storage events and update session if event fired from
// another window (but suppress firing another event to avoid a loop)
@@ -50,26 +62,30 @@ if (typeof window !== 'undefined' && !__NEXTAUTH._eventListenersAdded) {
// on how the session object is being used in the client; it is
// more robust to have each window/tab fetch it's own copy of the
// session object rather than share it across instances.
broadcast.receive(() => __NEXTAUTH._getSession({ event: 'storage' }))
broadcast.receive(() => __NEXTAUTH._getSession({ event: "storage" }))
// Listen for document visibility change events and
// if visibility of the document changes, re-fetch the session.
document.addEventListener('visibilitychange', () => {
!document.hidden && __NEXTAUTH._getSession({ event: 'visibilitychange' })
}, false)
document.addEventListener(
"visibilitychange",
() => {
!document.hidden && __NEXTAUTH._getSession({ event: "visibilitychange" })
},
false
)
}
// Context to store session data globally
/** @type {import("types/internals/client").SessionContext} */
const SessionContext = createContext()
export function useSession (session) {
export function useSession(session) {
const context = useContext(SessionContext)
if (context) return context
return _useSessionHook(session)
}
function _useSessionHook (session) {
function _useSessionHook(session) {
const [data, setData] = useState(session)
const [loading, setLoading] = useState(!data)
@@ -77,7 +93,7 @@ function _useSessionHook (session) {
__NEXTAUTH._getSession = async ({ event = null } = {}) => {
try {
const triggredByEvent = event !== null
const triggeredByStorageEvent = event === 'storage'
const triggeredByStorageEvent = event === "storage"
const clientMaxAge = __NEXTAUTH.clientMaxAge
const clientLastSync = parseInt(__NEXTAUTH._clientLastSync)
@@ -98,14 +114,19 @@ function _useSessionHook (session) {
// tab or window that will come through as a triggeredByStorageEvent
// event and will skip this logic)
return
} else if (clientMaxAge > 0 && currentTime < (clientLastSync + clientMaxAge)) {
} else if (
clientMaxAge > 0 &&
currentTime < clientLastSync + clientMaxAge
) {
// If the session freshness is within clientMaxAge then don't request
// it again on this call (avoids too many invokations).
return
}
}
if (clientSession === undefined) { __NEXTAUTH._clientSession = null }
if (clientSession === undefined) {
__NEXTAUTH._clientSession = null
}
// Update clientLastSync before making response to avoid repeated
// invokations that would otherwise be triggered while we are still
@@ -116,7 +137,7 @@ function _useSessionHook (session) {
// tell getSession not to trigger an event when it calls to avoid an
// infinate loop.
const newClientSessionData = await getSession({
triggerEvent: !triggeredByStorageEvent
triggerEvent: !triggeredByStorageEvent,
})
// Save session state internally, just so we can track that we've checked
@@ -126,7 +147,7 @@ function _useSessionHook (session) {
setData(newClientSessionData)
setLoading(false)
} catch (error) {
logger.error('CLIENT_USE_SESSION_ERROR', error)
logger.error("CLIENT_USE_SESSION_ERROR", error)
setLoading(false)
}
}
@@ -137,40 +158,41 @@ function _useSessionHook (session) {
return [data, loading]
}
export async function getSession (ctx) {
const session = await _fetchData('session', ctx)
export async function getSession(ctx) {
const session = await _fetchData("session", ctx)
if (ctx?.triggerEvent ?? true) {
broadcast.post({ event: 'session', data: { trigger: 'getSession' } })
broadcast.post({ event: "session", data: { trigger: "getSession" } })
}
return session
}
export async function getCsrfToken (ctx) {
return (await _fetchData('csrf', ctx))?.csrfToken
export async function getCsrfToken(ctx) {
return (await _fetchData("csrf", ctx))?.csrfToken
}
export async function getProviders () {
return _fetchData('providers')
export async function getProviders() {
return await _fetchData("providers")
}
export async function signIn (provider, options = {}, authorizationParams = {}) {
const {
callbackUrl = window.location,
redirect = true
} = options
export async function signIn(provider, options = {}, authorizationParams = {}) {
const { callbackUrl = window.location.href, redirect = true } = options
const baseUrl = _apiBaseUrl()
const providers = await getProviders()
// Redirect to sign in page if no valid provider specified
if (!(provider in providers)) {
// If Provider not recognized, redirect to sign in page
window.location = `${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
return
if (!providers) {
return window.location.replace(`${baseUrl}/error`)
}
const isCredentials = providers[provider].type === 'credentials'
const isEmail = providers[provider].type === 'email'
const canRedirectBeDisabled = isCredentials || isEmail
if (!(provider in providers)) {
return window.location.replace(
`${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
)
}
const isCredentials = providers[provider].type === "credentials"
const isEmail = providers[provider].type === "email"
const isSupportingReturn = isCredentials || isEmail
const signInUrl = isCredentials
? `${baseUrl}/callback/${provider}`
@@ -179,72 +201,71 @@ export async function signIn (provider, options = {}, authorizationParams = {})
// If is any other provider type, POST to provider URL with CSRF Token,
// callback URL and any other parameters supplied.
const fetchOptions = {
method: 'post',
method: "post",
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
...options,
csrfToken: await getCsrfToken(),
callbackUrl,
json: true
})
json: true,
}),
}
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
const res = await fetch(_signInUrl, fetchOptions)
const data = await res.json()
if (redirect || !canRedirectBeDisabled) {
const url = data.url ?? callbackUrl
window.location = url
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes('#')) window.location.reload()
if (redirect || !isSupportingReturn) {
const url = data.url ?? callbackUrl
window.location.replace(url)
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes("#")) window.location.reload()
return
}
const error = new URL(data.url).searchParams.get('error')
const error = new URL(data.url).searchParams.get("error")
if (res.ok) {
await __NEXTAUTH._getSession({ event: 'storage' })
await __NEXTAUTH._getSession({ event: "storage" })
}
return {
error,
status: res.status,
ok: res.ok,
url: error ? null : data.url
url: error ? null : data.url,
}
}
export async function signOut (options = {}) {
const {
callbackUrl = window.location,
redirect = true
} = options
export async function signOut(options = {}) {
const { callbackUrl = window.location.href, redirect = true } = options
const baseUrl = _apiBaseUrl()
const fetchOptions = {
method: 'post',
method: "post",
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
csrfToken: await getCsrfToken(),
callbackUrl,
json: true
})
json: true,
}),
}
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
const data = await res.json()
broadcast.post({ event: 'session', data: { trigger: 'signout' } })
broadcast.post({ event: "session", data: { trigger: "signout" } })
if (redirect) {
const url = data.url ?? callbackUrl
window.location = url
window.location.replace(url)
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes('#')) window.location.reload()
if (url.includes("#")) window.location.reload()
return
}
await __NEXTAUTH._getSession({ event: 'storage' })
await __NEXTAUTH._getSession({ event: "storage" })
return data
}
@@ -252,13 +273,18 @@ export async function signOut (options = {}) {
// Method to set options. The documented way is to use the provider, but this
// method is being left in as an alternative, that will be helpful if/when we
// expose a vanilla JavaScript version that doesn't depend on React.
export function setOptions ({ baseUrl, basePath, clientMaxAge, keepAlive } = {}) {
export function setOptions({
baseUrl,
basePath,
clientMaxAge,
keepAlive,
} = {}) {
if (baseUrl) __NEXTAUTH.baseUrl = baseUrl
if (basePath) __NEXTAUTH.basePath = basePath
if (clientMaxAge) __NEXTAUTH.clientMaxAge = clientMaxAge
if (keepAlive) {
__NEXTAUTH.keepAlive = keepAlive
if (typeof window === 'undefined') return
if (typeof window === "undefined") return
// Clear existing timer (if there is one)
if (__NEXTAUTH._clientSyncTimer !== null) {
@@ -269,12 +295,12 @@ export function setOptions ({ baseUrl, basePath, clientMaxAge, keepAlive } = {})
__NEXTAUTH._clientSyncTimer = setTimeout(async () => {
// Only invoke keepalive when a session exists
if (!__NEXTAUTH._clientSession) return
await __NEXTAUTH._getSession({ event: 'timer' })
await __NEXTAUTH._getSession({ event: "timer" })
}, keepAlive * 1000)
}
}
export function Provider ({ children, session, options }) {
export function Provider({ children, session, options }) {
setOptions(options)
return createElement(
SessionContext.Provider,
@@ -290,24 +316,25 @@ export function Provider ({ children, session, options }) {
* work seemlessly in getInitialProps() on server side
* pages *and* in _app.js.
*/
async function _fetchData (path, { ctx, req = ctx?.req } = {}) {
async function _fetchData(path, { ctx, req = ctx?.req } = {}) {
try {
const baseUrl = await _apiBaseUrl()
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
const res = await fetch(`${baseUrl}/${path}`, options)
const data = await res.json()
if (!res.ok) throw data
return Object.keys(data).length > 0 ? data : null // Return null if data empty
} catch (error) {
logger.error('CLIENT_FETCH_ERROR', path, error)
logger.error("CLIENT_FETCH_ERROR", path, error)
return null
}
}
function _apiBaseUrl () {
if (typeof window === 'undefined') {
function _apiBaseUrl() {
if (typeof window === "undefined") {
// NEXTAUTH_URL should always be set explicitly to support server side calls - log warning if not set
if (!process.env.NEXTAUTH_URL) {
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set")
}
// Return absolute path when called server side
@@ -318,7 +345,7 @@ function _apiBaseUrl () {
}
/** Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC. */
function _now () {
function _now() {
return Math.floor(Date.now() / 1000)
}
@@ -328,35 +355,34 @@ function _now () {
*
* https://caniuse.com/?search=broadcastchannel
*/
function BroadcastChannel (name = 'nextauth.message') {
function BroadcastChannel(name = "nextauth.message") {
return {
/**
* Get notified by other tabs/windows.
* @param {(message: import("types/internals/client").BroadcastMessage) => void} onReceive
*/
receive (onReceive) {
if (typeof window === 'undefined') return
window.addEventListener('storage', async (event) => {
receive(onReceive) {
if (typeof window === "undefined") return
window.addEventListener("storage", async (event) => {
if (event.key !== name) return
/** @type {import("types/internals/client").BroadcastMessage} */
const message = JSON.parse(event.newValue)
if (message?.event !== 'session' || !message?.data) return
if (message?.event !== "session" || !message?.data) return
onReceive(message)
})
},
/** Notify other tabs/windows. */
post (message) {
if (typeof localStorage === 'undefined') return
localStorage.setItem(name,
post(message) {
if (typeof localStorage === "undefined") return
localStorage.setItem(
name,
JSON.stringify({ ...message, timestamp: _now() })
)
}
},
}
}
// Some methods are exported with more than one name. This provides some
// flexibility over how they can be invoked and backwards compatibility
// with earlier releases. These should be removed in a newer release, as it only
@@ -368,7 +394,7 @@ export {
getProviders as providers,
getCsrfToken as csrfToken,
signIn as signin,
signOut as signout
signOut as signout,
}
export default {
@@ -390,5 +416,5 @@ export default {
providers: getProviders,
csrfToken: getCsrfToken,
signin: signIn,
signout: signOut
signout: signOut,
}

View File

@@ -1,4 +1,6 @@
export default function WorkOS(options) {
const domain = options.domain || 'api.workos.com';
return {
id: 'workos',
name: 'WorkOS',
@@ -10,9 +12,9 @@ export default function WorkOS(options) {
client_id: options.clientId,
client_secret: options.clientSecret
},
accessTokenUrl: 'https://api.workos.com/sso/token/',
authorizationUrl: `https://api.workos.com/sso/authorize/?response_type=code&domain=${options.domain}`,
profileUrl: 'https://api.workos.com/sso/profile/',
accessTokenUrl: `https://${domain}/sso/token`,
authorizationUrl: `https://${domain}/sso/authorize?response_type=code`,
profileUrl: `https://${domain}/sso/profile`,
profile: (profile) => {
return {
...profile,

View File

@@ -15,9 +15,9 @@ export default async function getAuthorizationUrl (req) {
if (provider.version?.startsWith('2.')) {
// Handle OAuth v2.x
let url = client.getAuthorizeUrl({
scope: provider.scope,
...params,
redirect_uri: provider.callbackUrl,
scope: provider.scope
redirect_uri: provider.callbackUrl
})
// If the authorizationUrl specified in the config has query parameters on it

View File

@@ -5,13 +5,16 @@
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
*/
export default function providers (req, res) {
export default function providers(req, res) {
const { providers } = req.options
const result = providers.reduce((acc, { id, name, type, signinUrl, callbackUrl }) => {
acc[id] = { id, name, type, signinUrl, callbackUrl }
return acc
}, {})
const result = providers.reduce(
(acc, { id, name, type, signinUrl, callbackUrl }) => {
acc[id] = { id, name, type, signinUrl, callbackUrl }
return acc
},
{}
)
res.json(result)
}

View File

@@ -3,31 +3,15 @@
"strictNullChecks": true,
"baseUrl": ".",
"paths": {
"types": [
"./types"
],
"next-auth": [
"./src/server"
],
"next-auth/adapters": [
"./src/adapters"
],
"next-auth/client": [
"./src/client"
],
"next-auth/jwt": [
"./src/lib/jwt"
],
"next-auth/providers": [
"./src/providers"
]
"types": ["./types"],
"next-auth": ["./src/server"],
"next-auth/adapters": ["./src/adapters"],
"next-auth/client": ["./src/client"],
"next-auth/jwt": ["./src/lib/jwt"],
"next-auth/providers": ["./src/providers"]
},
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
@@ -44,9 +28,8 @@
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"**/*.js"
"**/*.js",
".eslintrc.js"
],
"exclude": [
"node_modules"
]
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,71 @@
---
id: dynamodb
title: DynamoDB Adapter
---
# DynamoDB
This is the AWS DynamoDB Adapter for next-auth. This package can only be used in conjunction with the primary next-auth package. It is not a standalone package.
You need a table with a partition key `pk` and a sort key `sk`. Your table also needs a global secondary index named `GSI1` with `GSI1PK` as partition key and `GSI1SK` as sorting key. You can set whatever you want as the table name and the billing method.
You can find the full schema in the table structure section below.
## Getting Started
1. Install `next-auth` and `@next-auth/dynamodb-adapter`
```js
npm install next-auth @next-auth/dynamodb-adapter
```
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
You need to pass `aws-sdk` to the adapter in addition to the table name.
```javascript title="pages/api/auth/[...nextauth].js"
import AWS from "aws-sdk";
import NextAuth from "next-auth";
import Providers from "next-auth/providers";
import { DynamoDBAdapter } from "@next-auth/dynamodb-adapter"
AWS.config.update({
accessKeyId: process.env.NEXT_AUTH_AWS_ACCESS_KEY,
secretAccessKey: process.env.NEXT_AUTH_AWS_SECRET_KEY,
region: process.env.NEXT_AUTH_AWS_REGION,
});
export default NextAuth({
// Configure one or more authentication providers
providers: [
Providers.GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
Providers.Email({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
}),
// ...add more providers here
],
adapter: DynamoDBAdapter({
AWS,
tableName: "next-auth-test",
}),
...
});
```
(AWS secrets start with `NEXT_AUTH_` in order to not conflict with [Vercel's reserved environment variables](https://vercel.com/docs/environment-variables#reserved-environment-variables).)
## Schema
The table respects the single table design pattern. This has many advantages:
- Only one table to manage, monitor and provision.
- Querying relations is faster than with multi-table schemas (for eg. retreiving all sessions for a user).
- Only one table needs to be replicated, if you want to go multi-region.
Here is a schema of the table :
![DynamoDB Table](https://i.imgur.com/hGZtWDq.png)

View File

@@ -0,0 +1,84 @@
---
id: fauna
title: FaunaDB Adapter
---
# FaunaDB
This is the Fauna Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
You can find the Fauna schema and seed information in the docs at [next-auth.js.org/adapters/fauna](https://next-auth.js.org/adapters/fauna).
## Getting Started
1. Install `next-auth` and `@next-auth/fauna-adapter`
```js
npm install next-auth @next-auth/fauna-adapter
```
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
```javascript title="pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
import * as Fauna from "faunadb"
import { FaunaAdapter } from "@next-auth/fauna-adapter"
const client = new Fauna.Client({
secret: "secret",
scheme: "http",
domain: "localhost",
port: 8443,
})
// For more information on each option (and a full list of options) go to
// https://next-auth.js.org/configuration/options
export default NextAuth({
// https://next-auth.js.org/configuration/providers
providers: [
Providers.Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
],
adapter: FaunaAdapter({ faunaClient: client})
...
})
```
## Schema
```javascript
CreateCollection({ name: "accounts" })
CreateCollection({ name: "sessions" })
CreateCollection({ name: "users" })
CreateCollection({ name: "verification_requests" })
CreateIndex({
name: "account_by_provider_account_id",
source: Collection("accounts"),
unique: true,
terms: [
{ field: ["data", "providerId"] },
{ field: ["data", "providerAccountId"] },
],
})
CreateIndex({
name: "session_by_token",
source: Collection("sessions"),
unique: true,
terms: [{ field: ["data", "sessionToken"] }],
})
CreateIndex({
name: "user_by_email",
source: Collection("users"),
unique: true,
terms: [{ field: ["data", "email"] }],
})
CreateIndex({
name: "verification_request_by_token",
source: Collection("verification_requests"),
unique: true,
terms: [{ field: ["data", "token"] }, { field: ["data", "identifier"] }],
})
```

View File

@@ -0,0 +1,73 @@
---
id: firebase
title: Firebase Adapter
---
# Firebase
This is the Firebase Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
## Getting Started
1. Install `next-auth` and `@next-auth/firebase-adapter`
```js
npm install next-auth @next-auth/firebase-adapter
```
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
```javascript title="pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
import { FirebaseAdapter } from "@next-auth/firebase-adapter"
import firebase from "firebase/app"
import "firebase/firestore"
const firestore = (
firebase.apps[0] ?? firebase.initializeApp(/* your config */)
).firestore()
// For more information on each option (and a full list of options) go to
// https://next-auth.js.org/configuration/options
export default NextAuth({
// https://next-auth.js.org/configuration/providers
providers: [
Providers.Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
],
adapter: FirebaseAdapter(firestore),
...
})
```
## Options
When initializing the firestore adapter, you must pass in the firebase config object with the details from your project. More details on how to obtain that config object can be found [here](https://support.google.com/firebase/answer/7015592).
An example firebase config looks like this:
```js
const firebaseConfig = {
apiKey: "AIzaSyDOCAbC123dEf456GhI789jKl01-MnO",
authDomain: "myapp-project-123.firebaseapp.com",
databaseURL: "https://myapp-project-123.firebaseio.com",
projectId: "myapp-project-123",
storageBucket: "myapp-project-123.appspot.com",
messagingSenderId: "65211879809",
appId: "1:65211879909:web:3ae38ef1cdcb2e01fe5f0c",
measurementId: "G-8GSGZQ44ST",
}
```
See [firebase.google.com/docs/web/setup](https://firebase.google.com/docs/web/setup) for more details.
:::tip **From Firebase**
**Caution**: We do not recommend manually modifying an app's Firebase config file or object. If you initialize an app with invalid or missing values for any of these required "Firebase options", then your end users may experience serious issues.
For open source projects, we generally do not recommend including the app's Firebase config file or object in source control because, in most cases, your users should create their own Firebase projects and point their apps to their own Firebase resources (via their own Firebase config file or object).
:::

View File

@@ -0,0 +1,42 @@
---
id: overview
title: Overview
---
An **Adapter** in NextAuth.js connects your application to whatever database or backend system you want to use to store data for user accounts, sessions, etc.
The adapters can be found in their own repository under [`nextauthjs/adapters`](https://github.com/nextauthjs/adapters).
There you can find the following adapters:
- [`typeorm-legacy`](./typeorm/typeorm-overview)
- [`prisma`](./prisma)
- [`prisma-legacy`](./prisma-legacy)
- [`fauna`](./fauna)
- [`dynamodb`](./dynamodb)
- [`firebase`](./firebase)
## Custom Adapter
See the tutorial for [creating a database Adapter](/tutorials/creating-a-database-adapter) for more information on how to create a custom Adapter. Have a look at the [Adapter repository](https://github.com/nextauthjs/adapters) to see community maintained custom Adapter or add your own.
### Editor integration
When writing your own custom Adapter in plain JavaScript, note that you can use **JSDoc** to get helpful editor hints and auto-completion like so:
```js
/** @type { import("next-auth/adapters").Adapter } */
const MyAdapter = () => {
return {
async getAdapter() {
return {
// your adapter methods here
}
},
}
}
```
:::note
This will work in code editors with a strong TypeScript integration like VSCode or WebStorm. It might not work if you're using more lightweight editors like VIM or Atom.
:::

View File

@@ -1,62 +1,16 @@
---
id: adapters
title: Database Adapters
id: prisma-legacy
title: Prisma Adapter (Legacy)
---
An **Adapter** in NextAuth.js connects your application to whatever database or backend system you want to use to store data for user accounts, sessions, etc.
# Prisma (Legacy)
You do not need to specify an Adapter explicitly unless you want to use advanced options such as custom models or schemas, if you want to use the Prisma Adapter instead of the default TypeORM Adapter, or if you are creating a custom Adapter to connect to a database that is not one of the supported databases.
You can also use NextAuth.js with the built-in Adapter for [Prisma](https://www.prisma.io/docs/). This is included in the core `next-auth` package at the moment. The other adapter needs to be installed from its own additional package.
### Database Schemas
Configure your database by creating the tables and columns to match the schema expected by NextAuth.js.
- [MySQL Schema](/schemas/mysql)
- [Postgres Schema](/schemas/postgres)
- [Microsoft SQL Server Schema](/schemas/mssql)
## TypeORM Adapter
NextAuth.js comes with a default Adapter that uses [TypeORM](https://typeorm.io/) so that it can be used with many different databases without any further configuration, you simply add the node module for the database driver you want to use to your project and pass a database connection string to NextAuth.js.
The default Adapter is the TypeORM Adapter, the following configuration options are exactly equivalent.
```javascript
database: {
type: 'sqlite',
database: ':memory:',
synchronize: true
}
```
```javascript
adapter: Adapters.Default({
type: "sqlite",
database: ":memory:",
synchronize: true,
})
```
```javascript
adapter: Adapters.TypeORM.Adapter({
type: "sqlite",
database: ":memory:",
synchronize: true,
})
```
The tutorial [Custom models with TypeORM](/tutorials/typeorm-custom-models) explains how to extend the built in models and schemas used by the TypeORM Adapter. You can use these models in your own code.
:::tip
The `synchronize` option in TypeORM will generate SQL that exactly matches the documented schemas for MySQL and Postgres.
However, it should not be enabled against production databases as it may cause data loss if the configured schema does not match the expected schema!
:::info
You may have noticed there is a `prisma` and `prisma-legacy` adapter. This is due to historical reasons, but the code has mostly converged so that there is no longer much difference between the two. The legacy adapter, however, does have the ability to rename tables which the newer version does not.
:::
## Prisma Adapter
You can also use NextAuth.js with the experimental Adapter for [Prisma 2](https://www.prisma.io/docs/).
To use this Adapter, you need to install Prisma Client and Prisma CLI:
```
@@ -88,8 +42,9 @@ export default NextAuth({
:::tip
While Prisma includes an experimental feature in the migration command that is able to generate SQL from a schema, creating tables and columns using the provided SQL is currently recommended instead as SQL schemas automatically generated by Prisma may differ from the recommended schemas.
:::
Schema for the Prisma Adapter
### Prisma Schema
## Setup
Create a schema file in `prisma/schema.prisma` similar to this one:
@@ -104,7 +59,7 @@ datasource db {
}
model Account {
id Int @default(autoincrement()) @id
id Int @id @default(autoincrement())
compoundId String @unique @map(name: "compound_id")
userId Int @map(name: "user_id")
providerType String @map(name: "provider_type")
@@ -119,12 +74,11 @@ model Account {
@@index([providerAccountId], name: "providerAccountId")
@@index([providerId], name: "providerId")
@@index([userId], name: "userId")
@@map(name: "accounts")
}
model Session {
id Int @default(autoincrement()) @id
id Int @id @default(autoincrement())
userId Int @map(name: "user_id")
expires DateTime
sessionToken String @unique @map(name: "session_token")
@@ -136,7 +90,7 @@ model Session {
}
model User {
id Int @default(autoincrement()) @id
id Int @id @default(autoincrement())
name String?
email String? @unique
emailVerified DateTime? @map(name: "email_verified")
@@ -148,36 +102,19 @@ model User {
}
model VerificationRequest {
id Int @default(autoincrement()) @id
id Int @id @default(autoincrement())
identifier String
token String @unique
expires DateTime
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")
@@map(name: "verification_requests")
}
```
:::note
Set the `datasource` option appropriately for your database:
```json title="schema.prisma"
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
```
```json title="schema.prisma"
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
```
:::
### Generate Client
Once you have saved your schema, use the Prisma CLI to generate the Prisma Client:
@@ -235,28 +172,3 @@ if (process.env.NODE_ENV === "production") {
```
:::
## Custom Adapter
See the tutorial for [creating a database Adapter](/tutorials/creating-a-database-adapter) for more information on how to create a custom Adapter. Have a look at the [Adapter repository](https://github.com/nextauthjs/adapters) to see community maintained custom Adapter or add your own.
### Editor integration
When writing your own custom Adapter in plain JavaScript, note that you can use **JSDoc** to get helpful editor hints and auto-completion like so:
```js
/** @type { import("next-auth/adapters").Adapter } */
const MyAdapter = () => {
return {
async getAdapter() {
return {
// your adapter methods here
}
},
}
}
```
:::note
This will work in code editors with a strong TypeScript integration like VSCode or WebStorm. It might not work if you're using more lightweight editors like VIM or Atom.
:::

218
www/docs/adapters/prisma.md Normal file
View File

@@ -0,0 +1,218 @@
---
id: prisma
title: Prisma Adapter
---
# Prisma
You can also use NextAuth.js with the new experimental Adapter for [Prisma](https://www.prisma.io/docs/). This version of the Prisma Adapter is not included in the core `next-auth` package, and must be installed separately.
:::info
You may have noticed there is a `prisma` and `prisma-legacy` adapter. This is due to historical reasons, but the code has mostly converged so that there is no longer much difference between the two. The legacy adapter, however, does have the ability to rename tables which the newer version does not.
:::
To use this Adapter, you need to install Prisma Client, Prisma CLI, and the separate `@next-auth/prisma-adapter` package:
```
npm install @prisma/client @next-auth/prisma-adapter@canary
npm install prisma --save-dev
```
Configure your NextAuth.js to use the Prisma Adapter:
```javascript title="pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
export default NextAuth({
providers: [
Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
adapter: PrismaAdapter(prisma),
})
```
:::tip
While Prisma includes an experimental feature in the migration command that is able to generate SQL from a schema, creating tables and columns using the provided SQL is currently recommended instead as SQL schemas automatically generated by Prisma may differ from the recommended schemas.
:::
Schema for the Prisma Adapter (`@next-auth/prisma-adapter`)
## Setup
Create a schema file in `prisma/schema.prisma` similar to this one:
```json title="schema.prisma"
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
model Account {
id String @id @default(cuid())
userId String
providerType String
providerId String
providerAccountId String
refreshToken String?
accessToken String?
accessTokenExpires DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
@@unique([providerId, providerAccountId])
}
model Session {
id String @id @default(cuid())
userId String
expires DateTime
sessionToken String @unique
accessToken String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accounts Account[]
sessions Session[]
}
model VerificationRequest {
id String @id @default(cuid())
identifier String
token String @unique
expires DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([identifier, token])
}
```
### Generate Client
Once you have saved your schema, use the Prisma CLI to generate the Prisma Client:
```
npx prisma generate
```
To configure you database to use the new schema (i.e. create tables and columns) use the `prisma migrate` command:
```
npx prisma migrate dev
```
To generate a schema in this way with the above example code, you will need to specify your datbase connection string in the environment variable `DATABASE_URL`. You can do this by setting it in a `.env` file at the root of your project.
As this feature is experimental in Prisma, it is behind a feature flag. You should check your database schema manually after using this option. See the [Prisma documentation](https://www.prisma.io/docs/) for information on how to use `prisma migrate`.
## Schema History
Changes from the original Prisma Adapter
```diff
model Account {
- id Int @default(autoincrement()) @id
+ id String @id @default(cuid())
- compoundId String @unique @map(name: "compound_id")
- userId Int @map(name: "user_id")
+ userId String
+ user User @relation(fields: [userId], references: [id])
- providerType String @map(name: "provider_type")
+ providerType String
- providerId String @map(name: "provider_id")
+ providerId String
- providerAccountId String @map(name: "provider_account_id")
+ providerAccountId String
- refreshToken String? @map(name: "refresh_token")
+ refreshToken String?
- accessToken String? @map(name: "access_token")
+ accessToken String?
- accessTokenExpires DateTime? @map(name: "access_token_expires")
+ accessTokenExpires DateTime?
- createdAt DateTime @default(now()) @map(name: "created_at")
+ createdAt DateTime @default(now())
- updatedAt DateTime @default(now()) @map(name: "updated_at")
+ updatedAt DateTime @updatedAt
- @@index([providerAccountId], name: "providerAccountId")
- @@index([providerId], name: "providerId")
- @@index([userId], name: "userId")
- @@map(name: "accounts")
+ @@unique([providerId, providerAccountId])
}
model Session {
- id Int @default(autoincrement()) @id
+ id String @id @default(cuid())
- userId Int @map(name: "user_id")
+ userId String
+ user User @relation(fields: [userId], references: [id])
expires DateTime
- sessionToken String @unique @map(name: "session_token")
+ sessionToken String @unique
- accessToken String @unique @map(name: "access_token")
+ accessToken String @unique
- createdAt DateTime @default(now()) @map(name: "created_at")
+ createdAt DateTime @default(now())
- updatedAt DateTime @default(now()) @map(name: "updated_at")
+ updatedAt DateTime @updatedAt
-
- @@map(name: "sessions")
}
model User {
- id Int @default(autoincrement()) @id
+ id String @id @default(cuid())
name String?
email String? @unique
- emailVerified DateTime? @map(name: "email_verified")
+ emailVerified DateTime?
image String?
+ accounts Account[]
+ sessions Session[]
- createdAt DateTime @default(now()) @map(name: "created_at")
+ createdAt DateTime @default(now())
- updatedAt DateTime @default(now()) @map(name: "updated_at")
+ updatedAt DateTime @updatedAt
- @@map(name: "users")
}
model VerificationRequest {
- id Int @default(autoincrement()) @id
+ id String @id @default(cuid())
identifier String
token String @unique
expires DateTime
- createdAt DateTime @default(now()) @map(name: "created_at")
+ createdAt DateTime @default(now())
- updatedAt DateTime @default(now()) @map(name: "updated_at")
+ updatedAt DateTime @updatedAt
- @@map(name: "verification_requests")
+ @@unique([identifier, token])
}
```

View File

@@ -0,0 +1,49 @@
---
id: typeorm-overview
title: Overview
---
## TypeORM Adapter
NextAuth.js comes with a default Adapter that uses [TypeORM](https://typeorm.io/) so that it can be used with many different databases without any further configuration, you simply add the node module for the database driver you want to use in your project and pass a database connection string to NextAuth.js.
### Database Schemas
Configure your database by creating the tables and columns to match the schema expected by NextAuth.js.
- [MySQL Schema](./mysql)
- [Postgres Schema](./postgres)
- [Microsoft SQL Server Schema](./mssql)
- [MongoDB](./mongodb)
The default Adapter is the TypeORM Adapter and the default database type for TypeORM is SQLite, the following configuration options are exactly equivalent.
```javascript
database: {
type: 'sqlite',
database: ':memory:',
synchronize: true
}
```
```javascript
adapter: Adapters.Default({
type: "sqlite",
database: ":memory:",
synchronize: true,
})
```
```javascript
adapter: Adapters.TypeORM.Adapter({
type: "sqlite",
database: ":memory:",
synchronize: true,
})
```
The tutorial [Custom models with TypeORM](/tutorials/typeorm-custom-models) explains how to extend the built in models and schemas used by the TypeORM Adapter. You can use these models in your own code.
:::tip
The `synchronize` option in TypeORM will generate SQL that exactly matches the documented schemas for MySQL and Postgres. This will automatically apply any changes it finds in the entity model, therefore it **should not be enabled against production databases** as it may cause data loss if the configured schema does not match the expected schema!
:::

View File

@@ -78,11 +78,6 @@ When using NextAuth.js with a database, the User object will be either a user ob
When using NextAuth.js without a database, the user object it will always be a prototype user object, with information extracted from the profile.
:::
:::tip
If you only want to allow users who already have accounts in the database to sign in, you can check for the existence of a `user.id` property and reject any sign in attempts from accounts that do not have one.
If you are using NextAuth.js without database and want to control who can sign in, you can check their email address or profile against a hard coded list in the `signIn()` callback.
:::
## Redirect callback

View File

@@ -5,16 +5,20 @@ title: Databases
NextAuth.js comes with multiple ways of connecting to a database:
* **TypeORM** (default)<br/>
_The TypeORM adapter supports MySQL, Postgres, MsSql, SQLite and MongoDB databases._
* **Prisma**<br/>
_The Prisma 2 adapter supports MySQL, Postgres and SQLite databases._
* **Custom Adapter**<br/>
- **TypeORM** (default)<br/>
_The TypeORM adapter supports MySQL, PostgreSQL, MSSQL, SQLite and MongoDB databases._
- **Prisma**<br/>
_The Prisma 2 adapter supports MySQL, PostgreSQL and SQLite databases._
- **Fauna**<br/>
_The FaunaDB adapter only supports FaunaDB._
- **Custom Adapter**<br/>
_A custom Adapter can be used to connect to any database._
> There are currently efforts in the [`nextauthjs/adapters`](https://github.com/nextauthjs/adapters) repository to get community-based DynamoDB, Sanity, PouchDB and Sequelize Adapters merged. If you are interested in any of the above, feel free to check out the PRs in the `nextauthjs/adapters` repository!
**This document covers the default adapter (TypeORM).**
See the [documentation for adapters](/schemas/adapters) to learn more about using Prisma adapter or using a custom adapter.
See the [documentation for adapters](/adapters/overview) to learn more about using Prisma adapter or using a custom adapter.
To learn more about databases in NextAuth.js and how they are used, check out [databases in the FAQ](/faq#databases).
@@ -27,7 +31,7 @@ You can specify database credentials as as a connection string or a [TypeORM con
The following approaches are exactly equivalent:
```js
database: 'mysql://nextauth:password@127.0.0.1:3306/database_name'
database: "mysql://nextauth:password@127.0.0.1:3306/database_name"
```
```js
@@ -44,13 +48,14 @@ database: {
:::tip
You can pass in any valid [TypeORM configuration option](https://github.com/typeorm/typeorm/blob/master/docs/using-ormconfig.md).
*e.g. To set a prefix for all table names you can use the **entityPrefix** option as connection string parameter:*
_e.g. To set a prefix for all table names you can use the **entityPrefix** option as connection string parameter:_
```js
'mysql://nextauth:password@127.0.0.1:3306/database_name?entityPrefix=nextauth_'
"mysql://nextauth:password@127.0.0.1:3306/database_name?entityPrefix=nextauth_"
```
*…or as a database configuration object:*
_…or as a database configuration object:_
```js
database: {
@@ -63,6 +68,7 @@ database: {
entityPrefix: 'nextauth_'
}
```
:::
---
@@ -73,15 +79,15 @@ Using SQL to create tables and columns is the recommended way to set up an SQL d
Check out the links below for SQL you can run to set up a database for NextAuth.js.
* [MySQL Schema](/schemas/mysql)
* [Postgres Schema](/schemas/postgres)
- [MySQL Schema](/adapters/typeorm/mysql)
- [Postgres Schema](/adapters/typeorm/postgres)
_If you are running SQLite, MongoDB or a Document database you can skip this step._
Alternatively, you can also have your database configured automatically using the `synchronize: true` option:
```js
database: 'mysql://nextauth:password@127.0.0.1:3306/database_name?synchronize=true'
database: "mysql://nextauth:password@127.0.0.1:3306/database_name?synchronize=true"
```
```js
@@ -122,7 +128,7 @@ Install module:
#### Example
```js
database: 'mysql://username:password@127.0.0.1:3306/database_name'
database: "mysql://username:password@127.0.0.1:3306/database_name"
```
### MariaDB
@@ -133,7 +139,7 @@ Install module:
#### Example
```js
database: 'mariadb://username:password@127.0.0.1:3306/database_name'
database: "mariadb://username:password@127.0.0.1:3306/database_name"
```
### Postgres / CockroachDB
@@ -144,13 +150,15 @@ Install module:
#### Example
PostgresDB
```js
database: 'postgres://username:password@127.0.0.1:5432/database_name'
database: "postgres://username:password@127.0.0.1:5432/database_name"
```
CockroachDB
```js
database: 'postgres://username:password@127.0.0.1:26257/database_name'
database: "postgres://username:password@127.0.0.1:26257/database_name"
```
If the node is using Self-signed cert
@@ -182,7 +190,7 @@ Install module:
#### Example
```js
database: 'mssql://sa:password@localhost:1433/database_name'
database: "mssql://sa:password@localhost:1433/database_name"
```
### MongoDB
@@ -193,12 +201,12 @@ Install module:
#### Example
```js
database: 'mongodb://username:password@127.0.0.1:3306/database_name'
database: "mongodb://username:password@127.0.0.1:3306/database_name"
```
### SQLite
*SQLite is intended only for development / testing and not for production use.*
_SQLite is intended only for development / testing and not for production use._
Install module:
`npm i sqlite3`
@@ -206,9 +214,9 @@ Install module:
#### Example
```js
database: 'sqlite://localhost/:memory:'
database: "sqlite://localhost/:memory:"
```
## Other databases
See the [documentation for adapters](/schemas/adapters) for more information on advanced configuration, including how to use NextAuth.js with other databases using a [custom adapter](/tutorials/creating-a-database-adapter).
See the [documentation for adapters](/adapters/overview) for more information on advanced configuration, including how to use NextAuth.js with other databases using a [custom adapter](/tutorials/creating-a-database-adapter).

View File

@@ -309,7 +309,7 @@ By default NextAuth.js uses a database adapter that uses TypeORM and supports My
You can use the `adapter` option to use the Prisma adapter - or pass in your own adapter if you want to use a database that is not supported by one of the built-in adapters.
See the [adapter documentation](/schemas/adapters) for more information.
See the [adapter documentation](/adapters/overview) for more information.
:::note
If the `adapter` option is specified it overrides the `database` option, only specify one or the other.

View File

@@ -28,7 +28,7 @@ We purposefully restrict the returned error codes for increased security.
The following errors are passed as error query parameters to the default or overriden error page:
- **Configuration**: There is a problem with the server configuration. Check if your [options](/configuration/options#options) is correct.
- **AccessDenied**: Usually occurs, when you restriected access through the [`signIn` callback](/configuration/callbacks#sign-in-callback), or [`redirect` callback](/configuration/callbacks#redirect-callback)
- **AccessDenied**: Usually occurs, when you restricted access through the [`signIn` callback](/configuration/callbacks#sign-in-callback), or [`redirect` callback](/configuration/callbacks#redirect-callback)
- **Verification**: Related to the Email provider. The token has expired or has already been used
- **Default**: Catch all, will apply, if none of the above matched

View File

@@ -254,22 +254,26 @@ providers: [
username: { label: "Username", type: "text", placeholder: "jsmith" },
password: { label: "Password", type: "password" }
},
async authorize(credentials, req) {
const user = (credentials, req) => {
// You need to provide your own logic here that takes the credentials
// submitted and returns either a object representing a user or value
// that is false/null if the credentials are invalid.
// e.g. return { id: 1, name: 'J Smith', email: 'jsmith@example.com' }
// You can also use the request object to obtain additional parameters
// (i.e., the request IP address)
return null
}
if (user) {
// Any user object returned here will be saved in the JSON Web Token
async authorize(credentials, req) {
// You need to provide your own logic here that takes the credentials
// submitted and returns either a object representing a user or value
// that is false/null if the credentials are invalid.
// e.g. return { id: 1, name: 'J Smith', email: 'jsmith@example.com' }
// You can also use the `req` object to obtain additional parameters
// (i.e., the request IP address)
const res = await fetch("/your/endpoint", {
method: 'POST',
body: JSON.stringify(credentials),
headers: { "Content-Type": "application/json" }
})
const user = await res.json()
// If no error and we have user data, return it
if (res.ok && user) {
return user
} else {
return null
}
// Return null if user data could not be retrieved
return null
}
})
]

View File

@@ -216,7 +216,7 @@ By default tokens are signed (JWS) but not encrypted (JWE), as encryption adds a
* JSON Web Tokens in NextAuth.js use JWS and are signed using HS512 with an auto-generated key.
* If encryption is enabled by setting `jwt: { encrypt: true }` option then the JWT will _also_ use JWE to encrypt the token, using A256GCM with an auto-generated key.
* If encryption is enabled by setting `jwt: { encryption: true }` option then the JWT will _also_ use JWE to encrypt the token, using A256GCM with an auto-generated key.
You can specify other valid algorithms - [as specified in RFC 7518](https://tools.ietf.org/html/rfc7517) - with either a secret (for symmetric encryption) or a public/private key pair (for a symmetric encryption).

View File

@@ -266,7 +266,7 @@ You can also set these parameters through [`provider.authorizationParams`](/conf
:::
:::note
The following parameters are always overridden server-side: `redirect_uri`, `scope`, `state`
The following parameters are always overridden server-side: `redirect_uri`, `state`
:::
---
@@ -328,6 +328,24 @@ export default function App ({ Component, pageProps }) {
If you pass the `session` page prop to the `<Provider>` as in the example above you can avoid checking the session twice on pages that support both server and client side rendering.
This only works on pages where you provide the correct `pageProps`, however. This is normally done in `getInitialProps` or `getServerSideProps` like so:
```js title="pages/index.js"
import { getSession } from "next-auth/client"
...
export async function getServerSideProps(ctx) {
return {
props: {
session: await getSession(ctx)
}
}
}
```
If every one of your pages needs to be protected, you can do this in `_app`, otherwise you can do it on a page-by-page basis. Alternatively, there is you can do per page authentication checks client side, instead of having each auth check be blocking (SSR) by using the method described below in [alternative client session handling](#custom-client-session-handling).
### Options
The session state is automatically synchronized across all open tabs/windows and they are all updated whenever they gain or lose focus or the state changes in any of them (e.g. a user signs in or out).
@@ -386,3 +404,72 @@ The value for `keepAlive` should always be lower than the value of the session `
:::note
See [**the Next.js documentation**](https://nextjs.org/docs/advanced-features/custom-app) for more information on **_app.js** in Next.js applications.
:::
## Alternatives
### Custom Client Session Handling
Due to the way Next.js handles `getServerSideProps` / `getInitialProps`, every protected page load has to make a server-side query to check if the session is valid and then generate the requested page. This alternative solution allows for showing a loading state on the initial check and every page transition afterward will be client-side, without having to check with the server and regenerate pages.
```js title="pages/admin.jsx"
export default function AdminDashboard () {
const [session] = useSession()
// session is always non-null inside this page, all the way down the React tree.
return "Some super secret dashboard"
}
AdminDashboard.auth = true
```
```jsx title="pages/_app.jsx"
export default function App({ Component, pageProps }) {
return (
<SessionProvider session={pageProps.session}>
{Component.auth
? <Auth><Component {...pageProps} /></Auth>
: <Component {...pageProps} />
}
</SessionProvider>
)
}
function Auth({ children }) {
const [session, loading] = useSession()
const isUser = !!session?.user
React.useEffect(() => {
if (loading) return // Do nothing while loading
if (!isUser) signIn() // If not authenticated, force log in
}, [isUser, loading])
if (isUser) {
return children
}
// Session is being fetched, or no user.
// If no user, useEffect() will redirect.
return <div>Loading...</div>
}
```
It can be easily be extended/modified to support something like an options object for role based authentication on pages. An example:
```jsx title="pages/admin.jsx"
AdminDashboard.auth = {
role: "admin",
loading: <AdminLoadingSkeleton/>,
unauthorized: "/login-with-different-user" // redirect to this url
}
```
Because of how _app is done, it won't unnecessarily contant the /api/auth/session endpoint for pages that do not require auth.
More information can be found in the following [Github Issue](https://github.com/nextauthjs/next-auth/issues/1210).
### NextAuth.js + React-Query
There is also an alternative client-side API library based upon [`react-query`](https://www.npmjs.com/package/react-query) available under [`nextauthjs/react-query`](https://github.com/nextauthjs/react-query).
If you use `react-query` in your project already, you can leverage it with NextAuth.js to handle the client-side session management for you as well. This replaces NextAuth.js's native `useSession` and `Provider` from `next-auth/client`.
See repository [`README`](https://github.com/nextauthjs/react-query) for more details.

View File

@@ -5,10 +5,10 @@ title: Example Code
## Get started with NextAuth.js
The example code below describes to add authentication to a Next.js app.
The example code below describes how to add authentication to a Next.js app.
:::tip
The easiest way to get started is to clone the [example app](https://github.com/nextauthjs/next-auth-example) and follow the instructions in README.md. You can try out a live demo at [next-auth-example.now.sh](https://next-auth-example.now.sh)
The easiest way to get started is to clone the [example app](https://github.com/nextauthjs/next-auth-example) and follow the instructions in README.md. You can try out a live demo at [next-auth-example.vercel.app](https://next-auth-example.vercel.app)
:::
### Add API route

View File

@@ -7,6 +7,10 @@ title: WorkOS
https://workos.com/docs/sso/guide
## Configuration
https://dashboard.workos.com
## Options
The **WorkOS Provider** comes with a set of default options:
@@ -22,10 +26,87 @@ import Providers from `next-auth/providers`
...
providers: [
Providers.WorkOS({
clientId: process.env.WORKOS_ID,
clientSecret: process.env.WORKOS_SECRET,
domain: process.env.WORKOS_DOMAIN
clientId: process.env.WORKOS_CLIENT_ID,
clientSecret: process.env.WORKOS_API_KEY,
}),
],
...
```
WorkOS is not an identity provider itself, but, rather, a bridge to multiple single sign-on (SSO) providers. As a result, we need to make some additional changes to authenticate users using WorkOS.
In order to sign a user in using WorkOS, we need to specify which WorkOS Connection to use. A common way to do this is to collect the user's email address and extract the domain.
This can be done using a custom login page.
To add a custom login page, you can use the `pages` option:
```javascript title="pages/api/auth/[...nextauth].js"
...
pages: {
signIn: '/auth/signin',
}
```
We can then add a custom login page that displays an input where the user can enter their email address. We then extract the domain from the user's email address and pass it to the `authorizationParams` parameter on the `signIn` function:
```jsx title="pages/auth/signin.js"
import { getProviders, signIn } from 'next-auth/client'
export default function SignIn({ providers }) {
const [email, setEmail] = useState('')
return (
<>
{Object.values(providers).map((provider) => {
if (provider.id === 'workos') {
return (
<div key={provider.id}>
<input
type="email"
value={email}
placeholder="Email"
onChange={(event) => setEmail(event.target.value)}
/>
<button
onClick={() =>
signIn(provider.id, undefined, {
domain: email.split('@')[1],
})
}
>
Sign in with SSO
</button>
</div>
);
}
return (
<div key={provider.id}>
<button onClick={() => signIn(provider.id)}>
Sign in with {provider.name}
</button>
</div>
);
})}
</>
);
}
// This is the recommended way for Next.js 9.3 or newer
export async function getServerSideProps(context){
const providers = await getProviders()
return {
props: { providers }
}
}
/*
// If older than Next.js 9.3
SignIn.getInitialProps = async () => {
return {
providers: await getProviders()
}
}
*/
```

View File

@@ -86,3 +86,7 @@ This `dev.to` tutorial walks one through adding NextAuth.js to an existing proje
### [Adding Sign in With Apple Next JS](https://thesiddd.com/blog/apple-auth)
This tutorial walks step by step on how to get Sign In with Apple working (both locally and on a deployed website) using NextAuth.js.
### [How to Authenticate Next.js Apps with Twitter & NextAuth.js](https://spacejelly.dev/posts/how-to-authenticate-next-js-apps-with-twitter-nextauth-js/)
Learn how to add Twitter authentication and login to a Next.js app both clientside and serverside with NextAuth.js.

View File

@@ -3,7 +3,7 @@ id: typeorm-custom-models
title: Custom models with TypeORM
---
NextAuth.js provides a set of [models and schemas](/schemas/models) for the built-in TypeORM adapter that you can easily extend.
NextAuth.js provides a set of [models and schemas](/adapters/models) for the built-in TypeORM adapter that you can easily extend.
You can use these models with MySQL, MariaDB, Postgres, MongoDB and SQLite.
@@ -79,5 +79,3 @@ export default NextAuth({
),
})
```

View File

@@ -27,15 +27,28 @@ module.exports = {
},
{
type: "category",
label: "Models & Schemas",
label: "Database Adapters",
collapsed: true,
items: [
"schemas/models",
"schemas/mysql",
"schemas/postgres",
"schemas/mssql",
"schemas/mongodb",
"schemas/adapters",
"adapters/overview",
"adapters/models",
{
type: "category",
label: "TypeORM",
collapsed: true,
items: [
"adapters/typeorm/typeorm-overview",
"adapters/typeorm/mysql",
"adapters/typeorm/postgres",
"adapters/typeorm/mssql",
"adapters/typeorm/mongodb",
],
},
"adapters/fauna",
"adapters/prisma",
"adapters/prisma-legacy",
"adapters/dynamodb",
"adapters/firebase",
],
},
{
@@ -49,5 +62,7 @@ module.exports = {
},
],
},
"warnings",
"errors",
],
}

View File

@@ -117,7 +117,7 @@ function Home() {
"button button--outline button--secondary button--lg rounded-pill",
styles.button
)}
href="https://next-auth-example.now.sh"
href="https://next-auth-example.vercel.app"
>
Live Demo
</a>

10
www/vercel.json Normal file
View File

@@ -0,0 +1,10 @@
{
"redirects": [
{ "source": "/schemas/models", "destination": "/adapters/models", "permanent": true },
{ "source": "/schemas/mysql", "destination": "/adapters/typeorm/mysql", "permanent": true },
{ "source": "/schemas/postgres", "destination": "/adapters/typeorm/postgres", "permanent": true },
{ "source": "/schemas/mssql", "destination": "/adapters/typeorm/mssql", "permanent": true },
{ "source": "/schemas/mongodb", "destination": "/adapters/typeorm/mongodb", "permanent": true },
{ "source": "/schemas/adapters", "destination": "/adapters/overview", "permanent": true }
]
}