Compare commits

..

16 Commits

Author SHA1 Message Date
GitHub Actions
9388a56efa chore(release): bump package version(s) [skip ci] 2023-01-04 06:56:11 +00:00
Balázs Orbán
3a75fb955a docs: simplify refresh token guide 2023-01-04 07:49:11 +01:00
Balázs Orbán
01bb91612a fix(core): allow passing params only to endpoint configs
fixes #6273
2023-01-04 07:48:57 +01:00
Balázs Orbán
3b25935c83 fix(core): correctly pass user id in account
fixes #6209, fixes #6222
2023-01-03 16:26:49 +01:00
Balázs Orbán
394920dfd4 chore(examples): prefer useSession over prop drilling
closes #5891
2023-01-03 16:07:37 +01:00
Raouf Chebri
85dc5bede8 docs: Update strava provider option url (#6264)
Update strava.md
2023-01-03 15:55:36 +01:00
Balázs Orbán
8f854c61d0 fix(core): allow custom endpoint config when issuer is present
closes #6244
2023-01-03 12:11:13 +01:00
Balázs Orbán
e8fbe58997 docs: update refresh token guide 2023-01-03 12:09:13 +01:00
Cameron Downey
d2288ee4cc fix(docs): turn SvelteKitAuth to named import in code example (#6250) 2023-01-02 10:52:47 +00:00
Thang Vu
5a6f76bf2c fix: docs build (#6253)
* fix: docs build

* chore: move next-auth output to dist

* chore: add next-auth as deps for doc

* Revert "chore: move next-auth output to dist"

This reverts commit 9596a9134e6de4f4bd8dcfaa6d3002e98863d8f8.

* remove dist prefix
2023-01-02 16:57:14 +07:00
Thang Vu
15bed6260c chore: change Thang's email 2023-01-01 22:45:36 +07:00
Balázs Orbán
1423733d61 fix(adapters): define correct peer dependencies for DynamoDB (#6249)
* Add @aws-sdk/client-dynamodb as peer dependency

* Add missing DynamoDBClientConfig interface import

* Add missing installation requirements

Co-authored-by: Didi Keke <nyedidikeke@users.noreply.github.com>
2023-01-01 15:16:01 +00:00
Thang Vu
77d8f47f51 chore: restore turborepo next-auth#build.outputs 2023-01-01 22:06:50 +07:00
Balázs Orbán
3120d28299 chore: update lockfile 2023-01-01 16:03:09 +01:00
Nico Domino
e6a320bb0f chore(docs): fix homepage logo, build, and lighthouse improvements (#6238)
* chore(docs): fix homepage logo size

* chore(docs): fix sidebars.js solid-start doc path name

* chore(docs): image file and size optimizations

* chore(docs): fix semantic misordered headings

* chore(docs): make banner link more descriptive

* chore(docs): add solid-start redirect
2022-12-31 21:28:59 +01:00
Birk Skyum
7d4d436efe chore(examples): fix broken docs link in solidstart example (#6241) 2022-12-31 21:28:17 +01:00
32 changed files with 321 additions and 175 deletions

View File

@@ -3,9 +3,10 @@ import { authOptions } from "./api/auth/[...nextauth]"
import Layout from "../components/layout" import Layout from "../components/layout"
import type { GetServerSidePropsContext } from "next" import type { GetServerSidePropsContext } from "next"
import type { Session } from "next-auth" import { useSession } from "next-auth/react"
export default function ServerSidePage({ session }: { session: Session }) { export default function ServerSidePage() {
const { data: session } = useSession()
// As this page uses Server Side Rendering, the `session` will be already // As this page uses Server Side Rendering, the `session` will be already
// populated on render without needing to go through a loading stage. // populated on render without needing to go through a loading stage.
return ( return (

View File

@@ -29,7 +29,7 @@ const Home: ParentComponent = () => {
</A>{" "} </A>{" "}
with{" "} with{" "}
<A <A
href="https://authjs.dev/reference/solid-start/modules/main" href="https://authjs.dev/reference/solidstart"
class="text-blue-500 underline font-bold" class="text-blue-500 underline font-bold"
> >
SolidStart Auth SolidStart Auth

View File

@@ -2,119 +2,203 @@
title: Refresh token rotation title: Refresh token rotation
--- ---
While Auth.js doesn't automatically handle access token rotation for [OAuth providers](/reference/providers/oauth-builtin) yet, this functionality can be implemented using [callbacks](/guides/basics/callbacks). Refresh token rotation is the practice of updating an `access_token` on behalf of the user, without requiring interaction (eg.: re-sign in). `access_token`s are usually issued for a limited time. After they expire, the service verifying them will ignore the value. Instead of asking the user to sign in again to obtain a new `access_token`, certain providers support exchanging a `refresh_token` for a new `access_token`, renewing the expiry time. Let's see how this can be achieved.
## Source Code :::note
Our goal is to add zero-config support for built-in providers eventually. Let us know if you would like to help.
A working example can be accessed [here](https://github.com/nextauthjs/next-auth-refresh-token-example). :::
## Implementation ## Implementation
First, make sure that the provider you want to use supports `refresh_token`'s. Check out [The OAuth 2.0 Authorization Framework](https://www.rfc-editor.org/rfc/rfc6749#section-6) spec for more details.
### Server Side ### Server Side
Using a [JWT callback](https://authjs.dev/guides/basics/callbacks#jwt-callback) and a [session callback](https://authjs.dev/guides/basics/callbacks#session-callback), we can persist OAuth tokens and refresh them when they expire. Depending on the session strategy, `refresh_token` can be persisted either in a database, or in a cookie, in an encrypted JWT.
:::info
Using a JWT to store the `refresh_token` is less secure than saving it in a database, and you need to evaluate based on your requirements which strategy you choose.
:::
#### JWT strategy
Using the [jwt](../../reference/03-core/interfaces/types.CallbacksOptions.md#jwt) and [session](../../reference/03-core/interfaces/types.CallbacksOptions.md#session) callbacks, we can persist OAuth tokens and refresh them when they expire.
Below is a sample implementation using Google's Identity Provider. Please note that the OAuth 2.0 request in the `refreshAccessToken()` function will vary between different providers, but the core logic should remain similar. Below is a sample implementation using Google's Identity Provider. Please note that the OAuth 2.0 request in the `refreshAccessToken()` function will vary between different providers, but the core logic should remain similar.
```js title="pages/api/auth/[...nextauth].js" ```ts
import NextAuth from "next-auth" import { Auth } from "@auth/core"
import GoogleProvider from "next-auth/providers/google" import { type TokenSet } from "@auth/core/types"
import Google from "@auth/core/providers/google"
const GOOGLE_AUTHORIZATION_URL = export default Auth(new Request("https://example.com"), {
"https://accounts.google.com/o/oauth2/v2/auth?" +
new URLSearchParams({
prompt: "consent",
access_type: "offline",
response_type: "code",
})
/**
* Takes a token, and returns a new token with updated
* `accessToken` and `accessTokenExpires`. If an error occurs,
* returns the old token and an error property
*/
async function refreshAccessToken(token) {
try {
const url =
"https://oauth2.googleapis.com/token?" +
new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
grant_type: "refresh_token",
refresh_token: token.refreshToken,
})
const response = await fetch(url, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
method: "POST",
})
const refreshedTokens = await response.json()
if (!response.ok) {
throw refreshedTokens
}
return {
...token,
accessToken: refreshedTokens.access_token,
accessTokenExpires: Date.now() + refreshedTokens.expires_at * 1000,
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken, // Fall back to old refresh token
}
} catch (error) {
console.log(error)
return {
...token,
error: "RefreshAccessTokenError",
}
}
}
export default NextAuth({
providers: [ providers: [
GoogleProvider({ Google({
clientId: process.env.GOOGLE_CLIENT_ID, clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET, clientSecret: process.env.GOOGLE_SECRET,
authorization: GOOGLE_AUTHORIZATION_URL, authorization: { params: { access_type: "offline", prompt: "consent" } },
}), }),
], ],
callbacks: { callbacks: {
async jwt({ token, user, account }) { async jwt({ token, account }) {
// Initial sign in if (account) {
if (account && user) { // Save the access token and refresh token in the JWT on the initial login
return { return {
accessToken: account.access_token, access_token: account.access_token,
accessTokenExpires: Date.now() + account.expires_at * 1000, expires_at: Date.now() + account.expires_in * 1000,
refreshToken: account.refresh_token, refresh_token: account.refresh_token,
user, }
} else if (Date.now() < token.expires_at) {
// If the access token has not expired yet, return it
return token
} else {
// If the access token has expired, try to refresh it
try {
// https://accounts.google.com/.well-known/openid-configuration
// We need the `token_endpoint`.
const response = await fetch("https://oauth2.googleapis.com/token", {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: process.env.GOOGLE_ID,
client_secret: process.env.GOOGLE_SECRET,
grant_type: "refresh_token",
refresh_token: token.refresh_token,
}),
method: "POST",
})
const tokens: TokenSet = await response.json()
if (!response.ok) throw tokens
return {
...token, // Keep the previous token properties
access_token: tokens.access_token,
expires_at: Date.now() + tokens.expires_in * 1000,
// Fall back to old refresh token, but note that
// many providers may only allow using a refresh token once.
refresh_token: tokens.refresh_token ?? token.refresh_token,
}
} catch (error) {
console.error("Error refreshing access token", error)
// The error property will be used client-side to handle the refresh token error
return { ...token, error: "RefreshAccessTokenError" as const }
} }
} }
// Return previous token if the access token has not expired yet
if (Date.now() < token.accessTokenExpires) {
return token
}
// Access token has expired, try to update it
return refreshAccessToken(token)
}, },
async session({ session, token }) { async session({ session, token }) {
session.user = token.user
session.accessToken = token.accessToken
session.error = token.error session.error = token.error
return session return session
}, },
}, },
}) })
declare module "@auth/core/types" {
interface Session {
error?: "RefreshAccessTokenError"
}
}
declare module "@auth/core/jwt" {
interface JWT {
access_token: string
expires_at: number
refresh_token: string
error?: "RefreshAccessTokenError"
}
}
```
#### Database strategy
Using the database strategy is very similar, but instead of preserving the `access_token` and `refresh_token`, we save it, well, in the database.
```ts
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 { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
export default Auth(new Request("https://example.com"), {
adapter: PrismaAdapter(prisma),
providers: [
Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
authorization: { params: { access_type: "offline", prompt: "consent" } },
}),
],
callbacks: {
async session({ session, user }) {
const [google] = await prisma.account.findMany({
where: { userId: user.id, provider: "google" },
})
if (google.expires_at >= Date.now()) {
// If the access token has expired, try to refresh it
try {
// https://accounts.google.com/.well-known/openid-configuration
// We need the `token_endpoint`.
const response = await fetch("https://oauth2.googleapis.com/token", {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: process.env.GOOGLE_ID,
client_secret: process.env.GOOGLE_SECRET,
grant_type: "refresh_token",
refresh_token: google.refresh_token,
}),
method: "POST",
})
const tokens: TokenSet = await response.json()
if (!response.ok) throw tokens
await prisma.account.update({
data: {
access_token: tokens.access_token,
expires_at: Date.now() + tokens.expires_in * 1000,
refresh_token: tokens.refresh_token ?? google.refresh_token,
},
where: {
provider_providerAccountId: {
provider: "google",
providerAccountId: google.providerAccountId,
},
},
})
} catch (error) {
console.error("Error refreshing access token", error)
// The error property will be used client-side to handle the refresh token error
session.error = "RefreshAccessTokenError"
}
}
return session
},
},
})
declare module "@auth/core/types" {
interface Session {
error?: "RefreshAccessTokenError"
}
}
declare module "@auth/core/jwt" {
interface JWT {
access_token: string
expires_at: number
refresh_token: string
error?: "RefreshAccessTokenError"
}
}
``` ```
### Client Side ### Client Side
The `RefreshAccessTokenError` error that is caught in the `refreshAccessToken()` method is passed all the way to the client. This means that you can direct the user to the sign in flow if we cannot refresh their token. The `RefreshAccessTokenError` error that is caught in the `refreshAccessToken()` method is passed to the client. This means that you can direct the user to the sign-in flow if we cannot refresh their token.
We can handle this functionality as a side effect: We can handle this functionality as a side effect:
@@ -134,3 +218,8 @@ const HomePage() {
return (...) return (...)
} }
``` ```
## Source Code
A working example can be accessed [here](https://github.com/nextauthjs/next-auth-refresh-token-example).

View File

@@ -1,14 +1,14 @@
--- ---
title: Available OAuth providers title: Available OAuth providers
sidebar_label: Oauth providers sidebar_label: OAuth providers
--- ---
Authentication Providers in **Auth.js** are services that can be used to sign in a user. Authentication Providers in **Auth.js** are services that can be used to sign a user in.
Auth.js comes with a set of built-in providers. You can find them [here](https://github.com/nextauthjs/next-auth/tree/main/packages/core/src/providers). Each built-in provider has its own documentation page: Auth.js comes with a set of built-in providers. You can find them [here](https://github.com/nextauthjs/next-auth/tree/main/packages/core/src/providers). Each built-in provider has its own documentation page:
:::note :::note
Auth.js is designed to work with any OAuth service, it supports **OAuth 1.0**, **1.0A**, **2.0** and **OpenID Connect (OIDC)** and has built-in support for most popular sign-in services. Auth.js supports any **2.x** and **OpenID Connect (OIDC)** compliant providers and has built-in support for the most popular services.
::: :::
<ul> <ul>

View File

@@ -11,7 +11,7 @@ http://developers.strava.com/docs/reference/
The **Strava Provider** comes with a set of default options: The **Strava Provider** comes with a set of default options:
- [Strava Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/strava.js) - [Strava Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/strava.ts)
You can override any of the options to suit your own use case. You can override any of the options to suit your own use case.

View File

@@ -3,7 +3,7 @@ id: dynamodb
title: DynamoDB title: DynamoDB
--- ---
This is the AWS DynamoDB Adapter for next-auth. This package can only be used in conjunction with the primary next-auth package. It is not a standalone package. This is the AWS DynamoDB Adapter for `next-auth`. This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
By default, the adapter expects a table with a partition key `pk` and a sort key `sk`, as well as a global secondary index named `GSI1` with `GSI1PK` as partition key and `GSI1SK` as sorting key. To automatically delete sessions and verification requests after they expire using [dynamodb TTL](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html) you should [enable the TTL](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/time-to-live-ttl-how-to.html) with attribute name 'expires'. You can set whatever you want as the table name and the billing method. By default, the adapter expects a table with a partition key `pk` and a sort key `sk`, as well as a global secondary index named `GSI1` with `GSI1PK` as partition key and `GSI1SK` as sorting key. To automatically delete sessions and verification requests after they expire using [dynamodb TTL](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html) you should [enable the TTL](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/time-to-live-ttl-how-to.html) with attribute name 'expires'. You can set whatever you want as the table name and the billing method.
@@ -11,10 +11,10 @@ You can find the full schema in the table structure section below.
## Getting Started ## Getting Started
1. Install `next-auth` and `@next-auth/dynamodb-adapter` 1. Install `next-auth`, `@next-auth/dynamodb-adapter`, `@aws-sdk/client-dynamodb` and `@aws-sdk/lib-dynamodb`
```bash npm2yarn ```bash npm2yarn2pnpm
npm install next-auth @next-auth/dynamodb-adapter npm install next-auth @next-auth/dynamodb-adapter @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
``` ```
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object. 2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object.
@@ -23,7 +23,7 @@ You need to pass `DynamoDBDocument` client from the modular [`aws-sdk`](https://
The default table name is `next-auth`, but you can customise that by passing `{ tableName: 'your-table-name' }` as the second parameter in the adapter. The default table name is `next-auth`, but you can customise that by passing `{ tableName: 'your-table-name' }` as the second parameter in the adapter.
```javascript title="pages/api/auth/[...nextauth].js" ```javascript title="pages/api/auth/[...nextauth].js"
import { DynamoDB } from "@aws-sdk/client-dynamodb" import { DynamoDB, DynamoDBClientConfig } from "@aws-sdk/client-dynamodb"
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb" import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb"
import NextAuth from "next-auth"; import NextAuth from "next-auth";
import Providers from "next-auth/providers"; import Providers from "next-auth/providers";
@@ -73,7 +73,7 @@ The table respects the single table design pattern. This has many advantages:
- Only one table to manage, monitor and provision. - Only one table to manage, monitor and provision.
- Querying relations is faster than with multi-table schemas (for eg. retrieving all sessions for a user). - Querying relations is faster than with multi-table schemas (for eg. retrieving all sessions for a user).
- Only one table needs to be replicated, if you want to go multi-region. - Only one table needs to be replicated if you want to go multi-region.
> This schema is adapted for use in DynamoDB and based upon our main [schema](/reference/adapters/models) > This schema is adapted for use in DynamoDB and based upon our main [schema](/reference/adapters/models)
@@ -94,7 +94,7 @@ new dynamodb.Table(this, `NextAuthTable`, {
}) })
``` ```
Alternatively you can use this cloudformation template: Alternatively, you can use this cloudformation template:
```yaml title=cloudformation.yaml ```yaml title=cloudformation.yaml
NextAuthTable: NextAuthTable:

View File

@@ -46,7 +46,7 @@ const docusaurusConfig = {
title: "Auth.js", title: "Auth.js",
logo: { logo: {
alt: "Auth.js Logo", alt: "Auth.js Logo",
src: "img/logo/logo-xs.png", src: "img/logo/logo-xs.webp",
}, },
items: [ items: [
{ {
@@ -101,7 +101,7 @@ const docusaurusConfig = {
announcementBar: { announcementBar: {
id: "new-major-announcement", id: "new-major-announcement",
content: content:
"<a target='_blank' rel='noopener noreferrer' href='https://next-auth.js.org'>NextAuth.js</a> is becoming Auth.js! 🎉 We're creating Authentication for the Web. Everyone included. Starting with SvelteKit, check out the docs <a href='/reference/sveltekit'>here</a>.", "<a target='_blank' rel='noopener noreferrer' href='https://next-auth.js.org'>NextAuth.js</a> is becoming Auth.js! 🎉 We're creating Authentication for the Web. Everyone included. Starting with SvelteKit, check out <a href='/reference/sveltekit'>the docs</a>.",
backgroundColor: "#000", backgroundColor: "#000",
textColor: "#fff", textColor: "#fff",
}, },
@@ -121,6 +121,7 @@ const docusaurusConfig = {
alt="Powered by Vercel" alt="Powered by Vercel"
style="margin-top: 8px" style="margin-top: 8px"
height="32" height="32"
width="167"
src="https://raw.githubusercontent.com/nextauthjs/next-auth/main/docs/static/img/powered-by-vercel.svg" src="https://raw.githubusercontent.com/nextauthjs/next-auth/main/docs/static/img/powered-by-vercel.svg"
/> />
</a>`, </a>`,
@@ -181,7 +182,10 @@ const docusaurusConfig = {
lastVersion: "current", lastVersion: "current",
showLastUpdateAuthor: true, showLastUpdateAuthor: true,
showLastUpdateTime: true, showLastUpdateTime: true,
remarkPlugins: [require("@sapphire/docusaurus-plugin-npm2yarn2pnpm").npm2yarn2pnpm, require("remark-github")], remarkPlugins: [
require("@sapphire/docusaurus-plugin-npm2yarn2pnpm").npm2yarn2pnpm,
require("remark-github"),
],
versions: { versions: {
current: { current: {
label: "experimental", label: "experimental",
@@ -201,7 +205,15 @@ const docusaurusConfig = {
...typedocConfig, ...typedocConfig,
id: "core", id: "core",
plugin: ["./tyepdoc"], plugin: ["./tyepdoc"],
entryPoints: ["index.ts", "adapters.ts", "errors.ts", "jwt.ts", "types.ts"].map((e) => `${coreSrc}/${e}`).concat(providers), entryPoints: [
"index.ts",
"adapters.ts",
"errors.ts",
"jwt.ts",
"types.ts",
]
.map((e) => `${coreSrc}/${e}`)
.concat(providers),
tsconfig: "../packages/core/tsconfig.json", tsconfig: "../packages/core/tsconfig.json",
out: "reference/03-core", out: "reference/03-core",
watch: process.env.TYPEDOC_WATCH, watch: process.env.TYPEDOC_WATCH,
@@ -214,7 +226,9 @@ const docusaurusConfig = {
...typedocConfig, ...typedocConfig,
id: "sveltekit", id: "sveltekit",
plugin: ["./tyepdoc"], plugin: ["./tyepdoc"],
entryPoints: ["index.ts", "client.ts"].map((e) => `../packages/frameworks-sveltekit/src/lib/${e}`), entryPoints: ["index.ts", "client.ts"].map(
(e) => `../packages/frameworks-sveltekit/src/lib/${e}`
),
tsconfig: "../packages/frameworks-sveltekit/tsconfig.json", tsconfig: "../packages/frameworks-sveltekit/tsconfig.json",
out: "reference/04-sveltekit", out: "reference/04-sveltekit",
watch: process.env.TYPEDOC_WATCH, watch: process.env.TYPEDOC_WATCH,

View File

@@ -22,6 +22,7 @@
"classnames": "^2.3.2", "classnames": "^2.3.2",
"mdx-mermaid": "1.2.2", "mdx-mermaid": "1.2.2",
"mermaid": "9.0.1", "mermaid": "9.0.1",
"next-auth": "workspace:*",
"prism-react-renderer": "1.3.5", "prism-react-renderer": "1.3.5",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

View File

@@ -58,9 +58,9 @@ module.exports = {
label: "@auth/solid-start", label: "@auth/solid-start",
link: { link: {
type: "doc", type: "doc",
id: "reference/solid-start/index", id: "reference/solidstart/index",
}, },
items: ["reference/solid-start/client", "reference/solid-start/protected"], items: ["reference/solidstart/client", "reference/solidstart/protected"],
}, },
{ {
type: "category", type: "category",

View File

@@ -140,19 +140,19 @@ html[data-theme="dark"] hr {
border-radius: 10rem; border-radius: 10rem;
overflow: visible; overflow: visible;
box-shadow: 0 0 2rem rgba(0, 0, 0, 0.1); box-shadow: 0 0 2rem rgba(0, 0, 0, 0.1);
background-image: url("/img/mesh-1.jpg"); background-image: url("/img/mesh-1.webp");
background-size: cover; background-size: cover;
background-origin: center; background-origin: center;
} }
.home-main .section-features .row .col:nth-child(2) .feature-image-wrapper { .home-main .section-features .row .col:nth-child(2) .feature-image-wrapper {
background-image: url("/img/mesh-2.jpg"); background-image: url("/img/mesh-2.webp");
background-size: cover; background-size: cover;
background-origin: center; background-origin: center;
} }
.home-main .section-features .row .col:nth-child(3) .feature-image-wrapper { .home-main .section-features .row .col:nth-child(3) .feature-image-wrapper {
background-image: url("/img/mesh-3.jpg"); background-image: url("/img/mesh-3.webp");
background-size: cover; background-size: cover;
background-origin: center; background-origin: center;
} }

View File

@@ -6,6 +6,11 @@
margin-right: 1rem !important; margin-right: 1rem !important;
} }
.navbar__logo {
width: 29px;
height: 32px;
}
.navbar__title { .navbar__title {
font-size: 1.2rem; font-size: 1.2rem;
margin-left: 0.2rem; margin-left: 0.2rem;

View File

@@ -117,9 +117,11 @@ export default function Home() {
<div className="container"> <div className="container">
<div className="hero-inner"> <div className="hero-inner">
<img <img
src="/img/logo/logo-sm.png" src="/img/logo/logo-sm.webp"
alt="Shield with key icon" alt="Shield with key icon"
className={styles.heroLogo} className={styles.heroLogo}
height="142"
width="128"
/> />
<div className={styles.heroText}> <div className={styles.heroText}>
<h1 className="hero__title">{siteConfig.title}</h1> <h1 className="hero__title">{siteConfig.title}</h1>
@@ -214,9 +216,9 @@ export default function Home() {
<div className="row"> <div className="row">
<div className="col col--6"> <div className="col col--6">
<div className="code"> <div className="code">
<h4 className="code-heading"> <div className="code-heading">
Next.js <span>/pages/api/auth/[...nextauth].ts</span> Next.js <span>/pages/api/auth/[...nextauth].ts</span>
</h4> </div>
<CodeBlock className="prism-code language-js"> <CodeBlock className="prism-code language-js">
{nextJsCode} {nextJsCode}
</CodeBlock> </CodeBlock>
@@ -224,9 +226,9 @@ export default function Home() {
</div> </div>
<div className="col col--6"> <div className="col col--6">
<div className="code"> <div className="code">
<h4 className="code-heading"> <div className="code-heading">
SvelteKit <span>/hooks.server.ts</span> SvelteKit <span>/hooks.server.ts</span>
</h4> </div>
<CodeBlock className="prism-code language-js"> <CodeBlock className="prism-code language-js">
{svelteKitCode} {svelteKitCode}
</CodeBlock> </CodeBlock>
@@ -234,9 +236,9 @@ export default function Home() {
</div> </div>
<div className="col col--6"> <div className="col col--6">
<div className="code"> <div className="code">
<h4 className="code-heading"> <div className="code-heading">
SolidStart <span>/routes/api/auth/[...solidauth].ts</span> SolidStart <span>/routes/api/auth/[...solidauth].ts</span>
</h4> </div>
<CodeBlock className="prism-code language-js"> <CodeBlock className="prism-code language-js">
{solidStartCode} {solidStartCode}
</CodeBlock> </CodeBlock>

BIN
docs/static/img/logo/logo-sm.webp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
docs/static/img/logo/logo-xs.webp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
docs/static/img/logo/logo.webp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
docs/static/img/mesh-1.webp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
docs/static/img/mesh-2.webp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
docs/static/img/mesh-3.webp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -60,6 +60,11 @@
"destination": "https://github.com/nextauthjs/next-auth/discussions/categories/questions", "destination": "https://github.com/nextauthjs/next-auth/discussions/categories/questions",
"permanent": true "permanent": true
}, },
{
"source": "/reference/solid-start/:path*",
"destination": "/reference/solidstart/:path*",
"permanent": true
},
{ {
"source": "/", "source": "/",
"has": [ "has": [

View File

@@ -5,6 +5,7 @@
"repository": "https://github.com/nextauthjs/next-auth.git", "repository": "https://github.com/nextauthjs/next-auth.git",
"scripts": { "scripts": {
"build:app": "turbo run build --filter=next-auth-app", "build:app": "turbo run build --filter=next-auth-app",
"build:docs": "turbo run build --filter=docs",
"build": "turbo run build --filter=next-auth --filter=@next-auth/* --filter=@auth/* --no-deps", "build": "turbo run build --filter=next-auth --filter=@next-auth/* --filter=@auth/* --no-deps",
"test": "turbo run test --concurrency=1 --filter=[HEAD^1] --filter=./packages/* --filter=!*pouchdb-* --filter=!@*upstash* --filter=!*dynamodb-*", "test": "turbo run test --concurrency=1 --filter=[HEAD^1] --filter=./packages/* --filter=!*pouchdb-* --filter=!@*upstash* --filter=!*dynamodb-*",
"clean": "turbo run clean --no-cache", "clean": "turbo run clean --no-cache",

View File

@@ -1,7 +1,7 @@
{ {
"name": "@next-auth/dynamodb-adapter", "name": "@next-auth/dynamodb-adapter",
"repository": "https://github.com/nextauthjs/next-auth", "repository": "https://github.com/nextauthjs/next-auth",
"version": "1.0.5", "version": "1.0.6",
"description": "AWS DynamoDB adapter for next-auth.", "description": "AWS DynamoDB adapter for next-auth.",
"keywords": [ "keywords": [
"next-auth", "next-auth",
@@ -31,6 +31,7 @@
"author": "Pol Marnette", "author": "Pol Marnette",
"license": "ISC", "license": "ISC",
"peerDependencies": { "peerDependencies": {
"@aws-sdk/client-dynamodb": "^3.36.1",
"@aws-sdk/lib-dynamodb": "^3.36.1", "@aws-sdk/lib-dynamodb": "^3.36.1",
"next-auth": "^4" "next-auth": "^4"
}, },
@@ -43,4 +44,4 @@
"jest": "^27.4.3", "jest": "^27.4.3",
"next-auth": "workspace:*" "next-auth": "workspace:*"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@auth/core", "name": "@auth/core",
"version": "0.2.4", "version": "0.2.5",
"description": "Authentication for the Web.", "description": "Authentication for the Web.",
"keywords": [ "keywords": [
"authentication", "authentication",
@@ -20,7 +20,7 @@
"Balázs Orbán <info@balazsorban.com>", "Balázs Orbán <info@balazsorban.com>",
"Nico Domino <yo@ndo.dev>", "Nico Domino <yo@ndo.dev>",
"Lluis Agusti <hi@llu.lu>", "Lluis Agusti <hi@llu.lu>",
"Thang Huu Vu <thvu@hey.com>", "Thang Huu Vu <hi@thvu.dev>",
"Iain Collins <me@iaincollins.com" "Iain Collins <me@iaincollins.com"
], ],
"type": "module", "type": "module",

View File

@@ -22,7 +22,8 @@ export async function getAuthorizationUrl(
let url = provider.authorization?.url let url = provider.authorization?.url
let as: o.AuthorizationServer | undefined let as: o.AuthorizationServer | undefined
if (!url) { // Falls back to authjs.dev if the user only passed params
if (!url || url.host === "authjs.dev") {
// If url is undefined, we assume that issuer is always defined // If url is undefined, we assume that issuer is always defined
// We check this in assert.ts // We check this in assert.ts
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -48,9 +49,9 @@ export async function getAuthorizationUrl(
redirect_uri: provider.callbackUrl, redirect_uri: provider.callbackUrl,
// @ts-expect-error TODO: // @ts-expect-error TODO:
...provider.authorization?.params, ...provider.authorization?.params,
}, // Defaults },
Object.fromEntries(authParams), // From provider config Object.fromEntries(provider.authorization?.url.searchParams ?? []),
query // From `signIn` call query
) )
for (const k in params) authParams.set(k, params[k]) for (const k in params) authParams.set(k, params[k])

View File

@@ -31,7 +31,12 @@ export async function handleOAuth(
const { logger, provider } = options const { logger, provider } = options
let as: o.AuthorizationServer let as: o.AuthorizationServer
if (!provider.token?.url && !provider.userinfo?.url) { const { token, userinfo } = provider
// Falls back to authjs.dev if the user only passed params
if (
(!token?.url || token.url.host === "authjs.dev") &&
(!userinfo?.url || userinfo.url.host === "authjs.dev")
) {
// We assume that issuer is always defined as this has been asserted earlier // We assume that issuer is always defined as this has been asserted earlier
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const issuer = new URL(provider.issuer!) const issuer = new URL(provider.issuer!)
@@ -54,9 +59,9 @@ export async function handleOAuth(
as = discoveredAs as = discoveredAs
} else { } else {
as = { as = {
issuer: provider.issuer ?? "https://a", // TODO: review fallback issuer issuer: provider.issuer ?? "https://authjs.dev", // TODO: review fallback issuer
token_endpoint: provider.token?.url.toString(), token_endpoint: token?.url.toString(),
userinfo_endpoint: provider.userinfo?.url.toString(), userinfo_endpoint: userinfo?.url.toString(),
} }
} }
@@ -143,9 +148,9 @@ export async function handleOAuth(
throw new Error("TODO: Handle OAuth 2.0 response body error") throw new Error("TODO: Handle OAuth 2.0 response body error")
} }
if (provider.userinfo?.request) { if (userinfo?.request) {
profile = await provider.userinfo.request({ tokens, provider }) profile = await userinfo.request({ tokens, provider })
} else if (provider.userinfo?.url) { } else if (userinfo?.url) {
const userinfoResponse = await o.userInfoRequest( const userinfoResponse = await o.userInfoRequest(
as, as,
client, client,

View File

@@ -45,11 +45,11 @@ export default function parseProviders(params: {
} }
} }
// TODO: Also add discovery here, if some endpoints/config are missing.
// We should return both a client and authorization server config.
function normalizeOAuth( function normalizeOAuth(
c?: OAuthConfig<any> | OAuthUserConfig<any> c: OAuthConfig<any> | OAuthUserConfig<any>
): OAuthConfigInternal<any> | {} { ): OAuthConfigInternal<any> | {} {
if (!c) return {}
if (c.issuer) c.wellKnown ??= `${c.issuer}/.well-known/openid-configuration` if (c.issuer) c.wellKnown ??= `${c.issuer}/.well-known/openid-configuration`
const authorization = normalizeEndpoint(c.authorization, c.issuer) const authorization = normalizeEndpoint(c.authorization, c.issuer)
@@ -84,18 +84,18 @@ function normalizeEndpoint(
e?: OAuthConfig<any>[OAuthEndpointType], e?: OAuthConfig<any>[OAuthEndpointType],
issuer?: string issuer?: string
): OAuthConfigInternal<any>[OAuthEndpointType] { ): OAuthConfigInternal<any>[OAuthEndpointType] {
if (!e || issuer) return if (!e && issuer) return
if (typeof e === "string") { if (typeof e === "string") {
return { url: new URL(e) } return { url: new URL(e) }
} }
// If v.url is undefined, it's because the provider config // If e.url is undefined, it's because the provider config
// assumes that we will use the issuer endpoint. // assumes that we will use the issuer endpoint.
// The existence of either v.url or provider.issuer is checked in // The existence of either e.url or provider.issuer is checked in
// assert.ts // assert.ts. We fallback to "https://authjs.dev" to be able to pass around
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // a valid URL even if the user only provided params.
const url = new URL(e.url!) // NOTE: This need to be checked when constructing the URL
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion // for the authorization, token and userinfo endpoints.
for (const k in e.params) url.searchParams.set(k, e.params[k] as any) const url = new URL(e?.url ?? "https://authjs.dev")
for (const k in e?.params) url.searchParams.set(k, e?.params[k])
return { ...e, url } return { url, request: e?.request }
} }

View File

@@ -2,7 +2,7 @@ import { handleLogin } from "../callback-handler.js"
import { CallbackRouteError } from "../../errors.js" import { CallbackRouteError } from "../../errors.js"
import { handleOAuth } from "../oauth/callback.js" import { handleOAuth } from "../oauth/callback.js"
import { createHash } from "../web.js" import { createHash } from "../web.js"
import { handleAuthorized } from "./shared.js" import { getAdapterUserFromEmail, handleAuthorized } from "./shared.js"
import type { AdapterSession } from "../../adapters.js" import type { AdapterSession } from "../../adapters.js"
import type { import type {
@@ -10,6 +10,7 @@ import type {
ResponseInternal, ResponseInternal,
User, User,
InternalOptions, InternalOptions,
Account,
} from "../../types.js" } from "../../types.js"
import type { Cookie, SessionStore } from "../cookie.js" import type { Cookie, SessionStore } from "../cookie.js"
@@ -172,40 +173,40 @@ export async function callback(params: {
} }
// @ts-expect-error -- Verified in `assertConfig`. // @ts-expect-error -- Verified in `assertConfig`.
const profile = await getAdapterUserFromEmail(identifier, adapter) const user = await getAdapterUserFromEmail(identifier, adapter)
const account = { const account: Account = {
providerAccountId: profile.email, providerAccountId: user.email,
userId: user.id,
type: "email" as const, type: "email" as const,
provider: provider.id, provider: provider.id,
} }
// Check if user is allowed to sign in // Check if user is allowed to sign in
const unauthorizedOrError = await handleAuthorized( const unauthorizedOrError = await handleAuthorized(
{ user: profile, account }, { user, account },
options options
) )
if (unauthorizedOrError) return { ...unauthorizedOrError, cookies } if (unauthorizedOrError) return { ...unauthorizedOrError, cookies }
// Sign user in // Sign user in
const { user, session, isNewUser } = await handleLogin( const {
sessionStore.value, user: loggedInUser,
profile, session,
account, isNewUser,
options } = await handleLogin(sessionStore.value, user, account, options)
)
if (useJwtSession) { if (useJwtSession) {
const defaultToken = { const defaultToken = {
name: user.name, name: loggedInUser.name,
email: user.email, email: loggedInUser.email,
picture: user.image, picture: loggedInUser.image,
sub: user.id?.toString(), sub: loggedInUser.id?.toString(),
} }
const token = await callbacks.jwt({ const token = await callbacks.jwt({
token: defaultToken, token: defaultToken,
user, user: loggedInUser,
account, account,
isNewUser, isNewUser,
}) })
@@ -233,7 +234,7 @@ export async function callback(params: {
}) })
} }
await events.signIn?.({ user, account, isNewUser }) await events.signIn?.({ user: loggedInUser, account, isNewUser })
// Handle first logins on new accounts // Handle first logins on new accounts
// e.g. option to send users to a new account landing page on initial login // e.g. option to send users to a new account landing page on initial login

View File

@@ -33,7 +33,7 @@ export async function signin(
const account: Account = { const account: Account = {
providerAccountId: email, providerAccountId: email,
userId: email, userId: user.id,
type: "email", type: "email",
provider: provider.id, provider: provider.id,
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@auth/sveltekit", "name": "@auth/sveltekit",
"version": "0.1.11", "version": "0.1.12",
"description": "Authentication for SvelteKit.", "description": "Authentication for SvelteKit.",
"keywords": [ "keywords": [
"authentication", "authentication",

View File

@@ -18,7 +18,7 @@
* ## Usage * ## Usage
* *
* ```ts title="src/hooks.server.ts" * ```ts title="src/hooks.server.ts"
* import SvelteKitAuth from "@auth/sveltekit" * import { SvelteKitAuth } from "@auth/sveltekit"
* import GitHub from "@auth/core/providers/github" * import GitHub from "@auth/core/providers/github"
* import { GITHUB_ID, GITHUB_SECRET } from "$env/static/private" * import { GITHUB_ID, GITHUB_SECRET } from "$env/static/private"
* *

View File

@@ -9,7 +9,7 @@
"Balázs Orbán <info@balazsorban.com>", "Balázs Orbán <info@balazsorban.com>",
"Nico Domino <yo@ndo.dev>", "Nico Domino <yo@ndo.dev>",
"Lluis Agusti <hi@llu.lu>", "Lluis Agusti <hi@llu.lu>",
"Thang Huu Vu <thvu@hey.com>" "Thang Huu Vu <hi@thvu.dev>"
], ],
"main": "index.js", "main": "index.js",
"module": "index.js", "module": "index.js",

16
pnpm-lock.yaml generated
View File

@@ -258,6 +258,7 @@ importers:
docusaurus-plugin-typedoc: ^0.18.0 docusaurus-plugin-typedoc: ^0.18.0
mdx-mermaid: 1.2.2 mdx-mermaid: 1.2.2
mermaid: 9.0.1 mermaid: 9.0.1
next-auth: workspace:*
prism-react-renderer: 1.3.5 prism-react-renderer: 1.3.5
react: ^18.2.0 react: ^18.2.0
react-dom: ^18.2.0 react-dom: ^18.2.0
@@ -272,6 +273,7 @@ importers:
classnames: 2.3.2 classnames: 2.3.2
mdx-mermaid: 1.2.2_mermaid@9.0.1+react@18.2.0 mdx-mermaid: 1.2.2_mermaid@9.0.1+react@18.2.0
mermaid: 9.0.1 mermaid: 9.0.1
next-auth: link:../packages/next-auth
prism-react-renderer: 1.3.5_react@18.2.0 prism-react-renderer: 1.3.5_react@18.2.0
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0_react@18.2.0 react-dom: 18.2.0_react@18.2.0
@@ -13604,7 +13606,7 @@ packages:
/axios/0.21.4_debug@3.2.7: /axios/0.21.4_debug@3.2.7:
resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==}
dependencies: dependencies:
follow-redirects: 1.15.1 follow-redirects: 1.15.1_debug@3.2.7
transitivePeerDependencies: transitivePeerDependencies:
- debug - debug
dev: false dev: false
@@ -19295,6 +19297,18 @@ packages:
debug: debug:
optional: true optional: true
/follow-redirects/1.15.1_debug@3.2.7:
resolution: {integrity: sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
dependencies:
debug: 3.2.7
dev: false
/follow-redirects/1.15.1_debug@4.3.4: /follow-redirects/1.15.1_debug@4.3.4:
resolution: {integrity: sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==} resolution: {integrity: sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}

View File

@@ -1,19 +1,25 @@
{ {
"$schema": "https://turborepo.org/schema.json", "$schema": "https://turborepo.org/schema.json",
"pipeline": { "pipeline": {
"docs#build": {
"dependsOn": [
"^build",
"next-auth#build",
"@auth/core#build",
"@auth/sveltekit#build"
]
},
"build": { "build": {
"dependsOn": ["^build"] "dependsOn": ["^build"]
}, },
"next-auth#build": { "next-auth#build": {
"dependsOn": ["^build"] "dependsOn": ["^build"],
"outputs": [
"client/**",
"core/**",
"css/**",
"jwt/**",
"next/**",
"providers/**",
"react/**",
"index.d.ts",
"index.js",
"adapters.d.ts",
"middleware.d.ts",
"middleware.js"
]
}, },
"clean": { "clean": {
"cache": false "cache": false