Compare commits

..

36 Commits

Author SHA1 Message Date
GitHub Actions
3b414bd7b5 chore(release): bump package version(s) [skip ci] 2023-06-01 14:19:07 +00:00
Balázs Orbán
37bb6ebd2c fix(docs): update code example 2023-06-01 16:08:21 +02:00
Balázs Orbán
2ecf52c342 feat: introduce @auth/prisma-adapter (#7703)
BREAKING CHANGE:
If you are coming from the previous adapter, change your `package.json`:

```diff
-  "@next-auth/prisma-adapter": "0.0.0",
+  "@auth/prisma-adapter": "0.0.0",
```

And run `npm install`, `yarn install` or `pnpm install` respectively.

**Note:** This packages is published as ESM-only.
2023-06-01 16:06:22 +02:00
Balázs Orbán
cda07c239e chore: remove "nuxt postinstall" 2023-06-01 15:17:58 +02:00
Balázs Orbán
fa60b79abe chore: upgrade turbo 2023-06-01 15:15:23 +02:00
GitHub Actions
39e1a76e8f chore(release): bump package version(s) [skip ci] 2023-06-01 12:59:53 +00:00
Balázs Orbán
953ef9d04a chore: re-add pnpm caching
Related: #7332
2023-06-01 14:49:45 +02:00
Balázs Orbán
94f3031765 chore: allow manual release of any @auth/* package 2023-06-01 14:49:45 +02:00
Balázs Orbán
ad7bf07ddf chore: update lock file 2023-06-01 14:49:45 +02:00
Graham Charles
f30308ac30 docs: fix info card rendering in oauth-tutorial.mdx (#7662)
Info box is not being rendered; the raw `:::info` is displayed. Blind guess: it needs a blank line before it.
2023-06-01 14:49:45 +02:00
Tashrik Anam
6eaaeb15e9 docs: adapter card text color on hover when on dark mode (#7672) 2023-06-01 14:49:45 +02:00
Robert Soriano
8b3f0696a5 chore(playgrounds): Nuxt 3.5.1 (#7626)
* bump Nuxt to 3.5.1

* follow playground package names

* chore: update nuxt playground scripts

* fix: imports and types

* fix: more nuxt type imports

* fix: nuxt auth options types

* fix: nuxt client fetch types
2023-06-01 14:49:45 +02:00
Doron Sharon
c69a157832 chore: Add Descope as a 🥉 bronze financial sponsor (#7615)
Add Descope as a bronze sponsor
2023-06-01 14:49:45 +02:00
TATHAGATA ROY
60af446338 docs: Cypress.Cookies.defaults removed (#7574) 2023-06-01 14:49:45 +02:00
Nirmalya Ghosh
ce85444760 chore: Move next.config.js file into the correct directory (#7580)
fix: moves next config file into the correct directory
2023-06-01 14:49:45 +02:00
Balázs Orbán
142abe3eea feat: allow empty account mapper 2023-06-01 14:49:45 +02:00
Balázs Orbán
da211e6cbe chore: revert picture to image 2023-06-01 14:49:45 +02:00
Balázs Orbán
79ad6156ed feat: add update session to core (#7505)
* feat: add update session to core

Integrates #7056 into `@auth/core`

* resolve default user after jwt callback
2023-06-01 14:49:45 +02:00
Rémi Robichet
28f287d63e docs(example): update broken link (#7504)
Co-authored-by: Nico Domino <yo@ndo.dev>
2023-06-01 14:49:45 +02:00
Balázs Orbán
1ab77d0e11 chore: move build to root 2023-06-01 14:49:45 +02:00
Balázs Orbán
787c1ff7d0 chore: add build to manual publish 2023-06-01 14:49:45 +02:00
Balázs Orbán
208b3b4a43 chore: reduce breaking changes on Account mapping
Reverts some changes on #7369 so DB migration won't be needed
2023-06-01 14:49:45 +02:00
Balázs Orbán
c4f6330f70 chore: tweak manual release version 2023-06-01 14:49:45 +02:00
Balázs Orbán
44127068e1 chore: tweaks 2023-06-01 14:49:45 +02:00
Balázs Orbán
9e3f1aacf7 chore: tweak 2023-06-01 14:49:45 +02:00
Balázs Orbán
83051c6862 chore: skip test for manual release 2023-06-01 14:49:45 +02:00
Balázs Orbán
f1acab67e6 chore: separate manual release job 2023-06-01 14:49:45 +02:00
Balázs Orbán
6a31ed3216 chore: support release any package as experimental 2023-06-01 14:49:45 +02:00
Balázs Orbán
0998fc0b98 chore: use @ts-ignore 2023-06-01 14:49:45 +02:00
Balázs Orbán
bd20d750c2 fix(docs): remove extra heading
Fixes #7426
2023-06-01 14:49:45 +02:00
Balázs Orbán
8e29b4df0c fix: allow handling OAuth callback error response
related #7407
2023-06-01 14:49:45 +02:00
Balázs Orbán
9632a56d45 chore: type fixes 2023-06-01 14:49:45 +02:00
Balázs Orbán
12161b9613 fix: loosen profile types 2023-06-01 14:49:45 +02:00
Balázs Orbán
a3b5276a5a chore: improve errors, add more docs (#7415)
* JWT Token -> JWT

* document some errors

* improve errors, docs
2023-06-01 14:49:45 +02:00
Balázs Orbán
7c1078b9a9 feat(adapters): add Account mapping before database write (#7369)
* feat: map Account before saving to database

* document `acconut()`, explain default behaviour

* generate `expires_at` based on `expires_in`

Fixes #6538

* rename

* strip undefined on `defaultProfile`

* don't forward defaults to account callback

* improve internal namings, types, docs
2023-06-01 14:49:45 +02:00
Victor
37d3461155 docs: fix default maxAge formula (#7406) 2023-06-01 14:49:40 +02:00
49 changed files with 2906 additions and 1840 deletions

View File

@@ -29,7 +29,7 @@ body:
- "@next-auth/mongodb-adapter"
- "@next-auth/neo4j-adapter"
- "@next-auth/pouchdb-adapter"
- "@next-auth/prisma-adapter"
- "@auth/prisma-adapter"
- "@next-auth/sequelize-adapter"
- "@next-auth/supabase-adapter"
- "@next-auth/typeorm-legacy-adapter"

View File

@@ -25,7 +25,7 @@ pouchdb:
- "@next-auth/pouchdb-adapter"
prisma:
- "@next-auth/prisma-adapter"
- "@auth/prisma-adapter"
sequelize:
- "@next-auth/sequelize-adapter"

View File

@@ -15,16 +15,46 @@ on:
type: choice
description: Package name (npm)
options:
- "@auth/nextjs"
- "@auth/core"
- "@auth/nextjs"
- "@auth/dgraph-adapter"
- "@auth/drizzle-adapter"
- "@auth/dynamodb-adapter"
- "@auth/fauna-adapter"
- "@auth/firebase-adapter"
- "@auth/mikro-orm-adapter"
- "@auth/mongodb-adapter"
- "@auth/neo4j-adapter"
- "@auth/pouchdb-adapter"
- "@auth/prisma-adapter"
- "@auth/sequelize-adapter"
- "@auth/supabase-adapter"
- "@auth/typeorm-legacy-adapter"
- "@auth/upstash-redis-adapter"
- "@auth/xata-adapter"
- "next-auth"
# TODO: Infer from package name
path:
type: choice
description: Directory name (packages/*)
options:
- "frameworks-nextjs"
- "core"
- "frameworks-nextjs"
- "adapter-dgraph"
- "adapter-drizzle"
- "adapter-dynamodb"
- "adapter-fauna"
- "adapter-firebase"
- "adapter-mikro-orm"
- "adapter-mongodb"
- "adapter-neo4j"
- "adapter-pouchdb"
- "adapter-prisma"
- "adapter-sequelize"
- "adapter-supabase"
- "adapter-typeorm-legacy"
- "adapter-upstash-redis"
- "adapter-xata"
- "next-auth"
jobs:
@@ -42,6 +72,7 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Run tests
@@ -91,6 +122,7 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Publish to npm and GitHub
@@ -115,6 +147,7 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Determine version
@@ -153,6 +186,7 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Determine version
@@ -162,6 +196,7 @@ jobs:
PACKAGE_PATH: ${{ github.event.inputs.path }}
- name: Publish to npm
run: |
pnpm build
cd packages/$PACKAGE_PATH
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> .npmrc
pnpm publish --no-git-checks --access public --tag experimental

View File

@@ -15,7 +15,7 @@
"license": "ISC",
"dependencies": {
"@next-auth/fauna-adapter": "workspace:*",
"@next-auth/prisma-adapter": "workspace:*",
"@auth/prisma-adapter": "workspace:*",
"@next-auth/supabase-adapter": "workspace:*",
"@next-auth/typeorm-legacy-adapter": "workspace:*",
"@prisma/client": "^3",

View File

@@ -37,7 +37,7 @@ import WorkOS from "next-auth/providers/workos"
// // Prisma
// import { PrismaClient } from "@prisma/client"
// import { PrismaAdapter } from "@next-auth/prisma-adapter"
// import { PrismaAdapter } from "@auth/prisma-adapter"
// const client = globalThis.prisma || new PrismaClient()
// if (process.env.NODE_ENV !== "production") globalThis.prisma = client
// const adapter = PrismaAdapter(client)

View File

@@ -16,7 +16,7 @@
"dependencies": {
"@auth/core": "workspace:*",
"@next-auth/fauna-adapter": "workspace:*",
"@next-auth/prisma-adapter": "workspace:*",
"@auth/prisma-adapter": "workspace:*",
"@next-auth/supabase-adapter": "workspace:*",
"@next-auth/typeorm-legacy-adapter": "workspace:*",
"@prisma/client": "^3",

View File

@@ -6,24 +6,26 @@ import Asgardeo from "@auth/core/providers/asgardeo"
import Auth0 from "@auth/core/providers/auth0"
import AzureAD from "@auth/core/providers/azure-ad"
import AzureB2C from "@auth/core/providers/azure-ad-b2c"
import BeyondIdentity from "@auth/core/providers/beyondidentity"
import BoxyHQSAML from "@auth/core/providers/boxyhq-saml"
// import Cognito from "@auth/core/providers/cognito"
import Credentials from "@auth/core/providers/credentials"
// import Discord from "@auth/core/providers/discord"
// import DuendeIDS6 from "@auth/core/providers/duende-identity-server6"
import Discord from "@auth/core/providers/discord"
import DuendeIDS6 from "@auth/core/providers/duende-identity-server6"
// import Email from "@auth/core/providers/email"
// import Facebook from "@auth/core/providers/facebook"
// import Foursquare from "@auth/core/providers/foursquare"
// import Freshbooks from "@auth/core/providers/freshbooks"
// import GitHub from "@auth/core/providers/github"
// import Gitlab from "@auth/core/providers/gitlab"
// import Google from "@auth/core/providers/google"
import Facebook from "@auth/core/providers/facebook"
import Foursquare from "@auth/core/providers/foursquare"
import Freshbooks from "@auth/core/providers/freshbooks"
import GitHub from "@auth/core/providers/github"
import Gitlab from "@auth/core/providers/gitlab"
import Google from "@auth/core/providers/google"
// import IDS4 from "@auth/core/providers/identity-server4"
// import Instagram from "@auth/core/providers/instagram"
import Instagram from "@auth/core/providers/instagram"
// import Keycloak from "@auth/core/providers/keycloak"
// import Line from "@auth/core/providers/line"
// import LinkedIn from "@auth/core/providers/linkedin"
// import Mailchimp from "@auth/core/providers/mailchimp"
import Line from "@auth/core/providers/line"
import LinkedIn from "@auth/core/providers/linkedin"
import Mailchimp from "@auth/core/providers/mailchimp"
import Notion from "@auth/core/providers/notion"
// import Okta from "@auth/core/providers/okta"
import Osu from "@auth/core/providers/osu"
import Patreon from "@auth/core/providers/patreon"
@@ -39,7 +41,7 @@ import WorkOS from "@auth/core/providers/workos"
// // Prisma
// import { PrismaClient } from "@prisma/client"
// import { PrismaAdapter } from "@next-auth/prisma-adapter"
// import { PrismaAdapter } from "@auth/prisma-adapter"
// const client = globalThis.prisma || new PrismaClient()
// if (process.env.NODE_ENV !== "production") globalThis.prisma = client
// const adapter = PrismaAdapter(client)
@@ -75,21 +77,12 @@ export const authConfig: AuthConfig = {
logo: "https://next-auth.js.org/img/logo/logo-sm.png",
brandColor: "#1786fb",
},
pages: {
signIn: "/auth/signin",
},
providers: [
Credentials({
credentials: { password: { label: "Password", type: "password" } },
async authorize(credentials) {
// if (credentials.password !== "pw") return null
return {
name: "Fill Murray",
email: "bill@fillmurray.com",
image: "https://www.fillmurray.com/64/64",
id: "1",
foo: "",
}
if (credentials.password !== "pw") return null
return { name: "Fill Murray", email: "bill@fillmurray.com", image: "https://www.fillmurray.com/64/64", id: "1", foo: "" }
},
}),
Apple({ clientId: process.env.APPLE_ID, clientSecret: process.env.APPLE_SECRET }),

View File

@@ -1,196 +0,0 @@
import { useEffect, useState } from "react"
import "./style.css"
export default function() {
const [email, setEmail] = useState("")
const [name, setName] = useState("")
const [credentialId, setCredentialId] = useState()
const [challenge, setChallenge] = useState(new Uint8Array(32))
const [loginResponse, setLoginResponse] = useState()
const isWebAuthnSupported = () => {
return !!window.PublicKeyCredential
}
const isPlatformAuthenticatorSupported = () => {
if (!isWebAuthnSupported()) {
throw new Error("WebAuthn API is not available")
}
if (!PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable) {
return false
} else {
return PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
}
}
useEffect(() => {
isPlatformAuthenticatorSupported()
}, [])
const registerDevice = () => {
const publicKey = {
challenge,
rp: {
name: ".domino",
icon: "https://next-auth.js.org/img/logo/logo-sm.png",
},
user: {
id: new Uint8Array(16),
name: email,
displayName: name,
},
pubKeyCredParams: [
{ type: "public-key", alg: -7 },
{ type: "public-key", alg: -257 },
],
authenticatorSelection: {
userVerification: "required",
},
status: "ok",
}
navigator.credentials
.create({ publicKey })
.then((newCredentialInfo) => {
console.log("** REGISTRATION SUCCESS", newCredentialInfo)
setCredentialId(newCredentialInfo.id)
})
.catch((error) => {
console.log("FAIL", error)
})
}
const login = () => {
const publicKey = {
challenge: challenge,
allowCredentials: [
{ type: "public-key", id: Buffer.from(credentialId, "base64") },
],
userVerification: "required",
status: "ok",
}
navigator.credentials
.get({ publicKey })
.then((getAssertionResponse) => {
console.log("** ASSERTION RECEIVED", getAssertionResponse)
console.log(getAssertionResponse.response.authenticatorData.toString('base64'))
console.log(getAssertionResponse.response.clientDataJSON.toString('base64'))
console.log(getAssertionResponse.response.signature.toString('base64'))
})
.catch((error) => {
console.log("FAIL", error)
})
}
return (
<div
style={{
position: "absolute",
left: 0,
top: 0,
width: "100vw",
height: "100vh",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "2rem",
}}
>
<div
style={{
marginTop: "10vh",
backgroundColor: "#0d1117",
padding: "3rem",
borderRadius: "0.5rem",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "1rem",
}}
>
<img src="https://next-auth.js.org/img/logo/logo-sm.png" width="64" />
<div>WebAuthn Auth.js Demo</div>
</div>
<div
style={{
backgroundColor: "#0d1117",
padding: "3rem",
borderRadius: "0.5rem",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "1rem",
maxWidth: "400px",
}}
>
<h2 style={{ margin: "0" }}>Step 1</h2>
<label htmlFor="email">
Email <span style={{ color: "red" }}>*</span>
</label>
<input
type="text"
name="email"
placeholder="user@company.com"
onChange={(e) => setEmail(e.target.value)}
value={email}
/>
<label htmlFor="displayName">
Name <span style={{ color: "red" }}>*</span>
</label>
<input
type="text"
name="displayName"
placeholder="Joe"
onChange={(e) => setName(e.target.value)}
value={name}
/>
<button
type="button"
onClick={() => registerDevice()}
disabled={!email || !name}
>
Register
</button>
{credentialId && (
<pre
style={{
wordBreak: "break-all",
wordWrap: "break-word",
whiteSpace: "break-spaces",
}}
>
Credential Id: {credentialId}
</pre>
)}
</div>
<div
style={{
backgroundColor: "#0d1117",
padding: "3rem",
borderRadius: "0.5rem",
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "1rem",
}}
>
<h2>Step 2</h2>
<button type="button" onClick={() => login()} disabled={!credentialId}>
Login
</button>
{loginResponse && (
<pre
style={{
wordBreak: "break-all",
wordWrap: "break-word",
whiteSpace: "break-spaces",
}}
>
Credential Id: {credentialId}
</pre>
)}
</div>
</div>
)
}

View File

@@ -1,50 +0,0 @@
body {
background-color: #161b22;
color: #fff;
}
button {
margin: 0 0 0.75rem 0;
padding: 0.75rem 2rem;
color: white;
background-color: orange;
font-size: 1.1rem;
border-color: rgba(0, 0, 0, 0.1);
border-radius: 0.5rem;
transition: all 0.1s ease-in-out;
font-weight: 900;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
button:hover {
cursor: pointer;
}
button:active {
cursor: pointer;
}
button:disabled {
filter: grayscale(0.9);
}
button:disabled:hover {
cursor: auto !important;
}
label {
text-align: left;
}
input {
box-sizing: border-box;
display: block;
width: 100%;
padding: 0.5rem 1rem;
border: 1px solid #555;
background: white;
font-size: 1rem;
border-radius: 0.5rem;
color: black;
}
input:focus {
box-shadow: none;
}

View File

@@ -7,7 +7,7 @@ export default function Page() {
<p>Only admin users can see this page.</p>
<p>
To learn more about the NextAuth middleware see&nbsp;
<a href="https://docs-git-misc-docs-nextauthjs.vercel.app/configuration/nextjs#middleware">
<a href="https://next-auth.js.org/configuration/nextjs#middleware">
the docs
</a>
.

View File

@@ -1,4 +1,4 @@
import { Session } from "@auth/core"
import { Session } from "@auth/core/types"
export default function useSession() {
return useState<Session | null>("session", () => null)

View File

@@ -43,7 +43,7 @@ export async function signIn<
// TODO: Handle custom base path
// TODO: Remove this since Sveltekit offers the CSRF protection via origin check
const { csrfToken } = await $fetch("/api/auth/csrf")
const { csrfToken } = await $fetch<{ csrfToken: string }>("/api/auth/csrf")
console.log(_signInUrl)

View File

@@ -1,13 +1,14 @@
import { AuthHandler, AuthOptions, Session } from "@auth/core"
import { AuthConfig, Session } from "@auth/core/types"
import { Auth } from "@auth/core"
import { fromNodeMiddleware, H3Event } from "h3"
import getURL from "requrl"
import { createMiddleware } from "@hattip/adapter-node"
export function NuxtAuthHandler(options: AuthOptions) {
export function NuxtAuthHandler(options: AuthConfig) {
async function handler(ctx: { request: Request }) {
options.trustHost ??= true
return AuthHandler(ctx.request, options)
return Auth(ctx.request, options)
}
const middleware = createMiddleware(handler)
@@ -17,7 +18,7 @@ export function NuxtAuthHandler(options: AuthOptions) {
export async function getSession(
event: H3Event,
options: AuthOptions
options: AuthConfig
): Promise<Session | null> {
options.trustHost ??= true
@@ -30,7 +31,7 @@ export async function getSession(
nodeHeaders.append(key, headers[key] as any)
})
const response = await AuthHandler(
const response = await Auth(
new Request(url, { headers: nodeHeaders }),
options
)

View File

@@ -1,21 +1,21 @@
{
"name": "playground-nuxt",
"name": "next-auth-nuxt",
"private": true,
"scripts": {
"build": "nuxt prepare && nuxt build",
"dev": "nuxt prepare && export NODE_OPTIONS='--no-experimental-fetch' && nuxt dev",
"build": "nuxt build",
"dev": "nuxt prepare && nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview"
},
"devDependencies": {
"@nuxt/eslint-config": "^0.1.1",
"eslint": "^8.29.0",
"h3": "1.0.2",
"nuxt": "3.0.0"
"h3": "1.6.6",
"nuxt": "3.5.1"
},
"dependencies": {
"@auth/core": "workspace:*",
"@hattip/adapter-node": "^0.0.22",
"@hattip/adapter-node": "^0.0.34",
"requrl": "^3.0.2"
}
}

View File

@@ -1,4 +1,4 @@
import { Session } from "@auth/core"
import { Session } from "@auth/core/types"
export default defineNuxtPlugin(async () => {
const session = useSession()

View File

@@ -1,10 +1,10 @@
import { NuxtAuthHandler } from "@/lib/auth/server"
import GithubProvider from "@auth/core/providers/github"
import type { AuthOptions } from "@auth/core"
import type { AuthConfig } from "@auth/core"
const runtimeConfig = useRuntimeConfig()
export const authOptions: AuthOptions = {
export const authOptions = {
secret: runtimeConfig.secret,
providers: [
GithubProvider({
@@ -12,6 +12,6 @@ export const authOptions: AuthOptions = {
clientSecret: runtimeConfig.github.clientSecret,
}),
],
}
} as AuthConfig
export default NuxtAuthHandler(authOptions)

View File

@@ -243,10 +243,13 @@ http://localhost:5173/auth/callback/github
TODO Core
</TabItem>
</Tabs>
:::info
The last part of the URL, `[provider]`, is the ID of the provider you're using. In this case, we're using GitHub, so it's `github`. If you're using Google, it'll be `google`, etc... We keep track of the provider IDs internally.
The same id is used in the `signIn()` method we saw earlier.
:::
To register, tap on "Register application" button.
The next screen shows all the configurations for your newly created OAuth app. For now, we need two things from it - the **Client ID** and **Client Secret**:

View File

@@ -117,7 +117,7 @@ Using the database strategy is very similar, but instead of preserving the `acce
import { Auth } from "@auth/core"
import { type TokenSet } from "@auth/core/types"
import Google from "@auth/core/providers/google"
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()

View File

@@ -110,10 +110,6 @@ describe("Login page", () => {
secure: cookie.secure,
})
Cypress.Cookies.defaults({
preserve: cookieName,
})
// remove the two lines below if you need to stay logged in
// for your remaining tests
cy.visit("/api/auth/signout")

View File

@@ -96,8 +96,13 @@ erDiagram
string type
string provider
string providerAccountId
string refresh_token
string access_token
int expires_at
string token_type
string scope
string id_token
string session_state
}
VerificationToken {
string identifier
@@ -137,7 +142,7 @@ The Account model is for information about OAuth accounts associated with a User
A single User can have multiple Accounts, but each Account can only have one User.
Account creation in the database is automatic and happens when the user is logging in for the first time with a provider, or the [`Adapter.linkAccount`](/reference/core/adapters#linkaccount) method is invoked. The default data saved is `access_token`, `refresh_token`, `id_token` and `expires_at`. You can save other fields by returning them in the [OAuth provider](/guides/providers/custom-provider)'s [`account()`](/reference/core/providers#account) callback.
Account creation in the database is automatic and happens when the user is logging in for the first time with a provider, or the [`Adapter.linkAccount`](/reference/core/adapters#linkaccount) method is invoked. The default data saved is `access_token`, `expires_at`, `refresh_token`, `id_token`, `token_type`, `scope` and `session_state`. You can save other fields or remove the ones you don't need by returning them in the [OAuth provider](/guides/providers/custom-provider)'s [`account()`](/reference/core/providers#account) callback.
Linking Accounts to Users happen automatically, only when they have the same e-mail address, and the user is currently signed in. Check the [FAQ](/concepts/faq#security) for more information on why this is a requirement.

View File

@@ -29,6 +29,7 @@ html[data-theme="dark"] .adapter-card {
color: #f5f5f5;
}
html[data-theme="dark"] .adapter-card:hover,
.adapter-card:hover {
text-decoration: none;
color: black;

View File

@@ -43,7 +43,7 @@
"eslint-plugin-svelte3": "^4.0.0",
"prettier": "2.8.1",
"prettier-plugin-svelte": "^2.8.1",
"turbo": "1.8.8",
"turbo": "1.10.1",
"typescript": "4.9.4"
},
"engines": {

View File

@@ -8,14 +8,14 @@
</a>
<h3 align="center"><b>Prisma Adapter</b> - NextAuth.js / Auth.js</a></h3>
<p align="center" style="align: center;">
<a href="https://npm.im/@next-auth/prisma-adapter">
<a href="https://npm.im/@auth/prisma-adapter">
<img src="https://img.shields.io/badge/TypeScript-blue?style=flat-square" alt="TypeScript" />
</a>
<a href="https://npm.im/@next-auth/prisma-adapter">
<img alt="npm" src="https://img.shields.io/npm/v/@next-auth/prisma-adapter?color=green&label=@next-auth/prisma-adapter&style=flat-square">
<a href="https://npm.im/@auth/prisma-adapter">
<img alt="npm" src="https://img.shields.io/npm/v/@auth/prisma-adapter?color=green&label=@auth/prisma-adapter&style=flat-square">
</a>
<a href="https://www.npmtrends.com/@next-auth/prisma-adapter">
<img src="https://img.shields.io/npm/dm/@next-auth/prisma-adapter?label=%20downloads&style=flat-square" alt="Downloads" />
<a href="https://www.npmtrends.com/@auth/prisma-adapter">
<img src="https://img.shields.io/npm/dm/@auth/prisma-adapter?label=%20downloads&style=flat-square" alt="Downloads" />
</a>
<a href="https://github.com/nextauthjs/next-auth/stargazers">
<img src="https://img.shields.io/github/stars/nextauthjs/next-auth?style=flat-square" alt="Github Stars" />

View File

@@ -1,14 +1,29 @@
{
"name": "@next-auth/prisma-adapter",
"version": "1.0.6",
"description": "Prisma adapter for next-auth.",
"homepage": "https://authjs.dev",
"name": "@auth/prisma-adapter",
"version": "1.0.0",
"description": "Prisma adapter for Auth.js",
"homepage": "https://authjs.dev/reference/adapter/prisma",
"repository": "https://github.com/nextauthjs/next-auth",
"bugs": {
"url": "https://github.com/nextauthjs/next-auth/issues"
},
"author": "William Luke",
"main": "dist/index.js",
"contributors": [
"Balázs Orbán <info@balazsorban.com>"
],
"type": "module",
"types": "./index.d.ts",
"files": [
"*.js",
"*.d.ts*",
"src"
],
"exports": {
".": {
"types": "./index.d.ts",
"import": "./index.js"
}
},
"license": "ISC",
"keywords": [
"next-auth",
@@ -32,22 +47,19 @@
"dev": "prisma generate && tsc -w",
"studio": "prisma studio"
},
"files": [
"README.md",
"dist"
],
"dependencies": {
"@auth/core": "workspace:*"
},
"peerDependencies": {
"@prisma/client": ">=2.26.0 || >=3",
"next-auth": "^4"
"@prisma/client": ">=2.26.0 || >=3 || >=4"
},
"devDependencies": {
"@next-auth/adapter-test": "workspace:*",
"@next-auth/tsconfig": "workspace:*",
"@prisma/client": "^3.10.0",
"@prisma/client": "^4.15.0",
"jest": "^27.4.3",
"mongodb": "^4.4.0",
"next-auth": "workspace:*",
"prisma": "^3.10.0"
"prisma": "^4.15.0"
},
"jest": {
"preset": "@next-auth/adapter-test/jest"

View File

@@ -9,14 +9,14 @@
* ## Installation
*
* ```bash npm2yarn2pnpm
* npm install next-auth @prisma/client @next-auth/prisma-adapter
* npm install @prisma/client @auth/prisma-adapter
* npm install prisma --save-dev
* ```
*
* @module @next-auth/prisma-adapter
* @module @auth/prisma-adapter
*/
import type { PrismaClient, Prisma } from "@prisma/client"
import type { Adapter, AdapterAccount } from "next-auth/adapters"
import type { Adapter, AdapterAccount } from "@auth/core/adapters"
/**
* ## Setup
@@ -26,7 +26,7 @@ import type { Adapter, AdapterAccount } from "next-auth/adapters"
* ```js title="pages/api/auth/[...nextauth].js"
* import NextAuth from "next-auth"
* import GoogleProvider from "next-auth/providers/google"
* import { PrismaAdapter } from "@next-auth/prisma-adapter"
* import { PrismaAdapter } from "@auth/prisma-adapter"
* import { PrismaClient } from "@prisma/client"
*
* const prisma = new PrismaClient()

View File

@@ -1,9 +1,25 @@
{
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
"extends": "@next-auth/tsconfig/tsconfig.base.json",
"compilerOptions": {
"allowJs": true,
"baseUrl": ".",
"isolatedModules": true,
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"outDir": ".",
"rootDir": "src",
"outDir": "dist"
"skipDefaultLibCheck": true,
"strictNullChecks": true,
"stripInternal": true,
"declarationMap": true,
"declaration": true
},
"include": ["."],
"exclude": ["tests", "dist", "jest.config.js"]
}
"include": [
"src/**/*"
],
"exclude": [
"*.js",
"*.d.ts",
]
}

