Compare commits

..

20 Commits

Author SHA1 Message Date
ndom91
307ca24cb8 Merge branch 'main' into ndom91/webauthn-test 2023-05-09 19:12:56 +02:00
Balázs Orbán
b96f01319c chore: tweak manual release version 2023-05-05 02:38:47 +02:00
Balázs Orbán
8f416b68ec chore: tweaks 2023-05-04 23:58:54 +02:00
Balázs Orbán
eaf5080721 chore: tweak 2023-05-04 23:50:49 +02:00
Balázs Orbán
e0b5f18c5b chore: skip test for manual release 2023-05-04 23:42:05 +02:00
Balázs Orbán
99247ce446 chore: separate manual release job 2023-05-04 23:40:37 +02:00
Balázs Orbán
d6bc65f0d8 chore: support release any package as experimental 2023-05-04 23:25:29 +02:00
Balázs Orbán
6f5a50313f chore: use @ts-ignore 2023-05-04 22:21:36 +02:00
Balázs Orbán
e3bdb38df2 fix(docs): remove extra heading
Fixes #7426
2023-05-03 12:40:30 +02:00
Balázs Orbán
92a0fc42fa fix: allow handling OAuth callback error response
related #7407
2023-05-01 13:49:17 +02:00
Balázs Orbán
62e2ad115c chore: type fixes 2023-05-01 13:46:23 +02:00
Balázs Orbán
542c35d729 fix: loosen profile types 2023-05-01 13:22:16 +02:00
Balázs Orbán
5400645221 chore: improve errors, add more docs (#7415)
* JWT Token -> JWT

* document some errors

* improve errors, docs
2023-05-01 10:32:20 +01:00
Balázs Orbán
d739e8e04e 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-04-30 12:25:26 +01:00
Victor
c2eb9b3ad4 fix(docs): fix default maxAge formula (#7406) 2023-04-30 10:34:05 +02:00
Balázs Orbán
5e6b461908 Merge branch 'main' into ndom91/webauthn-test 2023-02-10 02:26:41 +01:00
ndom91
2f7f9ccfe3 feat: add simplewebauthn 2022-12-18 17:53:00 +01:00
Nico Domino
65ee6756bc Merge branch 'main' into ndom91/webauthn-test 2022-12-18 15:32:20 +01:00
Nico Domino
963e407d5f Merge branch 'main' into ndom91/webauthn-test 2022-12-18 00:36:10 +01:00
ndom91
4e294edec1 feat: test webauthn platform auth 2022-12-15 03:31:32 +01:00
75 changed files with 2121 additions and 3246 deletions

View File

@@ -29,10 +29,10 @@ body:
- "@next-auth/mongodb-adapter"
- "@next-auth/neo4j-adapter"
- "@next-auth/pouchdb-adapter"
- "@auth/prisma-adapter"
- "@next-auth/prisma-adapter"
- "@next-auth/sequelize-adapter"
- "@next-auth/supabase-adapter"
- "@auth/typeorm-adapter"
- "@next-auth/typeorm-legacy-adapter"
- "@next-auth/upstash-redis-adapter"
- "@next-auth/xata-adapter"
validations:

View File

@@ -25,7 +25,7 @@ pouchdb:
- "@next-auth/pouchdb-adapter"
prisma:
- "@auth/prisma-adapter"
- "@next-auth/prisma-adapter"
sequelize:
- "@next-auth/sequelize-adapter"
@@ -33,8 +33,8 @@ sequelize:
supabase:
- "@next-auth/supabase-adapter"
typeorm:
- "@auth/typeorm-adapter"
typeorm-legacy:
- "@next-auth/typeorm-legacy-adapter"
upstash-redis:
- "@next-auth/upstash-redis-adapter"

View File

@@ -21,6 +21,6 @@ solidjs: ["packages/frameworks-solid-start/**/*"]
supabase: ["packages/adapter-supabase/**/*"]
svelte: ["packages/frameworks-sveltekit/**/*"]
test: ["**test**/*"]
typeorm: ["packages/adapter-typeorm/**/*"]
typeorm-legacy: ["packages/adapter-typeorm-legacy/**/*"]
upstash-redis: ["packages/adapter-upstash-redis/**/*"]
xata: ["packages/adapter-xata/**/*"]

View File

@@ -15,46 +15,16 @@ on:
type: choice
description: Package name (npm)
options:
- "@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-adapter"
- "@auth/upstash-redis-adapter"
- "@auth/xata-adapter"
- "@auth/core"
- "next-auth"
# TODO: Infer from package name
path:
type: choice
description: Directory name (packages/*)
options:
- "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"
- "adapter-upstash-redis"
- "adapter-xata"
- "core"
- "next-auth"
jobs:
@@ -72,7 +42,6 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Run tests
@@ -122,7 +91,6 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Publish to npm and GitHub
@@ -147,7 +115,6 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Determine version
@@ -186,7 +153,6 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Determine version
@@ -196,7 +162,6 @@ 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,9 +15,9 @@
"license": "ISC",
"dependencies": {
"@next-auth/fauna-adapter": "workspace:*",
"@auth/prisma-adapter": "workspace:*",
"@next-auth/prisma-adapter": "workspace:*",
"@next-auth/supabase-adapter": "workspace:*",
"@auth/typeorm-adapter": "workspace:*",
"@next-auth/typeorm-legacy-adapter": "workspace:*",
"@prisma/client": "^3",
"@supabase/supabase-js": "^2.0.5",
"faunadb": "^4",

View File

@@ -37,7 +37,7 @@ import WorkOS from "next-auth/providers/workos"
// // Prisma
// import { PrismaClient } from "@prisma/client"
// import { PrismaAdapter } from "@auth/prisma-adapter"
// import { PrismaAdapter } from "@next-auth/prisma-adapter"
// const client = globalThis.prisma || new PrismaClient()
// if (process.env.NODE_ENV !== "production") globalThis.prisma = client
// const adapter = PrismaAdapter(client)
@@ -51,8 +51,8 @@ import WorkOS from "next-auth/providers/workos"
// const adapter = FaunaAdapter(client)
// // TypeORM
// import { TypeORMAdapter } from "@auth/typeorm-adapter"
// const adapter = TypeORMAdapter({
// import { TypeORMLegacyAdapter } from "@next-auth/typeorm-legacy-adapter"
// const adapter = TypeORMLegacyAdapter({
// type: "sqlite",
// name: "next-auth-test-memory",
// database: "./typeorm/dev.db",

View File

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

View File

@@ -6,26 +6,24 @@ 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 Notion from "@auth/core/providers/notion"
// import Line from "@auth/core/providers/line"
// import LinkedIn from "@auth/core/providers/linkedin"
// import Mailchimp from "@auth/core/providers/mailchimp"
// import Okta from "@auth/core/providers/okta"
import Osu from "@auth/core/providers/osu"
import Patreon from "@auth/core/providers/patreon"
@@ -41,7 +39,7 @@ import WorkOS from "@auth/core/providers/workos"
// // Prisma
// import { PrismaClient } from "@prisma/client"
// import { PrismaAdapter } from "@auth/prisma-adapter"
// import { PrismaAdapter } from "@next-auth/prisma-adapter"
// const client = globalThis.prisma || new PrismaClient()
// if (process.env.NODE_ENV !== "production") globalThis.prisma = client
// const adapter = PrismaAdapter(client)
@@ -55,8 +53,8 @@ import WorkOS from "@auth/core/providers/workos"
// const adapter = FaunaAdapter(client)
// // TypeORM
// import { TypeORMAdapter } from "@auth/typeorm-adapter"
// const adapter = TypeORMAdapter({
// import { TypeORMLegacyAdapter } from "@next-auth/typeorm-legacy-adapter"
// const adapter = TypeORMLegacyAdapter({
// type: "sqlite",
// name: "next-auth-test-memory",
// database: "./typeorm/dev.db",
@@ -77,12 +75,21 @@ 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

@@ -0,0 +1,196 @@
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

@@ -0,0 +1,50 @@
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://next-auth.js.org/configuration/nextjs#middleware">
<a href="https://docs-git-misc-docs-nextauthjs.vercel.app/configuration/nextjs#middleware">
the docs
</a>
.

View File

@@ -1,4 +1,4 @@
import { Session } from "@auth/core/types"
import { Session } from "@auth/core"
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<{ csrfToken: string }>("/api/auth/csrf")
const { csrfToken } = await $fetch("/api/auth/csrf")
console.log(_signInUrl)

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { Session } from "@auth/core/types"
import { Session } from "@auth/core"
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 { AuthConfig } from "@auth/core"
import type { AuthOptions } from "@auth/core"
const runtimeConfig = useRuntimeConfig()
export const authOptions = {
export const authOptions: AuthOptions = {
secret: runtimeConfig.secret,
providers: [
GithubProvider({
@@ -12,6 +12,6 @@ export const authOptions = {
clientSecret: runtimeConfig.github.clientSecret,
}),
],
} as AuthConfig
}
export default NuxtAuthHandler(authOptions)

View File

@@ -243,13 +243,10 @@ 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 "@auth/prisma-adapter"
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()

View File

@@ -22,18 +22,11 @@ Next you will have to create some configuration files for Cypress.
First, the primary cypress config:
```ts title="cypress.config.ts"
import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
chromeWebSecurity: false,
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
})
```js title="cypress.json"
{
"baseUrl": "http://localhost:3000",
"chromeWebSecurity": false
}
```
This initial Cypress config will tell Cypress where to find your site on initial launch as well as allow it to open up URLs at domains that aren't your page, for example to be able to login to a social provider.
@@ -53,24 +46,14 @@ You must change the login credentials you want to use, but you can also redefine
Third, if you're using the `cypress-social-login` plugin, you must add this to your `/cypress/plugins/index.js` file like so:
```js title="cypress.config.ts" {3-4,10-14}
import { defineConfig } from 'cypress'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { GoogleSocialLogin } = require('cypress-social-logins').plugins
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
chromeWebSecurity: false,
setupNodeEvents(on, config) {
on('task', {
GoogleSocialLogin,
})
},
},
})
```js title="cypress/plugins/index.js"
const { GoogleSocialLogin } = require("cypress-social-logins").plugins
module.exports = (on, config) => {
on("task", {
GoogleSocialLogin: GoogleSocialLogin,
})
}
```
Finally, you can also add the following npm scripts to your `package.json`:
@@ -127,6 +110,10 @@ 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,13 +96,8 @@ 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
@@ -142,7 +137,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`, `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.
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.
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

@@ -273,7 +273,19 @@ const docusaurusConfig = {
typedocAdapter("Neo4j"),
typedocAdapter("PouchDB"),
typedocAdapter("Prisma"),
typedocAdapter("TypeORM"),
[
"docusaurus-plugin-typedoc",
{
...typedocConfig,
id: "typeorm",
plugin: [require.resolve("./typedoc-mdn-links")],
watch: process.env.TYPEDOC_WATCH,
entryPoints: [`../packages/adapter-typeorm-legacy/src/index.ts`],
tsconfig: `../packages/adapter-typeorm-legacy/tsconfig.json`,
out: `reference/adapter/typeorm`,
sidebar: { indexLabel: "TypeORM" },
},
],
typedocAdapter("Sequelize"),
typedocAdapter("Supabase"),
typedocAdapter("Upstash Redis"),

View File

@@ -29,7 +29,6 @@ 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.10.1",
"turbo": "1.8.8",
"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/@auth/prisma-adapter">
<a href="https://npm.im/@next-auth/prisma-adapter">
<img src="https://img.shields.io/badge/TypeScript-blue?style=flat-square" alt="TypeScript" />
</a>
<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 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>
<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 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>
<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,29 +1,14 @@
{
"name": "@auth/prisma-adapter",
"version": "1.0.0",
"description": "Prisma adapter for Auth.js",
"homepage": "https://authjs.dev/reference/adapter/prisma",
"name": "@next-auth/prisma-adapter",
"version": "1.0.6",
"description": "Prisma adapter for next-auth.",
"homepage": "https://authjs.dev",
"repository": "https://github.com/nextauthjs/next-auth",
"bugs": {
"url": "https://github.com/nextauthjs/next-auth/issues"
},
"author": "William Luke",
"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"
}
},
"main": "dist/index.js",
"license": "ISC",
"keywords": [
"next-auth",
@@ -47,19 +32,22 @@
"dev": "prisma generate && tsc -w",
"studio": "prisma studio"
},
"dependencies": {
"@auth/core": "workspace:*"
},
"files": [
"README.md",
"dist"
],
"peerDependencies": {
"@prisma/client": ">=2.26.0 || >=3 || >=4"
"@prisma/client": ">=2.26.0 || >=3",
"next-auth": "^4"
},
"devDependencies": {
"@next-auth/adapter-test": "workspace:*",
"@next-auth/tsconfig": "workspace:*",
"@prisma/client": "^4.15.0",
"@prisma/client": "^3.10.0",
"jest": "^27.4.3",
"mongodb": "^4.4.0",
"prisma": "^4.15.0"
"next-auth": "workspace:*",
"prisma": "^3.10.0"
},
"jest": {
"preset": "@next-auth/adapter-test/jest"

View File

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

View File

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

View File

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

@@ -8,14 +8,14 @@
</a>
<h3 align="center"><b>TypeORM Adapter</b> - NextAuth.js / Auth.js</a></h3>
<p align="center" style="align: center;">
<a href="https://npm.im/@auth/typeorm-adapter">
<a href="https://npm.im/@next-auth/typeorm-legacy-adapter">
<img src="https://img.shields.io/badge/TypeScript-blue?style=flat-square" alt="TypeScript" />
</a>
<a href="https://npm.im/@auth/typeorm-adapter">
<img alt="npm" src="https://img.shields.io/npm/v/@auth/typeorm-adapter?color=green&label=@auth/typeorm-adapter&style=flat-square">
<a href="https://npm.im/@next-auth/typeorm-legacy-adapter">
<img alt="npm" src="https://img.shields.io/npm/v/@next-auth/typeorm-legacy-adapter?color=green&label=@next-auth/typeorm-legacy-adapter&style=flat-square">
</a>
<a href="https://www.npmtrends.com/@auth/typeorm-adapter">
<img src="https://img.shields.io/npm/dm/@auth/typeorm-adapter?label=%20downloads&style=flat-square" alt="Downloads" />
<a href="https://www.npmtrends.com/@next-auth/typeorm-legacy-adapter">
<img src="https://img.shields.io/npm/dm/@next-auth/typeorm-legacy-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,8 +1,8 @@
{
"name": "@auth/typeorm-adapter",
"version": "1.0.0",
"description": "TypeORM adapter for Auth.js.",
"homepage": "https://authjs.dev/reference/adapter/typeorm",
"name": "@next-auth/typeorm-legacy-adapter",
"version": "2.0.2",
"description": "TypeORM (legacy) adapter for next-auth.",
"homepage": "https://authjs.dev",
"repository": "https://github.com/nextauthjs/next-auth",
"bugs": {
"url": "https://github.com/nextauthjs/next-auth/issues"
@@ -11,19 +11,11 @@
"contributors": [
"Balázs Orbán <info@balazsorban.com>"
],
"type": "module",
"types": "./index.d.ts",
"main": "dist/index.js",
"files": [
"*.js",
"*.d.ts*",
"src"
"README.md",
"dist"
],
"exports": {
".": {
"types": "./index.d.ts",
"import": "./index.js"
}
},
"license": "ISC",
"keywords": [
"next-auth",
@@ -46,27 +38,26 @@
"test:containers": "tests/test.sh",
"test": "tests/test.sh"
},
"dependencies": {
"@auth/core": "workspace:*"
},
"devDependencies": {
"@next-auth/adapter-test": "workspace:*",
"@next-auth/tsconfig": "workspace:*",
"jest": "^27.4.3",
"mssql": "^7.2.1",
"mysql": "^2.18.1",
"next-auth": "workspace:*",
"pg": "^8.7.3",
"sqlite3": "^5.0.8",
"typeorm": "0.3.15",
"typeorm": "0.3.7",
"typeorm-naming-strategies": "^4.1.0",
"typescript": "^4.7.4"
},
"peerDependencies": {
"mssql": "^6.2.1 || 7",
"mysql": "^2.18.1",
"next-auth": "^4",
"pg": "^8.2.1",
"sqlite3": "^5.0.2",
"typeorm": "^0.3.7"
"typeorm": "0.3.7"
},
"peerDependenciesMeta": {
"mysql": {

View File

@@ -9,17 +9,17 @@
* ## Installation
*
* ```bash npm2yarn2pnpm
* npm install @auth/typeorm-adapter typeorm
* npm install next-auth @next-auth/typeorm-legacy-adapter typeorm
* ```
*
* @module @auth/typeorm-adapter
* @module @next-auth/typeorm-legacy-adapter
*/
import type {
Adapter,
AdapterUser,
AdapterAccount,
AdapterSession,
} from "@auth/core/adapters"
} from "next-auth/adapters"
import { DataSourceOptions, DataSource, EntityManager } from "typeorm"
import * as defaultEntities from "./entities"
import { parseDataSourceConfig, updateConnectionEntities } from "./utils"
@@ -29,7 +29,7 @@ export const entities = defaultEntities
export type Entities = typeof entities
/** This is the interface for the TypeORM adapter options. */
export interface TypeORMAdapterOptions {
export interface TypeORMLegacyAdapterOptions {
/**
* The {@link https://orkhan.gitbook.io/typeorm/docs/entities TypeORM entities} to create the database tables from.
*/
@@ -70,16 +70,16 @@ export async function getManager(options: {
*
* ```javascript title="pages/api/auth/[...nextauth].js"
* import NextAuth from "next-auth"
* import { TypeORMAdapter } from "@auth/typeorm-adapter"
* import { TypeORMLegacyAdapter } from "@next-auth/typeorm-legacy-adapter"
*
*
* export default NextAuth({
* adapter: TypeORMAdapter("yourconnectionstring"),
* adapter: TypeORMLegacyAdapter("yourconnectionstring"),
* ...
* })
* ```
*
* `TypeORMAdapter` takes either a connection string, or a [`ConnectionOptions`](https://github.com/typeorm/typeorm/blob/master/docs/connection-options.md) object as its first parameter.
* `TypeORMLegacyAdapter` takes either a connection string, or a [`ConnectionOptions`](https://github.com/typeorm/typeorm/blob/master/docs/connection-options.md) object as its first parameter.
*
* ## Advanced usage
*
@@ -93,7 +93,7 @@ export async function getManager(options: {
*
* 1. Create a file containing your modified entities:
*
* (The file below is based on the [default entities](https://github.com/nextauthjs/next-auth/blob/main/packages/adapter-typeorm/src/entities.ts))
* (The file below is based on the [default entities](https://github.com/nextauthjs/next-auth/blob/main/packages/adapter-typeorm-legacy/src/entities.ts))
*
* ```diff title="lib/entities.ts"
* import {
@@ -231,15 +231,15 @@ export async function getManager(options: {
* }
* ```
*
* 2. Pass them to `TypeORMAdapter`
* 2. Pass them to `TypeORMLegacyAdapter`
*
* ```javascript title="pages/api/auth/[...nextauth].js"
* import NextAuth from "next-auth"
* import { TypeORMAdapter } from "@auth/typeorm-adapter"
* import { TypeORMLegacyAdapter } from "@next-auth/typeorm-legacy-adapter"
* import * as entities from "lib/entities"
*
* export default NextAuth({
* adapter: TypeORMAdapter("yourconnectionstring", { entities }),
* adapter: TypeORMLegacyAdapter("yourconnectionstring", { entities }),
* ...
* })
* ```
@@ -260,7 +260,7 @@ export async function getManager(options: {
*
* ```javascript title="pages/api/auth/[...nextauth].js"
* import NextAuth from "next-auth"
* import { TypeORMAdapter } from "@auth/typeorm-adapter"
* import { TypeORMLegacyAdapter } from "@next-auth/typeorm-legacy-adapter"
* import { SnakeNamingStrategy } from 'typeorm-naming-strategies'
* import { ConnectionOptions } from "typeorm"
*
@@ -275,14 +275,14 @@ export async function getManager(options: {
* }
*
* export default NextAuth({
* adapter: TypeORMAdapter(connection),
* adapter: TypeORMLegacyAdapter(connection),
* ...
* })
* ```
*/
export function TypeORMAdapter(
export function TypeORMLegacyAdapter(
dataSource: string | DataSourceOptions,
options?: TypeORMAdapterOptions
options?: TypeORMLegacyAdapterOptions
): Adapter {
const entities = options?.entities
const c = {
@@ -328,10 +328,8 @@ export function TypeORMAdapter(
},
async getUserByAccount(provider_providerAccountId) {
const m = await getManager(c)
// @ts-expect-error
const account = await m.findOne<AdapterAccount & { user: AdapterUser }>(
"AccountEntity",
// @ts-expect-error
{ where: provider_providerAccountId, relations: ["user"] }
)
if (!account) return null

View File

@@ -1,5 +1,5 @@
import { runBasicTests } from "../../../adapter-test"
import { TypeORMAdapter } from "../../src"
import { TypeORMLegacyAdapter } from "../../src"
import * as entities from "../custom-entities"
import { db } from "../helpers"
import { SnakeNamingStrategy } from "typeorm-naming-strategies"
@@ -18,7 +18,7 @@ const mysqlConfig: ConnectionOptions = {
}
runBasicTests({
adapter: TypeORMAdapter(mysqlConfig, {
adapter: TypeORMLegacyAdapter(mysqlConfig, {
entities,
}),
db: db(mysqlConfig, entities),

View File

@@ -1,5 +1,5 @@
import { runBasicTests } from "../../../adapter-test"
import { TypeORMAdapter } from "../../src"
import { TypeORMLegacyAdapter } from "../../src"
import { db } from "../helpers"
const mysqlConfig = {
@@ -13,6 +13,6 @@ const mysqlConfig = {
}
runBasicTests({
adapter: TypeORMAdapter(mysqlConfig),
adapter: TypeORMLegacyAdapter(mysqlConfig),
db: db(mysqlConfig),
})

View File

@@ -1,5 +1,5 @@
import { runBasicTests } from "../../../adapter-test"
import { TypeORMAdapter } from "../../src"
import { TypeORMLegacyAdapter } from "../../src"
import * as entities from "../custom-entities"
import { db } from "../helpers"
@@ -7,7 +7,7 @@ const postgresConfig =
"postgres://nextauth:password@localhost:5432/nextauth?synchronize=true"
runBasicTests({
adapter: TypeORMAdapter(postgresConfig, {
adapter: TypeORMLegacyAdapter(postgresConfig, {
entities,
}),
db: db(postgresConfig, entities),

View File

@@ -1,11 +1,11 @@
import { runBasicTests } from "../../../adapter-test"
import { TypeORMAdapter } from "../../src"
import { TypeORMLegacyAdapter } from "../../src"
import { db } from "../helpers"
const postgresConfig =
"postgres://nextauth:password@localhost:5432/nextauth?synchronize=true"
runBasicTests({
adapter: TypeORMAdapter(postgresConfig),
adapter: TypeORMLegacyAdapter(postgresConfig),
db: db(postgresConfig),
})

View File

@@ -1,5 +1,5 @@
import { runBasicTests } from "../../../adapter-test"
import { TypeORMAdapter } from "../../src"
import { TypeORMLegacyAdapter } from "../../src"
import * as entities from "../custom-entities"
import { db } from "../helpers"
@@ -11,7 +11,7 @@ const sqliteConfig = {
}
runBasicTests({
adapter: TypeORMAdapter(sqliteConfig, {
adapter: TypeORMLegacyAdapter(sqliteConfig, {
entities,
}),
db: db(sqliteConfig, entities),

View File

@@ -1,5 +1,5 @@
import { runBasicTests } from "../../../adapter-test"
import { TypeORMAdapter } from "../../src"
import { TypeORMLegacyAdapter } from "../../src"
import { db } from "../helpers"
import { SnakeNamingStrategy } from "typeorm-naming-strategies"
@@ -14,6 +14,6 @@ const sqliteConfig: DataSourceOptions = {
}
runBasicTests({
adapter: TypeORMAdapter(sqliteConfig),
adapter: TypeORMLegacyAdapter(sqliteConfig),
db: db(sqliteConfig),
})

View File

@@ -0,0 +1,12 @@
{
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"stripInternal": true
},
"include": ["."],
"exclude": ["tests", "dist", "jest.config.js"]
}

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@auth/core",
"version": "0.8.2",
"version": "0.7.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 "@auth/prisma-adapter"
* import { PrismaAdapter } from "@next-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

@@ -159,17 +159,8 @@ export class SessionStore {
* The JWT Session or database Session ID
* constructed from the cookie chunks.
*/
get value() {
// Sort the chunks by their keys before joining
const sortedKeys = Object.keys(this.#chunks).sort((a, b) => {
const aSuffix = parseInt(a.split(".")[1] || "0");
const bSuffix = parseInt(b.split(".")[1] || "0");
return aSuffix - bSuffix;
});
// Use the sorted keys to join the chunks in the correct order
return sortedKeys.map(key => this.#chunks[key]).join("");
get value() {
return Object.values(this.#chunks)?.join("")
}
/** Given a cookie, return a list of cookies, chunked to fit the allowed cookie size. */

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,22 +177,6 @@ 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 getUserAndAccount(
const profileResult = await getUserAndProfile(
profile,
provider,
tokens,
logger
)
return { ...profileResult, profile, cookies: resCookies }
return { ...profileResult, cookies: resCookies }
}
/** Returns the user and account that is going to be created in the database. */
async function getUserAndAccount(
/** Returns profile, raw profile and auth provider details */
async function getUserAndProfile(
OAuthProfile: Profile,
provider: OAuthConfigInternal<any>,
tokens: TokenSet,
@@ -206,6 +206,7 @@ async function getUserAndAccount(
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,16 +106,14 @@ 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,
profile: OAuthProfile,
OAuthProfile,
} = authorizationResult
// If we don't have a profile object then either something went wrong
@@ -134,7 +134,6 @@ export async function callback(params: {
account,
profile: OAuthProfile,
isNewUser,
trigger: isNewUser ? "signUp" : "signIn",
})
// Clear cookies if token is null
@@ -245,7 +244,6 @@ export async function callback(params: {
user: loggedInUser,
account,
isNewUser,
trigger: isNewUser ? "signUp" : "signIn",
})
// Clear cookies if token is null
@@ -342,7 +340,6 @@ export async function callback(params: {
// @ts-expect-error
account,
isNewUser: false,
trigger: "signIn",
})
// Clear cookies if token is null

View File

@@ -6,13 +6,10 @@ 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(params: {
export async function session(
sessionStore: SessionStore,
options: InternalOptions
sessionStore: SessionStore
isUpdate?: boolean
newSession?: any
}): Promise<ResponseInternal<Session | null>> {
const { options, sessionStore, newSession, isUpdate } = params
): Promise<ResponseInternal<Session | null>> {
const {
adapter,
jwt,
@@ -36,24 +33,23 @@ export async function session(params: {
try {
const decodedToken = await jwt.decode({ ...jwt, token: sessionToken })
if (!decodedToken) throw new Error("Invalid JWT")
// @ts-expect-error
const token = await callbacks.jwt({
token: decodedToken,
...(isUpdate && { trigger: "update" }),
session: newSession,
})
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(),
}
// @ts-expect-error
const token = await callbacks.jwt({ token: decodedToken })
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 })
@@ -129,12 +125,14 @@ export async function session(params: {
// 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,14 +32,12 @@ export interface CredentialsConfig<
* @example
* ```ts
* //...
* async authorize(credentials, request) {
* if(!isValidCredentials(credentials)) return null
* async authorize(, request) {
* 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 = (tokens: TokenSet) => TokenSet | undefined | void
export type AccountCallback = (account: TokenSet) => TokenSet
export interface OAuthProviderButtonStyles {
logo: string
@@ -155,31 +155,7 @@ 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.
*
* :::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
* }
* }
* })
* ```
* Defaults to: `access_token` and `id_token`
*
* @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,15 +98,7 @@ 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
@@ -238,86 +230,40 @@ 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.
*
* @see [`jwt` callback](https://authjs.dev/reference/core/types#jwt)
* [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) |
*
*/
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>
session: (params: {
session: Session
user: User | AdapterUser
token: JWT
}) => Awaitable<Session>
/**
* 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 from your front-end.
* Anything else will be kept inaccessible from the client.
*
* The JWT is encrypted by default.
* 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.
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#session-callback)
* 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)
*/
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
/**
* 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"`.
*/
user?: User | AdapterUser
account?: A | null
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>
}
@@ -497,9 +443,7 @@ 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"`**:
* - **`GET**`: Returns the user's session if it exists, otherwise `null`.
* - **`POST**`: Updates the user's session and returns the updated session.
* - **`"session"`**: Returns the user's session if it exists, otherwise `null`.
* - **`"signin"`**:
* - **`GET`**: Renders the built-in sign-in page.
* - **`POST`**: Initiates the sign-in flow.

View File

@@ -224,13 +224,6 @@ 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,8 +65,10 @@
],
"license": "ISC",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@panva/hkdf": "^1.0.2",
"@babel/runtime": "^7.16.3",
"@panva/hkdf": "^1.0.1",
"@simplewebauthn/browser": "^6.2.2",
"@simplewebauthn/server": "^6.2.2",
"cookie": "^0.5.0",
"jose": "^4.11.4",
"oauth": "^0.9.15",
@@ -96,6 +98,7 @@
"@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

@@ -0,0 +1,73 @@
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

@@ -0,0 +1,27 @@
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

@@ -0,0 +1,78 @@
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

@@ -0,0 +1,7 @@
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,3 +625,36 @@ 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[]
}

4190
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/**/*", "*.js", "*.d.ts", "*.d.ts.map"]
"outputs": ["dist/**/*"]
},
"next-auth#build": {
"dependsOn": ["^build"],
@@ -53,7 +53,6 @@
"docs#dev": {
"dependsOn": [
"@auth/core#build",
"@auth/prisma-adapter#build",
"@auth/sveltekit#build",
"@next-auth/dgraph-adapter#build",
"@next-auth/dynamodb-adapter#build",
@@ -63,9 +62,10 @@
"@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",
"@auth/typeorm-adapter#build",
"@next-auth/typeorm-legacy-adapter#build",
"@next-auth/upstash-redis-adapter#build",
"@next-auth/xata-adapter#build",
"^build",
@@ -76,7 +76,6 @@
"docs#build": {
"dependsOn": [
"@auth/core#build",
"@auth/prisma-adapter#build",
"@auth/sveltekit#build",
"@next-auth/dgraph-adapter#build",
"@next-auth/dynamodb-adapter#build",
@@ -86,9 +85,10 @@
"@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",
"@auth/typeorm-adapter#build",
"@next-auth/typeorm-legacy-adapter#build",
"@next-auth/upstash-redis-adapter#build",
"@next-auth/xata-adapter#build",
"^build",