mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
80 Commits
next-auth@
...
next-auth@
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f06f3bbc96 | ||
|
|
aea27a1fa8 | ||
|
|
bd37c55241 | ||
|
|
169a5230db | ||
|
|
f48eb0478e | ||
|
|
b25a090c17 | ||
|
|
0167e9368b | ||
|
|
dcb576f01b | ||
|
|
9417822a41 | ||
|
|
14f8f0cb58 | ||
|
|
212272a839 | ||
|
|
a8e8b7542c | ||
|
|
14cecb9b73 | ||
|
|
28bec0fbcc | ||
|
|
bc683a5b72 | ||
|
|
e7b8597f73 | ||
|
|
5c89a21bfa | ||
|
|
6e9c8b5b3c | ||
|
|
91a9e5f601 | ||
|
|
cb916f4848 | ||
|
|
8259cd4fc6 | ||
|
|
7a8c0068c4 | ||
|
|
6edb6ddaaf | ||
|
|
0711d32a00 | ||
|
|
c261af4695 | ||
|
|
d69f311ddc | ||
|
|
ec8a34308b | ||
|
|
c0bf2f15fb | ||
|
|
d8901777bf | ||
|
|
319f2ce165 | ||
|
|
2d907f0004 | ||
|
|
2954588be7 | ||
|
|
4026183411 | ||
|
|
86d031faba | ||
|
|
1e3745d22a | ||
|
|
feaeda9e2a | ||
|
|
e127600ad4 | ||
|
|
cb3137133c | ||
|
|
b3eaf6329e | ||
|
|
8aa1789697 | ||
|
|
a7601d0b45 | ||
|
|
bb8d826bc7 | ||
|
|
f787809cd4 | ||
|
|
7789fa17b5 | ||
|
|
740c505901 | ||
|
|
1e579cbaa6 | ||
|
|
65aacbe97a | ||
|
|
7dbfa5da4d | ||
|
|
98bd774b75 | ||
|
|
3661ca68b0 | ||
|
|
7ba986b01e | ||
|
|
e638ec5eb1 | ||
|
|
7327468697 | ||
|
|
9a9c24897d | ||
|
|
e362653819 | ||
|
|
a92e348ed3 | ||
|
|
ab0857a99e | ||
|
|
50b117dfbb | ||
|
|
e6590ffc20 | ||
|
|
26c846594f | ||
|
|
2432ce9001 | ||
|
|
0a689b4f4e | ||
|
|
2fb34bab51 | ||
|
|
d0e7689d07 | ||
|
|
c004659174 | ||
|
|
c212e96f83 | ||
|
|
d41f2a4a02 | ||
|
|
5ecf20a804 | ||
|
|
9e423f3252 | ||
|
|
cf810f246a | ||
|
|
05fe398b1a | ||
|
|
8659c02366 | ||
|
|
2e039643b6 | ||
|
|
3943f9b7b2 | ||
|
|
f2e85c2113 | ||
|
|
c53c868288 | ||
|
|
0bc4fcb51a | ||
|
|
139c2edb50 | ||
|
|
4e94d89554 | ||
|
|
43d66fcb23 |
2
.github/pr-labeler.yml
vendored
2
.github/pr-labeler.yml
vendored
@@ -54,7 +54,7 @@ upstash-redis:
|
||||
xata:
|
||||
- packages/adapter-xata/**
|
||||
|
||||
core:
|
||||
legacy:
|
||||
- packages/next-auth/src/**/*
|
||||
|
||||
style:
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -34,13 +34,9 @@ packages/next-auth/utils
|
||||
packages/next-auth/core
|
||||
packages/next-auth/jwt
|
||||
packages/next-auth/react
|
||||
packages/next-auth/adapters.d.ts
|
||||
packages/next-auth/adapters.js
|
||||
packages/next-auth/index.d.ts
|
||||
packages/next-auth/index.js
|
||||
packages/next-auth/*.d.ts*
|
||||
packages/next-auth/*.js
|
||||
packages/next-auth/next
|
||||
packages/next-auth/middleware.d.ts
|
||||
packages/next-auth/middleware.js
|
||||
|
||||
# Development app
|
||||
apps/dev/src/css
|
||||
|
||||
14
apps/dev/app/api/auth/[...nextauth]/route.ts
Normal file
14
apps/dev/app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import NextAuth, { type NextAuthOptions } from "next-auth"
|
||||
import GitHub from "next-auth/providers/github"
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
providers: [
|
||||
GitHub({
|
||||
clientId: process.env.GITHUB_ID,
|
||||
clientSecret: process.env.GITHUB_SECRET,
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
const handler = NextAuth(authOptions)
|
||||
export { handler as GET, handler as POST }
|
||||
1
apps/dev/next-env.d.ts
vendored
1
apps/dev/next-env.d.ts
vendored
@@ -1,5 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference types="next/navigation-types/compat/navigation" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@prisma/client": "^3",
|
||||
"@supabase/supabase-js": "^2.0.5",
|
||||
"faunadb": "^4",
|
||||
"next": "13.0.6",
|
||||
"next": "13.3.0",
|
||||
"next-auth": "workspace:*",
|
||||
"nodemailer": "^6",
|
||||
"react": "^18",
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonwebtoken": "^8.5.5",
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react": "^18.0.37",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"fake-smtp-server": "^0.8.0",
|
||||
"pg": "^8.7.3",
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
],
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^17",
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react": "^18.0.37",
|
||||
"typescript": "^4"
|
||||
}
|
||||
}
|
||||
|
||||
21
docs/docs/adapters.md
Normal file
21
docs/docs/adapters.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
id: adapters
|
||||
title: Adapters
|
||||
---
|
||||
|
||||
Visit the [authjs.dev](https://authjs.dev/reference/adapters) page for the up-to-date documentation.
|
||||
|
||||
- [Dgraph](https://authjs.dev/reference/adapter/dgraph)
|
||||
- [DynamoDB](https://authjs.dev/reference/adapter/dynamodb)
|
||||
- [Fauna](https://authjs.dev/reference/adapter/fauna)
|
||||
- [Firebase](https://authjs.dev/reference/adapter/firebase)
|
||||
- [MongoDB](https://authjs.dev/reference/adapter/mongodb)
|
||||
- [Prisma](https://authjs.dev/reference/adapter/prisma)
|
||||
- [TypeORM](https://authjs.dev/reference/adapter/typeorm)
|
||||
- [MikroORM](https://authjs.dev/reference/adapter/mikro-orm)
|
||||
- [neo4j](https://authjs.dev/reference/adapter/neo4j)
|
||||
- [PouchDB](https://authjs.dev/reference/adapter/pouchdb)
|
||||
- [Sequelize](https://authjs.dev/reference/adapter/sequelize)
|
||||
- [Supabase](https://authjs.dev/reference/adapter/supabase)
|
||||
- [Upstash Redis](https://authjs.dev/reference/adapter/upstash-redis)
|
||||
- [Xata](https://authjs.dev/reference/adapter/xata)
|
||||
@@ -1,250 +0,0 @@
|
||||
---
|
||||
id: dgraph
|
||||
title: Dgraph
|
||||
---
|
||||
|
||||
# Dgraph
|
||||
|
||||
This is the Dgraph Adapter for [`next-auth`](https://next-auth.js.org).
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install the necessary packages
|
||||
|
||||
```bash npm2yarn2pnpm
|
||||
npm install next-auth @next-auth/dgraph-adapter
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import { DgraphAdapter } from "@next-auth/dgraph-adapter"
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
export default NextAuth({
|
||||
// https://next-auth.js.org/configuration/providers
|
||||
providers: [],
|
||||
adapter: DgraphAdapter({
|
||||
endpoint: process.env.DGRAPH_GRAPHQL_ENDPOINT,
|
||||
authToken: process.env.DGRAPH_GRAPHQL_KEY,
|
||||
|
||||
// you can omit the following properties if you are running an unsecure schema
|
||||
authHeader: process.env.AUTH_HEADER, // default: "Authorization",
|
||||
jwtSecret: process.env.SECRET,
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
## Quick start with the unsecure schema
|
||||
|
||||
The quickest way to use Dgraph is by applying the unsecure schema to your [local](https://dgraph.io/docs/graphql/admin/#modifying-a-schema) Dgraph instance or if using Dgraph [cloud](https://dgraph.io/docs/cloud/cloud-quick-start/#the-schema) you can paste the schema in the codebox to update.
|
||||
|
||||
:::warning
|
||||
This approach is not secure or for production use, and does not require a `jwtSecret`.
|
||||
:::
|
||||
|
||||
> This schema is adapted for use in Dgraph and based upon our main [schema](/adapters/models)
|
||||
|
||||
#### Unsecure schema
|
||||
|
||||
```graphql
|
||||
type Account {
|
||||
id: ID
|
||||
type: String
|
||||
provider: String @search(by: [hash])
|
||||
providerAccountId: String @search(by: [hash])
|
||||
refreshToken: String
|
||||
expires_at: Int64
|
||||
accessToken: String
|
||||
token_type: String
|
||||
refresh_token: String
|
||||
access_token: String
|
||||
scope: String
|
||||
id_token: String
|
||||
session_state: String
|
||||
user: User @hasInverse(field: "accounts")
|
||||
}
|
||||
type Session {
|
||||
id: ID
|
||||
expires: DateTime
|
||||
sessionToken: String @search(by: [hash])
|
||||
user: User @hasInverse(field: "sessions")
|
||||
}
|
||||
type User {
|
||||
id: ID
|
||||
name: String
|
||||
email: String @search(by: [hash])
|
||||
emailVerified: DateTime
|
||||
image: String
|
||||
accounts: [Account] @hasInverse(field: "user")
|
||||
sessions: [Session] @hasInverse(field: "user")
|
||||
}
|
||||
|
||||
type VerificationToken {
|
||||
id: ID
|
||||
identifier: String @search(by: [hash])
|
||||
token: String @search(by: [hash])
|
||||
expires: DateTime
|
||||
}
|
||||
```
|
||||
|
||||
## Securing your database
|
||||
|
||||
For production deployments you will want to restrict the access to the types used
|
||||
by next-auth. The main form of access control used in Dgraph is via `@auth` directive alongide types in the schema.
|
||||
|
||||
#### Secure schema
|
||||
|
||||
```graphql
|
||||
type Account
|
||||
@auth(
|
||||
delete: { rule: "{$nextAuth: { eq: true } }" }
|
||||
add: { rule: "{$nextAuth: { eq: true } }" }
|
||||
query: { rule: "{$nextAuth: { eq: true } }" }
|
||||
update: { rule: "{$nextAuth: { eq: true } }" }
|
||||
) {
|
||||
id: ID
|
||||
type: String
|
||||
provider: String @search(by: [hash])
|
||||
providerAccountId: String @search(by: [hash])
|
||||
refreshToken: String
|
||||
expires_at: Int64
|
||||
accessToken: String
|
||||
token_type: String
|
||||
refresh_token: String
|
||||
access_token: String
|
||||
scope: String
|
||||
id_token: String
|
||||
session_state: String
|
||||
user: User @hasInverse(field: "accounts")
|
||||
}
|
||||
type Session
|
||||
@auth(
|
||||
delete: { rule: "{$nextAuth: { eq: true } }" }
|
||||
add: { rule: "{$nextAuth: { eq: true } }" }
|
||||
query: { rule: "{$nextAuth: { eq: true } }" }
|
||||
update: { rule: "{$nextAuth: { eq: true } }" }
|
||||
) {
|
||||
id: ID
|
||||
expires: DateTime
|
||||
sessionToken: String @search(by: [hash])
|
||||
user: User @hasInverse(field: "sessions")
|
||||
}
|
||||
type User
|
||||
@auth(
|
||||
query: {
|
||||
or: [
|
||||
{
|
||||
rule: """
|
||||
query ($userId: String!) {queryUser(filter: { id: { eq: $userId } } ) {id}}
|
||||
"""
|
||||
}
|
||||
{ rule: "{$nextAuth: { eq: true } }" }
|
||||
]
|
||||
}
|
||||
delete: { rule: "{$nextAuth: { eq: true } }" }
|
||||
add: { rule: "{$nextAuth: { eq: true } }" }
|
||||
update: {
|
||||
or: [
|
||||
{
|
||||
rule: """
|
||||
query ($userId: String!) {queryUser(filter: { id: { eq: $userId } } ) {id}}
|
||||
"""
|
||||
}
|
||||
{ rule: "{$nextAuth: { eq: true } }" }
|
||||
]
|
||||
}
|
||||
) {
|
||||
id: ID
|
||||
name: String
|
||||
email: String @search(by: [hash])
|
||||
emailVerified: DateTime
|
||||
image: String
|
||||
accounts: [Account] @hasInverse(field: "user")
|
||||
sessions: [Session] @hasInverse(field: "user")
|
||||
}
|
||||
|
||||
type VerificationToken
|
||||
@auth(
|
||||
delete: { rule: "{$nextAuth: { eq: true } }" }
|
||||
add: { rule: "{$nextAuth: { eq: true } }" }
|
||||
query: { rule: "{$nextAuth: { eq: true } }" }
|
||||
update: { rule: "{$nextAuth: { eq: true } }" }
|
||||
) {
|
||||
id: ID
|
||||
identifier: String @search(by: [hash])
|
||||
token: String @search(by: [hash])
|
||||
expires: DateTime
|
||||
}
|
||||
|
||||
# Dgraph.Authorization {"VerificationKey":"<YOUR JWT SECRET HERE>","Header":"<YOUR AUTH HEADER HERE>","Namespace":"<YOUR CUSTOM NAMESPACE HERE>","Algo":"HS256"}
|
||||
```
|
||||
|
||||
#### Dgraph.Authorization
|
||||
|
||||
In order to secure your graphql backend define the `Dgraph.Authorization` object at the
|
||||
bottom of your schema and provide `authHeader` and `jwtSecret` values to the DgraphClient.
|
||||
|
||||
```js
|
||||
# Dgraph.Authorization {"VerificationKey":"<YOUR JWT SECRET HERE>","Header":"<YOUR AUTH HEADER HERE>","Namespace":"YOUR CUSTOM NAMESPACE HERE","Algo":"HS256"}
|
||||
```
|
||||
|
||||
#### VerificationKey and jwtSecret
|
||||
|
||||
This is the key used to sign the JWT. Ex. `process.env.SECRET` or `process.env.APP_SECRET`.
|
||||
|
||||
#### Header and authHeader
|
||||
|
||||
The `Header` tells Dgraph where to lookup a JWT within the headers of the incoming requests made to the dgraph server.
|
||||
You have to configure it at the bottom of your schema file. This header is the same as the `authHeader` property you
|
||||
provide when you instantiate the `DgraphClient`.
|
||||
|
||||
#### The nextAuth secret
|
||||
|
||||
The `$nextAuth` secret is securely generated using the `jwtSecret` and injected by the DgraphAdapter in order to allow interacting with the JWT DgraphClient for anonymous user requests made within the system `ie. login, register`. This allows
|
||||
secure interactions to be made with all the auth types required by next-auth. You have to specify it for each auth rule of
|
||||
each type defined in your secure schema.
|
||||
|
||||
```js
|
||||
type VerificationRequest
|
||||
@auth(
|
||||
delete: { rule: "{$nextAuth: { eq: true } }" },
|
||||
add: { rule: "{$nextAuth: { eq: true } }" },
|
||||
query: { rule: "{$nextAuth: { eq: true } }" },
|
||||
update: { rule: "{$nextAuth: { eq: true } }" }
|
||||
) {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Working with JWT session and @auth directive
|
||||
|
||||
Dgraph only works with HS256 or RS256 algorithms. If you want to use session jwt to securely interact with your dgraph
|
||||
database you must customize next-auth `encode` and `decode` functions, as the default algorithm is HS512. You can
|
||||
further customize the jwt with roles if you want to implement [`RBAC logic`](https://dgraph.io/docs/graphql/authorization/directive/#role-based-access-control).
|
||||
|
||||
```js
|
||||
import * as jwt from "jsonwebtoken"
|
||||
export default NextAuth({
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
},
|
||||
jwt: {
|
||||
secret: process.env.SECRET,
|
||||
encode: async ({ secret, token }) => {
|
||||
return jwt.sign({ ...token, userId: token.id }, secret, {
|
||||
algorithm: "HS256",
|
||||
expiresIn: 30 * 24 * 60 * 60, // 30 days
|
||||
})
|
||||
},
|
||||
decode: async ({ secret, token }) => {
|
||||
return jwt.verify(token, secret, { algorithms: ["HS256"] })
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Once your `Dgraph.Authorization` is defined in your schema and the JWT settings are set, this will allow you to define
|
||||
[`@auth rules`](https://dgraph.io/docs/graphql/authorization/authorization-overview/) for every part of your schema.
|
||||
@@ -1,147 +0,0 @@
|
||||
---
|
||||
id: dynamodb
|
||||
title: DynamoDB
|
||||
---
|
||||
|
||||
# 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.
|
||||
|
||||
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.
|
||||
|
||||
You can find the full schema in the table structure section below.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install `next-auth` and `@next-auth/dynamodb-adapter`
|
||||
|
||||
```bash npm2yarn2pnpm
|
||||
npm install next-auth @next-auth/dynamodb-adapter
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
You need to pass `DynamoDBDocument` client from the modular [`aws-sdk`](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/dynamodb-example-dynamodb-utilities.html) v3 to 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"
|
||||
import { DynamoDB } from "@aws-sdk/client-dynamodb"
|
||||
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb"
|
||||
import NextAuth from "next-auth";
|
||||
import Providers from "next-auth/providers";
|
||||
import { DynamoDBAdapter } from "@next-auth/dynamodb-adapter"
|
||||
|
||||
const config: DynamoDBClientConfig = {
|
||||
credentials: {
|
||||
accessKeyId: process.env.NEXT_AUTH_AWS_ACCESS_KEY as string,
|
||||
secretAccessKey: process.env.NEXT_AUTH_AWS_SECRET_KEY as string,
|
||||
},
|
||||
region: process.env.NEXT_AUTH_AWS_REGION,
|
||||
};
|
||||
|
||||
const client = DynamoDBDocument.from(new DynamoDB(config), {
|
||||
marshallOptions: {
|
||||
convertEmptyValues: true,
|
||||
removeUndefinedValues: true,
|
||||
convertClassInstanceToMap: true,
|
||||
},
|
||||
})
|
||||
|
||||
export default NextAuth({
|
||||
// Configure one or more authentication providers
|
||||
providers: [
|
||||
Providers.GitHub({
|
||||
clientId: process.env.GITHUB_ID,
|
||||
clientSecret: process.env.GITHUB_SECRET,
|
||||
}),
|
||||
Providers.Email({
|
||||
server: process.env.EMAIL_SERVER,
|
||||
from: process.env.EMAIL_FROM,
|
||||
}),
|
||||
// ...add more providers here
|
||||
],
|
||||
adapter: DynamoDBAdapter(
|
||||
client
|
||||
),
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
(AWS secrets start with `NEXT_AUTH_` in order to not conflict with [Vercel's reserved environment variables](https://vercel.com/docs/environment-variables#reserved-environment-variables).)
|
||||
|
||||
## Schema
|
||||
|
||||
The table respects the single table design pattern. This has many advantages:
|
||||
|
||||
- Only one table to manage, monitor and provision.
|
||||
- Querying relations is faster than with multi-table schemas (for eg. retrieving all sessions for a user).
|
||||
- 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](/adapters/models)
|
||||
|
||||

|
||||
|
||||
You can create this table with infrastructure as code using [`aws-cdk`](https://github.com/aws/aws-cdk) with the following table definition:
|
||||
|
||||
```javascript title=stack.ts
|
||||
new dynamodb.Table(this, `NextAuthTable`, {
|
||||
tableName: "next-auth",
|
||||
partitionKey: { name: "pk", type: dynamodb.AttributeType.STRING },
|
||||
sortKey: { name: "sk", type: dynamodb.AttributeType.STRING },
|
||||
timeToLiveAttribute: "expires",
|
||||
}).addGlobalSecondaryIndex({
|
||||
indexName: "GSI1",
|
||||
partitionKey: { name: "GSI1PK", type: dynamodb.AttributeType.STRING },
|
||||
sortKey: { name: "GSI1SK", type: dynamodb.AttributeType.STRING },
|
||||
})
|
||||
```
|
||||
|
||||
Alternatively you can use this cloudformation template:
|
||||
|
||||
```yaml title=cloudformation.yaml
|
||||
NextAuthTable:
|
||||
Type: "AWS::DynamoDB::Table"
|
||||
Properties:
|
||||
TableName: next-auth
|
||||
AttributeDefinitions:
|
||||
- AttributeName: pk
|
||||
AttributeType: S
|
||||
- AttributeName: sk
|
||||
AttributeType: S
|
||||
- AttributeName: GSI1PK
|
||||
AttributeType: S
|
||||
- AttributeName: GSI1SK
|
||||
AttributeType: S
|
||||
KeySchema:
|
||||
- AttributeName: pk
|
||||
KeyType: HASH
|
||||
- AttributeName: sk
|
||||
KeyType: RANGE
|
||||
GlobalSecondaryIndexes:
|
||||
- IndexName: GSI1
|
||||
Projection:
|
||||
ProjectionType: ALL
|
||||
KeySchema:
|
||||
- AttributeName: GSI1PK
|
||||
KeyType: HASH
|
||||
- AttributeName: GSI1SK
|
||||
KeyType: RANGE
|
||||
TimeToLiveSpecification:
|
||||
AttributeName: expires
|
||||
Enabled: true
|
||||
```
|
||||
|
||||
## Custom Schema
|
||||
|
||||
You can configure your custom table schema by passing the `options` key to the adapter constructor:
|
||||
|
||||
```
|
||||
const adapter = DynamoDBAdapter(client, {
|
||||
tableName: "custom-table-name",
|
||||
partitionKey: "custom-pk",
|
||||
sortKey: "custom-sk",
|
||||
indexName: "custom-index-name",
|
||||
indexPartitionKey: "custom-index-pk",
|
||||
indexSortKey: "custom-index-sk",
|
||||
})
|
||||
```
|
||||
@@ -1,85 +0,0 @@
|
||||
---
|
||||
id: fauna
|
||||
title: FaunaDB
|
||||
---
|
||||
|
||||
# FaunaDB
|
||||
|
||||
This is the Fauna Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
|
||||
|
||||
You can find the Fauna schema and seed information in the docs at [next-auth.js.org/adapters/fauna](https://next-auth.js.org/adapters/fauna).
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install the necessary packages
|
||||
|
||||
```bash npm2yarn2pnpm
|
||||
npm install next-auth @next-auth/fauna-adapter faunadb
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import { Client as FaunaClient } from "faunadb"
|
||||
import { FaunaAdapter } from "@next-auth/fauna-adapter"
|
||||
|
||||
const client = new FaunaClient({
|
||||
secret: "secret",
|
||||
scheme: "http",
|
||||
domain: "localhost",
|
||||
port: 8443,
|
||||
})
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
export default NextAuth({
|
||||
// https://next-auth.js.org/providers/overview
|
||||
providers: [],
|
||||
adapter: FaunaAdapter(client)
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
## Schema
|
||||
|
||||
Run the following commands inside of the `Shell` tab in the Fauna dashboard to setup the appropriate collections and indexes.
|
||||
|
||||
```javascript
|
||||
CreateCollection({ name: "accounts" })
|
||||
CreateCollection({ name: "sessions" })
|
||||
CreateCollection({ name: "users" })
|
||||
CreateCollection({ name: "verification_tokens" })
|
||||
```
|
||||
|
||||
```javascript
|
||||
CreateIndex({
|
||||
name: "account_by_provider_and_provider_account_id",
|
||||
source: Collection("accounts"),
|
||||
unique: true,
|
||||
terms: [
|
||||
{ field: ["data", "provider"] },
|
||||
{ field: ["data", "providerAccountId"] },
|
||||
],
|
||||
})
|
||||
CreateIndex({
|
||||
name: "session_by_session_token",
|
||||
source: Collection("sessions"),
|
||||
unique: true,
|
||||
terms: [{ field: ["data", "sessionToken"] }],
|
||||
})
|
||||
CreateIndex({
|
||||
name: "user_by_email",
|
||||
source: Collection("users"),
|
||||
unique: true,
|
||||
terms: [{ field: ["data", "email"] }],
|
||||
})
|
||||
CreateIndex({
|
||||
name: "verification_token_by_identifier_and_token",
|
||||
source: Collection("verification_tokens"),
|
||||
unique: true,
|
||||
terms: [{ field: ["data", "identifier"] }, { field: ["data", "token"] }],
|
||||
})
|
||||
```
|
||||
|
||||
> This schema is adapted for use in Fauna and based upon our main [schema](/adapters/models)
|
||||
@@ -1,91 +0,0 @@
|
||||
---
|
||||
id: firebase
|
||||
title: Firebase
|
||||
---
|
||||
|
||||
# Firebase
|
||||
|
||||
This is the Firebase (Firestore) Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install the necessary packages
|
||||
|
||||
```bash npm2yarn2pnpm
|
||||
npm install next-auth @next-auth/firebase-adapter
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import GoogleProvider from "next-auth/providers/google"
|
||||
import { FirestoreAdapter } from "@next-auth/firebase-adapter"
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
export default NextAuth({
|
||||
// https://next-auth.js.org/providers
|
||||
providers: [
|
||||
GoogleProvider({
|
||||
clientId: process.env.GOOGLE_ID,
|
||||
clientSecret: process.env.GOOGLE_SECRET,
|
||||
}),
|
||||
],
|
||||
adapter: FirestoreAdapter({
|
||||
apiKey: process.env.FIREBASE_API_KEY,
|
||||
appId: process.env.FIREBASE_APP_ID,
|
||||
authDomain: process.env.FIREBASE_AUTH_DOMAIN,
|
||||
databaseURL: process.env.FIREBASE_DATABASE_URL,
|
||||
projectId: process.env.FIREBASE_PROJECT_ID,
|
||||
storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
|
||||
messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
|
||||
// Optional emulator config (see below for options)
|
||||
emulator: {},
|
||||
}),
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
When initializing the firestore adapter, you must pass in the firebase config object with the details from your project. More details on how to obtain that config object can be found [here](https://support.google.com/firebase/answer/7015592).
|
||||
|
||||
An example firebase config looks like this:
|
||||
|
||||
```js
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyDOCAbC123dEf456GhI789jKl01-MnO",
|
||||
authDomain: "myapp-project-123.firebaseapp.com",
|
||||
databaseURL: "https://myapp-project-123.firebaseio.com",
|
||||
projectId: "myapp-project-123",
|
||||
storageBucket: "myapp-project-123.appspot.com",
|
||||
messagingSenderId: "65211879809",
|
||||
appId: "1:65211879909:web:3ae38ef1cdcb2e01fe5f0c",
|
||||
measurementId: "G-8GSGZQ44ST",
|
||||
}
|
||||
```
|
||||
|
||||
See [firebase.google.com/docs/web/setup](https://firebase.google.com/docs/web/setup) for more details.
|
||||
|
||||
You can optionally pass in emulator options to automatically connect to your local Firebase emulator.
|
||||
|
||||
```js
|
||||
FirestoreAdapter({
|
||||
// ...
|
||||
// Passing in an enable object will enable the emulator
|
||||
emulator: {
|
||||
// Optional host, defaults to `localhost`
|
||||
host: 'localhost',
|
||||
// Optional port, defaults to `3001`
|
||||
port: 3001,
|
||||
},
|
||||
}),
|
||||
```
|
||||
|
||||
:::tip **From Firebase**
|
||||
|
||||
**Caution**: We do not recommend manually modifying an app's Firebase config file or object. If you initialize an app with invalid or missing values for any of these required "Firebase options", then your end users may experience serious issues.
|
||||
|
||||
For open source projects, we generally do not recommend including the app's Firebase config file or object in source control because, in most cases, your users should create their own Firebase projects and point their apps to their own Firebase resources (via their own Firebase config file or object).
|
||||
:::
|
||||
@@ -1,113 +0,0 @@
|
||||
---
|
||||
id: mikro-orm
|
||||
title: MikroORM
|
||||
---
|
||||
|
||||
To use this Adapter, you need to install Mikro ORM, the driver that suits your database, and the separate `@next-auth/mikro-orm-adapter` package:
|
||||
|
||||
```bash npm2yarn2pnpm
|
||||
npm install next-auth @next-auth/mikro-orm-adapter @mikro-orm/core @mikro-orm/[YOUR DRIVER]
|
||||
```
|
||||
|
||||
Configure NextAuth.js to use the MikroORM Adapter:
|
||||
|
||||
```typescript title="pages/api/auth/[...nextauth].ts"
|
||||
import NextAuth from "next-auth"
|
||||
import { MikroOrmAdapter } from "@next-auth/mikro-orm-adapter"
|
||||
|
||||
export default NextAuth({
|
||||
adapter: MikroOrmAdapter({
|
||||
// MikroORM options object. Ref: https://mikro-orm.io/docs/next/configuration#driver
|
||||
dbName: "./db.sqlite",
|
||||
type: "sqlite",
|
||||
debug: process.env.DEBUG === "true" || process.env.DEBUG?.includes("db"),
|
||||
}),
|
||||
providers: [],
|
||||
})
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
### Passing custom entities
|
||||
|
||||
The MikroORM adapter ships with its own set of entities. If you'd like to extend them, you can optionally pass them to the adapter.
|
||||
|
||||
> This schema is adapted for use in MikroORM and based upon our main [schema](/adapters/models)
|
||||
|
||||
```typescript title="pages/api/auth/[...nextauth].ts"
|
||||
import config from "config/mikro-orm.ts"
|
||||
import {
|
||||
Cascade,
|
||||
Collection,
|
||||
Entity,
|
||||
OneToMany,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
Unique,
|
||||
} from "@mikro-orm/core"
|
||||
import { defaultEntities } from "@next-auth/mikro-orm-adapter"
|
||||
|
||||
const { Account, Session } = defaultEntities
|
||||
|
||||
@Entity()
|
||||
export class User implements defaultEntities.User {
|
||||
@PrimaryKey()
|
||||
id: string = randomUUID()
|
||||
|
||||
@Property({ nullable: true })
|
||||
name?: string
|
||||
|
||||
@Property({ nullable: true })
|
||||
@Unique()
|
||||
email?: string
|
||||
|
||||
@Property({ type: "Date", nullable: true })
|
||||
emailVerified: Date | null = null
|
||||
|
||||
@Property({ nullable: true })
|
||||
image?: string
|
||||
|
||||
@OneToMany({
|
||||
entity: () => Session,
|
||||
mappedBy: (session) => session.user,
|
||||
hidden: true,
|
||||
orphanRemoval: true,
|
||||
cascade: [Cascade.ALL],
|
||||
})
|
||||
sessions = new Collection<Session>(this)
|
||||
|
||||
@OneToMany({
|
||||
entity: () => Account,
|
||||
mappedBy: (account) => account.user,
|
||||
hidden: true,
|
||||
orphanRemoval: true,
|
||||
cascade: [Cascade.ALL],
|
||||
})
|
||||
accounts = new Collection<Account>(this)
|
||||
|
||||
@Enum({ hidden: true })
|
||||
role = "ADMIN"
|
||||
}
|
||||
|
||||
export default NextAuth({
|
||||
adapter: MikroOrmAdapter(config, { entities: { User } }),
|
||||
})
|
||||
```
|
||||
|
||||
### Including the default entities in your MikroORM config
|
||||
|
||||
You may want to include the defaultEntities in your MikroORM configuration to include them in Migrations etc.
|
||||
|
||||
To achieve that include them in your "entities" array:
|
||||
|
||||
```typescript title="config/mikro-orm.ts"
|
||||
import { Options } from "@mikro-orm/core";
|
||||
import { defaultEntities } from "@next-auth/mikro-orm-adapter"
|
||||
|
||||
const config: Options = {
|
||||
...
|
||||
entities: [VeryImportantEntity, ...Object.values(defaultEntities)],
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
@@ -1,118 +0,0 @@
|
||||
---
|
||||
id: models
|
||||
title: Models
|
||||
---
|
||||
|
||||
NextAuth.js can be used with any database. Models tell you what structures NextAuth.js expects from your database. Models will vary slightly depending on which adapter you use, but in general, will look something like this. Each adapter's model/schema will be slightly adapted for its needs, but will look very much like this schema below:
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
User ||--|{ Account : ""
|
||||
User {
|
||||
string id
|
||||
string name
|
||||
string email
|
||||
timestamp emailVerified
|
||||
string image
|
||||
}
|
||||
User ||--|{ Session : ""
|
||||
Session {
|
||||
string id
|
||||
timestamp expires
|
||||
string sessionToken
|
||||
string userId
|
||||
}
|
||||
Account {
|
||||
string id
|
||||
string userId
|
||||
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
|
||||
string oauth_token_secret
|
||||
string oauth_token
|
||||
}
|
||||
VerificationToken {
|
||||
string identifier
|
||||
string token
|
||||
timestamp expires
|
||||
}
|
||||
```
|
||||
|
||||
More information about each Model / Table can be found below.
|
||||
|
||||
:::note
|
||||
You can [create your own adapter](/tutorials/creating-a-database-adapter) if you want to use NextAuth.js with a database that is not supported out of the box, or you have to change fields on any of the models.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## User
|
||||
|
||||
The User model is for information such as the user's name and email address.
|
||||
|
||||
Email address is optional, but if one is specified for a User then it must be unique.
|
||||
|
||||
:::note
|
||||
If a user first signs in with OAuth then their email address is automatically populated using the one from their OAuth profile, if the OAuth provider returns one.
|
||||
|
||||
This provides a way to contact users and for users to maintain access to their account and sign in using email in the event they are unable to sign in with the OAuth provider in future (if the [Email Provider](/providers/email) is configured).
|
||||
:::
|
||||
|
||||
User creation in the database is automatic, and happens when the user is logging in for the first time with a provider. The default data saved is `id`, `name`, `email` and `image`. You can add more profile data by returning extra fields in your [OAuth provider's `profile()`](/configuration/providers/oauth#options) callback.
|
||||
|
||||
## Account
|
||||
|
||||
The Account model is for information about OAuth accounts associated with a User. It will usually contain `access_token`, `id_token` and other OAuth specific data. [`TokenSet`](https://github.com/panva/node-openid-client/blob/main/docs/README.md#new-tokensetinput) from `openid-client` might give you an idea of all the fields.
|
||||
|
||||
:::note
|
||||
In case of an OAuth 1.0 provider (like Twitter), you will have to look for `oauth_token` and `oauth_token_secret` string fields. GitHub also has an extra `refresh_token_expires_in` integer field. You have to make sure that your database schema includes these fields.
|
||||
:::
|
||||
|
||||
A single User can have multiple Accounts, but each Account can only have one User.
|
||||
|
||||
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](/faq#security) for more information why this is a requirement.
|
||||
|
||||
:::tip
|
||||
You can manually unlink accounts, if your adapter implements the `unlinkAccount` method. Make sure to take all the necessary security steps to avoid data loss.
|
||||
:::
|
||||
|
||||
:::note
|
||||
Linking and unlinking accounts through an API is a planned feature: https://github.com/nextauthjs/next-auth/issues/230
|
||||
:::
|
||||
|
||||
## Session
|
||||
|
||||
The Session model is used for database sessions. It is not used if JSON Web Tokens are enabled. Keep in mind, that you can use a database to persist Users and Accounts, and still use JWT for sessions. See the [`session.strategy`](/configuration/options#session) option.
|
||||
|
||||
A single User can have multiple Sessions, each Session can only have one User.
|
||||
|
||||
:::tip
|
||||
When a Session is read, we check if it's `expires` field indicates an invalid session, and delete it from the database. You can also do this clean-up periodically in the background to avoid our extra delete call to the database during an active session retrieval. This might result in a slight performance increase in a few cases.
|
||||
:::
|
||||
|
||||
## Verification Token
|
||||
|
||||
The Verification Token model is used to store tokens for passwordless sign in.
|
||||
|
||||
A single User can have multiple open Verification Tokens (e.g. to sign in to different devices).
|
||||
|
||||
It has been designed to be extendable for other verification purposes in the future (e.g. 2FA / short codes).
|
||||
|
||||
:::note
|
||||
NextAuth.js makes sure that every token is usable only once, and by default has a short (1 day, can be configured by [`maxAge`](/configuration/providers/email#options)) lifetime. If your user did not manage to finish the sign-in flow in time, they will have to start the sign-in process again.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
Due to users forgetting or failing at the sign-in flow, you might end up with unwanted rows in your database, that you might have to periodically clean up to avoid filling the database up with unnecessary data.
|
||||
:::
|
||||
|
||||
## RDBMS Naming Convention
|
||||
|
||||
In the NextAuth.js v4 some schemas for the providers which support classic RDBMS type databases, like Prisma and TypeORM, have ended up with column names with mixed casing, i.e. snake_case and camelCase. If this is an issue for you or your underlying database system, please take a look at the "Naming Convention" section in the Prisma or TypeORM page.
|
||||
@@ -1,66 +0,0 @@
|
||||
---
|
||||
id: mongodb
|
||||
title: MongoDB
|
||||
---
|
||||
|
||||
# MongoDB
|
||||
|
||||
The MongoDB adapter does not handle connections automatically, so you will have to make sure that you pass the Adapter a `MongoClient` that is connected already. Below you can see an example how to do this.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Install the necessary packages
|
||||
|
||||
```bash npm2yarn2pnpm
|
||||
npm install next-auth @next-auth/mongodb-adapter mongodb
|
||||
```
|
||||
|
||||
2. Add `lib/mongodb.ts`
|
||||
|
||||
```ts
|
||||
// This approach is taken from https://github.com/vercel/next.js/tree/canary/examples/with-mongodb
|
||||
import { MongoClient } from 'mongodb'
|
||||
|
||||
if (!process.env.MONGODB_URI) {
|
||||
throw new Error('Invalid/Missing environment variable: "MONGODB_URI"')
|
||||
}
|
||||
|
||||
const uri = process.env.MONGODB_URI
|
||||
const options = {}
|
||||
|
||||
let client
|
||||
let clientPromise: Promise<MongoClient>
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// In development mode, use a global variable so that the value
|
||||
// is preserved across module reloads caused by HMR (Hot Module Replacement).
|
||||
if (!global._mongoClientPromise) {
|
||||
client = new MongoClient(uri, options)
|
||||
global._mongoClientPromise = client.connect()
|
||||
}
|
||||
clientPromise = global._mongoClientPromise
|
||||
} else {
|
||||
// In production mode, it's best to not use a global variable.
|
||||
client = new MongoClient(uri, options)
|
||||
clientPromise = client.connect()
|
||||
}
|
||||
|
||||
// Export a module-scoped MongoClient promise. By doing this in a
|
||||
// separate module, the client can be shared across functions.
|
||||
export default clientPromise
|
||||
```
|
||||
|
||||
3. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
```js
|
||||
import NextAuth from "next-auth"
|
||||
import { MongoDBAdapter } from "@next-auth/mongodb-adapter"
|
||||
import clientPromise from "../../../lib/mongodb"
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
export default NextAuth({
|
||||
adapter: MongoDBAdapter(clientPromise),
|
||||
...
|
||||
})
|
||||
```
|
||||
@@ -1,117 +0,0 @@
|
||||
---
|
||||
id: neo4j
|
||||
title: Neo4j
|
||||
---
|
||||
|
||||
# Neo4j
|
||||
|
||||
This is the Neo4j Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install the necessary packages
|
||||
|
||||
```bash npm2yarn2pnpm
|
||||
npm install next-auth @next-auth/neo4j-adapter neo4j-driver
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import neo4j from "neo4j-driver"
|
||||
import { Neo4jAdapter } from "@next-auth/neo4j-adapter"
|
||||
|
||||
const driver = neo4j.driver(
|
||||
"bolt://localhost",
|
||||
neo4j.auth.basic("neo4j", "password")
|
||||
)
|
||||
|
||||
const neo4jSession = driver.session()
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
export default NextAuth({
|
||||
// https://next-auth.js.org/configuration/providers
|
||||
providers: [],
|
||||
adapter: Neo4jAdapter(neo4jSession),
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
## Schema
|
||||
|
||||
### Node labels
|
||||
|
||||
The following node labels are used.
|
||||
|
||||
- User
|
||||
- Account
|
||||
- Session
|
||||
- VerificationToken
|
||||
|
||||
### Relationships
|
||||
|
||||
The following relationships and relationship labels are used.
|
||||
|
||||
- (:User)-[:HAS_ACCOUNT]->(:Account)
|
||||
- (:User)-[:HAS_SESSION]->(:Session)
|
||||
|
||||
### Properties
|
||||
|
||||
This schema is adapted for use in Neo4J and is based upon our main [models](/adapters/models). Please check there for the node properties. Relationships have no properties.
|
||||
|
||||
### Indexes
|
||||
|
||||
Optimum indexes will vary on your edition of Neo4j i.e. community or enterprise, and in case you have your own additional data on the nodes. Below are basic suggested indexes.
|
||||
|
||||
1. For **both** Community Edition & Enterprise Edition create constraints and indexes
|
||||
|
||||
```cypher
|
||||
|
||||
CREATE CONSTRAINT user_id_constraint IF NOT EXISTS
|
||||
ON (u:User) ASSERT u.id IS UNIQUE;
|
||||
|
||||
CREATE INDEX user_id_index IF NOT EXISTS
|
||||
FOR (u:User) ON (u.id);
|
||||
|
||||
CREATE INDEX user_email_index IF NOT EXISTS
|
||||
FOR (u:User) ON (u.email);
|
||||
|
||||
CREATE CONSTRAINT session_session_token_constraint IF NOT EXISTS
|
||||
ON (s:Session) ASSERT s.sessionToken IS UNIQUE;
|
||||
|
||||
CREATE INDEX session_session_token_index IF NOT EXISTS
|
||||
FOR (s:Session) ON (s.sessionToken);
|
||||
```
|
||||
|
||||
2.a. For Community Edition **only** create single-property indexes
|
||||
|
||||
```cypher
|
||||
CREATE INDEX account_provider_index IF NOT EXISTS
|
||||
FOR (a:Account) ON (a.provider);
|
||||
|
||||
CREATE INDEX account_provider_account_id_index IF NOT EXISTS
|
||||
FOR (a:Account) ON (a.providerAccountId);
|
||||
|
||||
CREATE INDEX verification_token_identifier_index IF NOT EXISTS
|
||||
FOR (v:VerificationToken) ON (v.identifier);
|
||||
|
||||
CREATE INDEX verification_token_token_index IF NOT EXISTS
|
||||
FOR (v:VerificationToken) ON (v.token);
|
||||
```
|
||||
|
||||
2.b. For Enterprise Edition **only** create composite node key constraints and indexes
|
||||
|
||||
```cypher
|
||||
CREATE CONSTRAINT account_provider_composite_constraint IF NOT EXISTS
|
||||
ON (a:Account) ASSERT (a.provider, a.providerAccountId) IS NODE KEY;
|
||||
|
||||
CREATE INDEX account_provider_composite_index IF NOT EXISTS
|
||||
FOR (a:Account) ON (a.provider, a.providerAccountId);
|
||||
|
||||
CREATE CONSTRAINT verification_token_composite_constraint IF NOT EXISTS
|
||||
ON (v:VerificationToken) ASSERT (v.identifier, v.token) IS NODE KEY;
|
||||
|
||||
CREATE INDEX verification_token_composite_index IF NOT EXISTS
|
||||
FOR (v:VerificationToken) ON (v.identifier, v.token);
|
||||
```
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
id: overview
|
||||
title: Overview
|
||||
---
|
||||
|
||||
An **Adapter** in NextAuth.js connects your application to whatever database or backend system you want to use to store data for users, their accounts, sessions, etc. Adapters are optional, unless you need to persist user information in your own database, or you want to implement certain flows. The [Email Provider](/providers/email) requires an adapter to be able to save [Verification Tokens](/adapters/models#verification-token).
|
||||
|
||||
:::tip
|
||||
When using a database, you can still use JWT for session handling for fast access. See the [`session.strategy`](/configuration/options#session) option. Read about the trade-offs of JWT in the [FAQ](/faq#json-web-tokens).
|
||||
:::
|
||||
|
||||
We have a list of official adapters that are distributed as their own packages under the `@next-auth/{name}-adapter` namespace. Their source code is available in their various adapters package directories at [`nextauthjs/next-auth`](https://github.com/nextauthjs/next-auth/tree/main/packages).
|
||||
|
||||
- [`xata`](./xata)
|
||||
- [`prisma`](./prisma)
|
||||
- [`fauna`](./fauna)
|
||||
- [`dynamodb`](./dynamodb)
|
||||
- [`firebase`](./firebase)
|
||||
- [`pouchdb`](./pouchdb)
|
||||
- [`mongodb`](./mongodb)
|
||||
- [`neo4j`](./neo4j)
|
||||
- [`typeorm-legacy`](./typeorm)
|
||||
- [`sequelize`](./sequelize)
|
||||
- [`supabase`](./supabase)
|
||||
- [`dgraph`](./dgraph)
|
||||
- [`upstash-redis`](./upstash-redis)
|
||||
|
||||
## Custom Adapter
|
||||
|
||||
If you have a database/backend that we don't officially support, you can create your own adapter.
|
||||
See the tutorial for [creating a database Adapter](/tutorials/creating-a-database-adapter) for more information.
|
||||
|
||||
:::tip
|
||||
If you would like to see a new adapter in the official repository, please [open a PR](https://github.com/nextauthjs/next-auth/issues/new) and we will help you to get it merged. Tell us if you are interested in becoming one of the maintainers of any of the official adapters.
|
||||
:::
|
||||
|
||||
### Editor integration
|
||||
|
||||
Adapters are strongly typed, and they rely on the single `Adapter` interface imported from `next-auth/adapters`.
|
||||
|
||||
When writing your own custom Adapter in plain JavaScript, note that you can use **JSDoc** to get helpful editor hints and auto-completion like so:
|
||||
|
||||
```js
|
||||
/** @return { import("next-auth/adapters").Adapter } */
|
||||
function MyAdapter() {
|
||||
return {
|
||||
// your adapter methods here
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::note
|
||||
This will work in code editors with a strong TypeScript integration like VSCode or WebStorm. It might not work if you're using more lightweight editors like VIM or Atom.
|
||||
:::
|
||||
@@ -1,65 +0,0 @@
|
||||
---
|
||||
id: pouchdb
|
||||
title: PouchDB
|
||||
---
|
||||
|
||||
# PouchDB
|
||||
|
||||
:::warning
|
||||
This adapter is still experimental and does not work with NextAuth.js 4 or newer. If you would like to help out upgrading it, please [open a PR](https://github.com/nextauthjs/next-auth/tree/main/packages)
|
||||
:::
|
||||
|
||||
This is the PouchDB Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
|
||||
|
||||
Depending on your architecture you can use PouchDB's http adapter to reach any database compliant with the CouchDB protocol (CouchDB, Cloudant, ...) or use any other PouchDB compatible adapter (leveldb, in-memory, ...)
|
||||
|
||||
## Getting Started
|
||||
|
||||
> **Prerequisites**: Your PouchDB instance MUST provide the `pouchdb-find` plugin since it is used internally by the adapter to build and manage indexes
|
||||
|
||||
1. Install `next-auth` and `@next-auth/pouchdb-adapter`
|
||||
|
||||
```bash npm2yarn2pnpm
|
||||
npm install next-auth @next-auth/pouchdb-adapter
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import GoogleProvider from "next-auth/providers/google"
|
||||
import { PouchDBAdapter } from "@next-auth/pouchdb-adapter"
|
||||
import PouchDB from "pouchdb"
|
||||
|
||||
// Setup your PouchDB instance and database
|
||||
PouchDB.plugin(require("pouchdb-adapter-leveldb")) // Any other adapter
|
||||
.plugin(require("pouchdb-find")) // Don't forget the `pouchdb-find` plugin
|
||||
|
||||
const pouchdb = new PouchDB("auth_db", { adapter: "leveldb" })
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
export default NextAuth({
|
||||
// https://next-auth.js.org/providers/overview
|
||||
providers: [
|
||||
GoogleProvider({
|
||||
clientId: process.env.GOOGLE_ID,
|
||||
clientSecret: process.env.GOOGLE_SECRET,
|
||||
}),
|
||||
],
|
||||
adapter: PouchDBAdapter(pouchdb),
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
## Advanced
|
||||
|
||||
### Memory-First Caching Strategy
|
||||
|
||||
If you need to boost your authentication layer performance, you may use PouchDB's powerful sync features and various adapters, to build a memory-first caching strategy.
|
||||
|
||||
Use an in-memory PouchDB as your main authentication database, and synchronize it with any other persisted PouchDB. You may do a one way, one-off replication at startup from the persisted PouchDB into the in-memory PouchDB, then two-way, continuous, retriable sync.
|
||||
|
||||
This will most likely not increase performance much in a serverless environment due to various reasons such as concurrency, function startup time increases, etc.
|
||||
|
||||
For more details, please see https://pouchdb.com/api.html#sync
|
||||
@@ -1,226 +0,0 @@
|
||||
---
|
||||
id: prisma
|
||||
title: Prisma
|
||||
---
|
||||
|
||||
# Prisma
|
||||
|
||||
To use this Adapter, you need to install Prisma Client, Prisma CLI, and the separate `@next-auth/prisma-adapter` package:
|
||||
|
||||
```bash npm2yarn2pnpm
|
||||
npm install next-auth @prisma/client @next-auth/prisma-adapter
|
||||
npm install prisma --save-dev
|
||||
```
|
||||
|
||||
Create a file with your Prisma Client:
|
||||
|
||||
```typescript title="lib/prismadb.ts"
|
||||
import { PrismaClient } from "@prisma/client"
|
||||
|
||||
declare global {
|
||||
var prisma: PrismaClient | undefined
|
||||
}
|
||||
|
||||
const client = globalThis.prisma || new PrismaClient()
|
||||
if (process.env.NODE_ENV !== "production") globalThis.prisma = client
|
||||
|
||||
export default client
|
||||
```
|
||||
|
||||
Configure your NextAuth.js to use the Prisma Adapter:
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import GoogleProvider from "next-auth/providers/google"
|
||||
import { PrismaAdapter } from "@next-auth/prisma-adapter"
|
||||
import prisma from "../../../lib/prismadb"
|
||||
|
||||
export default NextAuth({
|
||||
adapter: PrismaAdapter(prisma),
|
||||
providers: [
|
||||
GoogleProvider({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
}),
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
Schema for the Prisma Adapter (`@next-auth/prisma-adapter`)
|
||||
|
||||
## Setup
|
||||
|
||||
### Create the Prisma schema
|
||||
|
||||
You need to use at least Prisma 2.26.0. Create a schema file in `prisma/schema.prisma` similar to this one:
|
||||
|
||||
> This schema is adapted for use in Prisma and based upon our main [schema](/adapters/models)
|
||||
|
||||
```json title="schema.prisma"
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
shadowDatabaseUrl = env("SHADOW_DATABASE_URL") // Only needed when using a cloud provider that doesn't support the creation of new databases, like Heroku. Learn more: https://pris.ly/migrate-shadow
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["referentialActions"] // You won't need this in Prisma 3.X or higher.
|
||||
}
|
||||
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
type String
|
||||
provider String
|
||||
providerAccountId String
|
||||
refresh_token String? @db.Text
|
||||
access_token String? @db.Text
|
||||
expires_at Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String? @db.Text
|
||||
session_state String?
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([provider, providerAccountId])
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
sessionToken String @unique
|
||||
userId String
|
||||
expires DateTime
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime?
|
||||
image String?
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
}
|
||||
|
||||
model VerificationToken {
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
|
||||
@@unique([identifier, token])
|
||||
}
|
||||
```
|
||||
|
||||
:::note
|
||||
When using the MySQL connector for Prisma, the [Prisma `String` type](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string) gets mapped to `varchar(191)` which may not be long enough to store fields such as `id_token` in the `Account` model. This can be avoided by explicitly using the `Text` type with `@db.Text`.
|
||||
:::
|
||||
|
||||
### Create the database schema with Prisma Migrate
|
||||
|
||||
**Warning:** Make sure to back up your database before running using Prisma Migrate.
|
||||
|
||||
```
|
||||
npx prisma migrate dev
|
||||
```
|
||||
|
||||
This will create an SQL migration file and execute it.
|
||||
|
||||
Note that you will need to specify your database connection string in the environment variable `DATABASE_URL`. You can do this by setting it in a `.env` file at the root of your project.
|
||||
|
||||
To learn more about [Prisma Migrate](https://www.prisma.io/migrate), check out the [Migrate docs](https://www.prisma.io/docs/concepts/components/prisma-migrate).
|
||||
|
||||
### Generate Client
|
||||
|
||||
Once you have saved your schema, use the Prisma CLI to generate the Prisma Client:
|
||||
|
||||
```
|
||||
npx prisma generate
|
||||
```
|
||||
|
||||
To configure your database to use the new schema (i.e. create tables and columns) use the `prisma migrate` command:
|
||||
|
||||
```
|
||||
npx prisma migrate dev
|
||||
```
|
||||
|
||||
### MongoDB
|
||||
|
||||
Prisma supports MongoDB, and so does NextAuth.js. Following the instructions of the [Prisma documentation](https://www.prisma.io/docs/concepts/database-connectors/mongodb) on the MongoDB connector, things you have to change are:
|
||||
|
||||
1. Make sure that the id fields are mapped correctly
|
||||
|
||||
```prisma
|
||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||
```
|
||||
|
||||
2. The Native database type attribute to `@db.String` from `@db.Text`.
|
||||
|
||||
```prisma
|
||||
refresh_token String? @db.String
|
||||
access_token String? @db.String
|
||||
id_token String? @db.String
|
||||
```
|
||||
|
||||
Everything else should be the same.
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
If mixed snake_case and camelCase column names is an issue for you and/or your underlying database system, we recommend using Prisma's `@map()`([see the documentation here](https://www.prisma.io/docs/concepts/components/prisma-schema/names-in-underlying-database)) feature to change the field names. This won't affect NextAuth.js, but will allow you to customize the column names to whichever naming convention you wish.
|
||||
|
||||
For example, moving to `snake_case` and plural table names.
|
||||
|
||||
```json title="schema.prisma"
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
userId String @map("user_id")
|
||||
type String
|
||||
provider String
|
||||
providerAccountId String @map("provider_account_id")
|
||||
refresh_token String? @db.Text
|
||||
access_token String? @db.Text
|
||||
expires_at Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String? @db.Text
|
||||
session_state String?
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([provider, providerAccountId])
|
||||
@@map("accounts")
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
sessionToken String @unique @map("session_token")
|
||||
userId String @map("user_id")
|
||||
expires DateTime
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("sessions")
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime? @map("email_verified")
|
||||
image String?
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model VerificationToken {
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
|
||||
@@unique([identifier, token])
|
||||
@@map("verificationtokens")
|
||||
}
|
||||
```
|
||||
@@ -1,88 +0,0 @@
|
||||
---
|
||||
id: sequelize
|
||||
title: Sequelize
|
||||
---
|
||||
|
||||
# Sequelize
|
||||
|
||||
This is the Sequelize Adapter for [`next-auth`](https://next-auth.js.org).
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install the necessary packages
|
||||
|
||||
```bash npm2yarn2pnpm
|
||||
npm install next-auth @next-auth/sequelize-adapter sequelize
|
||||
```
|
||||
|
||||
:::warning
|
||||
You'll also have to manually install [the driver for your database](https://sequelize.org/master/manual/getting-started.html) of choice.
|
||||
:::
|
||||
|
||||
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import SequelizeAdapter from "@next-auth/sequelize-adapter"
|
||||
import { Sequelize } from "sequelize"
|
||||
|
||||
// https://sequelize.org/master/manual/getting-started.html#connecting-to-a-database
|
||||
const sequelize = new Sequelize("yourconnectionstring")
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
export default NextAuth({
|
||||
// https://next-auth.js.org/providers/overview
|
||||
providers: [],
|
||||
adapter: SequelizeAdapter(sequelize),
|
||||
})
|
||||
```
|
||||
|
||||
## Updating the database schema
|
||||
|
||||
By default, the sequelize adapter will not create tables in your database. In production, best practice is to create the [required tables](https://next-auth.js.org/adapters/models) in your database via [migrations](https://sequelize.org/master/manual/migrations.html). In development, you are able to call [`sequelize.sync()`](https://sequelize.org/master/manual/model-basics.html#model-synchronization) to have sequelize create the necessary tables, foreign keys and indexes:
|
||||
|
||||
> This schema is adapted for use in Sequelize and based upon our main [schema](/adapters/models)
|
||||
|
||||
```js
|
||||
import NextAuth from "next-auth"
|
||||
import SequelizeAdapter from "@next-auth/sequelize-adapter"
|
||||
import Sequelize from 'sequelize'
|
||||
|
||||
const sequelize = new Sequelize("sqlite::memory:")
|
||||
const adapter = SequelizeAdapter(sequelize)
|
||||
|
||||
// Calling sync() is not recommended in production
|
||||
sequelize.sync()
|
||||
|
||||
export default NextAuth({
|
||||
...
|
||||
adapter
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
## Using custom models
|
||||
|
||||
Sequelize models are option to customization like so:
|
||||
|
||||
```js
|
||||
import NextAuth from "next-auth"
|
||||
import SequelizeAdapter, { models } from "@next-auth/sequelize-adapter"
|
||||
import Sequelize, { DataTypes } from "sequelize"
|
||||
|
||||
const sequelize = new Sequelize("sqlite::memory:")
|
||||
|
||||
export default NextAuth({
|
||||
// https://next-auth.js.org/providers/overview
|
||||
providers: [],
|
||||
adapter: SequelizeAdapter(sequelize, {
|
||||
models: {
|
||||
User: sequelize.define("user", {
|
||||
...models.User,
|
||||
phoneNumber: DataTypes.STRING,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
})
|
||||
```
|
||||
@@ -1,309 +0,0 @@
|
||||
---
|
||||
id: supabase
|
||||
title: Supabase
|
||||
---
|
||||
|
||||
# Supabase
|
||||
|
||||
This is the Supabase Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
|
||||
|
||||
:::note
|
||||
This adapter is developed by the community and not officially maintained or supported by Supabase. It uses the Supabase Database to store user and session data in a separate `next_auth` schema. It is a standalone Auth server that does not interface with Supabase Auth and therefore provides a different feature set.
|
||||
|
||||
If you’re looking for an officially maintained Auth server with additional features like [built-in email server](https://supabase.com/docs/guides/auth/auth-email#configure-email-settings?utm_source=next-auth-docs&medium=referral&campaign=next-auth), [phone auth](https://supabase.com/docs/guides/auth/auth-twilio?utm_source=next-auth-docs&medium=referral&campaign=next-auth), and [Multi Factor Authentication (MFA / 2FA)](https://supabase.com/contact/mfa?utm_source=next-auth-docs&medium=referral&campaign=next-auth), please use [Supabase Auth](https://supabase.com/auth) with the [Auth Helpers for Next.js](https://supabase.com/docs/guides/auth/auth-helpers/nextjs?utm_source=next-auth-docs&medium=referral&campaign=next-auth).
|
||||
:::
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install `@supabase/supabase-js`, `next-auth` and `@next-auth/supabase-adapter`.
|
||||
|
||||
```bash npm2yarn2pnpm
|
||||
npm install @supabase/supabase-js next-auth @next-auth/supabase-adapter
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import { SupabaseAdapter } from "@next-auth/supabase-adapter"
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
export default NextAuth({
|
||||
// https://next-auth.js.org/configuration/providers
|
||||
providers: [...],
|
||||
adapter: SupabaseAdapter({
|
||||
url: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
secret: process.env.SUPABASE_SERVICE_ROLE_KEY,
|
||||
}),
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
### Create the `next_auth` schema in Supabase
|
||||
|
||||
Setup your database as described in our main [schema](/adapters/models), by copying the SQL schema below in the Supabase [SQL Editor](https://app.supabase.com/project/_/sql).
|
||||
|
||||
Alternatively you can select the NextAuth Quickstart card on the [SQL Editor page](https://app.supabase.com/project/_/sql), or [create a migration with the Supabase CLI](https://supabase.com/docs/guides/cli/local-development#database-migrations?utm_source=next-auth-docs&medium=referral&campaign=next-auth).
|
||||
|
||||
```sql
|
||||
--
|
||||
-- Name: next_auth; Type: SCHEMA;
|
||||
--
|
||||
CREATE SCHEMA next_auth;
|
||||
|
||||
GRANT USAGE ON SCHEMA next_auth TO service_role;
|
||||
GRANT ALL ON SCHEMA next_auth TO postgres;
|
||||
|
||||
--
|
||||
-- Create users table
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS next_auth.users
|
||||
(
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
name text,
|
||||
email text,
|
||||
"emailVerified" timestamp with time zone,
|
||||
image text,
|
||||
CONSTRAINT users_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT email_unique UNIQUE (email)
|
||||
);
|
||||
|
||||
GRANT ALL ON TABLE next_auth.users TO postgres;
|
||||
GRANT ALL ON TABLE next_auth.users TO service_role;
|
||||
|
||||
--- uid() function to be used in RLS policies
|
||||
CREATE FUNCTION next_auth.uid() RETURNS uuid
|
||||
LANGUAGE sql STABLE
|
||||
AS $$
|
||||
select
|
||||
coalesce(
|
||||
nullif(current_setting('request.jwt.claim.sub', true), ''),
|
||||
(nullif(current_setting('request.jwt.claims', true), '')::jsonb ->> 'sub')
|
||||
)::uuid
|
||||
$$;
|
||||
|
||||
--
|
||||
-- Create sessions table
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS next_auth.sessions
|
||||
(
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
expires timestamp with time zone NOT NULL,
|
||||
"sessionToken" text NOT NULL,
|
||||
"userId" uuid,
|
||||
CONSTRAINT sessions_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT sessionToken_unique UNIQUE ("sessionToken"),
|
||||
CONSTRAINT "sessions_userId_fkey" FOREIGN KEY ("userId")
|
||||
REFERENCES next_auth.users (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
GRANT ALL ON TABLE next_auth.sessions TO postgres;
|
||||
GRANT ALL ON TABLE next_auth.sessions TO service_role;
|
||||
|
||||
--
|
||||
-- Create accounts table
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS next_auth.accounts
|
||||
(
|
||||
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
type text NOT NULL,
|
||||
provider text NOT NULL,
|
||||
"providerAccountId" text NOT NULL,
|
||||
refresh_token text,
|
||||
access_token text,
|
||||
expires_at bigint,
|
||||
token_type text,
|
||||
scope text,
|
||||
id_token text,
|
||||
session_state text,
|
||||
oauth_token_secret text,
|
||||
oauth_token text,
|
||||
"userId" uuid,
|
||||
CONSTRAINT accounts_pkey PRIMARY KEY (id),
|
||||
CONSTRAINT provider_unique UNIQUE (provider, "providerAccountId"),
|
||||
CONSTRAINT "accounts_userId_fkey" FOREIGN KEY ("userId")
|
||||
REFERENCES next_auth.users (id) MATCH SIMPLE
|
||||
ON UPDATE NO ACTION
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
GRANT ALL ON TABLE next_auth.accounts TO postgres;
|
||||
GRANT ALL ON TABLE next_auth.accounts TO service_role;
|
||||
|
||||
--
|
||||
-- Create verification_tokens table
|
||||
--
|
||||
CREATE TABLE IF NOT EXISTS next_auth.verification_tokens
|
||||
(
|
||||
identifier text,
|
||||
token text,
|
||||
expires timestamp with time zone NOT NULL,
|
||||
CONSTRAINT verification_tokens_pkey PRIMARY KEY (token),
|
||||
CONSTRAINT token_unique UNIQUE (token),
|
||||
CONSTRAINT token_identifier_unique UNIQUE (token, identifier)
|
||||
);
|
||||
|
||||
GRANT ALL ON TABLE next_auth.verification_tokens TO postgres;
|
||||
GRANT ALL ON TABLE next_auth.verification_tokens TO service_role;
|
||||
```
|
||||
|
||||
### Expose the `next_auth` schema in Supabase
|
||||
|
||||
Expose the `next_auth` schema via the Serverless API in the [API settings](https://app.supabase.com/project/_/settings/api) by adding `next_auth` to the "Exposed schemas" list.
|
||||
|
||||
When developing locally add `next_auth` to the `schemas` array in the `config.toml` file in the `supabase` folder that was generated by the [Supabase CLI](https://supabase.com/docs/guides/cli/local-development#initialize-your-project?utm_source=next-auth-docs&medium=referral&campaign=next-auth).
|
||||
|
||||
## Enabling Row Level Security (RLS)
|
||||
|
||||
Postgres provides a powerful feature called [Row Level Security (RLS)](https://supabase.com/docs/guides/auth/row-level-security?utm_source=next-auth-docs&medium=referral&campaign=next-auth) to limit access to data.
|
||||
|
||||
This works by sending a signed JWT to your [Supabase Serverless API](https://supabase.com/docs/guides/api?utm_source=next-auth-docs&medium=referral&campaign=next-auth). There is two steps to make this work with NextAuth:
|
||||
|
||||
### 1. Generate the Supabase `access_token` JWT in the session callback
|
||||
|
||||
To sign the JWT use the `jsonwebtoken` package:
|
||||
|
||||
```bash npm2yarn2pnpm
|
||||
npm install jsonwebtoken
|
||||
```
|
||||
|
||||
Using the [NexthAuth Session callback](https://next-auth.js.org/configuration/callbacks#session-callback) create the Supabase `access_token` and append it to the `session` object.
|
||||
|
||||
To sign the JWT use the Supabase JWT secret which can be found in the [API settings](https://app.supabase.com/project/_/settings/api)
|
||||
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import { SupabaseAdapter } from "@next-auth/supabase-adapter"
|
||||
import jwt from "jsonwebtoken"
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
export default NextAuth({
|
||||
// https://next-auth.js.org/configuration/providers
|
||||
providers: [...],
|
||||
adapter: SupabaseAdapter({
|
||||
url: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
secret: process.env.SUPABASE_SERVICE_ROLE_KEY,
|
||||
}),
|
||||
callbacks: {
|
||||
async session({ session, user }) {
|
||||
const signingSecret = process.env.SUPABASE_JWT_SECRET
|
||||
if (signingSecret) {
|
||||
const payload = {
|
||||
aud: "authenticated",
|
||||
exp: Math.floor(new Date(session.expires).getTime() / 1000),
|
||||
sub: user.id,
|
||||
email: user.email,
|
||||
role: "authenticated",
|
||||
}
|
||||
session.supabaseAccessToken = jwt.sign(payload, signingSecret)
|
||||
}
|
||||
return session
|
||||
},
|
||||
},
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
### 2. Inject the Supabase `access_token` JWT into the Supabase Client
|
||||
|
||||
For example, given the following public schema:
|
||||
|
||||
```sql
|
||||
/**
|
||||
* USERS
|
||||
* Note: This table contains user data. Users should only be able to view and update their own data.
|
||||
*/
|
||||
create table users (
|
||||
-- UUID from next_auth.users
|
||||
id uuid not null primary key,
|
||||
name text,
|
||||
email text,
|
||||
image text,
|
||||
constraint "users_id_fkey" foreign key ("id")
|
||||
references next_auth.users (id) match simple
|
||||
on update no action
|
||||
on delete cascade -- if user is deleted in NextAuth they will also be deleted in our public table.
|
||||
);
|
||||
alter table users enable row level security;
|
||||
create policy "Can view own user data." on users for select using (next_auth.uid() = id);
|
||||
create policy "Can update own user data." on users for update using (next_auth.uid() = id);
|
||||
|
||||
/**
|
||||
* This trigger automatically creates a user entry when a new user signs up via NextAuth.
|
||||
*/
|
||||
create function public.handle_new_user()
|
||||
returns trigger as $$
|
||||
begin
|
||||
insert into public.users (id, name, email, image)
|
||||
values (new.id, new.name, new.email, new.image);
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql security definer;
|
||||
create trigger on_auth_user_created
|
||||
after insert on next_auth.users
|
||||
for each row execute procedure public.handle_new_user();
|
||||
```
|
||||
|
||||
The `supabaseAccessToken` is now available on the `session` object and can be passed to the supabase-js client. This works in any environment: client-side, server-side (API routes, SSR), as well as in middleware edge functions!
|
||||
|
||||
```js
|
||||
// ...
|
||||
// Use `useSession()` or `getServerSession()` to get the NextAuth session.
|
||||
|
||||
const { supabaseAccessToken } = session
|
||||
|
||||
const supabase = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
|
||||
{
|
||||
global: {
|
||||
headers: {
|
||||
Authorization: `Bearer ${supabaseAccessToken}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
// Now you can query with RLS enabled.
|
||||
const { data, error } = await supabase.from("users").select("*")
|
||||
```
|
||||
|
||||
## Usage with TypeScript
|
||||
|
||||
You can pass types that were [generated with the Supabase CLI](https://supabase.com/docs/reference/javascript/typescript-support#generating-types) to the Supabase Client to get enhanced type safety and auto completion.
|
||||
|
||||
Creating a new supabase client object:
|
||||
|
||||
```tsx
|
||||
import { createClient } from "@supabase/supabase-js"
|
||||
import { Database } from "../database.types"
|
||||
|
||||
const supabase = createClient<Database>()
|
||||
```
|
||||
|
||||
### Extend the session type with the `supabaseAccessToken`
|
||||
|
||||
In order to extend the `session` object with the `supabaseAccessToken` we need to extend the `session` interface in a `types/next-auth.d.ts` file:
|
||||
|
||||
```ts title="types/next-auth.d.ts"
|
||||
import NextAuth, { DefaultSession } from "next-auth"
|
||||
|
||||
declare module "next-auth" {
|
||||
/**
|
||||
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
|
||||
*/
|
||||
interface Session {
|
||||
// A JWT which can be used as Authorization header with supabase-js for RLS.
|
||||
supabaseAccessToken?: string
|
||||
user: {
|
||||
/** The user's postal address. */
|
||||
address: string
|
||||
} & DefaultSession["user"]
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,237 +0,0 @@
|
||||
---
|
||||
id: typeorm
|
||||
title: TypeORM
|
||||
---
|
||||
|
||||
# TypeORM
|
||||
|
||||
This Adapter is used to support SQL-flavored databases (like SQLite, MySQL, MSSQL, MariaDB, CockroachDB, etc.) through [TypeORM](https://typeorm.io).
|
||||
|
||||
:::note
|
||||
If you previously used this Adapter with MongoDB, check out the [MongoDB Adapter](/adapters/mongodb) instead.
|
||||
:::
|
||||
|
||||
:::note
|
||||
In the future, we might split up this adapter to support single flavors of SQL for easier maintenance and reduced bundle size.
|
||||
:::
|
||||
|
||||
## Usage
|
||||
|
||||
:::warning
|
||||
[`typeorm`](https://github.com/typeorm/typeorm) is still in active development and has not yet published a stable release. Because of this, you can expect breaking changes in minor versions. This adapter expects `typeorm@0.3.7` and is not validated against previous or future releases.
|
||||
:::
|
||||
|
||||
To use this Adapter, you need to install the following packages:
|
||||
|
||||
```bash npm2yarn2pnpm
|
||||
npm install next-auth @next-auth/typeorm-legacy-adapter typeorm
|
||||
```
|
||||
|
||||
Configure your NextAuth.js to use the TypeORM Adapter:
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import { TypeORMLegacyAdapter } from "@next-auth/typeorm-legacy-adapter"
|
||||
|
||||
|
||||
export default NextAuth({
|
||||
adapter: TypeORMLegacyAdapter("yourconnectionstring"),
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
`TypeORMLegacyAdapter` takes either a connection string, or a [`DataSourceOptions`](https://github.com/typeorm/typeorm/blob/master/docs/data-source-options.md) object as its first parameter.
|
||||
|
||||
## Custom models
|
||||
|
||||
The TypeORM adapter uses [`Entity` classes](https://github.com/typeorm/typeorm/blob/master/docs/entities.md) to define the shape of your data.
|
||||
|
||||
If you want to override the default entities (for example to add a `role` field to your `UserEntity`), you will have to do the following:
|
||||
|
||||
> This schema is adapted for use in TypeORM and based upon our main [schema](/adapters/models)
|
||||
|
||||
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-legacy/src/entities.ts))
|
||||
|
||||
```diff title="lib/entities.ts"
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
ValueTransformer,
|
||||
} from "typeorm"
|
||||
|
||||
const transformer: Record<"date" | "bigint", ValueTransformer> = {
|
||||
date: {
|
||||
from: (date: string | null) => date && new Date(parseInt(date, 10)),
|
||||
to: (date?: Date) => date?.valueOf().toString(),
|
||||
},
|
||||
bigint: {
|
||||
from: (bigInt: string | null) => bigInt && parseInt(bigInt, 10),
|
||||
to: (bigInt?: number) => bigInt?.toString(),
|
||||
},
|
||||
}
|
||||
|
||||
@Entity({ name: "users" })
|
||||
export class UserEntity {
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id!: string
|
||||
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
name!: string | null
|
||||
|
||||
@Column({ type: "varchar", nullable: true, unique: true })
|
||||
email!: string | null
|
||||
|
||||
@Column({ type: "varchar", nullable: true, transformer: transformer.date })
|
||||
emailVerified!: string | null
|
||||
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
image!: string | null
|
||||
|
||||
+ @Column({ type: "varchar", nullable: true })
|
||||
+ role!: string | null
|
||||
|
||||
@OneToMany(() => SessionEntity, (session) => session.userId)
|
||||
sessions!: SessionEntity[]
|
||||
|
||||
@OneToMany(() => AccountEntity, (account) => account.userId)
|
||||
accounts!: AccountEntity[]
|
||||
}
|
||||
|
||||
@Entity({ name: "accounts" })
|
||||
export class AccountEntity {
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id!: string
|
||||
|
||||
@Column({ type: "uuid" })
|
||||
userId!: string
|
||||
|
||||
@Column()
|
||||
type!: string
|
||||
|
||||
@Column()
|
||||
provider!: string
|
||||
|
||||
@Column()
|
||||
providerAccountId!: string
|
||||
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
refresh_token!: string | null
|
||||
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
access_token!: string | null
|
||||
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: "bigint",
|
||||
transformer: transformer.bigint,
|
||||
})
|
||||
expires_at!: number | null
|
||||
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
token_type!: string | null
|
||||
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
scope!: string | null
|
||||
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
id_token!: string | null
|
||||
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
session_state!: string | null
|
||||
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
oauth_token_secret!: string | null
|
||||
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
oauth_token!: string | null
|
||||
|
||||
@ManyToOne(() => UserEntity, (user) => user.accounts, {
|
||||
createForeignKeyConstraints: true,
|
||||
})
|
||||
user!: UserEntity
|
||||
}
|
||||
|
||||
@Entity({ name: "sessions" })
|
||||
export class SessionEntity {
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id!: string
|
||||
|
||||
@Column({ unique: true })
|
||||
sessionToken!: string
|
||||
|
||||
@Column({ type: "uuid" })
|
||||
userId!: string
|
||||
|
||||
@Column({ transformer: transformer.date })
|
||||
expires!: string
|
||||
|
||||
@ManyToOne(() => UserEntity, (user) => user.sessions)
|
||||
user!: UserEntity
|
||||
}
|
||||
|
||||
@Entity({ name: "verification_tokens" })
|
||||
export class VerificationTokenEntity {
|
||||
@PrimaryGeneratedColumn("uuid")
|
||||
id!: string
|
||||
|
||||
@Column()
|
||||
token!: string
|
||||
|
||||
@Column()
|
||||
identifier!: string
|
||||
|
||||
@Column({ transformer: transformer.date })
|
||||
expires!: string
|
||||
}
|
||||
```
|
||||
|
||||
2. Pass them to `TypeORMLegacyAdapter`
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import { TypeORMLegacyAdapter } from "@next-auth/typeorm-legacy-adapter"
|
||||
import * as entities from "lib/entities"
|
||||
|
||||
export default NextAuth({
|
||||
adapter: TypeORMLegacyAdapter("yourconnectionstring", { entities }),
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
:::tip Synchronize your database ♻
|
||||
The `synchronize: true` option in TypeORM will generate SQL that exactly matches the entities. This will automatically apply any changes it finds in the entity model. This is a useful option in development.
|
||||
:::
|
||||
|
||||
:::warning Using synchronize in production
|
||||
`synchronize: true` should not be enabled against production databases as it may cause data loss if the configured schema does not match the expected schema! We recommend that you synchronize/migrate your production database at build-time.
|
||||
:::
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
If mixed snake_case and camelCase column names are an issue for you and/or your underlying database system, we recommend using TypeORM's naming strategy feature to change the target field names. There is a package called `typeorm-naming-strategies` which includes a `snake_case` strategy which will translate the fields from how NextAuth.js expects them, to snake_case in the actual database.
|
||||
|
||||
For example, you can add the naming convention option to the connection object in your NextAuth config.
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import { TypeORMLegacyAdapter } from "@next-auth/typeorm-legacy-adapter"
|
||||
import { SnakeNamingStrategy } from 'typeorm-naming-strategies'
|
||||
|
||||
export default NextAuth({
|
||||
adapter: TypeORMLegacyAdapter({
|
||||
type: "mysql",
|
||||
host: "localhost",
|
||||
port: 3306,
|
||||
username: "test",
|
||||
password: "test",
|
||||
database: "test",
|
||||
namingStrategy: new SnakeNamingStrategy()
|
||||
}),
|
||||
...
|
||||
})
|
||||
```
|
||||
@@ -1,69 +0,0 @@
|
||||
---
|
||||
id: upstash-redis
|
||||
title: Upstash Redis
|
||||
---
|
||||
|
||||
# Upstash Redis
|
||||
|
||||
To use this Adapter, you need to install `@upstash/redis` and `@next-auth/upstash-redis-adapter` package:
|
||||
|
||||
```bash npm2yarn2pnpm
|
||||
npm install @upstash/redis @next-auth/upstash-redis-adapter
|
||||
```
|
||||
|
||||
Configure your NextAuth.js to use the Upstash Redis Adapter:
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import GoogleProvider from "next-auth/providers/google"
|
||||
import { UpstashRedisAdapter } from "@next-auth/upstash-redis-adapter"
|
||||
import { Redis } from "@upstash/redis"
|
||||
|
||||
const redis = new Redis({
|
||||
url: process.env.UPSTASH_REDIS_URL,
|
||||
token: process.env.UPSTASH_REDIS_TOKEN
|
||||
})
|
||||
|
||||
export default NextAuth({
|
||||
adapter: UpstashRedisAdapter(redis),
|
||||
providers: [
|
||||
GoogleProvider({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
}),
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
## Using Multiple Apps with a Single Upstash Redis Instance
|
||||
|
||||
The Upstash free-tier allows for only one Redis instance. If you have multiple Next-Auth connected apps using this instance, you need different key prefixes for every app.
|
||||
|
||||
You can change the prefixes by passing an `options` object as the second argument to the adapter factory function.
|
||||
|
||||
The default values for this object are:
|
||||
|
||||
```js
|
||||
const defaultOptions = {
|
||||
baseKeyPrefix: "",
|
||||
accountKeyPrefix: "user:account:",
|
||||
accountByUserIdPrefix: "user:account:by-user-id:",
|
||||
emailKeyPrefix: "user:email:",
|
||||
sessionKeyPrefix: "user:session:",
|
||||
sessionByUserIdKeyPrefix: "user:session:by-user-id:",
|
||||
userKeyPrefix: "user:",
|
||||
verificationTokenKeyPrefix: "user:token:",
|
||||
}
|
||||
```
|
||||
|
||||
Usually changing the `baseKeyPrefix` should be enough for this scenario, but for more custom setups, you can also change the prefixes of every single key.
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
export default NextAuth({
|
||||
...
|
||||
adapter: UpstashRedisAdapter(redis, {baseKeyPrefix: "app2:"})
|
||||
...
|
||||
})
|
||||
```
|
||||
@@ -1,242 +0,0 @@
|
||||
---
|
||||
id: xata
|
||||
title: Xata
|
||||
---
|
||||
|
||||
# Xata
|
||||
|
||||
This adapter allows using next-auth with Xata as a database to store users, sessions, and more. The preferred way to create a Xata project and use Xata databases is using the [Xata Command Line Interface (CLI)](https://docs.xata.io/cli/getting-started). The CLI allows generating a `XataClient` that will help you work with Xata in a safe way, and that this adapter depends on.
|
||||
|
||||
<!-- @todo add GIFs -->
|
||||
|
||||
## Getting Started
|
||||
|
||||
Let's first make sure we have everything installed and configured. We're going to need:
|
||||
|
||||
- next-auth + adapter
|
||||
- the Xata CLI
|
||||
- to configure the CLI
|
||||
|
||||
We can do this like so:
|
||||
|
||||
```bash npm2yarn2pnpm
|
||||
# Install next-auth + adapter
|
||||
npm install next-auth @next-auth/xata-adapter
|
||||
|
||||
# Install the Xata CLI globally if you don't already have it
|
||||
npm install --location=global @xata.io/cli
|
||||
|
||||
# Login
|
||||
xata auth login
|
||||
```
|
||||
|
||||
Now that we're ready, let's create a new Xata project using our next-auth schema that the Xata adapter can work with. To do that, copy and paste this schema file into your project's directory:
|
||||
|
||||
```json title="schema.json"
|
||||
{
|
||||
"formatVersion": "",
|
||||
"tables": [
|
||||
{
|
||||
"name": "nextauth_users",
|
||||
"columns": [
|
||||
{
|
||||
"name": "email",
|
||||
"type": "email"
|
||||
},
|
||||
{
|
||||
"name": "emailVerified",
|
||||
"type": "datetime"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "image",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "nextauth_accounts",
|
||||
"columns": [
|
||||
{
|
||||
"name": "user",
|
||||
"type": "link",
|
||||
"link": {
|
||||
"table": "nextauth_users"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "provider",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "providerAccountId",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "refresh_token",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "access_token",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "expires_at",
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"name": "token_type",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "scope",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "id_token",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "session_state",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "nextauth_verificationTokens",
|
||||
"columns": [
|
||||
{
|
||||
"name": "identifier",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "token",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "expires",
|
||||
"type": "datetime"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "nextauth_users_accounts",
|
||||
"columns": [
|
||||
{
|
||||
"name": "user",
|
||||
"type": "link",
|
||||
"link": {
|
||||
"table": "nextauth_users"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "account",
|
||||
"type": "link",
|
||||
"link": {
|
||||
"table": "nextauth_accounts"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "nextauth_users_sessions",
|
||||
"columns": [
|
||||
{
|
||||
"name": "user",
|
||||
"type": "link",
|
||||
"link": {
|
||||
"table": "nextauth_users"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "session",
|
||||
"type": "link",
|
||||
"link": {
|
||||
"table": "nextauth_sessions"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "nextauth_sessions",
|
||||
"columns": [
|
||||
{
|
||||
"name": "sessionToken",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "expires",
|
||||
"type": "datetime"
|
||||
},
|
||||
{
|
||||
"name": "user",
|
||||
"type": "link",
|
||||
"link": {
|
||||
"table": "nextauth_users"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Now, run the following command:
|
||||
|
||||
```bash
|
||||
xata init --schema=./path/to/your/schema.json
|
||||
```
|
||||
|
||||
The CLI will walk you through a setup process where you choose a [workspace](https://docs.xata.io/concepts/workspaces) (kind of like a GitHub org or a Vercel team) and an appropriate database. We recommend using a fresh database for this, as we'll augment it with tables that next-auth needs.
|
||||
|
||||
Once you're done, you can continue using next-auth in your project as expected, like creating a `./pages/api/auth/[...nextauth]` route.
|
||||
|
||||
```typescript title="pages/api/auth/[...nextauth].ts"
|
||||
import NextAuth from "next-auth"
|
||||
import GoogleProvider from "next-auth/providers/google"
|
||||
|
||||
const client = new XataClient()
|
||||
|
||||
export default NextAuth({
|
||||
providers: [
|
||||
GoogleProvider({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
}),
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
Now to Xata-fy this route, let's add the Xata client and adapter:
|
||||
|
||||
```diff
|
||||
import NextAuth from "next-auth"
|
||||
import GoogleProvider from "next-auth/providers/google"
|
||||
+import { XataAdapter } from "@next-auth/xata-adapter"
|
||||
+import { XataClient } from "../../../xata" // or wherever you've chosen to create the client
|
||||
|
||||
+const client = new XataClient()
|
||||
|
||||
export default NextAuth({
|
||||
+ adapter: XataAdapter(client),
|
||||
providers: [
|
||||
GoogleProvider({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
}),
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
This fully sets up your next-auth site to work with Xata.
|
||||
|
||||
## Contributing
|
||||
|
||||
This is an open-source project created by humans, and as such, might have a few issues. If you experience any of these, we recommend [opening issues](https://github.com/nextauthjs/next-auth/issues/new?assignees=&labels=triage&template=1_bug_framework.yml&title=Issue%20on%20Xata%20adapter&description=I%20experienced%20this%20issue:\n##%20Reproduction%20Steps:\n\n-) that can help us solve problems and build reliable software.
|
||||
@@ -139,8 +139,8 @@ The session callback is called whenever a session is checked. By default, **only
|
||||
|
||||
e.g. `getSession()`, `useSession()`, `/api/auth/session`
|
||||
|
||||
- When using database sessions, the User object is passed as an argument.
|
||||
- When using JSON Web Tokens for sessions, the JWT payload is provided instead.
|
||||
- When using database sessions, the User (`user`) object is passed as an argument.
|
||||
- When using JSON Web Tokens for sessions, the JWT payload (`token`) is provided instead.
|
||||
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
...
|
||||
|
||||
@@ -3,7 +3,7 @@ id: databases
|
||||
title: Databases
|
||||
---
|
||||
|
||||
NextAuth.js offers multiple database adapters. Check out [the overview](/adapters/overview).
|
||||
NextAuth.js offers multiple database adapters. Check out [the overview](https://authjs.dev/reference/adapters).
|
||||
|
||||
> As of **v4** NextAuth.js no longer ships with an adapter included by default. If you would like to persist any information, you need to install one of the many available adapters yourself. See the individual adapter documentation pages for more details.
|
||||
|
||||
@@ -13,4 +13,4 @@ To learn more about databases in NextAuth.js and how they are used, check out [d
|
||||
|
||||
## How to use a database
|
||||
|
||||
See the [documentation for adapters](/adapters/overview) for more information on advanced configuration, including how to use NextAuth.js with other databases using a [custom adapter](/tutorials/creating-a-database-adapter).
|
||||
See the [documentation for adapters](https://authjs.dev/reference/adapters) for more information on advanced configuration, including how to use NextAuth.js with other databases using a [custom adapter](/tutorials/creating-a-database-adapter).
|
||||
|
||||
@@ -3,17 +3,23 @@ id: initialization
|
||||
title: Initialization
|
||||
---
|
||||
|
||||
The main entry point of NextAuth.js is the `NextAuth` method that you import from `next-auth`. It handles different types of requests, as defined in the [REST API](../getting-started/rest-api.md) section.
|
||||
|
||||
|
||||
:::info
|
||||
NextAuth.js cannot use the run [Edge Runtime](https://nextjs.org/docs/api-reference/edge-runtime) for initialization. The upcoming [`@auth/nextjs` library](https://authjs.dev/reference/nextjs) (which will replace `next-auth`) on the other hand will be fully compatible.
|
||||
:::
|
||||
|
||||
You can initialize NextAuth.js in a few different ways.
|
||||
|
||||
## Simple initialization
|
||||
### API Routes (`pages`)
|
||||
|
||||
In Next.js, you can define an API route that will catch all requests that begin with a certain path. Conveniently, this is called [Catch all API routes](https://nextjs.org/docs/api-routes/dynamic-api-routes#catch-all-api-routes).
|
||||
|
||||
When you define a `/pages/api/auth/[...nextauth]` JS/TS file, you instruct NextAuth.js that every API request beginning with `/api/auth/*` should be handled by the code written in the `[...nextauth]` file.
|
||||
|
||||
Depending on your use case, you can initialize NextAuth.js in two different ways:
|
||||
|
||||
## Simple initialization
|
||||
|
||||
In most cases, you won't need to worry about what `NextAuth.js` does, and you will get by just fine with the following initialization:
|
||||
|
||||
```ts title="/pages/api/auth/[...nextauth].js"
|
||||
```ts title="/pages/api/auth/[...nextauth].ts"
|
||||
import NextAuth from "next-auth"
|
||||
|
||||
export default NextAuth({
|
||||
@@ -25,9 +31,37 @@ Here, you only need to pass your [options](/configuration/options) to `NextAuth`
|
||||
|
||||
This is the preferred initialization in tutorials/other parts of the documentation, as it simplifies the code and reduces potential errors in the authentication flow.
|
||||
|
||||
### Route Handlers (`app/`)
|
||||
|
||||
[Next.js 13.2](https://nextjs.org/blog/next-13-2#custom-route-handlers) introduced [Route Handlers](https://beta.nextjs.org/docs/routing/route-handlers), the preferred way to handle REST-like requests in App Router (`app/`).
|
||||
|
||||
You can initialize NextAuth.js with a Route Handler too, very similar to API Routes.
|
||||
|
||||
```ts title="/app/api/auth/[...nextauth]/route.ts"
|
||||
import NextAuth from "next-auth"
|
||||
|
||||
const handler = NextAuth({
|
||||
...
|
||||
})
|
||||
|
||||
export { handler as GET, handler as POST }
|
||||
```
|
||||
|
||||
Internally, NextAuth.js detects that it is being initialized in a Route Handler (by understanding that it is passed a Web [`Request` instance](https://developer.mozilla.org/en-US/docs/Web/API/Request)), and will return a handler that returns a [`Response` instance](https://developer.mozilla.org/en-US/docs/Web/API/Response). A Route Handler file expects you to export some named handler functions that handle a request and return a response. NextAuth.js needs the `GET` and `POST` handlers to function properly, so we export those two.
|
||||
|
||||
:::info
|
||||
Technically, in a Route Handler, the `api/` prefix is not necessary, but we decided to keep it required for an easier migration.
|
||||
:::
|
||||
|
||||
## Advanced initialization
|
||||
|
||||
If you have a specific use case and need to make NextAuth.js do something slightly different than what it is designed for, keep in mind, the `[...nextauth].js` config file is still just **a regular [API Route](https://nextjs.org/docs/api-routes/introduction)** at the end of the day.
|
||||
:::info
|
||||
The following describes the advanced initialization with API Routes, but everything will apply similarily when using [Route Handlers](https://beta.nextjs.org/docs/routing/route-handlers) too.
|
||||
Instead, `NextAuth` will receive the first two arguments of a Route Handler, and the third argument will be the [auth options](../configuration/options.md)
|
||||
:::
|
||||
|
||||
If you have a specific use case and need to make NextAuth.js do something slightly different than what it is designed for, keep in mind, the `[...nextauth].ts` config file is just **a regular [API Route](https://nextjs.org/docs/api-routes/introduction)**.
|
||||
|
||||
|
||||
That said, you can initialize NextAuth.js like this:
|
||||
|
||||
@@ -91,7 +125,7 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
||||
A practical example could be to not show a certain provider on the default sign-in page, but still be able to sign in with it. (The idea is taken from [this discussion](https://github.com/nextauthjs/next-auth/discussions/3133)):
|
||||
|
||||
```js title="/pages/api/auth/[...nextauth].js"
|
||||
```js title="/pages/api/auth/[...nextauth].ts"
|
||||
import NextAuth from "next-auth"
|
||||
import CredentialsProvider from "next-auth/providers/credentials"
|
||||
import GoogleProvider from "next-auth/providers/google"
|
||||
|
||||
@@ -6,7 +6,7 @@ This method was renamed to `getServerSession`. See the documentation below.
|
||||
|
||||
## `getServerSession`
|
||||
|
||||
When calling from server-side i.e. in API routes or in `getServerSideProps`, we recommend using this function instead of `getSession` to retrieve the `session` object. This method is especially useful when you are using NextAuth.js with a database. This method can _drastically_ reduce response time when used over `getSession` server-side, due to avoiding an extra `fetch` to an API Route (this is generally [not recommended in Next.js](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props#getserversideprops-or-api-routes)). In addition, `getServerSession` will correctly update the cookie expiry time and update the session content if `callbacks.jwt` or `callbacks.session` changed something.
|
||||
When calling from server-side i.e. in API routes or in `getServerSideProps`, we recommend using this function instead of `getSession` to retrieve the `session` object. This method is especially useful when you are using NextAuth.js with a database. This method can _drastically_ reduce response time when used over `getSession` on server-side, due to avoiding an extra `fetch` to an API Route (this is generally [not recommended in Next.js](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props#getserversideprops-or-api-routes)). In addition, `getServerSession` will correctly update the cookie expiry time and update the session content if `callbacks.jwt` or `callbacks.session` changed something.
|
||||
|
||||
Otherwise, if you only want to get the session token, see [`getToken`](/tutorials/securing-pages-and-api-routes#using-gettoken).
|
||||
|
||||
@@ -55,7 +55,7 @@ import { authOptions } from 'pages/api/auth/[...nextauth]'
|
||||
import { getServerSession } from "next-auth/next"
|
||||
|
||||
|
||||
export async function handler(req, res) {
|
||||
export default async function handler(req, res) {
|
||||
const session = await getServerSession(req, res, authOptions)
|
||||
|
||||
if (!session) {
|
||||
@@ -84,9 +84,13 @@ export default async function Page() {
|
||||
```
|
||||
|
||||
:::warning
|
||||
Currently, the underlying Next.js `cookies()` method does [only provides read access](https://beta.nextjs.org/docs/api-reference/cookies) to the request cookies. This means that the `expires` value is stripped away from `session` in Server Components. Furthermore, there is a hard expiry on sessions, after which the user will be required to sign in again. (The default expiry is 30 days).
|
||||
Currently, the underlying Next.js `cookies()` method [only provides read access](https://beta.nextjs.org/docs/api-reference/cookies) to the request cookies. This means that the `expires` value is stripped away from `session` in Server Components. Furthermore, there is a hard expiry on sessions, after which the user will be required to sign in again. (The default expiry is 30 days).
|
||||
:::
|
||||
|
||||
### Caching
|
||||
|
||||
Note that using this function implies personalized data and that you should not store pages or APIs using this in a [public cache](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control). For example a host like [Vercel](https://vercel.com/docs/concepts/functions/serverless-functions/edge-caching) will implicitly prevent you from caching publicly due to the `set-cookie` header set by this function.
|
||||
|
||||
## Middleware
|
||||
|
||||
You can use a Next.js Middleware with NextAuth.js to protect your site.
|
||||
|
||||
@@ -27,7 +27,7 @@ Using [System Environment Variables](https://vercel.com/docs/concepts/projects/e
|
||||
|
||||
### NEXTAUTH_SECRET
|
||||
|
||||
Used to encrypt the NextAuth.js JWT, and to hash [email verification tokens](/adapters/models#verification-token). This is the default value for the `secret` option in [NextAuth](/configuration/options#secret) and [Middleware](/configuration/nextjs#secret).
|
||||
Used to encrypt the NextAuth.js JWT, and to hash [email verification tokens](https://authjs.dev/reference/adapters#verification-token). This is the default value for the `secret` option in [NextAuth](/configuration/options#secret) and [Middleware](/configuration/nextjs#secret).
|
||||
|
||||
|
||||
### NEXTAUTH_URL_INTERNAL
|
||||
@@ -310,7 +310,7 @@ events: {
|
||||
|
||||
#### Description
|
||||
|
||||
By default NextAuth.js does not include an adapter any longer. If you would like to persist user / account data, please install one of the many available adapters. More information can be found in the [adapter documentation](/adapters/overview).
|
||||
By default NextAuth.js does not include an adapter any longer. If you would like to persist user / account data, please install one of the many available adapters. More information can be found in the [adapter documentation](https://authjs.dev/reference/adapters).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -77,10 +77,13 @@ In addition, you can define a `theme.brandColor` to define a custom accent color
|
||||
|
||||
In order to get the available authentication providers and the URLs to use for them, you can make a request to the API endpoint `/api/auth/providers`:
|
||||
|
||||
```jsx title="pages/auth/signin.js"
|
||||
```tsx title="pages/auth/signin.tsx"
|
||||
import type { GetServerSidePropsContext, InferGetServerSidePropsType } from "next";
|
||||
import { getProviders, signIn } from "next-auth/react"
|
||||
import { getServerSession } from "next-auth/next"
|
||||
import { authOptions } from "../api/auth/[...nextauth]";
|
||||
|
||||
export default function SignIn({ providers }) {
|
||||
export default function SignIn({ providers }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
return (
|
||||
<>
|
||||
{Object.values(providers).map((provider) => (
|
||||
@@ -94,10 +97,20 @@ export default function SignIn({ providers }) {
|
||||
)
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context) {
|
||||
const providers = await getProviders()
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const session = await getServerSession(context.req, context.res, authOptions);
|
||||
|
||||
// If the user is already logged in, redirect.
|
||||
// Note: Make sure not to redirect to the same page
|
||||
// To avoid an infinite loop!
|
||||
if (session) {
|
||||
return { redirect: { destination: "/" } };
|
||||
}
|
||||
|
||||
const providers = await getProviders();
|
||||
|
||||
return {
|
||||
props: { providers },
|
||||
props: { providers: providers ?? [] },
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -108,10 +121,11 @@ There is another, more fully styled example signin page available [here](https:/
|
||||
|
||||
If you create a custom sign in form for email sign in, you will need to submit both fields for the **email** address and **csrfToken** from **/api/auth/csrf** in a POST request to **/api/auth/signin/email**.
|
||||
|
||||
```jsx title="pages/auth/email-signin.js"
|
||||
```tsx title="pages/auth/email-signin.tsx"
|
||||
import type { GetServerSidePropsContext, InferGetServerSidePropsType } from "next";
|
||||
import { getCsrfToken } from "next-auth/react"
|
||||
|
||||
export default function SignIn({ csrfToken }) {
|
||||
export default function SignIn({ csrfToken }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
return (
|
||||
<form method="post" action="/api/auth/signin/email">
|
||||
<input name="csrfToken" type="hidden" defaultValue={csrfToken} />
|
||||
@@ -124,7 +138,7 @@ export default function SignIn({ csrfToken }) {
|
||||
)
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context) {
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const csrfToken = await getCsrfToken(context)
|
||||
return {
|
||||
props: { csrfToken },
|
||||
@@ -134,7 +148,7 @@ export async function getServerSideProps(context) {
|
||||
|
||||
You can also use the `signIn()` function which will handle obtaining the CSRF token for you:
|
||||
|
||||
```js
|
||||
```ts
|
||||
signIn("email", { email: "jsmith@example.com" })
|
||||
```
|
||||
|
||||
@@ -142,10 +156,11 @@ signIn("email", { email: "jsmith@example.com" })
|
||||
|
||||
If you create a sign in form for credentials based authentication, you will need to pass a **csrfToken** from **/api/auth/csrf** in a POST request to **/api/auth/callback/credentials**.
|
||||
|
||||
```jsx title="pages/auth/credentials-signin.js"
|
||||
```tsx title="pages/auth/credentials-signin.tsx"
|
||||
import type { GetServerSidePropsContext, InferGetServerSidePropsType } from "next";
|
||||
import { getCsrfToken } from "next-auth/react"
|
||||
|
||||
export default function SignIn({ csrfToken }) {
|
||||
export default function SignIn({ csrfToken }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
return (
|
||||
<form method="post" action="/api/auth/callback/credentials">
|
||||
<input name="csrfToken" type="hidden" defaultValue={csrfToken} />
|
||||
@@ -162,7 +177,7 @@ export default function SignIn({ csrfToken }) {
|
||||
)
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context) {
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
return {
|
||||
props: {
|
||||
csrfToken: await getCsrfToken(context),
|
||||
@@ -173,7 +188,7 @@ export async function getServerSideProps(context) {
|
||||
|
||||
You can also use the `signIn()` function which will handle obtaining the CSRF token for you:
|
||||
|
||||
```js
|
||||
```ts
|
||||
signIn("credentials", { username: "jsmith", password: "1234" })
|
||||
```
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ providers: [
|
||||
// You can pass any HTML attribute to the <input> tag through the object.
|
||||
credentials: {
|
||||
username: { label: "Username", type: "text", placeholder: "jsmith" },
|
||||
password: { label: "Password", type: "password" }
|
||||
password: { label: "Password", type: "password" }
|
||||
},
|
||||
async authorize(credentials, req) {
|
||||
// You need to provide your own logic here that takes the credentials
|
||||
|
||||
@@ -3,6 +3,12 @@ id: email
|
||||
title: Email
|
||||
---
|
||||
|
||||
### Install nodemailer
|
||||
|
||||
```bash npm2yarn2pnpm
|
||||
npm install nodemailer
|
||||
```
|
||||
|
||||
### How to
|
||||
|
||||
The Email provider sends "magic links" via email that the user can click on to sign in.
|
||||
@@ -35,10 +41,10 @@ The email provider requires a database, it cannot be used without one.
|
||||
|
||||
| Name | Description | Type | Required |
|
||||
| :---------------------: | :---------------------------------------------------------------------------------: | :------------------------------: | :------: |
|
||||
| id | Unique ID for the provider | `string` | Yes |
|
||||
| name | Descriptive name for the provider | `string` | Yes |
|
||||
| type | Type of provider, in this case `email` | `"email"` | Yes |
|
||||
| server | Path or object pointing to the email server | `string` or `Object` | Yes |
|
||||
| sendVerificationRequest | Callback to execute when a verification request is sent | `(params) => Promise<undefined>` | Yes |
|
||||
| id | Unique ID for the provider | `string` | No |
|
||||
| name | Descriptive name for the provider | `string` | No |
|
||||
| type | Type of provider, in this case `email` | `"email"` | No |
|
||||
| server | Path or object pointing to the email server | `string` or `Object` | No |
|
||||
| sendVerificationRequest | Callback to execute to send a verification request, default uses nodemailer | `(params) => Promise<undefined>` | No |
|
||||
| from | The email address from which emails are sent, default: "<no-reply@example.com>" | `string` | No |
|
||||
| maxAge | How long until the e-mail can be used to log the user in seconds. Defaults to 1 day | `number` | No |
|
||||
|
||||
@@ -40,7 +40,7 @@ sequenceDiagram
|
||||
Note left of Browser: User inserts their<br/>credentials in Github
|
||||
Browser->>Auth Server (Github): Github validates the inserted credentials
|
||||
Auth Server (Github)->>Auth Server (Github): Generates one time access code<br/>and calls callback<br>URL defined in<br/>App settings
|
||||
Auth Server (Github)->>App Server: GET<br/>"api/auth/github/callback?code=123"
|
||||
Auth Server (Github)->>App Server: GET<br/>"api/auth/callback/github?code=123"
|
||||
App Server->>App Server: Grabs code<br/>to exchange it for<br/>access token
|
||||
App Server->>Auth Server (Github): POST<br/>"github.com/login/oauth/access_token"<br/>{code: 123}
|
||||
Auth Server (Github)->>Auth Server (Github): Verifies code is<br/>valid and generates<br/>access token
|
||||
@@ -424,17 +424,3 @@ GoogleProvider({
|
||||
allowDangerousEmailAccountLinking: true,
|
||||
})
|
||||
```
|
||||
|
||||
### Adding a new built-in provider
|
||||
|
||||
If you think your custom provider might be useful to others, we encourage you to open a PR and add it to the built-in list so others can discover it much more easily!
|
||||
|
||||
You only need to add three changes:
|
||||
|
||||
1. Add your config: [`src/providers/{provider}.ts`](https://github.com/nextauthjs/next-auth/tree/main/packages/next-auth/src/providers)<br />
|
||||
- Make sure you use a named default export, like this: `export default function YourProvider`
|
||||
- Add two SVG's of the provider logo, like `google-dark.svg` (dark mode) and `google.svg` (light mode), to the `/packages/next-auth/provider-logos/` directory as well as the styling config to the provider config object. See existing provider for example
|
||||
2. Add provider documentation: [`/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/docs/docs/providers)
|
||||
3. Add the new provider name to the `Provider type` dropdown options in [`the provider issue template`](https://github.com/nextauthjs/next-auth/edit/main/.github/ISSUE_TEMPLATE/2_bug_provider.yml)
|
||||
|
||||
That's it! 🎉 Others will be able to discover and use this provider much more easily now!
|
||||
|
||||
@@ -18,8 +18,8 @@ See below for more detailed provider settings.
|
||||
|
||||
1. Make sure to expose the Vercel [System Environment Variables](https://vercel.com/docs/concepts/projects/environment-variables#system-environment-variables) in your project settings.
|
||||
2. Create a `NEXTAUTH_SECRET` environment variable for all environments.
|
||||
a. You can use `openssl rand -base64 32` or https://generate-secret.vercel.app/32 to generate a random value.
|
||||
b. You **do not** need the `NEXTAUTH_URL` environment variable in Vercel.
|
||||
- You can use `openssl rand -base64 32` or https://generate-secret.vercel.app/32 to generate a random value.
|
||||
- You **do not** need the `NEXTAUTH_URL` environment variable in Vercel.
|
||||
3. Add your provider's client ID and client secret to environment variables. _(Skip this step if not using an [OAuth Provider](/configuration/providers/oauth))_
|
||||
4. Deploy!
|
||||
|
||||
@@ -79,7 +79,7 @@ export default NextAuth({
|
||||
|
||||
#### Using the branch based preview URL
|
||||
|
||||
Preview deployments at Vercel are often available via multiple URLs. For example, PR's merged to `master` or `main`, will be available the commit and PR specific preview URLs, but also the branch specific preview URLs. This branch specific URL will obviously not change as long as you work with that same branch. Therefore, you could add to your OAuth provider your `{project}-git-main-{user}.vercel.app` preview URL. As this will stay constant for that branch, you can reuse that preview deployment / URL for testing any authentication related deployments.
|
||||
Preview deployments at Vercel are often available via multiple URLs. For example, PR's merged to `master` or `main`, will be available via commit and PR specific preview URLs, but also the branch specific preview URLs. This branch specific URL will obviously not change as long as you work with that same branch. Therefore, you could add to your OAuth provider your `{project}-git-main-{user}.vercel.app` preview URL. As this will stay constant for that branch, you can reuse that preview deployment / URL for testing any authentication related deployments.
|
||||
|
||||
## Netlify
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ _If you use a custom credentials provider user accounts will not be persisted in
|
||||
</summary>
|
||||
<p>
|
||||
|
||||
NextAuth.js was originally designed for use with Next.js and Serverless. However, today you could use the NextAuth.js core with any other framework. Checkout the examples for <a href="https://github.com/nextauthjs/next-auth/tree/main/apps/playground-gatsby" target="_blank">Gatsby</a> and <a href="https://sveltekit.authjs.dev/" target="_blank">SvelteKit</a>. If you would add another integration with other frameworks, feel free to work on it and send a pull request. Make sure to check if there's any on-going work before open a new issue.
|
||||
NextAuth.js was originally designed for use with Next.js and Serverless. However, today you could use the NextAuth.js core with any other framework. Checkout the examples for <a href="https://github.com/nextauthjs/next-auth/tree/main/apps/playground-gatsby" target="_blank">Gatsby</a> and <a href="https://sveltekit.authjs.dev/" target="_blank">SvelteKit</a>. If you would add another integration with other frameworks, feel free to work on it and send a pull request. Make sure to check if there's any on-going work before opening a new issue.
|
||||
|
||||
</p>
|
||||
</details>
|
||||
@@ -207,7 +207,7 @@ NextAuth.js records Refresh Tokens and Access Tokens on sign in (if supplied by
|
||||
|
||||
You can then look them up from the database or persist them to the JSON Web Token.
|
||||
|
||||
Note: NextAuth.js does not currently handle Access Token rotation for OAuth providers for you, however you can check out [this tutorial](/tutorials/refresh-token-rotation) if you want to implement it.
|
||||
Note: NextAuth.js does not currently handle Access Token rotation for OAuth providers for you, however you can check out [this tutorial](https://authjs.dev/guides/basics/refresh-token-rotation) if you want to implement it.
|
||||
|
||||
We also have an [example repository](https://github.com/nextauthjs/next-auth-refresh-token-example) / project based upon NextAuth.js v4 where we demonstrate how to use a refresh token to refresh the provided access token.
|
||||
|
||||
@@ -289,7 +289,7 @@ Ultimately if your request is not accepted or is not actively in development, yo
|
||||
</summary>
|
||||
<p>
|
||||
|
||||
NextAuth.js by default uses JSON Web Tokens for saving the user's session. However, if you use a [database adapter](/adapters/overview), the database will be used to persist the user's session. You can force the usage of JWT when using a database [through the configuration options](/configuration/options#session). Since v4 all our JWT tokens are now encrypted by default with A256GCM.
|
||||
NextAuth.js by default uses JSON Web Tokens for saving the user's session. However, if you use a [database adapter](https://authjs.dev/reference/adapters), the database will be used to persist the user's session. You can force the usage of JWT when using a database [through the configuration options](/configuration/options#session). Since v4 all our JWT tokens are now encrypted by default with A256GCM.
|
||||
|
||||
</p>
|
||||
</details>
|
||||
|
||||
@@ -148,10 +148,133 @@ Because of how `_app` is written, it won't unnecessarily contact the `/api/auth/
|
||||
|
||||
More information can be found in the following [GitHub Issue](https://github.com/nextauthjs/next-auth/issues/1210).
|
||||
|
||||
### NextAuth.js + React Query
|
||||
### Updating the session
|
||||
|
||||
You can create your own session management solution using data fetching libraries like [React Query](https://tanstack.com/query/v4/docs/adapters/react-query) or [SWR](https://swr.vercel.app). You can use the [original implementation of `@next-auth/react-query`](https://github.com/nextauthjs/react-query) and look at the [`next-auth/react` source code](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/react/index.tsx) as a starting point.
|
||||
The `useSession()` hook exposes a `update(data?: any): Promise<Session | null>` method that can be used to update the session, without reloading the page.
|
||||
|
||||
You can optionally pass an arbitrary object as the first argument, which will be accessible on the server to merge with the session object.
|
||||
|
||||
If you are not passing any argument, the session will be reloaded from the server. (This is useful if you want to update the session after a server-side mutation, like updating in the database.)
|
||||
|
||||
:::caution
|
||||
The data object is coming from the client, so it needs to be validated on the server before saving.
|
||||
:::
|
||||
|
||||
#### Example
|
||||
|
||||
```tsx title="pages/profile.tsx"
|
||||
import { useSession } from "next-auth/react"
|
||||
|
||||
export default function Page() {
|
||||
const { data: session, status, update } = useSession()
|
||||
|
||||
if (status === "authenticated") {
|
||||
return (
|
||||
<>
|
||||
<p>Signed in as {session.user.name}</p>
|
||||
|
||||
{/* Update the value by sending it to the backend. */}
|
||||
<button onClick={() => update({ name: "John Doe" })}>
|
||||
Edit name
|
||||
</button>
|
||||
{/*
|
||||
* Only trigger a session update, assuming you already updated the value server-side.
|
||||
* All `useSession().data` references will be updated.
|
||||
*/}
|
||||
<button onClick={() => update()}>
|
||||
Edit name
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return <a href="/api/auth/signin">Sign in</a>
|
||||
}
|
||||
```
|
||||
|
||||
Assuming a `strategy: "jwt"` is used, the `update()` method will trigger a `jwt` callback with the `trigger: "update"` option. You can use this to update the session object on the server.
|
||||
|
||||
```ts title="pages/api/auth/[...nextauth].ts"
|
||||
...
|
||||
export default NextAuth({
|
||||
...
|
||||
callbacks: {
|
||||
// Using the `...rest` parameter to be able to narrow down the type based on `trigger`
|
||||
jwt({ token, trigger, session }) {
|
||||
if (trigger === "update" && session?.name) {
|
||||
// Note, that `session` can be any arbitrary object, remember to validate it!
|
||||
token.name = session
|
||||
}
|
||||
return token
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Assuming a `strategy: "database"` is used, the `update()` method will trigger the `session` callback with the `trigger: "update"` option. You can use this to update the session object on the server.
|
||||
|
||||
```ts title="pages/api/auth/[...nextauth].ts"
|
||||
...
|
||||
const adapter = PrismaAdapter(prisma)
|
||||
export default NextAuth({
|
||||
...
|
||||
adapter,
|
||||
callbacks: {
|
||||
// Using the `...rest` parameter to be able to narrow down the type based on `trigger`
|
||||
async session({ session, trigger, newSession }) {
|
||||
// Note, that `rest.session` can be any arbitrary object, remember to validate it!
|
||||
if (trigger === "update" && newSession?.name) {
|
||||
// You can update the session in the database if it's not already updated.
|
||||
// await adapter.updateUser(session.user.id, { name: newSession.name })
|
||||
|
||||
// Make sure the updated value is reflected on the client
|
||||
session.name = newSession.name
|
||||
}
|
||||
return session
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Refetching the session
|
||||
|
||||
[`SessionProvider#refetchInterval`](#refetch-interval) and [`SessionProvider#refetchOnWindowFocus`](#refetch-on-window-focus) can be replaced with the `update()` method too.
|
||||
|
||||
:::note
|
||||
The `update()` method won't sync between tabs as the `refetchInterval` and `refetchOnWindowFocus` options do.
|
||||
:::
|
||||
|
||||
```tsx title="pages/profile.tsx"
|
||||
import {useEffect} from "react"
|
||||
import { useSession } from "next-auth/react"
|
||||
|
||||
export default function Page() {
|
||||
const { data: session, status, update } = useSession()
|
||||
|
||||
// Polling the session every 1 hour
|
||||
useEffect(() => {
|
||||
// TIP: You can also use `navigator.onLine` and some extra event handlers
|
||||
// to check if the user is online and only update the session if they are.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine
|
||||
const interval = setInterval(() => update(), 1000 * 60 * 60)
|
||||
return () => clearInterval(interval)
|
||||
}, [update])
|
||||
|
||||
// Listen for when the page is visible, if the user switches tabs
|
||||
// and makes our tab visible again, re-fetch the session
|
||||
useEffect(() => {
|
||||
const visibilityHandler = () => document.visibilityState === "visible" && update()
|
||||
window.addEventListener("visibilitychange", visibilityHandler, false)
|
||||
return () => window.removeEventListener("visibilitychange", visibilityHandler, false)
|
||||
}, [update])
|
||||
|
||||
return (
|
||||
<pre>
|
||||
{JSON.stringify(session, null, 2)}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
```
|
||||
---
|
||||
|
||||
## getSession()
|
||||
@@ -236,7 +359,7 @@ export default async (req, res) => {
|
||||
```
|
||||
|
||||
:::note
|
||||
Unlike and `getCsrfToken()`, when calling `getProviders()` server side, you don't need to pass anything, just as calling it client side.
|
||||
Unlike `getCsrfToken()`, when calling `getProviders()` server side, you don't need to pass anything, just as calling it client side.
|
||||
:::
|
||||
|
||||
---
|
||||
@@ -396,7 +519,7 @@ where `data.url` is the validated URL you can redirect the user to without any f
|
||||
|
||||
## SessionProvider
|
||||
|
||||
Using the supplied `<SessionProvider>` allows instances of `useSession()` to share the session object across components, by using [React Context](https://reactjs.org/docs/context.html) under the hood. It also takes care of keeping the session updated and synced between tabs/windows.
|
||||
Using the supplied `<SessionProvider>` allows instances of `useSession()` to share the session object across components, by using [React Context](https://react.dev/learn/passing-data-deeply-with-context) under the hood. It also takes care of keeping the session updated and synced between tabs/windows.
|
||||
|
||||
```jsx title="pages/_app.js"
|
||||
import { SessionProvider } from "next-auth/react"
|
||||
@@ -479,6 +602,8 @@ If you are using a custom base path, and your application entry point is not at
|
||||
|
||||
#### Refetch interval
|
||||
|
||||
See [Session Refetching](#refetching-the-session) for an alternative option.
|
||||
|
||||
The `refetchInterval` option can be used to contact the server to avoid a session expiring.
|
||||
|
||||
When `refetchInterval` is set to `0` (the default) there will be no session polling.
|
||||
@@ -491,6 +616,8 @@ By default, session polling will keep trying, even when the device has no intern
|
||||
|
||||
#### Refetch On Window Focus
|
||||
|
||||
See [Session Refetching](#refetching-the-session) for an alternative option.
|
||||
|
||||
The `refetchOnWindowFocus` option can be used to control whether it automatically updates the session state when you switch a focus on tabs/windows.
|
||||
|
||||
When `refetchOnWindowFocus` is set to `true` (the default) tabs/windows will be updated and initialize the components' state when they gain or lose focus.
|
||||
|
||||
@@ -26,6 +26,8 @@ If you are using TypeScript, NextAuth.js comes with its types definitions within
|
||||
|
||||
To add NextAuth.js to a project create a file called `[...nextauth].js` in `pages/api/auth`. This contains the dynamic route handler for NextAuth.js which will also contain all of your global NextAuth.js configurations.
|
||||
|
||||
If you're using [Next.js 13.2](https://nextjs.org/blog/next-13-2#custom-route-handlers) or above with the new App Router (`app/`), you can initialize the configuration using the new [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) by following our [guide](https://next-auth.js.org/configuration/initialization#route-handlers-app).
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js" showLineNumbers
|
||||
import NextAuth from "next-auth"
|
||||
import GithubProvider from "next-auth/providers/github"
|
||||
|
||||
@@ -16,7 +16,7 @@ It is designed from the ground up to support Next.js and Serverless.
|
||||
- Designed to work with any [OAuth service, it supports OAuth 1.0, 1.0A, 2.0 and OpenID Connect](/providers)
|
||||
- Built-in support for [many popular sign-in services](/configuration/providers/oauth)
|
||||
- Supports [email / passwordless authentication](/providers/email)
|
||||
- Supports stateless authentication with [any backend](/adapters/overview) (Active Directory, LDAP, etc)
|
||||
- Supports stateless authentication with [any backend](https://authjs.dev/reference/adapters) (Active Directory, LDAP, etc)
|
||||
- Supports both JSON Web Tokens and database sessions
|
||||
- Designed for Serverless but runs anywhere (AWS Lambda, Docker, Heroku, etc…)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ title: TypeScript
|
||||
NextAuth.js has its own type definitions to use in your TypeScript projects safely. Even if you don't use TypeScript, IDEs like VSCode will pick this up to provide you with a better developer experience. While you are typing, you will get suggestions about what certain objects/functions look like, and sometimes links to documentation, examples, and other valuable resources.
|
||||
|
||||
Check out the example repository showcasing how to use `next-auth` on a Next.js application with TypeScript:
|
||||
https://github.com/nextauthjs/next-auth-typescript-example
|
||||
https://github.com/nextauthjs/next-auth-example
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -311,7 +311,7 @@ export default NextAuth({
|
||||
|
||||
3. The `typeorm-legacy` adapter has been upgraded to use the newer adapter API, but has retained the `typeorm-legacy` name. We aim to migrate this to individual lighter weight adapters for each database type in the future, or switch out `typeorm`.
|
||||
|
||||
4. MongoDB has been moved to its own adapter under `@next-auth/mongodb-adapter`. See the [MongoDB Adapter docs](/adapters/mongodb).
|
||||
4. MongoDB has been moved to its own adapter under `@next-auth/mongodb-adapter`. See the [MongoDB Adapter docs](https://authjs.dev/reference/adapter/mongodb).
|
||||
|
||||
Introduced in https://github.com/nextauthjs/next-auth/releases/tag/v4.0.0-next.8 and https://github.com/nextauthjs/next-auth/pull/2361
|
||||
|
||||
@@ -319,7 +319,7 @@ Introduced in https://github.com/nextauthjs/next-auth/releases/tag/v4.0.0-next.8
|
||||
|
||||
**This does not require any changes from the user - these are adapter specific changes only**
|
||||
|
||||
The Adapter API has been rewritten and significantly simplified in NextAuth.js v4. The adapters now have less work to do as some functionality has been migrated to the core of NextAuth, like hashing the [verification token](/adapters/models/#verification-token).
|
||||
The Adapter API has been rewritten and significantly simplified in NextAuth.js v4. The adapters now have less work to do as some functionality has been migrated to the core of NextAuth, like hashing the [verification token](https://authjs.dev/reference/adapters#verification-token).
|
||||
|
||||
If you are an adapter maintainer or are interested in writing your own adapter, you can find more information about this change in https://github.com/nextauthjs/next-auth/pull/2361 and release https://github.com/nextauthjs/next-auth/releases/tag/v4.0.0-next.22.
|
||||
|
||||
@@ -405,7 +405,7 @@ VerificationToken {
|
||||
</pre>
|
||||
</details>
|
||||
|
||||
For more info, see the [Models page](/adapters/models).
|
||||
For more info, see the [Models page](https://authjs.dev/reference/adapters#models).
|
||||
|
||||
### Database migration
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ id: fullstack
|
||||
title: Fullstack
|
||||
---
|
||||
|
||||
### [Refresh Token Rotation](/tutorials/refresh-token-rotation)
|
||||
### [Refresh Token Rotation](https://authjs.dev/guides/basics/refresh-token-rotation)
|
||||
|
||||
- How to implement refresh token rotation.
|
||||
|
||||
@@ -21,7 +21,7 @@ title: Fullstack
|
||||
|
||||
## Database
|
||||
|
||||
### [Custom models with TypeORM](/adapters/typeorm#custom-models)
|
||||
### [Custom models with TypeORM](https://authjs.dev/reference/adapter/typeorm#custom-models)
|
||||
|
||||
- How to use models with custom properties using the TypeORM adapter.
|
||||
|
||||
@@ -29,6 +29,6 @@ title: Fullstack
|
||||
|
||||
- How to create a custom adapter, to use any database to fetch and store user / account data.
|
||||
|
||||
### [Adding role based login to database session strategy](/tutorials/role-based-login-strategy)
|
||||
### [Adding role based login to database session strategy](https://authjs.dev/guides/basics/role-based-access-control)
|
||||
|
||||
- Implement a role based login system by adding a custom session callback.
|
||||
|
||||
@@ -4,7 +4,7 @@ title: 42 School
|
||||
---
|
||||
|
||||
:::note
|
||||
42 returns a field on `Account` called `created_at` which is a number. See the [docs](https://api.intra.42.fr/apidoc/guides/getting_started#make-basic-requests). Make sure to add this field to your database schema, in case if you are using an [Adapter](/adapters/overview).
|
||||
42 returns a field on `Account` called `created_at` which is a number. See the [docs](https://api.intra.42.fr/apidoc/guides/getting_started#make-basic-requests). Make sure to add this field to your database schema, in case if you are using an [Adapter](https://authjs.dev/reference/adapters).
|
||||
:::
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -64,7 +64,7 @@ Edit your host file and point your site to `127.0.0.1`.
|
||||
_Linux/macOS_
|
||||
|
||||
```
|
||||
sudo echo '127.0.0.1 dev.example.com' >> /etc/hosts
|
||||
echo '127.0.0.1 dev.example.com' | sudo tee -a /etc/hosts
|
||||
```
|
||||
|
||||
_Windows_ (run PowerShell as administrator)
|
||||
|
||||
@@ -11,7 +11,7 @@ Azure AD B2C returns the following fields on `Account`:
|
||||
- `id_token_expires_in` (number)
|
||||
- `profile_info` (string).
|
||||
|
||||
See their [docs](https://docs.microsoft.com/en-us/azure/active-directory-b2c/access-tokens). Remember to add these fields to your database schema, in case if you are using an [Adapter](/adapters/overview).
|
||||
See their [docs](https://docs.microsoft.com/en-us/azure/active-directory-b2c/access-tokens). Remember to add these fields to your database schema, in case if you are using an [Adapter](https://authjs.dev/reference/adapters).
|
||||
:::
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -3,6 +3,17 @@ id: azure-ad
|
||||
title: Azure Active Directory
|
||||
---
|
||||
|
||||
:::note
|
||||
Azure Active Directory returns the following fields on `Account`:
|
||||
|
||||
- `token_type` (string)
|
||||
- `expires_in` (number)
|
||||
- `ext_expires_in` (number)
|
||||
- `access_token` (string).
|
||||
|
||||
Remember to add these fields to your database schema, in case if you are using an [Adapter](https://authjs.dev/reference/adapters).
|
||||
:::
|
||||
|
||||
## Documentation
|
||||
|
||||
https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
|
||||
@@ -20,7 +31,7 @@ https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-regis
|
||||
- Pay close attention to "Who can use this application or access this API?"
|
||||
- This allows you to scope access to specific types of user accounts
|
||||
- Only your tenant, all azure tenants, or all azure tenants and public Microsoft accounts (Skype, Xbox, Outlook.com, etc.)
|
||||
- When asked for a redirection URL, use `https://yourapplication.com/api/auth/callback/azure-ad` or for development `http://localhost:3000/api/auth/callback/azure-ad`.
|
||||
- When asked for a redirection URL, select the platform type "Web" and use `https://yourapplication.com/api/auth/callback/azure-ad` or for development `http://localhost:3000/api/auth/callback/azure-ad`.
|
||||
- After your App Registration is created, under "Client Credential" create your Client secret.
|
||||
- Now copy your:
|
||||
- Application (client) ID
|
||||
@@ -37,6 +48,10 @@ AZURE_AD_TENANT_ID=<copy the tenant id here>
|
||||
|
||||
That will default the tenant to use the `common` authorization endpoint. [For more details see here](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols#endpoints).
|
||||
|
||||
:::note
|
||||
When you see `ResourceNotFound` error code while accessing an API, make sure to use the correct tenant ID. For instance, when the intended access is for a personal account, the tenant ID should not be provided.
|
||||
:::
|
||||
|
||||
:::note
|
||||
Azure AD returns the profile picture in an ArrayBuffer, instead of just a URL to the image, so our provider converts it to a base64 encoded image string and returns that instead. See: https://docs.microsoft.com/en-us/graph/api/profilephoto-get?view=graph-rest-1.0#examples. The default image size is 48x48 to avoid [running out of space](https://next-auth.js.org/faq#:~:text=What%20are%20the%20disadvantages%20of%20JSON%20Web%20Tokens%3F) in case the session is saved as a JWT.
|
||||
:::
|
||||
|
||||
53
docs/docs/providers/duende-identity-server6.md
Normal file
53
docs/docs/providers/duende-identity-server6.md
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
id: duende-identityserver6
|
||||
title: DuendeIdentityServer6
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
https://docs.duendesoftware.com/identityserver/v6
|
||||
|
||||
## Options
|
||||
|
||||
The **DuendeIdentityServer6 Provider** comes with a set of default options:
|
||||
|
||||
- [DuendeIdentityServer6 Provider options](https://github.com/nextauthjs/next-auth/blob/v4/packages/next-auth/src/providers/duende-identity-server6.ts)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import DuendeIDS6Provider from "next-auth/providers/duende-identity-server6"
|
||||
|
||||
...
|
||||
providers: [
|
||||
DuendeIDS6Provider({
|
||||
clientId: process.env.DUENDE_IDS6_ID,
|
||||
clientSecret: process.env.DUENDE_IDS6_SECRET,
|
||||
issuer: process.env.DUENDE_IDS6_ISSUER,
|
||||
})
|
||||
]
|
||||
...
|
||||
```
|
||||
|
||||
## Demo IdentityServer
|
||||
|
||||
The configuration below is for the demo server at https://demo.duendesoftware.com/
|
||||
|
||||
If you want to try it out, you can copy and paste the configuration below.
|
||||
|
||||
You can sign in to the demo service with either <b>bob/bob</b> or <b>alice/alice</b>.
|
||||
|
||||
```js
|
||||
import DuendeIDS6Provider from "next-auth/providers/duende-identity-server6"
|
||||
...
|
||||
providers: [
|
||||
DuendeIDS6Provider({
|
||||
clientId: "interactive.confidential",
|
||||
clientSecret: "secret",
|
||||
issuer: "https://demo.duendesoftware.com",
|
||||
})
|
||||
]
|
||||
...
|
||||
```
|
||||
@@ -36,7 +36,7 @@ You can override any of the options to suit your own use case.
|
||||
2. You will need an SMTP account; ideally for one of the [services known to work with `nodemailer`](https://community.nodemailer.com/2-0-0-beta/setup-smtp/well-known-services/).
|
||||
3. There are two ways to configure the SMTP server connection.
|
||||
|
||||
You can either use a connection string or a `nodemailer` configuration object.
|
||||
You can either use a connection string or a `nodemailer` configuration object or transport.
|
||||
|
||||
2.1 **Using a connection string**
|
||||
|
||||
@@ -92,7 +92,7 @@ providers: [
|
||||
],
|
||||
```
|
||||
|
||||
3. Do not forget to setup one of the database [adapters](/adapters/overview) for storing the Email verification token.
|
||||
3. Do not forget to setup one of the database [adapters](https://authjs.dev/reference/adapters) for storing the Email verification token.
|
||||
|
||||
4. You can now sign in with an email address at `/api/auth/signin`.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: GitHub
|
||||
---
|
||||
|
||||
:::note
|
||||
GitHub returns a field on `Account` called `refresh_token_expires_in` which is a number. See their [docs](https://docs.github.com/en/developers/apps/building-github-apps/refreshing-user-to-server-access-tokens#response). Remember to add this field to your database schema, in case if you are using an [Adapter](/adapters/overview).
|
||||
GitHub returns a field on `Account` called `refresh_token_expires_in` which is a number. See their [docs](https://docs.github.com/en/developers/apps/building-github-apps/refreshing-user-to-server-access-tokens#response). Remember to add this field to your database schema, in case if you are using an [Adapter](https://authjs.dev/reference/adapters).
|
||||
:::
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -15,7 +15,7 @@ https://developers.kakao.com/docs/latest/en/kakaologin/common
|
||||
|
||||
The **Kakao Provider** comes with a set of default options:
|
||||
|
||||
- [Kakao Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/kakao.js)
|
||||
- [Kakao Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/kakao.ts)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ title: Twitter
|
||||
---
|
||||
|
||||
:::note
|
||||
Twitter is currently the only built-in provider using the OAuth 1.0 spec. This means that you won't receive an `access_token` or `refresh_token`, but an `oauth_token` and `oauth_token_secret` respectively. Remember to add these to your database schema, in case if you are using an [Adapter](/adapters/overview).
|
||||
Twitter is currently the only built-in provider using the OAuth 1.0 spec. This means that you won't receive an `access_token` or `refresh_token`, but an `oauth_token` and `oauth_token_secret` respectively. Remember to add these to your database schema, in case if you are using an [Adapter](https://authjs.dev/reference/adapters).
|
||||
:::
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -15,7 +15,7 @@ https://vk.com/apps?act=manage
|
||||
|
||||
The **VK Provider** comes with a set of default options:
|
||||
|
||||
- [VK Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/vk.js)
|
||||
- [VK Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/vk.ts)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
@@ -34,7 +34,7 @@ providers: [
|
||||
```
|
||||
|
||||
:::note
|
||||
By default the provider uses `5.126` version of the API. See https://vk.com/dev/versions for more info.
|
||||
By default the provider uses `5.131` version of the API. See https://vk.com/dev/versions for more info.
|
||||
:::
|
||||
|
||||
If you want to use a different version, you can pass it to provider's options object:
|
||||
@@ -42,7 +42,7 @@ If you want to use a different version, you can pass it to provider's options ob
|
||||
```js
|
||||
// pages/api/auth/[...nextauth].js
|
||||
|
||||
const apiVersion = "5.126"
|
||||
const apiVersion = "5.131"
|
||||
...
|
||||
providers: [
|
||||
VkProvider({
|
||||
|
||||
@@ -30,7 +30,7 @@ import NextAuth from "next-auth"
|
||||
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
||||
if(req.method === "HEAD") {
|
||||
return res.status(200)
|
||||
return res.status(200).end()
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
@@ -7,7 +7,7 @@ Using a custom adapter you can connect to any database back-end or even several
|
||||
|
||||
## How to create an adapter
|
||||
|
||||
For more information about the data these methods need to manage see [models](/adapters/models).
|
||||
For more information about the data these methods need to manage see [models](https://authjs.dev/reference/adapters#models).
|
||||
|
||||
_See the code below for practical example._
|
||||
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
---
|
||||
id: refresh-token-rotation
|
||||
title: Refresh Token Rotation
|
||||
---
|
||||
|
||||
While NextAuth.js doesn't automatically handle access token rotation for OAuth providers yet, this functionality can be implemented using [callbacks](https://next-auth.js.org/configuration/callbacks).
|
||||
|
||||
## Source Code
|
||||
|
||||
A working example can be accessed [here](https://github.com/nextauthjs/next-auth-refresh-token-example).
|
||||
|
||||
## Implementation
|
||||
|
||||
### Server Side
|
||||
|
||||
Using a [JWT callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) and a [session callback](https://next-auth.js.org/configuration/callbacks#session-callback), 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.
|
||||
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import GoogleProvider from "next-auth/providers/google"
|
||||
|
||||
const GOOGLE_AUTHORIZATION_URL =
|
||||
"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: [
|
||||
GoogleProvider({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
authorization: GOOGLE_AUTHORIZATION_URL,
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, user, account }) {
|
||||
// Initial sign in
|
||||
if (account && user) {
|
||||
return {
|
||||
accessToken: account.access_token,
|
||||
accessTokenExpires: Date.now() + account.expires_at * 1000,
|
||||
refreshToken: account.refresh_token,
|
||||
user,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 }) {
|
||||
session.user = token.user
|
||||
session.accessToken = token.accessToken
|
||||
session.error = token.error
|
||||
|
||||
return session
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
We can handle this functionality as a side effect:
|
||||
|
||||
```js title="pages/home.js"
|
||||
import { signIn, useSession } from "next-auth/react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const HomePage() {
|
||||
const { data: session } = useSession();
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.error === "RefreshAccessTokenError") {
|
||||
signIn(); // Force sign in to hopefully resolve error
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
return (...)
|
||||
}
|
||||
```
|
||||
@@ -1,61 +0,0 @@
|
||||
To add role based authentication to your application, you must do three things.
|
||||
|
||||
1. Update your database schema
|
||||
2. Add the `role` to the session object
|
||||
3. Check for `role` in your pages/components
|
||||
|
||||
First modify the `user` table and add a `role` column with the type of `String?`.
|
||||
|
||||
Below is an example Prisma schema file.
|
||||
|
||||
```javascript title="/prisma/schema.prisma"
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime?
|
||||
image String?
|
||||
role String? // New Column
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Next, implement a custom session callback in the `[...nextauth].js` file, as shown below.
|
||||
|
||||
```javascript title="/pages/api/auth/[...nextauth].js"
|
||||
callbacks: {
|
||||
async session({ session, token, user }) {
|
||||
session.user.role = user.role; // Add role value to user object so it is passed along with session
|
||||
return session;
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
Going forward, when using the `getSession` hook, check that `session.user.role` matches the required role. The example below assumes the role `'admin'` is required.
|
||||
|
||||
```javascript title="/pages/admin.js"
|
||||
import { getSession } from "next-auth/react"
|
||||
|
||||
export default function Page() {
|
||||
const session = await getSession({ req })
|
||||
|
||||
if (session && session.user.role === "admin") {
|
||||
return (
|
||||
<div>
|
||||
<h1>Admin</h1>
|
||||
<p>Welcome to the Admin Portal!</p>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
<h1>You are not authorized to view this page!</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then it is up to you how you manage your roles, either through direct database access or building your own role update API.
|
||||
@@ -7,7 +7,7 @@
|
||||
"name": "next-auth-docs",
|
||||
"version": "0.2.0",
|
||||
"scripts": {
|
||||
"start": "npm run generate-providers && docusaurus start --no-open --port 8000",
|
||||
"start": "npm run generate-providers && docusaurus start --no-open",
|
||||
"dev": "npm run start",
|
||||
"build": "npm run generate-providers && docusaurus build",
|
||||
"docusaurus": "docusaurus",
|
||||
|
||||
@@ -49,28 +49,7 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Adapters",
|
||||
link: { type: "doc", id: "adapters/overview" },
|
||||
collapsed: true,
|
||||
items: [
|
||||
"adapters/models",
|
||||
"adapters/prisma",
|
||||
"adapters/fauna",
|
||||
"adapters/dynamodb",
|
||||
"adapters/firebase",
|
||||
"adapters/pouchdb",
|
||||
"adapters/mongodb",
|
||||
"adapters/neo4j",
|
||||
"adapters/typeorm",
|
||||
"adapters/sequelize",
|
||||
"adapters/supabase",
|
||||
"adapters/mikro-orm",
|
||||
"adapters/dgraph",
|
||||
"adapters/upstash-redis",
|
||||
],
|
||||
},
|
||||
"adapters",
|
||||
"warnings",
|
||||
"errors",
|
||||
"deployment",
|
||||
|
||||
@@ -1,30 +1,17 @@
|
||||
{
|
||||
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||
"headers": [
|
||||
{
|
||||
"source": "/(.*)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "X-Content-Type-Options",
|
||||
"value": "nosniff"
|
||||
},
|
||||
{
|
||||
"key": "X-Frame-Options",
|
||||
"value": "DENY"
|
||||
},
|
||||
{
|
||||
"key": "X-XSS-Protection",
|
||||
"value": "1; mode=block"
|
||||
}
|
||||
{ "key": "X-Content-Type-Options", "value": "nosniff" },
|
||||
{ "key": "X-Frame-Options", "value": "DENY" },
|
||||
{ "key": "X-XSS-Protection", "value": "1; mode=block" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/beta(.*)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "X-Robots-Tag",
|
||||
"value": "noindex"
|
||||
}
|
||||
]
|
||||
"headers": [{ "key": "X-Robots-Tag", "value": "noindex" }]
|
||||
}
|
||||
],
|
||||
"redirects": [
|
||||
@@ -57,6 +44,66 @@
|
||||
"source": "/schemas/adapters",
|
||||
"destination": "/adapters/overview",
|
||||
"permanent": true
|
||||
},
|
||||
{
|
||||
"source": "/tutorials/role-based-login-strategy",
|
||||
"destination": "https://authjs.dev/guides/basics/role-based-authentication",
|
||||
"permanent": true
|
||||
},
|
||||
{
|
||||
"source": "/adapters/firebase",
|
||||
"destination": "https://authjs.dev/reference/adapter/firebase",
|
||||
"permanent": true
|
||||
},
|
||||
{
|
||||
"source": "/adapters/dgraph",
|
||||
"destination": "https://authjs.dev/reference/adapter/dgraph",
|
||||
"permanent": true
|
||||
},
|
||||
{
|
||||
"source": "/adapters/prisma",
|
||||
"destination": "https://authjs.dev/reference/adapter/prisma",
|
||||
"permanent": true
|
||||
},
|
||||
{
|
||||
"source": "/adapters/typeorm",
|
||||
"destination": "https://authjs.dev/reference/adapter/typeorm",
|
||||
"permanent": true
|
||||
},
|
||||
{
|
||||
"source": "/adapters/mongodb",
|
||||
"destination": "https://authjs.dev/reference/adapter/mongodb",
|
||||
"permanent": true
|
||||
},
|
||||
{
|
||||
"source": "/adapters/dynamodb",
|
||||
"destination": "https://authjs.dev/reference/adapter/dynamodb",
|
||||
"permanent": true
|
||||
},
|
||||
{
|
||||
"source": "/adapters/fauna",
|
||||
"destination": "https://authjs.dev/reference/adapter/fauna",
|
||||
"permanent": true
|
||||
},
|
||||
{
|
||||
"source": "/adapters/pouchdb",
|
||||
"destination": "https://authjs.dev/reference/adapter/pouchdb",
|
||||
"permanent": true
|
||||
},
|
||||
{
|
||||
"source": "/adapters/overview",
|
||||
"destination": "https://authjs.dev/reference/adapters",
|
||||
"permanent": true
|
||||
},
|
||||
{
|
||||
"source": "/adapters/models",
|
||||
"destination": "https://authjs.dev/reference/adapters#models",
|
||||
"permanent": true
|
||||
},
|
||||
{
|
||||
"source": "/tutorials/refresh-token-rotation",
|
||||
"destination": "https://authjs.dev/guides/basics/refresh-token-rotation",
|
||||
"permanent": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ NextAuth.js comes with multiple ways of connecting to a database:
|
||||
|
||||
**This document covers the default adapter (TypeORM).**
|
||||
|
||||
See the [documentation for adapters](/adapters/overview) to learn more about using Prisma adapter or using a custom adapter.
|
||||
See the [documentation for adapters](https://authjs.dev/reference/adapters) to learn more about using Prisma adapter or using a custom adapter.
|
||||
|
||||
To learn more about databases in NextAuth.js and how they are used, check out [databases in the FAQ](/faq#databases).
|
||||
|
||||
@@ -218,4 +218,4 @@ database: "sqlite://localhost/:memory:"
|
||||
|
||||
## Other databases
|
||||
|
||||
See the [documentation for adapters](/adapters/overview) for more information on advanced configuration, including how to use NextAuth.js with other databases using a [custom adapter](/tutorials/creating-a-database-adapter).
|
||||
See the [documentation for adapters](https://authjs.dev/reference/adapters) for more information on advanced configuration, including how to use NextAuth.js with other databases using a [custom adapter](/tutorials/creating-a-database-adapter).
|
||||
|
||||
@@ -309,7 +309,7 @@ By default NextAuth.js uses a database adapter that uses TypeORM and supports My
|
||||
|
||||
You can use the `adapter` option to use the Prisma adapter - or pass in your own adapter if you want to use a database that is not supported by one of the built-in adapters.
|
||||
|
||||
See the [adapter documentation](/adapters/overview) for more information.
|
||||
See the [adapter documentation](https://authjs.dev/reference/adapters) for more information.
|
||||
|
||||
:::note
|
||||
If the `adapter` option is specified it overrides the `database` option, only specify one or the other.
|
||||
|
||||
@@ -117,7 +117,7 @@ NextAuth.js records Refresh Tokens and Access Tokens on sign in (if supplied by
|
||||
|
||||
You can then look them up from the database or persist them to the JSON Web Token.
|
||||
|
||||
Note: NextAuth.js does not currently handle Access Token rotation for OAuth providers for you, however you can check out [this tutorial](/tutorials/refresh-token-rotation) if you want to implement it.
|
||||
Note: NextAuth.js does not currently handle Access Token rotation for OAuth providers for you, however you can check out [this tutorial](https://authjs.dev/guides/basics/refresh-token-rotation) if you want to implement it.
|
||||
|
||||
### When I sign in with another account with the same email address, why are accounts not linked automatically?
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ id: typeorm-custom-models
|
||||
title: Custom models with TypeORM
|
||||
---
|
||||
|
||||
NextAuth.js provides a set of [models and schemas](/adapters/models) for the built-in TypeORM adapter that you can easily extend.
|
||||
NextAuth.js provides a set of [models and schemas](https://authjs.dev/reference/adapters#models) for the built-in TypeORM adapter that you can easily extend.
|
||||
|
||||
You can use these models with MySQL, MariaDB, Postgres, MongoDB and SQLite.
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ This is a monorepo containing the following packages / projects:
|
||||
## Getting Started
|
||||
|
||||
```
|
||||
npm install --save next-auth
|
||||
npm install next-auth
|
||||
```
|
||||
|
||||
The easiest way to continue getting started, is to follow the [getting started](https://next-auth.js.org/getting-started/example) section in our docs.
|
||||
@@ -204,8 +204,8 @@ We're happy to announce we've recently created an [OpenCollective](https://openc
|
||||
<sub>🥉 Bronze Financial Sponsor</sub>
|
||||
</td>
|
||||
<td align="center" valign="top">
|
||||
<a href="https://clerk.dev" target="_blank">
|
||||
<img width="128px" src="https://avatars.githubusercontent.com/u/49538330?s=200&v=4" alt="Prisma Logo" />
|
||||
<a href="https://clerk.com" target="_blank">
|
||||
<img width="128px" src="https://avatars.githubusercontent.com/u/49538330?s=200&v=4" alt="Clerk Logo" />
|
||||
</a><br />
|
||||
<div>Clerk</div><br />
|
||||
<sub>🥉 Bronze Financial Sponsor</sub>
|
||||
@@ -247,7 +247,7 @@ We're happy to announce we've recently created an [OpenCollective](https://openc
|
||||
## Contributing
|
||||
|
||||
We're open to all community contributions! If you'd like to contribute in any way, please first read
|
||||
our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/main/CONTRIBUTING.md).
|
||||
our [Contributing Guide](https://github.com/nextauthjs/.github/blob/main/CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next-auth",
|
||||
"version": "4.19.1",
|
||||
"version": "4.22.2",
|
||||
"description": "Authentication for Next.js",
|
||||
"homepage": "https://next-auth.js.org",
|
||||
"repository": "https://github.com/nextauthjs/next-auth.git",
|
||||
@@ -50,29 +50,27 @@
|
||||
"lint": "eslint src config tests"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"client",
|
||||
"core",
|
||||
"css",
|
||||
"jwt",
|
||||
"react",
|
||||
"lib",
|
||||
"next",
|
||||
"client",
|
||||
"providers",
|
||||
"core",
|
||||
"index.d.ts",
|
||||
"index.js",
|
||||
"adapters.d.ts",
|
||||
"middleware.d.ts",
|
||||
"middleware.js",
|
||||
"utils"
|
||||
"react",
|
||||
"src",
|
||||
"utils",
|
||||
"*.d.ts*",
|
||||
"*.js"
|
||||
],
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.16.3",
|
||||
"@panva/hkdf": "^1.0.1",
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@panva/hkdf": "^1.0.2",
|
||||
"cookie": "^0.5.0",
|
||||
"jose": "^4.9.3",
|
||||
"jose": "^4.11.4",
|
||||
"oauth": "^0.9.15",
|
||||
"openid-client": "^5.1.0",
|
||||
"openid-client": "^5.4.0",
|
||||
"preact": "^10.6.3",
|
||||
"preact-render-to-string": "^5.1.19",
|
||||
"uuid": "^8.3.2"
|
||||
@@ -109,7 +107,7 @@
|
||||
"@types/node": "^17.0.42",
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
"@types/oauth": "^0.9.1",
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react": "18.0.37",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"babel-plugin-jsx-pragmatic": "^1.0.2",
|
||||
@@ -120,7 +118,7 @@
|
||||
"jest-environment-jsdom": "^28.1.1",
|
||||
"jest-watch-typeahead": "^1.1.0",
|
||||
"msw": "^0.42.3",
|
||||
"next": "13.0.6",
|
||||
"next": "13.3.0",
|
||||
"postcss": "^8.4.14",
|
||||
"postcss-cli": "^9.1.0",
|
||||
"postcss-nested": "^5.0.6",
|
||||
|
||||
@@ -59,24 +59,7 @@ export interface VerificationToken {
|
||||
* [Adapters Overview](https://next-auth.js.org/adapters/overview) |
|
||||
* [Create a custom adapter](https://next-auth.js.org/tutorials/creating-a-database-adapter)
|
||||
*/
|
||||
export type Adapter<WithVerificationToken = boolean> = DefaultAdapter &
|
||||
(WithVerificationToken extends true
|
||||
? {
|
||||
createVerificationToken: (
|
||||
verificationToken: VerificationToken
|
||||
) => Awaitable<VerificationToken | null | undefined>
|
||||
/**
|
||||
* Return verification token from the database
|
||||
* and delete it so it cannot be used again.
|
||||
*/
|
||||
useVerificationToken: (params: {
|
||||
identifier: string
|
||||
token: string
|
||||
}) => Awaitable<VerificationToken | null>
|
||||
}
|
||||
: {})
|
||||
|
||||
export interface DefaultAdapter {
|
||||
export interface Adapter {
|
||||
createUser: (user: Omit<AdapterUser, "id">) => Awaitable<AdapterUser>
|
||||
getUser: (id: string) => Awaitable<AdapterUser | null>
|
||||
getUserByEmail: (email: string) => Awaitable<AdapterUser | null>
|
||||
@@ -84,7 +67,7 @@ export interface DefaultAdapter {
|
||||
getUserByAccount: (
|
||||
providerAccountId: Pick<AdapterAccount, "provider" | "providerAccountId">
|
||||
) => Awaitable<AdapterUser | null>
|
||||
updateUser: (user: Partial<AdapterUser>) => Awaitable<AdapterUser>
|
||||
updateUser: (user: Partial<AdapterUser> & Pick<AdapterUser, 'id'>) => Awaitable<AdapterUser>
|
||||
/** @todo Implement */
|
||||
deleteUser?: (
|
||||
userId: string
|
||||
|
||||
@@ -18,8 +18,8 @@ export interface AuthClientConfig {
|
||||
}
|
||||
|
||||
export interface CtxOrReq {
|
||||
req?: IncomingMessage
|
||||
ctx?: { req: IncomingMessage }
|
||||
req?: Partial<IncomingMessage> & { body?: any }
|
||||
ctx?: { req: Partial<IncomingMessage> & { body?: any } }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,9 +37,18 @@ export async function fetchData<T = any>(
|
||||
): Promise<T | null> {
|
||||
const url = `${apiBaseUrl(__NEXTAUTH)}/${path}`
|
||||
try {
|
||||
const options = req?.headers.cookie
|
||||
? { headers: { cookie: req.headers.cookie } }
|
||||
: {}
|
||||
const options: RequestInit = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(req?.headers?.cookie ? { cookie: req.headers.cookie } : {}),
|
||||
},
|
||||
}
|
||||
|
||||
if (req?.body) {
|
||||
options.body = JSON.stringify(req.body)
|
||||
options.method = "POST"
|
||||
}
|
||||
|
||||
const res = await fetch(url, options)
|
||||
const data = await res.json()
|
||||
if (!res.ok) throw data
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import logger, { setLogger } from "../utils/logger"
|
||||
import { detectHost } from "../utils/detect-host"
|
||||
import { detectOrigin } from "../utils/detect-origin"
|
||||
import * as routes from "./routes"
|
||||
import renderPage from "./pages"
|
||||
import { init } from "./init"
|
||||
@@ -13,7 +13,7 @@ import { parse as parseCookie } from "cookie"
|
||||
|
||||
export interface RequestInternal {
|
||||
/** @default "http://localhost:3000" */
|
||||
host?: string
|
||||
origin?: string
|
||||
method?: string
|
||||
cookies?: Partial<Record<string, string>>
|
||||
headers?: Record<string, any>
|
||||
@@ -70,10 +70,18 @@ async function toInternalRequest(
|
||||
cookies: parseCookie(req.headers.get("cookie") ?? ""),
|
||||
providerId: nextauth[1],
|
||||
error: url.searchParams.get("error") ?? nextauth[1],
|
||||
host: detectHost(headers["x-forwarded-host"] ?? headers.host),
|
||||
origin: detectOrigin(
|
||||
headers["x-forwarded-host"] ?? headers.host,
|
||||
headers["x-forwarded-proto"]
|
||||
),
|
||||
query,
|
||||
}
|
||||
}
|
||||
|
||||
const { headers } = req
|
||||
const host = headers?.["x-forwarded-host"] ?? headers?.host
|
||||
req.origin = detectOrigin(host, headers?.["x-forwarded-proto"])
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
@@ -132,7 +140,7 @@ export async function AuthHandler<
|
||||
authOptions,
|
||||
action,
|
||||
providerId,
|
||||
host: req.host,
|
||||
origin: req.origin,
|
||||
callbackUrl: req.body?.callbackUrl ?? req.query?.callbackUrl,
|
||||
csrfToken: req.body?.csrfToken,
|
||||
cookies: req.cookies,
|
||||
@@ -231,7 +239,7 @@ export async function AuthHandler<
|
||||
} else if (method === "POST") {
|
||||
switch (action) {
|
||||
case "signin":
|
||||
// Verified CSRF Token required for all sign in routes
|
||||
// Verified CSRF Token required for all sign-in routes
|
||||
if (options.csrfTokenVerified && options.provider) {
|
||||
const signin = await routes.signin({
|
||||
query: req.query,
|
||||
@@ -274,7 +282,7 @@ export async function AuthHandler<
|
||||
return { ...callback, cookies }
|
||||
}
|
||||
break
|
||||
case "_log":
|
||||
case "_log": {
|
||||
if (authOptions.logger) {
|
||||
try {
|
||||
const { code, level, ...metadata } = req.body ?? {}
|
||||
@@ -285,6 +293,24 @@ export async function AuthHandler<
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
case "session": {
|
||||
// Verified CSRF Token required for session updates
|
||||
if (options.csrfTokenVerified) {
|
||||
const session = await routes.session({
|
||||
options,
|
||||
sessionStore,
|
||||
newSession: req.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, body: {} as any, cookies }
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import type { InternalOptions } from "./types"
|
||||
import parseUrl from "../utils/parse-url"
|
||||
|
||||
interface InitParams {
|
||||
host?: string
|
||||
origin?: string
|
||||
authOptions: AuthOptions
|
||||
providerId?: string
|
||||
action: InternalOptions["action"]
|
||||
@@ -33,7 +33,7 @@ export async function init({
|
||||
authOptions,
|
||||
providerId,
|
||||
action,
|
||||
host,
|
||||
origin,
|
||||
cookies: reqCookies,
|
||||
callbackUrl: reqCallbackUrl,
|
||||
csrfToken: reqCsrfToken,
|
||||
@@ -42,7 +42,7 @@ export async function init({
|
||||
options: InternalOptions
|
||||
cookies: cookie.Cookie[]
|
||||
}> {
|
||||
const url = parseUrl(host)
|
||||
const url = parseUrl(origin)
|
||||
|
||||
const secret = createSecret({ authOptions, url })
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ export function assertConfig(params: {
|
||||
const warnings: WarningCode[] = []
|
||||
|
||||
if (!warned) {
|
||||
if (!req.host) warnings.push("NEXTAUTH_URL")
|
||||
if (!req.origin) warnings.push("NEXTAUTH_URL")
|
||||
|
||||
// TODO: Make this throw an error in next major. This will also get rid of `NODE_ENV`
|
||||
if (!options.secret && process.env.NODE_ENV !== "production")
|
||||
@@ -70,7 +70,7 @@ export function assertConfig(params: {
|
||||
|
||||
const callbackUrlParam = req.query?.callbackUrl as string | undefined
|
||||
|
||||
const url = parseUrl(req.host)
|
||||
const url = parseUrl(req.origin)
|
||||
|
||||
if (callbackUrlParam && !isValidHttpUrl(callbackUrlParam, url.base)) {
|
||||
return new InvalidCallbackUrl(
|
||||
|
||||
@@ -12,6 +12,7 @@ export default async function getAdapterUserFromEmail({
|
||||
email: string
|
||||
adapter: InternalOptions<"email">["adapter"]
|
||||
}): Promise<AdapterUser> {
|
||||
// @ts-expect-error -- adapter is checked to be defined in `init`
|
||||
const { getUserByEmail } = adapter
|
||||
const adapterUser = email ? await getUserByEmail(email) : null
|
||||
if (adapterUser) return adapterUser
|
||||
|
||||
@@ -36,7 +36,8 @@ export default async function email(
|
||||
theme,
|
||||
}),
|
||||
// Save in database
|
||||
adapter.createVerificationToken({
|
||||
// @ts-expect-error -- adapter is checked to be defined in `init`
|
||||
adapter.createVerificationToken?.({
|
||||
identifier,
|
||||
token: hashToken(token, options),
|
||||
expires,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { openidClient } from "./client"
|
||||
import { oAuth1Client, oAuth1TokenStore } from "./client-legacy"
|
||||
import { createState } from "./state-handler"
|
||||
import { createNonce } from "./nonce-handler"
|
||||
import { createPKCE } from "./pkce-handler"
|
||||
import * as checks from "./checks"
|
||||
|
||||
import type { AuthorizationParameters } from "openid-client"
|
||||
import type { InternalOptions } from "../../types"
|
||||
@@ -54,24 +52,9 @@ export default async function getAuthorizationUrl({
|
||||
const authorizationParams: AuthorizationParameters = params
|
||||
const cookies: Cookie[] = []
|
||||
|
||||
const state = await createState(options)
|
||||
if (state) {
|
||||
authorizationParams.state = state.value
|
||||
cookies.push(state.cookie)
|
||||
}
|
||||
|
||||
const nonce = await createNonce(options)
|
||||
if (nonce) {
|
||||
authorizationParams.nonce = nonce.value
|
||||
cookies.push(nonce.cookie)
|
||||
}
|
||||
|
||||
const pkce = await createPKCE(options)
|
||||
if (pkce) {
|
||||
authorizationParams.code_challenge = pkce.code_challenge
|
||||
authorizationParams.code_challenge_method = pkce.code_challenge_method
|
||||
cookies.push(pkce.cookie)
|
||||
}
|
||||
await checks.state.create(options, cookies, authorizationParams)
|
||||
await checks.pkce.create(options, cookies, authorizationParams)
|
||||
await checks.nonce.create(options, cookies, authorizationParams)
|
||||
|
||||
const url = client.authorizationUrl(authorizationParams)
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { TokenSet } from "openid-client"
|
||||
import { openidClient } from "./client"
|
||||
import { oAuth1Client, oAuth1TokenStore } from "./client-legacy"
|
||||
import { useState } from "./state-handler"
|
||||
import { usePKCECodeVerifier } from "./pkce-handler"
|
||||
import { useNonce } from "./nonce-handler"
|
||||
import * as _checks from "./checks"
|
||||
import { OAuthCallbackError } from "../../errors"
|
||||
|
||||
import type { CallbackParamsType, OpenIDCallbackChecks } from "openid-client"
|
||||
import type { CallbackParamsType } from "openid-client"
|
||||
import type { LoggerInstance, Profile } from "../../.."
|
||||
import type { OAuthChecks, OAuthConfig } from "../../../providers"
|
||||
import type { InternalOptions } from "../../types"
|
||||
@@ -73,24 +71,9 @@ export default async function oAuthCallback(params: {
|
||||
const checks: OAuthChecks = {}
|
||||
const resCookies: Cookie[] = []
|
||||
|
||||
const state = await useState(cookies?.[options.cookies.state.name], options)
|
||||
if (state) {
|
||||
checks.state = state.value
|
||||
resCookies.push(state.cookie)
|
||||
}
|
||||
|
||||
const nonce = await useNonce(cookies?.[options.cookies.nonce.name], options)
|
||||
if (nonce && provider.idToken) {
|
||||
;(checks as OpenIDCallbackChecks).nonce = nonce.value
|
||||
resCookies.push(nonce.cookie)
|
||||
}
|
||||
|
||||
const codeVerifier = cookies?.[options.cookies.pkceCodeVerifier.name]
|
||||
const pkce = await usePKCECodeVerifier(codeVerifier, options)
|
||||
if (pkce) {
|
||||
checks.code_verifier = pkce.codeVerifier
|
||||
resCookies.push(pkce.cookie)
|
||||
}
|
||||
await _checks.state.use(cookies, resCookies, options, checks)
|
||||
await _checks.pkce.use(cookies, resCookies, options, checks)
|
||||
await _checks.nonce.use(cookies, resCookies, options, checks)
|
||||
|
||||
const params: CallbackParamsType = {
|
||||
...client.callbackParams({
|
||||
|
||||
181
packages/next-auth/src/core/lib/oauth/checks.ts
Normal file
181
packages/next-auth/src/core/lib/oauth/checks.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import {
|
||||
AuthorizationParameters,
|
||||
generators,
|
||||
OpenIDCallbackChecks,
|
||||
} from "openid-client"
|
||||
import * as jwt from "../../../jwt"
|
||||
|
||||
import type { RequestInternal } from "../.."
|
||||
import type { OAuthChecks } from "../../../providers"
|
||||
import type { CookiesOptions, InternalOptions } from "../../types"
|
||||
import type { Cookie } from "../cookie"
|
||||
|
||||
/** Returns a signed cookie. */
|
||||
export async function signCookie(
|
||||
type: keyof CookiesOptions,
|
||||
value: string,
|
||||
maxAge: number,
|
||||
options: InternalOptions<"oauth">
|
||||
): Promise<Cookie> {
|
||||
const { cookies, logger } = options
|
||||
|
||||
logger.debug(`CREATE_${type.toUpperCase()}`, { value, maxAge })
|
||||
|
||||
const expires = new Date()
|
||||
expires.setTime(expires.getTime() + maxAge * 1000)
|
||||
return {
|
||||
name: cookies[type].name,
|
||||
value: await jwt.encode({ ...options.jwt, maxAge, token: { value } }),
|
||||
options: { ...cookies[type].options, expires },
|
||||
}
|
||||
}
|
||||
|
||||
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
|
||||
export const PKCE_CODE_CHALLENGE_METHOD = "S256"
|
||||
export const pkce = {
|
||||
async create(
|
||||
options: InternalOptions<"oauth">,
|
||||
cookies: Cookie[],
|
||||
resParams: AuthorizationParameters
|
||||
) {
|
||||
if (!options.provider?.checks?.includes("pkce")) return
|
||||
const code_verifier = generators.codeVerifier()
|
||||
const value = generators.codeChallenge(code_verifier)
|
||||
resParams.code_challenge = value
|
||||
resParams.code_challenge_method = PKCE_CODE_CHALLENGE_METHOD
|
||||
|
||||
const maxAge =
|
||||
options.cookies.pkceCodeVerifier.options.maxAge ?? PKCE_MAX_AGE
|
||||
|
||||
cookies.push(
|
||||
await signCookie("pkceCodeVerifier", code_verifier, maxAge, options)
|
||||
)
|
||||
},
|
||||
/**
|
||||
* Returns code_verifier if the provider is configured to use PKCE,
|
||||
* and clears the container cookie afterwards.
|
||||
* An error is thrown if the code_verifier is missing or invalid.
|
||||
* @see https://www.rfc-editor.org/rfc/rfc7636
|
||||
* @see https://danielfett.de/2020/05/16/pkce-vs-nonce-equivalent-or-not/#pkce
|
||||
*/
|
||||
async use(
|
||||
cookies: RequestInternal["cookies"],
|
||||
resCookies: Cookie[],
|
||||
options: InternalOptions<"oauth">,
|
||||
checks: OAuthChecks
|
||||
): Promise<string | undefined> {
|
||||
if (!options.provider?.checks?.includes("pkce")) return
|
||||
|
||||
const codeVerifier = cookies?.[options.cookies.pkceCodeVerifier.name]
|
||||
|
||||
if (!codeVerifier)
|
||||
throw new TypeError("PKCE code_verifier cookie was missing.")
|
||||
|
||||
const value = (await jwt.decode({
|
||||
...options.jwt,
|
||||
token: codeVerifier,
|
||||
})) as any
|
||||
|
||||
if (!value?.value)
|
||||
throw new TypeError("PKCE code_verifier value could not be parsed.")
|
||||
|
||||
resCookies.push({
|
||||
name: options.cookies.pkceCodeVerifier.name,
|
||||
value: "",
|
||||
options: { ...options.cookies.pkceCodeVerifier.options, maxAge: 0 },
|
||||
})
|
||||
|
||||
checks.code_verifier = value.value
|
||||
},
|
||||
}
|
||||
|
||||
const STATE_MAX_AGE = 60 * 15 // 15 minutes in seconds
|
||||
export const state = {
|
||||
async create(
|
||||
options: InternalOptions<"oauth">,
|
||||
cookies: Cookie[],
|
||||
resParams: AuthorizationParameters
|
||||
) {
|
||||
if (!options.provider.checks?.includes("state")) return
|
||||
const value = generators.state()
|
||||
resParams.state = value
|
||||
const maxAge = options.cookies.state.options.maxAge ?? STATE_MAX_AGE
|
||||
cookies.push(await signCookie("state", value, maxAge, options))
|
||||
},
|
||||
/**
|
||||
* Returns state if the provider is configured to use state,
|
||||
* and clears the container cookie afterwards.
|
||||
* An error is thrown if the state is missing or invalid.
|
||||
* @see https://www.rfc-editor.org/rfc/rfc6749#section-10.12
|
||||
* @see https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1
|
||||
*/
|
||||
async use(
|
||||
cookies: RequestInternal["cookies"],
|
||||
resCookies: Cookie[],
|
||||
options: InternalOptions<"oauth">,
|
||||
checks: OAuthChecks
|
||||
) {
|
||||
if (!options.provider.checks?.includes("state")) return
|
||||
|
||||
const state = cookies?.[options.cookies.state.name]
|
||||
|
||||
if (!state) throw new TypeError("State cookie was missing.")
|
||||
|
||||
const value = (await jwt.decode({ ...options.jwt, token: state })) as any
|
||||
|
||||
if (!value?.value) throw new TypeError("State value could not be parsed.")
|
||||
|
||||
resCookies.push({
|
||||
name: options.cookies.state.name,
|
||||
value: "",
|
||||
options: { ...options.cookies.state.options, maxAge: 0 },
|
||||
})
|
||||
|
||||
checks.state = value.value
|
||||
},
|
||||
}
|
||||
|
||||
const NONCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
|
||||
export const nonce = {
|
||||
async create(
|
||||
options: InternalOptions<"oauth">,
|
||||
cookies: Cookie[],
|
||||
resParams: AuthorizationParameters
|
||||
) {
|
||||
if (!options.provider.checks?.includes("nonce")) return
|
||||
const value = generators.nonce()
|
||||
resParams.nonce = value
|
||||
const maxAge = options.cookies.nonce.options.maxAge ?? NONCE_MAX_AGE
|
||||
cookies.push(await signCookie("nonce", value, maxAge, options))
|
||||
},
|
||||
/**
|
||||
* Returns nonce if the provider is configured to use nonce,
|
||||
* and clears the container cookie afterwards.
|
||||
* An error is thrown if the nonce is missing or invalid.
|
||||
* @see https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes
|
||||
* @see https://danielfett.de/2020/05/16/pkce-vs-nonce-equivalent-or-not/#nonce
|
||||
*/
|
||||
async use(
|
||||
cookies: RequestInternal["cookies"],
|
||||
resCookies: Cookie[],
|
||||
options: InternalOptions<"oauth">,
|
||||
checks: OpenIDCallbackChecks
|
||||
): Promise<string | undefined> {
|
||||
if (!options.provider?.checks?.includes("nonce")) return
|
||||
|
||||
const nonce = cookies?.[options.cookies.nonce.name]
|
||||
if (!nonce) throw new TypeError("Nonce cookie was missing.")
|
||||
|
||||
const value = (await jwt.decode({ ...options.jwt, token: nonce })) as any
|
||||
|
||||
if (!value?.value) throw new TypeError("Nonce value could not be parsed.")
|
||||
|
||||
resCookies.push({
|
||||
name: options.cookies.nonce.name,
|
||||
value: "",
|
||||
options: { ...options.cookies.nonce.options, maxAge: 0 },
|
||||
})
|
||||
|
||||
checks.nonce = value.value
|
||||
},
|
||||
}
|
||||
@@ -25,6 +25,7 @@ export async function openidClient(
|
||||
authorization_endpoint: provider.authorization?.url,
|
||||
token_endpoint: provider.token?.url,
|
||||
userinfo_endpoint: provider.userinfo?.url,
|
||||
jwks_uri: provider.jwks_endpoint,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import * as jwt from "../../../jwt"
|
||||
import { generators } from "openid-client"
|
||||
import type { InternalOptions } from "../../types"
|
||||
import type { Cookie } from "../cookie"
|
||||
|
||||
const NONCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
|
||||
|
||||
/**
|
||||
* Returns nonce if the provider supports it
|
||||
* and saves it in a cookie */
|
||||
export async function createNonce(options: InternalOptions<"oauth">): Promise<
|
||||
| undefined
|
||||
| {
|
||||
value: string
|
||||
cookie: Cookie
|
||||
}
|
||||
> {
|
||||
const { cookies, logger, provider } = options
|
||||
if (!provider.checks?.includes("nonce")) {
|
||||
// Provider does not support nonce, return nothing.
|
||||
return
|
||||
}
|
||||
|
||||
const nonce = generators.nonce()
|
||||
|
||||
const expires = new Date()
|
||||
expires.setTime(expires.getTime() + NONCE_MAX_AGE * 1000)
|
||||
|
||||
// Encrypt nonce and save it to an encrypted cookie
|
||||
const encryptedNonce = await jwt.encode({
|
||||
...options.jwt,
|
||||
maxAge: NONCE_MAX_AGE,
|
||||
token: { nonce },
|
||||
})
|
||||
|
||||
logger.debug("CREATE_ENCRYPTED_NONCE", {
|
||||
nonce,
|
||||
maxAge: NONCE_MAX_AGE,
|
||||
})
|
||||
|
||||
return {
|
||||
cookie: {
|
||||
name: cookies.nonce.name,
|
||||
value: encryptedNonce,
|
||||
options: { ...cookies.nonce.options, expires },
|
||||
},
|
||||
value: nonce,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns nonce from if the provider supports nonce,
|
||||
* and clears the container cookie afterwards.
|
||||
*/
|
||||
export async function useNonce(
|
||||
nonce: string | undefined,
|
||||
options: InternalOptions<"oauth">
|
||||
): Promise<{ value: string; cookie: Cookie } | undefined> {
|
||||
const { cookies, provider } = options
|
||||
|
||||
if (!provider?.checks?.includes("nonce") || !nonce) {
|
||||
return
|
||||
}
|
||||
|
||||
const value = (await jwt.decode({...options.jwt, token: nonce })) as any
|
||||
|
||||
return {
|
||||
value: value?.nonce ?? undefined,
|
||||
cookie: {
|
||||
name: cookies.nonce.name,
|
||||
value: "",
|
||||
options: { ...cookies.nonce.options, maxAge: 0 },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import * as jwt from "../../../jwt"
|
||||
import { generators } from "openid-client"
|
||||
import type { InternalOptions } from "../../types"
|
||||
import type { Cookie } from "../cookie"
|
||||
|
||||
const PKCE_CODE_CHALLENGE_METHOD = "S256"
|
||||
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
|
||||
|
||||
/**
|
||||
* Returns `code_challenge` and `code_challenge_method`
|
||||
* and saves them in a cookie.
|
||||
*/
|
||||
export async function createPKCE(options: InternalOptions<"oauth">): Promise<
|
||||
| undefined
|
||||
| {
|
||||
code_challenge: string
|
||||
code_challenge_method: "S256"
|
||||
cookie: Cookie
|
||||
}
|
||||
> {
|
||||
const { cookies, logger, provider } = options
|
||||
if (!provider.checks?.includes("pkce")) {
|
||||
// Provider does not support PKCE, return nothing.
|
||||
return
|
||||
}
|
||||
const code_verifier = generators.codeVerifier()
|
||||
const code_challenge = generators.codeChallenge(code_verifier)
|
||||
|
||||
const maxAge = cookies.pkceCodeVerifier.options.maxAge ?? PKCE_MAX_AGE
|
||||
|
||||
const expires = new Date()
|
||||
expires.setTime(expires.getTime() + maxAge * 1000)
|
||||
|
||||
// Encrypt code_verifier and save it to an encrypted cookie
|
||||
const encryptedCodeVerifier = await jwt.encode({
|
||||
...options.jwt,
|
||||
maxAge,
|
||||
token: { code_verifier },
|
||||
})
|
||||
|
||||
logger.debug("CREATE_PKCE_CHALLENGE_VERIFIER", {
|
||||
code_challenge,
|
||||
code_challenge_method: PKCE_CODE_CHALLENGE_METHOD,
|
||||
code_verifier,
|
||||
maxAge,
|
||||
})
|
||||
|
||||
return {
|
||||
code_challenge,
|
||||
code_challenge_method: PKCE_CODE_CHALLENGE_METHOD,
|
||||
cookie: {
|
||||
name: cookies.pkceCodeVerifier.name,
|
||||
value: encryptedCodeVerifier,
|
||||
options: { ...cookies.pkceCodeVerifier.options, expires },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns code_verifier if provider uses PKCE,
|
||||
* and clears the container cookie afterwards.
|
||||
*/
|
||||
export async function usePKCECodeVerifier(
|
||||
codeVerifier: string | undefined,
|
||||
options: InternalOptions<"oauth">
|
||||
): Promise<{ codeVerifier: string; cookie: Cookie } | undefined> {
|
||||
const { cookies, provider } = options
|
||||
|
||||
if (!provider?.checks?.includes("pkce") || !codeVerifier) {
|
||||
return
|
||||
}
|
||||
|
||||
const pkce = (await jwt.decode({
|
||||
...options.jwt,
|
||||
token: codeVerifier,
|
||||
})) as any
|
||||
|
||||
return {
|
||||
codeVerifier: pkce?.code_verifier ?? undefined,
|
||||
cookie: {
|
||||
name: cookies.pkceCodeVerifier.name,
|
||||
value: "",
|
||||
options: { ...cookies.pkceCodeVerifier.options, maxAge: 0 },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import { generators } from "openid-client"
|
||||
|
||||
import type { InternalOptions } from "../../types"
|
||||
import type { Cookie } from "../cookie"
|
||||
|
||||
const STATE_MAX_AGE = 60 * 15 // 15 minutes in seconds
|
||||
|
||||
/** Returns state if the provider supports it */
|
||||
export async function createState(
|
||||
options: InternalOptions<"oauth">
|
||||
): Promise<{ cookie: Cookie; value: string } | undefined> {
|
||||
const { logger, provider, jwt, cookies } = options
|
||||
|
||||
if (!provider.checks?.includes("state")) {
|
||||
// Provider does not support state, return nothing
|
||||
return
|
||||
}
|
||||
|
||||
const state = generators.state()
|
||||
const maxAge = cookies.state.options.maxAge ?? STATE_MAX_AGE
|
||||
|
||||
const encodedState = await jwt.encode({
|
||||
...jwt,
|
||||
maxAge,
|
||||
token: { state },
|
||||
})
|
||||
|
||||
logger.debug("CREATE_STATE", { state, maxAge })
|
||||
|
||||
const expires = new Date()
|
||||
expires.setTime(expires.getTime() + maxAge * 1000)
|
||||
return {
|
||||
value: state,
|
||||
cookie: {
|
||||
name: cookies.state.name,
|
||||
value: encodedState,
|
||||
options: { ...cookies.state.options, expires },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns state from if the provider supports states,
|
||||
* and clears the container cookie afterwards.
|
||||
*/
|
||||
export async function useState(
|
||||
state: string | undefined,
|
||||
options: InternalOptions<"oauth">
|
||||
): Promise<{ value: string; cookie: Cookie } | undefined> {
|
||||
const { cookies, provider, jwt } = options
|
||||
|
||||
if (!provider.checks?.includes("state") || !state) return
|
||||
|
||||
const value = (await jwt.decode({ ...options.jwt, token: state })) as any
|
||||
|
||||
return {
|
||||
value: value?.state ?? undefined,
|
||||
cookie: {
|
||||
name: cookies.state.name,
|
||||
value: "",
|
||||
options: { ...cookies.pkceCodeVerifier.options, maxAge: 0 },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
import { merge } from "../../utils/merge"
|
||||
|
||||
import type { InternalProvider } from "../types"
|
||||
import type {
|
||||
OAuthConfigInternal,
|
||||
OAuthConfig,
|
||||
Provider,
|
||||
} from "../../providers"
|
||||
import type { InternalProvider, OAuthConfigInternal } from "../types"
|
||||
import type { OAuthConfig, Provider } from "../../providers"
|
||||
import type { InternalUrl } from "../../utils/parse-url"
|
||||
|
||||
/**
|
||||
|
||||
@@ -107,6 +107,7 @@ export default function SigninPage(props: SignInServerPageParams) {
|
||||
/>
|
||||
)}
|
||||
<div className="card">
|
||||
{theme.logo && <img src={theme.logo} alt="Logo" className="logo" />}
|
||||
{error && (
|
||||
<div className="error">
|
||||
<p>{error}</p>
|
||||
|
||||
@@ -130,6 +130,7 @@ export default async function callback(params: {
|
||||
account,
|
||||
profile: OAuthProfile,
|
||||
isNewUser,
|
||||
trigger: isNewUser ? "signUp" : "signIn",
|
||||
})
|
||||
|
||||
// Encode token
|
||||
@@ -219,7 +220,6 @@ export default async function callback(params: {
|
||||
|
||||
const profile = await getAdapterUserFromEmail({
|
||||
email: identifier,
|
||||
// @ts-expect-error -- Verified in `assertConfig`. adapter: Adapter<true>
|
||||
adapter,
|
||||
})
|
||||
|
||||
@@ -269,6 +269,7 @@ export default async function callback(params: {
|
||||
user,
|
||||
account,
|
||||
isNewUser,
|
||||
trigger: isNewUser ? "signUp" : "signIn",
|
||||
})
|
||||
|
||||
// Encode token
|
||||
@@ -393,6 +394,7 @@ export default async function callback(params: {
|
||||
// @ts-expect-error
|
||||
account,
|
||||
isNewUser: false,
|
||||
trigger: "signIn",
|
||||
})
|
||||
|
||||
// Encode token
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { fromDate } from "../lib/utils"
|
||||
|
||||
import type { Adapter } from "../../adapters"
|
||||
import type { InternalOptions } from "../types"
|
||||
import type { ResponseInternal } from ".."
|
||||
import type { Session } from "../.."
|
||||
@@ -9,6 +8,8 @@ import type { SessionStore } from "../lib/cookie"
|
||||
interface SessionParams {
|
||||
options: InternalOptions
|
||||
sessionStore: SessionStore
|
||||
isUpdate?: boolean
|
||||
newSession?: any
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -19,7 +20,7 @@ interface SessionParams {
|
||||
export default async function session(
|
||||
params: SessionParams
|
||||
): Promise<ResponseInternal<Session | {}>> {
|
||||
const { options, sessionStore } = params
|
||||
const { options, sessionStore, newSession, isUpdate } = params
|
||||
const {
|
||||
adapter,
|
||||
jwt,
|
||||
@@ -41,31 +42,37 @@ export default async function session(
|
||||
|
||||
if (sessionStrategy === "jwt") {
|
||||
try {
|
||||
const decodedToken = await jwt.decode({
|
||||
...jwt,
|
||||
token: sessionToken,
|
||||
const decodedToken = await jwt.decode({ ...jwt, token: sessionToken })
|
||||
|
||||
if (!decodedToken) throw new Error("JWT invalid")
|
||||
|
||||
// @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 })
|
||||
// @ts-expect-error
|
||||
const newSession = await callbacks.session({ session, token })
|
||||
// @ts-expect-error Property 'user' is missing in type
|
||||
const updatedSession = await callbacks.session({
|
||||
session: {
|
||||
user: {
|
||||
name: decodedToken?.name,
|
||||
email: decodedToken?.email,
|
||||
image: decodedToken?.picture,
|
||||
},
|
||||
expires: newExpires.toISOString(),
|
||||
},
|
||||
token,
|
||||
})
|
||||
|
||||
// Return session payload as response
|
||||
response.body = newSession
|
||||
response.body = updatedSession
|
||||
|
||||
// Refresh JWT expiry by re-signing it, with an updated expiry date
|
||||
const newToken = await jwt.encode({
|
||||
@@ -81,7 +88,7 @@ export default async function session(
|
||||
|
||||
response.cookies?.push(...sessionCookies)
|
||||
|
||||
await events.session?.({ session: newSession, token })
|
||||
await events.session?.({ session: updatedSession, token })
|
||||
} catch (error) {
|
||||
// If JWT not verifiable, make sure the cookie for it is removed and return empty object
|
||||
logger.error("JWT_SESSION_ERROR", error as Error)
|
||||
@@ -90,8 +97,9 @@ export default async function session(
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
// @ts-expect-error -- adapter is checked to be defined in `init`
|
||||
const { getSessionAndUser, deleteSession, updateSession } =
|
||||
adapter as Adapter
|
||||
adapter
|
||||
let userAndSession = await getSessionAndUser(sessionToken)
|
||||
|
||||
// If session has expired, clean up the database
|
||||
@@ -123,19 +131,18 @@ export default async function session(
|
||||
}
|
||||
|
||||
// Pass Session through to the session callback
|
||||
// @ts-expect-error
|
||||
|
||||
// @ts-expect-error Property 'token' is missing in type
|
||||
const sessionPayload = await callbacks.session({
|
||||
// By default, only exposes a limited subset of information to the client
|
||||
// as needed for presentation purposes (e.g. "you are logged in as...").
|
||||
session: {
|
||||
user: {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
image: user.image,
|
||||
},
|
||||
user: { name: user.name, email: user.email, image: user.image },
|
||||
expires: session.expires.toISOString(),
|
||||
},
|
||||
user,
|
||||
newSession,
|
||||
...(isUpdate ? { trigger: "update" } : {}),
|
||||
})
|
||||
|
||||
// Return session payload as response
|
||||
|
||||
@@ -57,7 +57,6 @@ export default async function signin(params: {
|
||||
|
||||
const user = await getAdapterUserFromEmail({
|
||||
email,
|
||||
// @ts-expect-error -- Verified in `assertConfig`. adapter: Adapter<true>
|
||||
adapter: options.adapter,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { Adapter } from "../../adapters"
|
||||
import type { InternalOptions } from "../types"
|
||||
import type { ResponseInternal } from ".."
|
||||
import type { SessionStore } from "../lib/cookie"
|
||||
@@ -28,7 +27,8 @@ export default async function signout(params: {
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const session = await (adapter as Adapter).deleteSession(sessionToken)
|
||||
// @ts-expect-error -- adapter is checked to be defined in `init`
|
||||
const session = await adapter.deleteSession(sessionToken)
|
||||
// Dispatch signout event
|
||||
// @ts-expect-error
|
||||
await events.signOut?.({ session })
|
||||
|
||||
@@ -5,7 +5,10 @@ import type {
|
||||
ProviderType,
|
||||
EmailConfig,
|
||||
CredentialsConfig,
|
||||
OAuthConfigInternal,
|
||||
OAuthConfig,
|
||||
AuthorizationEndpointHandler,
|
||||
TokenEndpointHandler,
|
||||
UserinfoEndpointHandler,
|
||||
} from "../providers"
|
||||
import type { TokenSetParameters } from "openid-client"
|
||||
import type { JWT, JWTOptions } from "../jwt"
|
||||
@@ -49,8 +52,8 @@ export interface AuthOptions {
|
||||
*/
|
||||
secret?: string
|
||||
/**
|
||||
* Configure your session like if you want to use JWT or a database,
|
||||
* how long until an idle session expires, or to throttle write operations in case you are using a database.
|
||||
* Configure your session settings, such as determining whether to use JWT or a database,
|
||||
* setting the idle session expiration duration, or implementing write operation throttling for database usage.
|
||||
* * **Default value**: See the documentation page
|
||||
* * **Required**: No
|
||||
*
|
||||
@@ -313,11 +316,25 @@ export interface CallbacksOptions<P = Profile, A = Account> {
|
||||
* [`getSession`](https://next-auth.js.org/getting-started/client#getsession) |
|
||||
*
|
||||
*/
|
||||
session: (params: {
|
||||
session: Session
|
||||
user: User | AdapterUser
|
||||
token: JWT
|
||||
}) => Awaitable<Session>
|
||||
session: (
|
||||
params:
|
||||
| {
|
||||
session: Session
|
||||
/** Available when {@link SessionOptions.strategy} is set to `"jwt"` */
|
||||
token: JWT
|
||||
/** Available when {@link SessionOptions.strategy} is set to `"database"`. */
|
||||
user: AdapterUser
|
||||
} & {
|
||||
/**
|
||||
* Available when using {@link SessionOptions.strategy} `"database"`, 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.
|
||||
*/
|
||||
newSession: any
|
||||
trigger: "update"
|
||||
}
|
||||
) => Awaitable<Session | DefaultSession>
|
||||
/**
|
||||
* This callback is called whenever a JSON Web Token is created (i.e. at sign in)
|
||||
* or updated (i.e whenever a session is accessed in the client).
|
||||
@@ -325,18 +342,61 @@ export interface CallbacksOptions<P = Profile, A = Account> {
|
||||
* where you can control what should be returned to the client.
|
||||
* Anything else will be kept from your front-end.
|
||||
*
|
||||
* ⚠ By default the JWT is signed, but not encrypted.
|
||||
* The JWT is encrypted by default.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
|
||||
* [`session` callback](https://next-auth.js.org/configuration/callbacks#session-callback)
|
||||
*/
|
||||
jwt: (params: {
|
||||
token: JWT
|
||||
user?: User | AdapterUser
|
||||
account?: A | null
|
||||
profile?: P
|
||||
isNewUser?: boolean
|
||||
}) => Awaitable<JWT>
|
||||
jwt: (
|
||||
// TODO: remove in `@auth/core` in favor of `trigger: "signUp"`
|
||||
params: {
|
||||
/**
|
||||
* When `trigger` is `"signIn"` or `"signUp"`, it will be a subset of {@link JWT},
|
||||
* `name`, `email` and `picture` 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://next-auth.js.org/providers/credentials)
|
||||
* - [User database model](https://authjs.dev/reference/adapters#user)
|
||||
*/
|
||||
user: User | AdapterUser
|
||||
/**
|
||||
* Contains information about the provider that was used to sign in.
|
||||
* Also includes {@link TokenSet}
|
||||
* @note available when `trigger` is `"signIn"` or `"signUp"`
|
||||
*/
|
||||
account: A | null
|
||||
/**
|
||||
* The OAuth profile returned from your provider.
|
||||
* (In case of OIDC it will be the decoded ID Token or /userinfo response)
|
||||
* @note available when `trigger` is `"signIn"`.
|
||||
*/
|
||||
profile?: P
|
||||
/**
|
||||
* Check why was the jwt callback invoked. Possible reasons are:
|
||||
* - user sign-in: First time the callback is invoked, `user`, `profile` and `account` will be present.
|
||||
* - user sign-up: a user is created for the first time in the database (when {@link SessionOptions.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 SessionOptions.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>
|
||||
}
|
||||
|
||||
/** [Documentation](https://next-auth.js.org/configuration/options#cookies) */
|
||||
@@ -489,6 +549,14 @@ export interface User extends DefaultUser {}
|
||||
|
||||
// Below are types that are only supposed be used by next-auth internally
|
||||
|
||||
/** @internal */
|
||||
export interface OAuthConfigInternal<P>
|
||||
extends Omit<OAuthConfig<P>, "authorization" | "token" | "userinfo"> {
|
||||
authorization?: AuthorizationEndpointHandler
|
||||
token?: TokenEndpointHandler
|
||||
userinfo?: UserinfoEndpointHandler
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export type InternalProvider<T = ProviderType> = (T extends "oauth"
|
||||
? OAuthConfigInternal<any>
|
||||
@@ -515,11 +583,10 @@ export type AuthAction =
|
||||
/** @internal */
|
||||
export interface InternalOptions<
|
||||
TProviderType = ProviderType,
|
||||
WithVerificationToken = TProviderType extends "email" ? true : false
|
||||
> {
|
||||
providers: InternalProvider[]
|
||||
/**
|
||||
* Parsed from `NEXTAUTH_URL` or `x-forwarded-host` on Vercel.
|
||||
* Parsed from `NEXTAUTH_URL` or `x-forwarded-host` and `x-forwarded-proto` if the host is trusted.
|
||||
* @default "http://localhost:3000/api/auth"
|
||||
*/
|
||||
url: InternalUrl
|
||||
@@ -535,9 +602,7 @@ export interface InternalOptions<
|
||||
pages: Partial<PagesOptions>
|
||||
jwt: JWTOptions
|
||||
events: Partial<EventCallbacks>
|
||||
adapter: WithVerificationToken extends true
|
||||
? Adapter<WithVerificationToken>
|
||||
: Adapter<WithVerificationToken> | undefined
|
||||
adapter?: Adapter
|
||||
callbacks: CallbacksOptions
|
||||
cookies: CookiesOptions
|
||||
callbackUrl: string
|
||||
|
||||
@@ -21,7 +21,7 @@ export interface JWTEncodeParams {
|
||||
secret: string | Buffer
|
||||
/**
|
||||
* The maximum age of the NextAuth.js issued JWT in seconds.
|
||||
* @default 30 * 24 * 30 * 60 // 30 days
|
||||
* @default 30 * 24 * 60 * 60 // 30 days
|
||||
*/
|
||||
maxAge?: number
|
||||
}
|
||||
@@ -42,7 +42,7 @@ export interface JWTOptions {
|
||||
secret: string
|
||||
/**
|
||||
* The maximum age of the NextAuth.js issued JWT in seconds.
|
||||
* @default 30 * 24 * 30 * 60 // 30 days
|
||||
* @default 30 * 24 * 60 * 60 // 30 days
|
||||
*/
|
||||
maxAge: number
|
||||
/** Override this method to control the NextAuth.js issued JWT encoding. */
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { AuthHandler } from "../core"
|
||||
import { detectHost } from "../utils/detect-host"
|
||||
import { setCookie } from "./utils"
|
||||
import { setCookie, getBody, toResponse } from "./utils"
|
||||
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
NextApiRequest,
|
||||
NextApiResponse,
|
||||
} from "next"
|
||||
import { type NextRequest } from "next/server"
|
||||
import type { AuthOptions, Session } from ".."
|
||||
import type {
|
||||
CallbacksOptions,
|
||||
@@ -15,19 +15,21 @@ import type {
|
||||
NextAuthResponse,
|
||||
} from "../core/types"
|
||||
|
||||
async function NextAuthHandler(
|
||||
interface RouteHandlerContext {
|
||||
params: { nextauth: string[] }
|
||||
}
|
||||
|
||||
async function NextAuthApiHandler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
options: AuthOptions
|
||||
) {
|
||||
const { nextauth, ...query } = req.query
|
||||
|
||||
options.secret =
|
||||
options.secret ?? options.jwt?.secret ?? process.env.NEXTAUTH_SECRET
|
||||
options.secret ??= options.jwt?.secret ?? process.env.NEXTAUTH_SECRET
|
||||
|
||||
const handler = await AuthHandler({
|
||||
req: {
|
||||
host: detectHost(req.headers["x-forwarded-host"]),
|
||||
body: req.body,
|
||||
query,
|
||||
cookies: req.cookies,
|
||||
@@ -61,6 +63,50 @@ async function NextAuthHandler(
|
||||
return res.send(handler.body)
|
||||
}
|
||||
|
||||
// @see https://beta.nextjs.org/docs/routing/route-handlers
|
||||
async function NextAuthRouteHandler(
|
||||
req: NextRequest,
|
||||
context: { params: { nextauth: string[] } },
|
||||
options: AuthOptions
|
||||
) {
|
||||
options.secret ??= process.env.NEXTAUTH_SECRET
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { headers, cookies } = require("next/headers")
|
||||
const nextauth = context.params?.nextauth
|
||||
const query = Object.fromEntries(req.nextUrl.searchParams)
|
||||
const body = await getBody(req)
|
||||
const internalResponse = await AuthHandler({
|
||||
req: {
|
||||
body,
|
||||
query,
|
||||
cookies: Object.fromEntries(
|
||||
cookies()
|
||||
.getAll()
|
||||
.map((c) => [c.name, c.value])
|
||||
),
|
||||
headers: Object.fromEntries(headers() as Headers),
|
||||
method: req.method,
|
||||
action: nextauth?.[0] as AuthAction,
|
||||
providerId: nextauth?.[1],
|
||||
error: query.error ?? nextauth?.[1],
|
||||
},
|
||||
options,
|
||||
})
|
||||
|
||||
const response = toResponse(internalResponse)
|
||||
const redirect = response.headers.get("Location")
|
||||
if (body?.json === "true" && redirect) {
|
||||
response.headers.delete("Location")
|
||||
response.headers.set("Content-Type", "application/json")
|
||||
return new Response(JSON.stringify({ url: redirect }), {
|
||||
headers: response.headers,
|
||||
})
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
function NextAuth(options: AuthOptions): any
|
||||
function NextAuth(
|
||||
req: NextApiRequest,
|
||||
@@ -73,11 +119,32 @@ function NextAuth(
|
||||
...args: [AuthOptions] | [NextApiRequest, NextApiResponse, AuthOptions]
|
||||
) {
|
||||
if (args.length === 1) {
|
||||
return async (req: NextAuthRequest, res: NextAuthResponse) =>
|
||||
await NextAuthHandler(req, res, args[0])
|
||||
return async (
|
||||
req: NextAuthRequest | NextRequest,
|
||||
res: NextAuthResponse | RouteHandlerContext
|
||||
) => {
|
||||
if ((res as unknown as any)?.params) {
|
||||
return await NextAuthRouteHandler(
|
||||
req as unknown as NextRequest,
|
||||
res as RouteHandlerContext,
|
||||
args[0]
|
||||
)
|
||||
}
|
||||
return await NextAuthApiHandler(
|
||||
req as NextApiRequest,
|
||||
res as NextApiResponse,
|
||||
args[0]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return NextAuthHandler(args[0], args[1], args[2])
|
||||
if ((args[1] as any)?.params) {
|
||||
return NextAuthRouteHandler(
|
||||
...(args as unknown as Parameters<typeof NextAuthRouteHandler>)
|
||||
)
|
||||
}
|
||||
|
||||
return NextAuthApiHandler(...args)
|
||||
}
|
||||
|
||||
export default NextAuth
|
||||
@@ -90,18 +157,18 @@ type GetServerSessionOptions = Partial<Omit<AuthOptions, "callbacks">> & {
|
||||
}
|
||||
}
|
||||
|
||||
type GetServerSessionParams<O extends GetServerSessionOptions> =
|
||||
| [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"], O]
|
||||
| [NextApiRequest, NextApiResponse, O]
|
||||
| [O]
|
||||
| []
|
||||
|
||||
export async function getServerSession<
|
||||
O extends GetServerSessionOptions,
|
||||
R = O["callbacks"] extends { session: (...args: any[]) => infer U }
|
||||
? U
|
||||
: Session
|
||||
>(
|
||||
...args:
|
||||
| [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"], O]
|
||||
| [NextApiRequest, NextApiResponse, O]
|
||||
| [O]
|
||||
| []
|
||||
): Promise<R | null> {
|
||||
>(...args: GetServerSessionParams<O>): Promise<R | null> {
|
||||
const isRSC = args.length === 0 || args.length === 1
|
||||
if (
|
||||
!experimentalRSCWarningShown &&
|
||||
@@ -138,12 +205,11 @@ export async function getServerSession<
|
||||
options = Object.assign({}, args[2], { providers: [] })
|
||||
}
|
||||
|
||||
options.secret = options.secret ?? process.env.NEXTAUTH_SECRET
|
||||
options.secret ??= process.env.NEXTAUTH_SECRET
|
||||
|
||||
const session = await AuthHandler<Session | {} | string>({
|
||||
options,
|
||||
req: {
|
||||
host: detectHost(req.headers["x-forwarded-host"]),
|
||||
action: "session",
|
||||
method: "GET",
|
||||
cookies: req.cookies,
|
||||
@@ -175,9 +241,7 @@ export async function unstable_getServerSession<
|
||||
R = O["callbacks"] extends { session: (...args: any[]) => infer U }
|
||||
? U
|
||||
: Session
|
||||
>(
|
||||
...args: Parameters<typeof getServerSession<O, R>>
|
||||
): ReturnType<typeof getServerSession<O, R>> {
|
||||
>(...args: GetServerSessionParams<O>): Promise<R | null> {
|
||||
if (!deprecatedWarningShown && process.env.NODE_ENV !== "production") {
|
||||
console.warn(
|
||||
"`unstable_getServerSession` has been renamed to `getServerSession`."
|
||||
|
||||
@@ -186,7 +186,39 @@ export type WithAuthArgs =
|
||||
* ---
|
||||
* [Documentation](https://next-auth.js.org/configuration/nextjs#middleware)
|
||||
*/
|
||||
export function withAuth(...args: WithAuthArgs) {
|
||||
|
||||
export function withAuth(): ReturnType<NextMiddlewareWithAuth>
|
||||
|
||||
export function withAuth(
|
||||
req: NextRequestWithAuth
|
||||
): ReturnType<NextMiddlewareWithAuth>
|
||||
|
||||
export function withAuth(
|
||||
req: NextRequestWithAuth,
|
||||
event: NextFetchEvent
|
||||
): ReturnType<NextMiddlewareWithAuth>
|
||||
|
||||
export function withAuth(
|
||||
req: NextRequestWithAuth,
|
||||
options: NextAuthMiddlewareOptions
|
||||
): ReturnType<NextMiddlewareWithAuth>
|
||||
|
||||
export function withAuth(
|
||||
middleware: NextMiddlewareWithAuth,
|
||||
options: NextAuthMiddlewareOptions
|
||||
): NextMiddlewareWithAuth
|
||||
|
||||
export function withAuth(
|
||||
middleware: NextMiddlewareWithAuth
|
||||
): NextMiddlewareWithAuth
|
||||
|
||||
export function withAuth(
|
||||
options: NextAuthMiddlewareOptions
|
||||
): NextMiddlewareWithAuth
|
||||
|
||||
export function withAuth(
|
||||
...args: WithAuthArgs
|
||||
): ReturnType<NextMiddlewareWithAuth> | NextMiddlewareWithAuth {
|
||||
if (!args.length || args[0] instanceof Request) {
|
||||
// @ts-expect-error
|
||||
return handleMiddleware(...args)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { serialize } from "cookie"
|
||||
import { Cookie } from "../core/lib/cookie"
|
||||
import { type ResponseInternal } from "../core"
|
||||
|
||||
export function setCookie(res, cookie: Cookie) {
|
||||
// Preserve any existing cookies that have already been set in the same session
|
||||
@@ -13,3 +14,47 @@ export function setCookie(res, cookie: Cookie) {
|
||||
setCookieHeader.push(cookieHeader)
|
||||
res.setHeader("Set-Cookie", setCookieHeader)
|
||||
}
|
||||
|
||||
export async function getBody(
|
||||
req: Request
|
||||
): Promise<Record<string, any> | undefined> {
|
||||
if (!("body" in req) || !req.body || req.method !== "POST") return
|
||||
|
||||
const contentType = req.headers.get("content-type")
|
||||
if (contentType?.includes("application/json")) {
|
||||
return await req.json()
|
||||
} else if (contentType?.includes("application/x-www-form-urlencoded")) {
|
||||
const params = new URLSearchParams(await req.text())
|
||||
return Object.fromEntries(params)
|
||||
}
|
||||
}
|
||||
|
||||
export function toResponse(res: ResponseInternal): Response {
|
||||
const headers = new Headers(
|
||||
res.headers?.reduce((acc, { key, value }) => {
|
||||
acc[key] = value
|
||||
return acc
|
||||
}, {})
|
||||
)
|
||||
|
||||
res.cookies?.forEach((cookie) => {
|
||||
const { name, value, options } = cookie
|
||||
const cookieHeader = serialize(name, value, options)
|
||||
if (headers.has("Set-Cookie")) headers.append("Set-Cookie", cookieHeader)
|
||||
else headers.set("Set-Cookie", cookieHeader)
|
||||
})
|
||||
|
||||
let body = res.body
|
||||
|
||||
if (headers.get("content-type") === "application/json")
|
||||
body = JSON.stringify(res.body)
|
||||
else if (headers.get("content-type") === "application/x-www-form-urlencoded")
|
||||
body = new URLSearchParams(res.body).toString()
|
||||
|
||||
const status = res.redirect ? 302 : res.status ?? 200
|
||||
const response = new Response(body, { headers, status })
|
||||
|
||||
if (res.redirect) response.headers.set("Location", res.redirect)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { createTransport } from "nodemailer"
|
||||
|
||||
import type { CommonProviderOptions } from "."
|
||||
import type { Options as SMTPTransportOptions } from "nodemailer/lib/smtp-transport"
|
||||
import { Transport, TransportOptions, createTransport } from "nodemailer"
|
||||
import * as JSONTransport from "nodemailer/lib/json-transport.js"
|
||||
import * as SendmailTransport from "nodemailer/lib/sendmail-transport/index.js"
|
||||
import * as SESTransport from "nodemailer/lib/ses-transport.js"
|
||||
import * as SMTPPool from "nodemailer/lib/smtp-pool/index.js"
|
||||
import * as SMTPTransport from "nodemailer/lib/smtp-transport.js"
|
||||
import * as StreamTransport from "nodemailer/lib/stream-transport.js"
|
||||
import type { Awaitable } from ".."
|
||||
import type { CommonProviderOptions } from "."
|
||||
import type { Theme } from "../core/types"
|
||||
|
||||
// TODO: Make use of https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html for the string
|
||||
type AllTransportOptions = string | SMTPTransport | SMTPTransport.Options | SMTPPool | SMTPPool.Options | SendmailTransport | SendmailTransport.Options | StreamTransport | StreamTransport.Options | JSONTransport | JSONTransport.Options | SESTransport | SESTransport.Options | Transport<any> | TransportOptions
|
||||
|
||||
export interface SendVerificationRequestParams {
|
||||
identifier: string
|
||||
url: string
|
||||
@@ -14,10 +21,9 @@ export interface SendVerificationRequestParams {
|
||||
theme: Theme
|
||||
}
|
||||
|
||||
export interface EmailConfig extends CommonProviderOptions {
|
||||
type: "email"
|
||||
// TODO: Make use of https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html
|
||||
server: string | SMTPTransportOptions
|
||||
export interface EmailUserConfig {
|
||||
server?: AllTransportOptions
|
||||
type?: "email"
|
||||
/** @default "NextAuth <no-reply@example.com>" */
|
||||
from?: string
|
||||
/**
|
||||
@@ -27,7 +33,7 @@ export interface EmailConfig extends CommonProviderOptions {
|
||||
*/
|
||||
maxAge?: number
|
||||
/** [Documentation](https://next-auth.js.org/providers/email#customizing-emails) */
|
||||
sendVerificationRequest: (
|
||||
sendVerificationRequest?: (
|
||||
params: SendVerificationRequestParams
|
||||
) => Awaitable<void>
|
||||
/**
|
||||
@@ -61,10 +67,31 @@ export interface EmailConfig extends CommonProviderOptions {
|
||||
* [Documentation](https://next-auth.js.org/providers/email#normalizing-the-e-mail-address) | [RFC 2821](https://tools.ietf.org/html/rfc2821) | [Email syntax](https://en.wikipedia.org/wiki/Email_address#Syntax)
|
||||
*/
|
||||
normalizeIdentifier?: (identifier: string) => string
|
||||
options: EmailUserConfig
|
||||
}
|
||||
|
||||
export type EmailUserConfig = Partial<Omit<EmailConfig, "options">>
|
||||
export interface EmailConfig extends CommonProviderOptions {
|
||||
// defaults
|
||||
id: "email"
|
||||
type: "email"
|
||||
name: "Email"
|
||||
server: AllTransportOptions
|
||||
from: string
|
||||
maxAge: number
|
||||
sendVerificationRequest: (
|
||||
params: SendVerificationRequestParams
|
||||
) => Awaitable<void>
|
||||
|
||||
/**
|
||||
* This is copied into EmailConfig in parseProviders() don't use elsewhere
|
||||
*/
|
||||
options: EmailUserConfig
|
||||
|
||||
// user options
|
||||
// TODO figure out a better way than copying from EmailUserConfig
|
||||
secret?: string
|
||||
generateVerificationToken?: () => Awaitable<string>
|
||||
normalizeIdentifier?: (identifier: string) => string
|
||||
}
|
||||
|
||||
export type EmailProvider = (options: EmailUserConfig) => EmailConfig
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export interface CommonProviderOptions {
|
||||
id: string
|
||||
name: string
|
||||
type: ProviderType
|
||||
options?: Record<string, unknown>
|
||||
options?: any
|
||||
}
|
||||
|
||||
export type Provider = OAuthConfig<any> | EmailConfig | CredentialsConfig
|
||||
|
||||
@@ -74,7 +74,7 @@ export type TokenEndpointHandler = EndpointHandler<
|
||||
params: CallbackParamsType
|
||||
/**
|
||||
* When using this custom flow, make sure to do all the necessary security checks.
|
||||
* Thist object contains parameters you have to match against the request to make sure it is valid.
|
||||
* This object contains parameters you have to match against the request to make sure it is valid.
|
||||
*/
|
||||
checks: OAuthChecks
|
||||
},
|
||||
@@ -109,6 +109,7 @@ export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
|
||||
* [Authorization Server Metadata](https://datatracker.ietf.org/doc/html/rfc8414#section-3)
|
||||
*/
|
||||
wellKnown?: string
|
||||
jwks_endpoint?: string
|
||||
/**
|
||||
* The login process will be initiated by sending the user to this URL.
|
||||
*
|
||||
@@ -159,14 +160,6 @@ export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
|
||||
allowDangerousEmailAccountLinking?: boolean
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface OAuthConfigInternal<P>
|
||||
extends Omit<OAuthConfig<P>, "authorization" | "token" | "userinfo"> {
|
||||
authorization?: AuthorizationEndpointHandler
|
||||
token?: TokenEndpointHandler
|
||||
userinfo?: UserinfoEndpointHandler
|
||||
}
|
||||
|
||||
export type OAuthUserConfig<P> = Omit<
|
||||
Partial<OAuthConfig<P>>,
|
||||
"options" | "type"
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/** @type {import(".").OAuthProvider} */
|
||||
export default function Yandex(options) {
|
||||
return {
|
||||
id: "yandex",
|
||||
name: "Yandex",
|
||||
type: "oauth",
|
||||
authorization:
|
||||
"https://oauth.yandex.ru/authorize?scope=login:email+login:info",
|
||||
token: "https://oauth.yandex.ru/token",
|
||||
userinfo: "https://login.yandex.ru/info?format=json",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.real_name,
|
||||
email: profile.default_email,
|
||||
image: profile.is_avatar_empty
|
||||
? null
|
||||
: `https://avatars.yandex.net/get-yapic/${profile.default_avatar_id}/islands-200`,
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
155
packages/next-auth/src/providers/yandex.ts
Normal file
155
packages/next-auth/src/providers/yandex.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* <div style={{backgroundColor: "#ffcc00", display: "flex", justifyContent: "space-between", color: "#000", padding: 16}}>
|
||||
* <span>Built-in <b>Yandex</b> integration.</span>
|
||||
* <a href="https://github.com">
|
||||
* <img style={{display: "block"}} src="https://authjs.dev/img/providers/yandex.svg" height="48" width="48"/>
|
||||
* </a>
|
||||
* </div>
|
||||
*
|
||||
* ---
|
||||
* @module providers/yandex
|
||||
*/
|
||||
|
||||
import { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
/**
|
||||
* @see [Getting information about the user](https://yandex.com/dev/id/doc/en/user-information)
|
||||
* @see [Access to email address](https://yandex.com/dev/id/doc/en/user-information#email-access)
|
||||
* @see [Access to the user's profile picture](https://yandex.com/dev/id/doc/en/user-information#avatar-access)
|
||||
* @see [Access to the date of birth](https://yandex.com/dev/id/doc/en/user-information#birthday-access)
|
||||
* @see [Access to login, first name, last name, and gender](https://yandex.com/dev/id/doc/en/user-information#name-access)
|
||||
* @see [Access to the phone number](https://yandex.com/dev/id/doc/en/user-information#phone-access)
|
||||
*/
|
||||
export interface YandexProfile {
|
||||
/** User's Yandex login. */
|
||||
login: string
|
||||
/** Yandex user's unique ID. */
|
||||
id: string
|
||||
/**
|
||||
* The ID of the app the OAuth token in the request was issued for.
|
||||
* Available in the [app properties](https://oauth.yandex.com/). To open properties, click the app name.
|
||||
*/
|
||||
client_id: string
|
||||
/** Authorized Yandex user ID. It is formed on the Yandex side based on the `client_id` and `user_id` pair. */
|
||||
psuid: string
|
||||
/** An array of the user's email addresses. Currently only includes the default email address. */
|
||||
emails?: string[]
|
||||
/** The default email address for contacting the user. */
|
||||
default_email?: string
|
||||
/**
|
||||
* Indicates that the stub (profile picture that is automatically assigned when registering in Yandex)
|
||||
* ID is specified in the `default_avatar_id` field.
|
||||
*/
|
||||
is_avatar_empty?: boolean
|
||||
/**
|
||||
* ID of the Yandex user's profile picture.
|
||||
* The profile picture with this ID can be downloaded via a link that looks like this:
|
||||
* @example "https://avatars.yandex.net/get-yapic/31804/BYkogAC6AoB17bN1HKRFAyKiM4-1/islands-200"
|
||||
*/
|
||||
default_avatar_id?:
|
||||
| "islands-small"
|
||||
| "islands-34"
|
||||
| "islands-middle"
|
||||
| "islands-50"
|
||||
| "islands-retina-small"
|
||||
| "islands-68"
|
||||
| "islands-75"
|
||||
| "islands-retina-middle"
|
||||
| "islands-retina-50"
|
||||
| "islands-200"
|
||||
/**
|
||||
* The user's date of birth in YYYY-MM-DD format.
|
||||
* Unknown elements of the date are filled in with zeros, such as: `0000-12-23`.
|
||||
* If the user's date of birth is unknow, birthday will be `null`
|
||||
*/
|
||||
birthday?: string | null
|
||||
first_name?: string
|
||||
last_name?: string
|
||||
display_name?: string
|
||||
/**
|
||||
* The first and last name that the user specified in Yandex ID.
|
||||
* Non-Latin characters of the first and last names are presented in Unicode format.
|
||||
*/
|
||||
real_name?: string
|
||||
/** User's gender. Possible values: Male: `male', Female: `female`, Unknown gender: `null` */
|
||||
sex?: string
|
||||
/**
|
||||
* The default phone number for contacting the user.
|
||||
* The API can exclude the user's phone number from the response at its discretion.
|
||||
* The field contains the following parameters:
|
||||
* id: Phone number ID.
|
||||
* number: The user's phone number.
|
||||
*/
|
||||
default_phone?: { id: number; number: string }
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Yandex login to your page
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
* ```ts
|
||||
* import { Auth } from "@auth/core"
|
||||
* import Yandex from "@auth/core/providers/yandex"
|
||||
*
|
||||
* const request = new Request("https://example.com")
|
||||
* const response = await Auth(request, {
|
||||
* providers: [Yandex({ clientId: "", clientSecret: "" })],
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* ## Resources
|
||||
*
|
||||
* @see [Yandex - Creating an OAuth app](https://yandex.com/dev/id/doc/en/register-client#create)
|
||||
* @see [Yandex - Manage OAuth apps](https://oauth.yandex.com/)
|
||||
* @see [Yandex - OAuth documentation](https://yandex.com/dev/id/doc/en/)
|
||||
* @see [Learn more about OAuth](https://authjs.dev/concepts/oauth)
|
||||
* @see [Source code](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/yandex.ts)
|
||||
*
|
||||
*:::tip
|
||||
* The Yandex provider comes with a [default configuration](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/yandex.ts).
|
||||
* To override the defaults for your use case, check out [customizing a built-in OAuth provider](https://authjs.dev/guides/providers/custom-provider#override-default-options).
|
||||
* :::
|
||||
*
|
||||
* :::info **Disclaimer**
|
||||
* If you think you found a bug in the default configuration, you can [open an issue](https://authjs.dev/new/provider-issue).
|
||||
*
|
||||
* Auth.js strictly adheres to the specification and it cannot take responsibility for any deviation from
|
||||
* the spec by the provider. You can open an issue, but if the problem is non-compliance with the spec,
|
||||
* we might not pursue a resolution. You can ask for more help in [Discussions](https://authjs.dev/new/github-discussions).
|
||||
* :::
|
||||
*/
|
||||
export default function Yandex(
|
||||
options: OAuthUserConfig<YandexProfile>
|
||||
): OAuthConfig<YandexProfile> {
|
||||
return {
|
||||
id: "yandex",
|
||||
name: "Yandex",
|
||||
type: "oauth",
|
||||
/** @see [Data access](https://yandex.com/dev/id/doc/en/register-client#access) */
|
||||
authorization:
|
||||
"https://oauth.yandex.ru/authorize?scope=login:info+login:email+login:avatar",
|
||||
token: "https://oauth.yandex.ru/token",
|
||||
userinfo: "https://login.yandex.ru/info?format=json",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.display_name ?? profile.real_name ?? profile.first_name,
|
||||
email: profile.default_email ?? profile.emails?.[0] ?? null,
|
||||
image:
|
||||
!profile.is_avatar_empty && profile.default_avatar_id
|
||||
? `https://avatars.yandex.net/get-yapic/${profile.default_avatar_id}/islands-200`
|
||||
: null,
|
||||
}
|
||||
},
|
||||
style: {
|
||||
logo: "/yandex.svg",
|
||||
logoDark: "/yandex.svg",
|
||||
bg: "#ffcc00",
|
||||
text: "#000",
|
||||
bgDark: "#ffcc00",
|
||||
textDark: "#000",
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
@@ -87,13 +87,19 @@ function useOnline() {
|
||||
return isOnline
|
||||
}
|
||||
|
||||
type UpdateSession = (data?: any) => Promise<Session | null>
|
||||
|
||||
export type SessionContextValue<R extends boolean = false> = R extends true
|
||||
?
|
||||
| { data: Session; status: "authenticated" }
|
||||
| { data: null; status: "loading" }
|
||||
| { update: UpdateSession; data: Session; status: "authenticated" }
|
||||
| { update: UpdateSession; data: null; status: "loading" }
|
||||
:
|
||||
| { data: Session; status: "authenticated" }
|
||||
| { data: null; status: "unauthenticated" | "loading" }
|
||||
| { update: UpdateSession; data: Session; status: "authenticated" }
|
||||
| {
|
||||
update: UpdateSession
|
||||
data: null
|
||||
status: "unauthenticated" | "loading"
|
||||
}
|
||||
|
||||
export const SessionContext = React.createContext?.<
|
||||
SessionContextValue | undefined
|
||||
@@ -105,7 +111,9 @@ export const SessionContext = React.createContext?.<
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#usesession)
|
||||
*/
|
||||
export function useSession<R extends boolean>(options?: UseSessionOptions<R>) {
|
||||
export function useSession<R extends boolean>(
|
||||
options?: UseSessionOptions<R>
|
||||
): SessionContextValue<R> {
|
||||
if (!SessionContext) {
|
||||
throw new Error("React Context is unavailable in Server Components")
|
||||
}
|
||||
@@ -134,7 +142,11 @@ export function useSession<R extends boolean>(options?: UseSessionOptions<R>) {
|
||||
}, [requiredAndNotLoading, onUnauthenticated])
|
||||
|
||||
if (requiredAndNotLoading) {
|
||||
return { data: value.data, status: "loading" } as const
|
||||
return {
|
||||
data: value.data,
|
||||
update: value.update,
|
||||
status: "loading",
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
@@ -456,6 +468,22 @@ export function SessionProvider(props: SessionProviderProps) {
|
||||
: session
|
||||
? "authenticated"
|
||||
: "unauthenticated",
|
||||
async update(data) {
|
||||
if (loading || !session) return
|
||||
setLoading(true)
|
||||
const newSession = await fetchData<Session>(
|
||||
"session",
|
||||
__NEXTAUTH,
|
||||
logger,
|
||||
{ req: { body: { csrfToken: await getCsrfToken(), data } } }
|
||||
)
|
||||
setLoading(false)
|
||||
if (newSession) {
|
||||
setSession(newSession)
|
||||
broadcast.post({ event: "session", data: { trigger: "getSession" } })
|
||||
}
|
||||
return newSession
|
||||
},
|
||||
}),
|
||||
[session, loading]
|
||||
)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/** Extract the host from the environment */
|
||||
export function detectHost(forwardedHost: any) {
|
||||
/** Extract the origin from the environment */
|
||||
export function detectOrigin(forwardedHost: any, protocol: any) {
|
||||
// If we detect a Vercel environment, we can trust the host
|
||||
if (process.env.VERCEL ?? process.env.AUTH_TRUST_HOST)
|
||||
return forwardedHost
|
||||
return `${protocol === "http" ? "http" : "https"}://${forwardedHost}`
|
||||
|
||||
// If `NEXTAUTH_URL` is `undefined` we fall back to "http://localhost:3000"
|
||||
return process.env.NEXTAUTH_URL
|
||||
}
|
||||
145
packages/next-auth/tests/checks.test.ts
Normal file
145
packages/next-auth/tests/checks.test.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { compareDates, mockLogger } from "./lib"
|
||||
import * as checks from "../src/core/lib/oauth/checks"
|
||||
import { AuthorizationParameters } from "openid-client"
|
||||
import { Cookie } from "../src/core/lib/cookie"
|
||||
|
||||
let options: any
|
||||
|
||||
beforeEach(() => {
|
||||
options = {
|
||||
logger: mockLogger(),
|
||||
secret: "secret",
|
||||
url: {
|
||||
origin: "http://localhost:3000",
|
||||
host: "localhost:3000",
|
||||
path: "/api/auth",
|
||||
base: "http://localhost:3000/api/auth",
|
||||
toString: () => "http://localhost:3000/api/auth",
|
||||
},
|
||||
action: "session",
|
||||
provider: {
|
||||
type: "oauth",
|
||||
id: "testId",
|
||||
name: "testName",
|
||||
signinUrl: "/",
|
||||
callbackUrl: "/",
|
||||
checks: ["pkce", "state"],
|
||||
profile() {
|
||||
return { id: "", name: "", email: "" }
|
||||
},
|
||||
},
|
||||
jwt: {
|
||||
secret: "secret",
|
||||
maxAge: 0,
|
||||
encode() {
|
||||
throw new Error("Function not implemented.")
|
||||
},
|
||||
decode() {
|
||||
throw new Error("Function not implemented.")
|
||||
},
|
||||
},
|
||||
cookies: {
|
||||
pkceCodeVerifier: { name: "", options: {} },
|
||||
state: { name: "", options: {} },
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe("PKCE", () => {
|
||||
it("sets a code challenge, code challenge method, and cookie", async () => {
|
||||
const cookies = []
|
||||
const params: AuthorizationParameters = {}
|
||||
await checks.pkce.create(options, cookies, params)
|
||||
|
||||
expect(params?.code_challenge).not.toBeNull()
|
||||
expect(params?.code_challenge_method).toEqual("S256")
|
||||
expect(cookies.length).not.toBe(0)
|
||||
})
|
||||
|
||||
it("does not set PKCE values when unsupported", async () => {
|
||||
options.provider.checks = ["state"]
|
||||
const cookies = []
|
||||
const params: AuthorizationParameters = {}
|
||||
await checks.pkce.create(options, cookies, params)
|
||||
|
||||
expect(Object.keys(params).length).toBe(0)
|
||||
expect(cookies.length).toBe(0)
|
||||
})
|
||||
|
||||
it("sets the cookie expiration to a default of 15 minutes when the max age option is not provided", async () => {
|
||||
const cookies: Cookie[] = []
|
||||
const params: AuthorizationParameters = {}
|
||||
await checks.pkce.create(options, cookies, params)
|
||||
|
||||
const defaultMaxAge = 60 * 15 // 15 minutes in seconds
|
||||
const expires = new Date()
|
||||
expires.setTime(expires.getTime() + defaultMaxAge * 1000)
|
||||
|
||||
compareDates(expires, cookies[0].options.expires)
|
||||
expect(cookies[0].options.maxAge).toBeUndefined()
|
||||
})
|
||||
|
||||
it("sets the cookie expiration and max age to the provided max age from the options", async () => {
|
||||
const maxAge = 60 * 20 // 20 minutes
|
||||
options.cookies.pkceCodeVerifier.options.maxAge = maxAge
|
||||
|
||||
const pkceCookies: Cookie[] = []
|
||||
const params: AuthorizationParameters = {}
|
||||
await checks.pkce.create(options, pkceCookies, params)
|
||||
|
||||
const expires = new Date()
|
||||
expires.setTime(expires.getTime() + maxAge * 1000)
|
||||
|
||||
compareDates(expires, pkceCookies[0].options.expires)
|
||||
expect(pkceCookies[0].options.maxAge).toEqual(maxAge)
|
||||
})
|
||||
})
|
||||
|
||||
describe("state", () => {
|
||||
it("returns a state, and cookie", async () => {
|
||||
const cookies = []
|
||||
const params: AuthorizationParameters = {}
|
||||
await checks.state.create(options, cookies, params)
|
||||
|
||||
expect(params.state).toBeDefined()
|
||||
expect(cookies.length).not.toBe(0)
|
||||
})
|
||||
|
||||
it("does not set state when unsupported", async () => {
|
||||
options.provider.checks = ["pkce"]
|
||||
const cookies = []
|
||||
const params: AuthorizationParameters = {}
|
||||
await checks.state.create(options, cookies, params)
|
||||
|
||||
expect(Object.keys(params).length).toBe(0)
|
||||
expect(cookies.length).toBe(0)
|
||||
})
|
||||
|
||||
it("sets the cookie expiration to a default of 15 minutes when the max age option is not provided", async () => {
|
||||
const cookies: Cookie[] = []
|
||||
const params: AuthorizationParameters = {}
|
||||
await checks.state.create(options, cookies, params)
|
||||
|
||||
const defaultMaxAge = 60 * 15 // 15 minutes in seconds
|
||||
const expires = new Date()
|
||||
expires.setTime(expires.getTime() + defaultMaxAge * 1000)
|
||||
|
||||
compareDates(expires, cookies[0].options.expires)
|
||||
expect(cookies[0].options.maxAge).toBeUndefined()
|
||||
})
|
||||
|
||||
it("sets the cookie expiration and max age to the provided max age from the options", async () => {
|
||||
const maxAge = 60 * 20 // 20 minutes
|
||||
options.cookies.state.options.maxAge = maxAge
|
||||
|
||||
const stateCookies: Cookie[] = []
|
||||
const params: AuthorizationParameters = {}
|
||||
await checks.state.create(options, stateCookies, params)
|
||||
|
||||
const expires = new Date()
|
||||
expires.setTime(expires.getTime() + maxAge * 1000)
|
||||
|
||||
compareDates(expires, stateCookies[0].options.expires)
|
||||
expect(stateCookies[0].options.maxAge).toEqual(maxAge)
|
||||
})
|
||||
})
|
||||
@@ -66,3 +66,11 @@ export function mockAdapter(): Adapter {
|
||||
} as unknown as Adapter
|
||||
return adapter
|
||||
}
|
||||
|
||||
export function compareDates(a: Date, b: Date) {
|
||||
expect(a.getFullYear()).toEqual(b.getFullYear())
|
||||
expect(a.getMonth()).toEqual(b.getMonth())
|
||||
expect(a.getFullYear()).toEqual(b.getFullYear())
|
||||
expect(a.getHours()).toEqual(b.getHours())
|
||||
expect(a.getMinutes()).toEqual(b.getMinutes())
|
||||
}
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
import { mockLogger } from "./lib"
|
||||
import type {
|
||||
InternalOptions,
|
||||
LoggerInstance,
|
||||
InternalProvider,
|
||||
CallbacksOptions,
|
||||
Awaitable,
|
||||
CookiesOptions,
|
||||
} from "../src"
|
||||
import { createPKCE } from "../src/core/lib/oauth/pkce-handler"
|
||||
import { InternalUrl } from "../src/utils/parse-url"
|
||||
import { JWT, JWTDecodeParams, JWTEncodeParams, JWTOptions } from "../src/jwt"
|
||||
|
||||
let logger: LoggerInstance
|
||||
let url: InternalUrl
|
||||
let provider: InternalProvider<"oauth">
|
||||
let jwt: JWTOptions
|
||||
let callbacks: CallbacksOptions
|
||||
let cookies: CookiesOptions
|
||||
let options: InternalOptions<"oauth">
|
||||
|
||||
beforeEach(() => {
|
||||
logger = mockLogger()
|
||||
|
||||
url = {
|
||||
origin: "http://localhost:3000",
|
||||
host: "localhost:3000",
|
||||
path: "/api/auth",
|
||||
base: "http://localhost:3000/api/auth",
|
||||
toString: () => "http://localhost:3000/api/auth",
|
||||
}
|
||||
|
||||
provider = {
|
||||
type: "oauth",
|
||||
id: "testId",
|
||||
name: "testName",
|
||||
signinUrl: "/",
|
||||
callbackUrl: "/",
|
||||
checks: ["pkce", "state"],
|
||||
profile() {
|
||||
return { id: "", name: "", email: "" }
|
||||
},
|
||||
}
|
||||
|
||||
jwt = {
|
||||
secret: "secret",
|
||||
maxAge: 0,
|
||||
encode: function (params: JWTEncodeParams): Awaitable<string> {
|
||||
throw new Error("Function not implemented.")
|
||||
},
|
||||
decode: function (params: JWTDecodeParams): Awaitable<JWT | null> {
|
||||
throw new Error("Function not implemented.")
|
||||
},
|
||||
}
|
||||
|
||||
callbacks = {
|
||||
signIn: function () {
|
||||
throw new Error("Function not implemented.")
|
||||
},
|
||||
redirect: function () {
|
||||
throw new Error("Function not implemented.")
|
||||
},
|
||||
session: function () {
|
||||
throw new Error("Function not implemented.")
|
||||
},
|
||||
jwt: function () {
|
||||
throw new Error("Function not implemented.")
|
||||
},
|
||||
}
|
||||
|
||||
cookies = {
|
||||
sessionToken: { name: "", options: undefined },
|
||||
callbackUrl: { name: "", options: undefined },
|
||||
csrfToken: { name: "", options: undefined },
|
||||
pkceCodeVerifier: { name: "", options: {} },
|
||||
state: { name: "", options: undefined },
|
||||
nonce: { name: "", options: undefined },
|
||||
}
|
||||
|
||||
options = {
|
||||
url,
|
||||
adapter: undefined,
|
||||
action: "session",
|
||||
provider,
|
||||
secret: "",
|
||||
debug: false,
|
||||
logger,
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
maxAge: 0,
|
||||
updateAge: 0,
|
||||
generateSessionToken() {
|
||||
return ""
|
||||
},
|
||||
},
|
||||
pages: {},
|
||||
jwt,
|
||||
events: {},
|
||||
callbacks,
|
||||
cookies,
|
||||
callbackUrl: "",
|
||||
providers: [],
|
||||
theme: {},
|
||||
}
|
||||
})
|
||||
|
||||
describe("createPKCE", () => {
|
||||
it("returns a code challenge, code challenge method, and cookie", async () => {
|
||||
const pkce = await createPKCE(options)
|
||||
|
||||
expect(pkce?.code_challenge).not.toBeNull()
|
||||
expect(pkce?.code_challenge_method).toEqual("S256")
|
||||
expect(pkce?.cookie).not.toBeNull()
|
||||
})
|
||||
it("does not return a pkce when the provider does not support pkce", async () => {
|
||||
options.provider.checks = ["state"]
|
||||
|
||||
const pkce = await createPKCE(options)
|
||||
|
||||
expect(pkce).toBeUndefined()
|
||||
})
|
||||
it("sets the cookie expiration to a default of 15 minutes when the max age option is not provided", async () => {
|
||||
const pkce = await createPKCE(options)
|
||||
|
||||
const defaultMaxAge = 60 * 15 // 15 minutes in seconds
|
||||
const expires = new Date()
|
||||
expires.setTime(expires.getTime() + defaultMaxAge * 1000)
|
||||
|
||||
validateCookieExpiration({ pkce, expires })
|
||||
expect(pkce?.cookie.options.maxAge).toBeUndefined()
|
||||
})
|
||||
|
||||
it("sets the cookie expiration and max age to the provided max age from the options", async () => {
|
||||
const maxAge = 60 * 20 // 20 minutes
|
||||
cookies.pkceCodeVerifier.options.maxAge = maxAge
|
||||
|
||||
const pkce = await createPKCE(options)
|
||||
|
||||
const expires = new Date()
|
||||
expires.setTime(expires.getTime() + maxAge * 1000)
|
||||
|
||||
validateCookieExpiration({ pkce, expires })
|
||||
expect(pkce?.cookie.options.maxAge).toEqual(maxAge)
|
||||
})
|
||||
})
|
||||
|
||||
// comparing the parts instead of getTime() because the milliseconds
|
||||
// will not match since the two Date objects are created milliseconds apart
|
||||
const validateCookieExpiration = ({ pkce, expires }) => {
|
||||
const cookieExpires = pkce?.cookie.options.expires
|
||||
expect(cookieExpires.getFullYear()).toEqual(expires.getFullYear())
|
||||
expect(cookieExpires.getMonth()).toEqual(expires.getMonth())
|
||||
expect(cookieExpires.getFullYear()).toEqual(expires.getFullYear())
|
||||
expect(cookieExpires.getHours()).toEqual(expires.getHours())
|
||||
expect(cookieExpires.getMinutes()).toEqual(expires.getMinutes())
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user