View File

@@ -1,4 +1,4 @@
import type { Adapter } from "next-auth/adapters"
import type { Adapter } from "@auth/core/adapters"
import { createHash, randomUUID } from "crypto"
const requiredMethods = [
@@ -58,7 +58,8 @@ export async function runBasicTests(options: TestOptions) {
await options.db.connect?.()
})
const { adapter, db, skipTests } = options
const { adapter: _adapter, db, skipTests } = options
const adapter = _adapter as Required<Adapter>
afterAll(async () => {
// @ts-expect-error This is only used for the TypeORM adapter
@@ -88,7 +89,7 @@ export async function runBasicTests(options: TestOptions) {
providerAccountId: randomUUID(),
type: "oauth",
access_token: randomUUID(),
expires_at: ONE_MONTH,
expires_at: ONE_MONTH / 1000,
id_token: randomUUID(),
refresh_token: randomUUID(),
token_type: "bearer",

View File

@@ -12,13 +12,13 @@
"license": "ISC",
"private": true,
"devDependencies": {
"@auth/core": "workspace:*",
"@babel/cli": "^7.14.3",
"@babel/plugin-transform-runtime": "^7.14.3",
"@babel/preset-env": "^7.14.2",
"@types/jest": "^26.0.23",
"@types/nodemailer": "^6.4.4",
"jest": "^27.0.3",
"next-auth": "workspace:*",
"ts-jest": "^27.0.3",
"typescript": "^4.2.4"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@auth/core",
"version": "0.7.1",
"version": "0.8.1",
"description": "Authentication for the Web.",
"keywords": [
"authentication",

View File

@@ -71,7 +71,7 @@
*
* ```ts title=my-adapter.ts
* import { type Adapter } from "@auth/core/adapters"
* import { PrismaAdapter } from "@next-auth/prisma-adapter"
* import { PrismaAdapter } from "@auth/prisma-adapter"
* import { PrismaClient } from "@prisma/client"
*
* const prisma = new PrismaClient()

View File

@@ -158,7 +158,7 @@ export async function handleLogin(
const { provider: p } = options as InternalOptions<"oauth" | "oidc">
const { type, provider, providerAccountId, userId, ...tokenSet } = account
const defaults = { providerAccountId, provider, type, userId }
account = Object.assign(p.account(tokenSet), defaults)
account = Object.assign(p.account(tokenSet) ?? {}, defaults)
if (user) {
// If the user is already signed in and the OAuth account isn't already associated

View File

@@ -47,7 +47,7 @@ export async function AuthInternal<
case "providers":
return (await routes.providers(options.providers)) as any
case "session": {
const session = await routes.session(sessionStore, options)
const session = await routes.session({ sessionStore, options })
if (session.cookies) cookies.push(...session.cookies)
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
return { ...session, cookies } as any
@@ -177,6 +177,22 @@ export async function AuthInternal<
return { ...callback, cookies }
}
break
case "session": {
if (options.csrfTokenVerified) {
const session = await routes.session({
options,
sessionStore,
newSession: request.body?.data,
isUpdate: true,
})
if (session.cookies) cookies.push(...session.cookies)
return { ...session, cookies } as any
}
// If CSRF token is invalid, return a 400 status code
// we should not redirect to a page as this is an API route
return { status: 400, cookies }
}
default:
}
}

View File

@@ -171,18 +171,18 @@ export async function handleOAuth(
Math.floor(Date.now() / 1000) + Number(tokens.expires_in)
}
const profileResult = await getUserAndProfile(
const profileResult = await getUserAndAccount(
profile,
provider,
tokens,
logger
)
return { ...profileResult, cookies: resCookies }
return { ...profileResult, profile, cookies: resCookies }
}
/** Returns profile, raw profile and auth provider details */
async function getUserAndProfile(
/** Returns the user and account that is going to be created in the database. */
async function getUserAndAccount(
OAuthProfile: Profile,
provider: OAuthConfigInternal<any>,
tokens: TokenSet,
@@ -206,7 +206,6 @@ async function getUserAndProfile(
providerAccountId: user.id.toString(),
...tokens,
},
OAuthProfile,
}
} catch (e) {
// If we didn't get a response either there was a problem with the provider

View File

@@ -106,14 +106,16 @@ const defaultProfile: ProfileCallback<Profile> = (profile) => {
* @see https://www.ietf.org/rfc/rfc6749.html#section-5.1
* @see https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
* @see https://authjs.dev/reference/adapters#account
*
* @todo Return `refresh_token` and `expires_at` as well when built-in
* refresh token support is added. (Can make it opt-in first with a flag).
*/
const defaultAccount: AccountCallback = (account) => {
return stripUndefined({
access_token: account.access_token,
id_token: account.id_token,
refresh_token: account.refresh_token,
expires_at: account.expires_at,
scope: account.scope,
token_type: account.token_type,
session_state: account.session_state,
})
}

View File

@@ -75,7 +75,7 @@ export async function callback(params: {
const {
user: userFromProvider,
account,
OAuthProfile,
profile: OAuthProfile,
} = authorizationResult
// If we don't have a profile object then either something went wrong
@@ -134,6 +134,7 @@ export async function callback(params: {
account,
profile: OAuthProfile,
isNewUser,
trigger: isNewUser ? "signUp" : "signIn",
})
// Clear cookies if token is null
@@ -244,6 +245,7 @@ export async function callback(params: {
user: loggedInUser,
account,
isNewUser,
trigger: isNewUser ? "signUp" : "signIn",
})
// Clear cookies if token is null
@@ -340,6 +342,7 @@ export async function callback(params: {
// @ts-expect-error
account,
isNewUser: false,
trigger: "signIn",
})
// Clear cookies if token is null

View File

@@ -6,10 +6,13 @@ import type { InternalOptions, ResponseInternal, Session } from "../../types.js"
import type { SessionStore } from "../cookie.js"
/** Return a session object filtered via `callbacks.session` */
export async function session(
sessionStore: SessionStore,
export async function session(params: {
options: InternalOptions
): Promise<ResponseInternal<Session | null>> {
sessionStore: SessionStore
isUpdate?: boolean
newSession?: any
}): Promise<ResponseInternal<Session | null>> {
const { options, sessionStore, newSession, isUpdate } = params
const {
adapter,
jwt,
@@ -33,23 +36,24 @@ export async function session(
try {
const decodedToken = await jwt.decode({ ...jwt, token: sessionToken })
const newExpires = fromDate(sessionMaxAge)
// By default, only exposes a limited subset of information to the client
// as needed for presentation purposes (e.g. "you are logged in as...").
const session = {
user: {
name: decodedToken?.name,
email: decodedToken?.email,
image: decodedToken?.picture,
},
expires: newExpires.toISOString(),
}
if (!decodedToken) throw new Error("Invalid JWT")
// @ts-expect-error
const token = await callbacks.jwt({ token: decodedToken })
const token = await callbacks.jwt({
token: decodedToken,
...(isUpdate && { trigger: "update" }),
session: newSession,
})
const newExpires = fromDate(sessionMaxAge)
if (token !== null) {
// By default, only exposes a limited subset of information to the client
// as needed for presentation purposes (e.g. "you are logged in as...").
const session = {
user: { name: token.name, email: token.email, image: token.picture },
expires: newExpires.toISOString(),
}
// @ts-expect-error
const newSession = await callbacks.session({ session, token })
@@ -125,14 +129,12 @@ export async function session(
// By default, only exposes a limited subset of information to the client
// as needed for presentation purposes (e.g. "you are logged in as...").
session: {
user: {
name: user.name,
email: user.email,
image: user.image,
},
user: { name: user.name, email: user.email, image: user.image },
expires: session.expires.toISOString(),
},
user,
newSession,
...(isUpdate ? { trigger: "update" } : {}),
})
// Return session payload as response

View File

@@ -32,12 +32,14 @@ export interface CredentialsConfig<
* @example
* ```ts
* //...
* async authorize(, request) {
* async authorize(credentials, request) {
* if(!isValidCredentials(credentials)) return null
* const response = await fetch(request)
* if(!response.ok) return null
* return await response.json() ?? null
* }
* //...
* ```
*/
authorize: (
/**

View File

@@ -95,7 +95,7 @@ export type ProfileCallback<Profile> = (
tokens: TokenSet
) => Awaitable<User>
export type AccountCallback = (account: TokenSet) => TokenSet
export type AccountCallback = (tokens: TokenSet) => TokenSet | undefined | void
export interface OAuthProviderButtonStyles {
logo: string
@@ -155,7 +155,31 @@ export interface OAuth2Config<Profile>
* Receives the full {@link TokenSet} returned by the OAuth provider, and returns a subset.
* It is used to create the account associated with a user in the database.
*
* Defaults to: `access_token` and `id_token`
* :::note
* You need to adjust your database's [Account model](https://authjs.dev/reference/adapters#account) to match the returned properties.
* Check out the documentation of your [database adapter](https://authjs.dev/reference/adapters) for more information.
* :::
*
* Defaults to: `access_token`, `id_token`, `refresh_token`, `expires_at`, `scope`, `token_type`, `session_state`
*
* @example
* ```ts
* import GitHub from "@auth/core/providers/github"
* // ...
* GitHub({
* account(account) {
* // https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/refreshing-user-access-tokens#refreshing-a-user-access-token-with-a-refresh-token
* const refresh_token_expires_at =
* Math.floor(Date.now() / 1000) + Number(account.refresh_token_expires_in)
* return {
* access_token: account.access_token,
* expires_at: account.expires_at,
* refresh_token: account.refresh_token,
* refresh_token_expires_at
* }
* }
* })
* ```
*
* @see [Database Adapter: Account model](https://authjs.dev/reference/adapters#account)
* @see https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse

View File

@@ -98,7 +98,15 @@ export interface Theme {
*/
export type TokenSet = Partial<
OAuth2TokenEndpointResponse | OpenIDTokenEndpointResponse
>
> & {
/**
* Date of when the `access_token` expires in seconds.
* This value is calculated from the `expires_in` value.
*
* @see https://www.ietf.org/rfc/rfc6749.html#section-4.2.2
*/
expires_at?: number
}
/**
* Usually contains information about the provider being used
@@ -230,40 +238,86 @@ export interface CallbacksOptions<P = Profile, A = Account> {
* If you want to make something available you added to the token through the `jwt` callback,
* you have to explicitly forward it here to make it available to the client.
*
* [Documentation](https://authjs.dev/guides/basics/callbacks#session-callback) |
* [`jwt` callback](https://authjs.dev/guides/basics/callbacks#jwt-callback) |
* [`useSession`](https://authjs.dev/reference/react/#usesession) |
* [`getSession`](https://authjs.dev/reference/utilities/#getsession) |
*
* @see [`jwt` callback](https://authjs.dev/reference/core/types#jwt)
*/
session: (params: {
session: Session
user: User | AdapterUser
token: JWT
}) => Awaitable<Session>
session: (
params:
| {
session: Session
/** Available when {@link AuthConfig.session} is set to `strategy: "jwt"` */
token: JWT
/** Available when {@link AuthConfig.session} is set to `strategy: "database"`. */
user: AdapterUser
} & {
/**
* Available when using {@link AuthConfig.session} `strategy: "database"` and an update is triggered for the session.
*
* :::note
* You should validate this data before using it.
* :::
*/
newSession: any
trigger: "update"
}
) => Awaitable<Session | DefaultSession>
/**
* This callback is called whenever a JSON Web Token is created (i.e. at sign in)
* or updated (i.e whenever a session is accessed in the client).
* Its content is forwarded to the `session` callback,
* where you can control what should be returned to the client.
* Anything else will be kept inaccessible from the client.
* Anything else will be kept from your front-end.
*
* Returning `null` will invalidate the JWT session by clearing
* the user's cookies. You'll still have to monitor and invalidate
* unexpired tokens from future requests yourself to prevent
* unauthorized access.
* The JWT is encrypted by default.
*
* By default the JWT is encrypted.
*
* [Documentation](https://authjs.dev/guides/basics/callbacks#jwt-callback) |
* [`session` callback](https://authjs.dev/guides/basics/callbacks#session-callback)
* [Documentation](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#session-callback)
*/
jwt: (params: {
/**
* When `trigger` is `"signIn"` or `"signUp"`, it will be a subset of {@link JWT},
* `name`, `email` and `image` will be included.
*
* Otherwise, it will be the full {@link JWT} for subsequent calls.
*/
token: JWT
user?: User | AdapterUser
account?: A | null
/**
* Either the result of the {@link OAuthConfig.profile} or the {@link CredentialsConfig.authorize} callback.
* @note available when `trigger` is `"signIn"` or `"signUp"`.
*
* Resources:
* - [Credentials Provider](https://authjs.dev/reference/core/providers_credentials)
* - [User database model](https://authjs.dev/reference/adapters#user)
*/
user: User | AdapterUser
/**
* Contains information about the provider that was used to sign in.
* Also includes {@link TokenSet}
* @note available when `trigger` is `"signIn"` or `"signUp"`
*/
account: A | null
/**
* The OAuth profile returned from your provider.
* (In case of OIDC it will be the decoded ID Token or /userinfo response)
* @note available when `trigger` is `"signIn"`.
*/
profile?: P
/**
* Check why was the jwt callback invoked. Possible reasons are:
* - user sign-in: First time the callback is invoked, `user`, `profile` and `account` will be present.
* - user sign-up: a user is created for the first time in the database (when {@link AuthConfig.session}.strategy is set to `"database"`)
* - update event: Triggered by the [`useSession().update`](https://next-auth.js.org/getting-started/client#update-session) method.
* In case of the latter, `trigger` will be `undefined`.
*/
trigger?: "signIn" | "signUp" | "update"
/** @deprecated use `trigger === "signUp"` instead */
isNewUser?: boolean
/**
* When using {@link AuthConfig.session} `strategy: "jwt"`, this is the data
* sent from the client via the [`useSession().update`](https://next-auth.js.org/getting-started/client#update-session) method.
*
* ⚠ Note, you should validate this data before using it.
*/
session?: any
}) => Awaitable<JWT | null>
}
@@ -443,7 +497,9 @@ export type InternalProvider<T = ProviderType> = (T extends "oauth"
* :::
* - **`"error"`**: Renders the built-in error page.
* - **`"providers"`**: Returns a client-safe list of all configured providers.
* - **`"session"`**: Returns the user's session if it exists, otherwise `null`.
* - **`"session"`**:
* - **`GET**`: Returns the user's session if it exists, otherwise `null`.
* - **`POST**`: Updates the user's session and returns the updated session.
* - **`"signin"`**:
* - **`GET`**: Renders the built-in sign-in page.
* - **`POST`**: Initiates the sign-in flow.

View File

@@ -224,6 +224,13 @@ We're happy to announce we've recently created an [OpenCollective](https://openc
<div>WorkOS</div><br />
<sub>🥉 Bronze Financial Sponsor</sub>
</td>
<td align="center" valign="top">
<a href="https://www.descope.com" target="_blank">
<img width="128px" src="https://avatars.githubusercontent.com/u/97479186?v=4" alt="Descope Logo" />
</a><br />
<div>Descope</div><br />
<sub>🥉 Bronze Financial Sponsor</sub>
</td>
<td align="center" valign="top">
<a href="https://checklyhq.com" target="_blank">
<img width="128px" src="https://avatars.githubusercontent.com/u/25982255?v=4" alt="Checkly Logo" />

View File

@@ -65,10 +65,8 @@
],
"license": "ISC",
"dependencies": {
"@babel/runtime": "^7.16.3",
"@panva/hkdf": "^1.0.1",
"@simplewebauthn/browser": "^6.2.2",
"@simplewebauthn/server": "^6.2.2",
"@babel/runtime": "^7.20.13",
"@panva/hkdf": "^1.0.2",
"cookie": "^0.5.0",
"jose": "^4.11.4",
"oauth": "^0.9.15",
@@ -98,7 +96,6 @@
"@babel/preset-typescript": "^7.17.12",
"@edge-runtime/jest-environment": "1.1.0-beta.35",
"@next-auth/tsconfig": "workspace:*",
"@simplewebauthn/typescript-types": "6.3.0-alpha.1",
"@swc/core": "^1.2.198",
"@swc/jest": "^0.2.21",
"@testing-library/dom": "^8.13.0",

View File

@@ -1,73 +0,0 @@
import {
generateAuthenticationOptions,
verifyAuthenticationResponse,
} from '@simplewebauthn/server';
import { getUserFromDb } from "./helpers"
import type { } from '@simplewebauthn/typescript-types'
/*
* @desc GET /api/auth/webauthn/authenticate
* @type object
**/
const generateAuthOptions = (userId) => {
// (Pseudocode) Retrieve the logged-in user
const user: UserModel = getUserFromDb(userId)
// (Pseudocode) Retrieve any of the user's previously-
// registered authenticators
const userAuthenticators: Authenticator[] = getUserAuthenticators(user)
const options = generateAuthenticationOptions({
// Require users to use a previously-registered authenticator
allowCredentials: userAuthenticators.map((authenticator) => ({
id: authenticator.credentialID,
type: "public-key",
// Optional
transports: authenticator.transports,
})),
userVerification: "preferred",
})
// (Pseudocode) Remember this challenge for this user
setUserCurrentChallenge(user, options.challenge)
return options
}
/*
* @desc POST to /api/auth/webauthn/authenticate
* @type object
* @param body: AuthenticationCredentialJSON
**/
const verifyAuthResponse = (req) => {
const { body } = req;
// (Pseudocode) Retrieve the logged-in user
const user: UserModel = getUserFromDB(loggedInUserId);
// (Pseudocode) Get `options.challenge` that was saved above
const expectedChallenge: string = getUserCurrentChallenge(user);
// (Pseudocode} Retrieve an authenticator from the DB that
// should match the `id` in the returned credential
const authenticator = getUserAuthenticator(user, body.id);
if (!authenticator) {
throw new Error(`Could not find authenticator ${body.id} for user ${user.id}`);
}
let verification;
try {
verification = await verifyAuthenticationResponse({
credential: body,
expectedChallenge,
expectedOrigin: origin,
expectedRPID: rpID,
authenticator,
});
} catch (error) {
console.error(error);
return res.status(400).send({ error: error.message });
}
const { verified } = verification;
return verified
}

View File

@@ -1,27 +0,0 @@
export const saveRegistrationInfo = ({ verification, registrationInfo }) => {
const { registrationInfo } = verification
const { credentialPublicKey, credentialID, counter } = registrationInfo
const newAuthenticator: Authenticator = {
credentialID,
credentialPublicKey,
counter,
}
// (Pseudocode) Save the authenticator info so that we can
// get it by user ID later
saveNewUserAuthenticatorInDB(user, newAuthenticator)
}
// TODO:
// (Pseudocode) Fetch User from adapter
export const getUserFromDb = (userId) => {
return db.query('Users').where({ userId })
}
export const updateRegistrationCounter = ({ verification, authenticationInfo }) = {
const { authenticationInfo } = verification;
const { newCounter } = authenticationInfo;
saveUpdatedAuthenticatorCounter(authenticator, newCounter);
}

View File

@@ -1,78 +0,0 @@
import SimpleWebAuthnServer from "@simplewebauthn/server"
import { saveRegistrationInfo } from './helpers'
import type { Authenticator } from '../../types'
/*
* @desc GET - /api/auth/webauthn/register
* @type object
**/
const generateRegistration = () => {
// Human-readable title for your website
const rpName = "SimpleWebAuthn Example"
// A unique identifier for your website
const rpID = "localhost"
// The URL at which registrations and authentications should occur
const origin = `https://${rpID}`
// (Pseudocode) Retrieve the user from the database
// after they've logged in
const user: UserModel = getUserFromDB(loggedInUserId)
// (Pseudocode) Retrieve any of the user's previously-
// registered authenticators
const userAuthenticators: Authenticator[] = getUserAuthenticators(user)
const options = generateRegistrationOptions({
rpName,
rpID,
userID: user.id,
userName: user.username,
// Don't prompt users for additional information about the authenticator
// (Recommended for smoother UX)
attestationType: "none",
// Prevent users from re-registering existing authenticators
excludeCredentials: userAuthenticators.map((authenticator) => ({
id: authenticator.credentialID,
type: "public-key",
// Optional
transports: authenticator.transports,
})),
})
// (Pseudocode) Remember the challenge for this user
setUserCurrentChallenge(user, options.challenge)
return options
}
/*
* @desc POST /api/auth/webauthn/register
* @type object
**/
const verifyRegistration = (req) => {
const { body } = req
// (Pseudocode) Retrieve the logged-in user
const user: UserModel = getUserFromDB(loggedInUserId)
// (Pseudocode) Get `options.challenge` that was saved above
const expectedChallenge: string = getUserCurrentChallenge(user)
let verification
try {
verification = await verifyRegistrationResponse({
credential: body,
expectedChallenge,
expectedOrigin: origin,
expectedRPID: rpID,
})
} catch (error) {
console.error(error)
return res.status(400).send({ error: error.message })
}
const { verified } = verification
if (verified) {
saveRegistrationInfo({ verification, registrationInfo })
}
return { verified }
}

View File

@@ -1,7 +0,0 @@
import type { ResponseInternal } from ".."
export default function webauthn(): ResponseInternal<> {
// TODO:
// Assign `/core/lib/webauthn/{authentication-handler,registration-handler}.ts`
// handler functions to the endpoint/http verb combo listed in their @desc
}

View File

@@ -625,36 +625,3 @@ export type NextAuthApiHandler<Result = void, Response = any> = (
req: NextAuthRequest,
res: NextAuthResponse<Response>
) => Awaitable<Result>
/** @internal */
export type UserModel = {
id: string
username: string
currentChallenge?: string
}
/**
* It is strongly advised that authenticators get their own DB
* table, ideally with a foreign key to a specific UserModel.
*
* "SQL" tags below are suggestions for column data types and
* how best to store data received during registration for use
* in subsequent authentications.
* @internal
*/
export type Authenticator = {
// SQL: Encode to base64url then store as `TEXT`. Index this column
credentialID: Buffer
// SQL: Store raw bytes as `BYTEA`/`BLOB`/etc...
credentialPublicKey: Buffer
// SQL: Consider `BIGINT` since some authenticators return atomic timestamps as counters
counter: number
// SQL: `VARCHAR(32)` or similar, longest possible value is currently 12 characters
// Ex: 'singleDevice' | 'multiDevice'
credentialDeviceType: CredentialDeviceType
// SQL: `BOOL` or whatever similar type is supported
credentialBackedUp: boolean
// SQL: `VARCHAR(255)` and store string array as a CSV string
// Ex: ['usb' | 'ble' | 'nfc' | 'internal']
transports?: AuthenticatorTransport[]
}

3799
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**/*"]
"outputs": ["dist/**/*", "*.js", "*.d.ts", "*.d.ts.map"]
},
"next-auth#build": {
"dependsOn": ["^build"],
@@ -53,6 +53,7 @@
"docs#dev": {
"dependsOn": [
"@auth/core#build",
"@auth/prisma-adapter#build",
"@auth/sveltekit#build",
"@next-auth/dgraph-adapter#build",
"@next-auth/dynamodb-adapter#build",
@@ -62,7 +63,6 @@
"@next-auth/mongodb-adapter#build",
"@next-auth/neo4j-adapter#build",
"@next-auth/pouchdb-adapter#build",
"@next-auth/prisma-adapter#build",
"@next-auth/sequelize-adapter#build",
"@next-auth/supabase-adapter#build",
"@next-auth/typeorm-legacy-adapter#build",
@@ -76,6 +76,7 @@
"docs#build": {
"dependsOn": [
"@auth/core#build",
"@auth/prisma-adapter#build",
"@auth/sveltekit#build",
"@next-auth/dgraph-adapter#build",
"@next-auth/dynamodb-adapter#build",
@@ -85,7 +86,6 @@
"@next-auth/mongodb-adapter#build",
"@next-auth/neo4j-adapter#build",
"@next-auth/pouchdb-adapter#build",
"@next-auth/prisma-adapter#build",
"@next-auth/sequelize-adapter#build",
"@next-auth/supabase-adapter#build",
"@next-auth/typeorm-legacy-adapter#build",