Compare commits

..

2 Commits

Author SHA1 Message Date
Balázs Orbán
be6a85d7b5 import Account as type 2022-09-14 14:15:48 +02:00
Balázs Orbán
946717523b feat(adapters): add Hasura Adapter 2022-09-14 14:05:26 +02:00
99 changed files with 980 additions and 2632 deletions

View File

@@ -3,27 +3,10 @@ const path = require("path")
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
overrides: [
{
files: ["*.ts", "*.tsx"],
extends: ["standard-with-typescript", "prettier"],
rules: {
camelcase: "off",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/restrict-template-expressions": "off",
},
parserOptions: {
project: [
path.resolve(__dirname, "./packages/**/tsconfig.eslint.json"),
path.resolve(__dirname, "./apps/**/tsconfig.json"),
],
},
},
],
extends: ["prettier"],
parserOptions: {
project: [path.resolve(__dirname, "./packages/**/tsconfig.eslint.json")],
},
extends: ["standard-with-typescript", "prettier"],
globals: {
localStorage: "readonly",
location: "readonly",
@@ -31,6 +14,10 @@ module.exports = {
},
rules: {
camelcase: "off",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/restrict-template-expressions": "off",
},
plugins: ["jest"],
env: {

View File

@@ -5,7 +5,6 @@ body:
- type: markdown
attributes:
value: |
**NOTE:** Issues that are potentially security related should be reported to us by following the [Security guidelines](https://next-auth.js.org/security) rather than on GitHub.
Thanks for taking the time to fill out this issue after reading/searching through the [documentation](https://next-auth.js.org) first!
Is this your first time contributing? Check out this video: https://www.youtube.com/watch?v=cuoNzXFLitc

View File

@@ -5,7 +5,6 @@ body:
- type: markdown
attributes:
value: |
**NOTE:** Issues that are potentially security related should be reported to us by following the [Security guidelines](https://next-auth.js.org/security) rather than on GitHub.
Thanks for taking the time to fill out this [Provider](https://next-auth.js.org/providers/overview) related issue!
Is this your first time contributing? Check out this video: https://www.youtube.com/watch?v=cuoNzXFLitc

View File

@@ -5,7 +5,6 @@ body:
- type: markdown
attributes:
value: |
**NOTE:** Issues that are potentially security related should be reported to us by following the [Security guidelines](https://next-auth.js.org/security) rather than on GitHub.
Thanks for taking the time to fill out this [Adapter](https://next-auth.js.org/adapters/overview) related issue!
Is this your first time contributing? Check out this video: https://www.youtube.com/watch?v=cuoNzXFLitc

View File

@@ -9,7 +9,6 @@ body:
- type: markdown
attributes:
value: |
**NOTE:** Issues that are potentially security related should be reported to us by following the [Security guidelines](https://next-auth.js.org/security) rather than on GitHub.
Thank you very much for reaching out to us regarding the awesome feature that you believe should be included in the NextAuth.js library.
_NOTE: Feature requests are converted to [discussions (Ideas 💡)](https://github.com/nextauthjs/next-auth/discussions/categories/ideas). Make sure your idea hasn't been asked yet, and upvote the existing one before opening a new instead._

View File

@@ -17,7 +17,6 @@ body:
- type: markdown
attributes:
value: |
**NOTE:** Issues that are potentially security related should be reported to us by following the [Security guidelines](https://next-auth.js.org/security) rather than on GitHub.
Make sure you [link]() to external documentation if necessary and provide inline code examples like so:
```js

View File

@@ -9,7 +9,6 @@ body:
- type: markdown
attributes:
value: |
**NOTE:** Issues that are potentially security related should be reported to us by following the [Security guidelines](https://next-auth.js.org/security) rather than on GitHub.
We are glad that you have a question about this library. Please provide the following information:
- type: textarea

View File

@@ -5,14 +5,9 @@ Please fill out the information below to expedite the review and (hopefully)
merge of your pull request!
-->
> _NOTE_:
>
> - It's a good idea to open an issue first to discuss potential changes.
> - Please make sure that you are _NOT_ opening a PR to fix a potential security vulnerability. Instead, please follow the [Security guidelines](../Security.md) to disclose the issue to us confidentially.
## ☕️ Reasoning
<!-- What changes are being made? What feature/bug is being fixed here? -->
What changes are being made? What feature/bug is being fixed here?
## 🧢 Checklist
@@ -28,7 +23,6 @@ Fixes: INSERT_ISSUE_LINK_HERE
## 📌 Resources
- [Security guidelines](../Security.md)
- [Contributing guidelines](../CONTRIBUTING.md)
- [Code of conduct](../CODE_OF_CONDUCT.md)
- [Contributing guidelines](./CONTRIBUTING.md)
- [Code of conduct](./CODE_OF_CONDUCT.md)
- [Contributing to Open Source](https://kcd.im/pull-request)

View File

@@ -55,7 +55,7 @@ further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting hi@thvu.dev, info@balazsorban.com, yo@ndo.dev and me@iaincollins.com.
reported by contacting info@balazsorban.com, yo@ndo.dev, thvu@hey.com and me@iaincollins.com.
All complaints will be reviewed and investigated and will result in a response
that is deemed necessary and appropriate to the circumstances. The project team
is obligated to maintain confidentiality with regard to the reporter of an

View File

@@ -13,7 +13,7 @@ If you contact us regarding a serious issue:
- We will disclose the issue (and credit you, with your consent) once a fix to resolve the issue has been released.
- If 90 days has elapsed and we still don't have a fix, we will disclose the issue publicly.
The best way to report an issue is by contacting us via email at hi@thvu.dev, info@balazsorban.com, yo@ndo.dev and me@iaincollins.com, or raise a public issue requesting someone get in touch with you via whatever means you prefer for more details. (Please do not disclose sensitive details publicly at this stage.)
The best way to report an issue is by contacting us via email at info@balazsorban.com, yo@ndo.dev, thvu@hey.com and me@iaincollins.com, or raise a public issue requesting someone get in touch with you via whatever means you prefer for more details. (Please do not disclose sensitive details publicly at this stage.)
> For less serious issues (e.g. RFC compliance for unsupported flows or potential issues that may cause a problem in the future) it is appropriate to submit these publicly as bug reports or feature requests or to raise a question to open a discussion around them.

View File

@@ -6,7 +6,6 @@
"scripts": {
"clean": "rm -rf .next",
"dev": "next dev",
"lint": "next lint",
"build": "next build",
"start": "next start",
"email": "fake-smtp-server",

View File

@@ -18,7 +18,6 @@ import Freshbooks from "next-auth/providers/freshbooks"
import GitHub from "next-auth/providers/github"
import Gitlab from "next-auth/providers/gitlab"
import Google from "next-auth/providers/google"
import Hubspot from "next-auth/providers/hubspot"
import IDS4 from "next-auth/providers/identity-server4"
import Instagram from "next-auth/providers/instagram"
import Keycloak from "next-auth/providers/keycloak"
@@ -36,7 +35,6 @@ import Twitter, { TwitterLegacy } from "next-auth/providers/twitter"
import Vk from "next-auth/providers/vk"
import Wikimedia from "next-auth/providers/wikimedia"
import WorkOS from "next-auth/providers/workos"
import Zitadel from "next-auth/providers/zitadel"
// Adapters
import { PrismaClient } from "@prisma/client"
@@ -104,7 +102,6 @@ export const authOptions: NextAuthOptions = {
GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET }),
Gitlab({ clientId: process.env.GITLAB_ID, clientSecret: process.env.GITLAB_SECRET }),
Google({ clientId: process.env.GOOGLE_ID, clientSecret: process.env.GOOGLE_SECRET }),
Hubspot({ clientId: process.env.HUBSPOT_ID, clientSecret: process.env.HUBSPOT_SECRET }),
IDS4({ clientId: process.env.IDS4_ID, clientSecret: process.env.IDS4_SECRET, issuer: process.env.IDS4_ISSUER }),
Instagram({ clientId: process.env.INSTAGRAM_ID, clientSecret: process.env.INSTAGRAM_SECRET }),
Keycloak({ clientId: process.env.KEYCLOAK_ID, clientSecret: process.env.KEYCLOAK_SECRET, issuer: process.env.KEYCLOAK_ISSUER }),
@@ -123,7 +120,6 @@ export const authOptions: NextAuthOptions = {
Vk({ clientId: process.env.VK_ID, clientSecret: process.env.VK_SECRET }),
Wikimedia({ clientId: process.env.WIKIMEDIA_ID, clientSecret: process.env.WIKIMEDIA_SECRET }),
WorkOS({ clientId: process.env.WORKOS_ID, clientSecret: process.env.WORKOS_SECRET }),
Zitadel({ issuer: process.env.ZITADEL_ISSUER, clientId: process.env.ZITADEL_CLIENT_ID, clientSecret: process.env.ZITADEL_CLIENT_SECRET }),
],
}

View File

@@ -2,16 +2,12 @@ import { SessionProvider } from "next-auth/react"
import "./styles.css"
import type { AppProps } from "next/app"
import type { Session } from "next-auth"
// Use of the <SessionProvider> is mandatory to allow components that call
// `useSession()` anywhere in your application to access the `session` object.
export default function App({
Component,
pageProps: { session, ...pageProps },
}: AppProps<{ session: Session }>) {
export default function App({ Component, pageProps }: AppProps) {
return (
<SessionProvider session={session}>
<SessionProvider session={pageProps.session} refetchInterval={0}>
<Component {...pageProps} />
</SessionProvider>
)

View File

@@ -4,7 +4,8 @@ import Layout from "../components/layout"
import AccessDenied from "../components/access-denied"
export default function ProtectedPage() {
const { data: session } = useSession()
const { data: session, status } = useSession()
const loading = status === "loading"
const [content, setContent] = useState()
// Fetch content from protected route
@@ -18,7 +19,9 @@ export default function ProtectedPage() {
}
fetchData()
}, [session])
// When rendering client side don't display anything until loading is complete
if (typeof window !== "undefined" && loading) return null
// If no session exists, display access denied message
if (!session) {

View File

@@ -1161,9 +1161,9 @@ isexe@^2.0.0:
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
jose@^4.1.4, jose@^4.3.7:
version "4.9.3"
resolved "https://registry.yarnpkg.com/jose/-/jose-4.9.3.tgz#890abd3f26725fe0f2aa720bc2f7835702b624db"
integrity sha512-f8E/z+T3Q0kA9txzH2DKvH/ds2uggcw0m3vVPSB9HrSkrQ7mojjifvS7aR8cw+lQl2Fcmx9npwaHpM/M3GD8UQ==
version "4.5.0"
resolved "https://registry.yarnpkg.com/jose/-/jose-4.5.0.tgz#92829d8cf846351eb55aaaf94f252fb1d191f2d5"
integrity sha512-GFcVFQwYQKbQTUOo2JlpFGXTkgBw26uzDsRMD2q1WgSKNSnpKS9Ug7bdQ8dS+p4sZHNH6iRPu6WK2jLIjspaMA==
js-yaml@^4.1.0:
version "4.1.0"

View File

@@ -12,28 +12,15 @@ 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"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
export default NextAuth({
adapter: PrismaAdapter(prisma),

View File

@@ -112,16 +112,15 @@ Requests to `/api/auth/signin`, `/api/auth/session` and calls to `getSession()`,
- As with database persisted session expiry times, token expiry time is extended whenever a session is active.
- The arguments _user_, _account_, _profile_ and _isNewUser_ are only passed the first time this callback is called on a new session, after the user signs in. In subsequent calls, only `token` will be available.
The contents _user_, _account_, _profile_ and _isNewUser_ will vary depending on the provider and if you are using a database. You can persist data such as User ID, OAuth Access Token in this token, see the example below for `access_token` and `user.id`. To expose it on the client side, check out the [`session()` callback](#session-callback) as well.
The contents _user_, _account_, _profile_ and _isNewUser_ will vary depending on the provider and on if you are using a database or not. You can persist data such as User ID, OAuth Access Token in this token. To make it available in the browser, check out the [`session()` callback](#session-callback) as well.
```js title="pages/api/auth/[...nextauth].js"
...
callbacks: {
async jwt({ token, account, profile }) {
// Persist the OAuth access_token and or the user id to the token right after signin
async jwt({ token, account }) {
// Persist the OAuth access_token to the token right after signin
if (account) {
token.accessToken = account.access_token
token.id = profile.id
}
return token
}
@@ -135,7 +134,7 @@ Use an if branch to check for the existence of parameters (apart from `token`).
## Session callback
The session callback is called whenever a session is checked. By default, **only a subset of the token is returned for increased security**. If you want to make something available you added to the token (like `access_token` and `user.id` from above) via the `jwt()` callback, you have to explicitly forward it here to make it available to the client.
The session callback is called whenever a session is checked. By default, only a subset of the token is returned for increased security. If you want to make something available you added to the token through the `jwt()` callback, you have to explicitly forward it here to make it available to the client.
e.g. `getSession()`, `useSession()`, `/api/auth/session`
@@ -146,10 +145,8 @@ e.g. `getSession()`, `useSession()`, `/api/auth/session`
...
callbacks: {
async session({ session, token, user }) {
// Send properties to the client, like an access_token and user id from a provider.
// Send properties to the client, like an access_token from a provider.
session.accessToken = token.accessToken
session.user.id = token.id
return session
}
}
@@ -158,7 +155,7 @@ callbacks: {
:::tip
When using JSON Web Tokens the `jwt()` callback is invoked before the `session()` callback, so anything you add to the
JSON Web Token will be immediately available in the session callback, like for example an `access_token` or `id` from a provider.
JSON Web Token will be immediately available in the session callback, like for example an `access_token` from a provider.
:::
:::warning

View File

@@ -114,12 +114,6 @@ session: {
// Use it to limit write operations. Set to 0 to always update the database.
// Note: This option is ignored if using JSON Web Tokens
updateAge: 24 * 60 * 60, // 24 hours
// The session token is usually either a random UUID or string, however if you
// need a more customized session token string, you can define your own generate function.
generateSessionToken: () => {
return randomUUID?.() ?? randomBytes(32).toString("hex")
}
}
```

View File

@@ -156,7 +156,7 @@ interface OAuthConfig {
*/
id: string
version: string
profile(profile: P, tokens: TokenSet): Awaitable<User>
profile(profile: P, tokens: TokenSet): Awaitable<User & { id: string }>
checks?: ChecksType | ChecksType[]
clientId: string
clientSecret: string

View File

@@ -136,7 +136,7 @@ The `callbackUrl` provided was either invalid or not defined. See [specifying a
#### JWT_SESSION_ERROR
JWTKeySupport: the key does not support HS512 verify algorithm
JWKKeySupport: the key does not support HS512 verify algorithm
The algorithm used for generating your key isn't listed as supported. You can generate a HS512 key using

View File

@@ -67,7 +67,7 @@ export default function Component() {
Due to the way how Next.js handles `getServerSideProps` and `getInitialProps`, every protected page load has to make a server-side request to check if the session is valid and then generate the requested page (SSR). This increases server load, and if you are good with making the requests from the client, there is an alternative. You can use `useSession` in a way that makes sure you always have a valid session. If after the initial loading state there was no session found, you can define the appropriate action to respond.
The default behavior is to redirect the user to the sign-in page, from where - after a successful login - they will be sent back to the page they started on. You can also define an `onUnauthenticated()` callback, if you would like to do something else:
The default behavior is to redirect the user to the sign-in page, from where - after a successful login - they will be sent back to the page they started on. You can also define an `onFail()` callback, if you would like to do something else:
#### Example

View File

@@ -24,11 +24,7 @@ providers: [
AtlassianProvider({
clientId: process.env.ATLASSIAN_CLIENT_ID,
clientSecret: process.env.ATLASSIAN_CLIENT_SECRET,
authorization: {
params: {
scope: "write:jira-work read:jira-work read:jira-user offline_access read:me"
}
}
scope: "write:jira-work read:jira-work read:jira-user offline_access read:me"
})
]
...

View File

@@ -50,7 +50,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) {
// Add logic here to look up the user from the credentials supplied

View File

@@ -1,87 +0,0 @@
---
id: zitadel
title: Zitadel
---
## Documentation
https://docs.zitadel.com/docs/apis/openidoauth/endpoints
## Configuration
https://docs.zitadel.com/docs/guides/integrate/oauth-recommended-flows
The Redirect URIs used when creating the credentials must include your full domain and end in the callback path. For example:
- For production: `https://{YOUR_DOMAIN}/api/auth/callback/zitadel`
- For development: `http://localhost:3000/api/auth/callback/zitadel`
Make sure to enable **dev mode** in ZITADEL console to allow redirects for local development.
## Options
The **ZITADEL Provider** comes with a set of default options:
- [ZITADEL Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/zitadel.ts)
You can override any of the options to suit your own use case.
## Example
```js
import ZitadelProvider from "next-auth/providers/zitadel";
...
providers: [
ZitadelProvider({
issuer: process.env.ZITADEL_ISSUER,
clientId: process.env.ZITADEL_CLIENT_ID,
clientSecret: process.env.ZITADEL_CLIENT_SECRET,
})
]
...
```
If you need access to ZITADEL APIs or need additional information, make sure to add the corresponding scopes.
To get the full list of supported claims take a look [here](https://docs.zitadel.com/docs/apis/openidoauth/endpoints).
```js
const options = {
...
providers: [
ZitadelProvider({
clientId: process.env.ZITADEL_CLIENT_ID,
authorization: {
params: {
scope: `openid email profile urn:zitadel:iam:org:project:id:${process.env.ZITADEL_PROJECT_ID}:aud`
}
}
})
],
...
}
```
:::
:::tip
ZITADEL also returns a `email_verified` boolean property in the profile.
You can use this property to restrict access to people with verified accounts.
```js
const options = {
...
callbacks: {
async signIn({ account, profile }) {
if (account.provider === "zitadel") {
return profile.email_verified;
}
return true; // Do different verification for other providers that don't have `email_verified`
},
}
...
}
```
:::

View File

@@ -16,7 +16,7 @@ If you contact us regarding a serious issue:
- We will disclose the issue (and credit you, with your consent) once a fix to resolve the issue has been released.
- If 90 days has elapsed and we still don't have a fix, we will disclose the issue publicly.
The best way to report an issue is by contacting us via email at hi@thvu.dev, info@balazsorban.com, yo@ndo.dev and me@iaincollins.com, or raise a public issue requesting someone get in touch with you via whatever means you prefer for more details. (Please do not disclose sensitive details publicly at this stage.)
The best way to report an issue is by contacting us via email at info@balazsorban.com, yo@ndo.dev, thvu@hey.com and me@iaincollins.com, or raise a public issue requesting someone get in touch with you via whatever means you prefer for more details. (Please do not disclose sensitive details publicly at this stage.)
:::note
For less serious issues (e.g. RFC compliance for unsupported flows or potential issues that may cause a problem in the future) it is appropriate to submit these these publically as bug reports or feature requests or to raise a question to open a discussion around them.

View File

@@ -105,11 +105,6 @@ This tutorial covers:
## Database
#### [Create a NextAuth.js Custom Adapter with HarperDB & Next.js](https://spacejelly.dev/posts/how-to-create-a-nextauth-js-custom-adapter-with-harperdb-next-js/) <svg xmlns="http://www.w3.org/2000/svg" style={{ marginLeft: '5px', marginBottom:'-6px'}} height="20" width="20" fill="none" viewBox="0 0 24 24" stroke="currentColor"><title>External</title> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> </svg>
- Use a custom database in a Custom Adapter for persisted NextAuth.js sessions using HarperDB as an example.
- Video tutorial also available: <https://www.youtube.com/watch?v=pu7xBv7sZ8s>
#### [Using NextAuth.js with Prisma and PlanetScale serverless databases](https://github.com/planetscale/nextjs-planetscale-starter) <svg xmlns="http://www.w3.org/2000/svg" style={{ marginLeft: '5px', marginBottom:'-6px'}} height="20" width="20" fill="none" viewBox="0 0 24 24" stroke="currentColor"><title>External</title> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> </svg>
- How to set up a PlanetScale database to fetch and store user / account data with the Prisma adapter.

View File

@@ -42,30 +42,18 @@ export default function Page() {
### Next.js (Middleware)
With NextAuth.js 4.2.0 and Next.js 12, you can now protect your pages via the middleware pattern more easily. If you would like to protect all pages, you can create a `middleware.js` file in your root `pages` directory which looks like this:
With NextAuth.js 4.2.0 and Next.js 12, you can now protect your pages via the middleware pattern more easily. If you would like to protect all pages, you can create a `_middleware.js` file in your root `pages` directory which looks like this.
```js title="/middleware.js"
export { default } from "next-auth/middleware"
```
If you only want to secure certain pages, export a `config` object with a `matcher`:
```js
export { default } from "next-auth/middleware"
export const config = { matcher: ["/dashboard"] }
```
Otherwise, if you only want to protect a subset of pages, you could put it in a subdirectory as well, for example in `/pages/admin/_middleware.js` would protect all pages under `/admin`.
For the time being, the `withAuth` middleware only supports `"jwt"` as [session strategy](https://next-auth.js.org/configuration/options#session).
More details can be found [here](https://next-auth.js.org/configuration/nextjs#middleware).
:::tip
To inclue all `dashboard` nested routes (sub pages like `/dashboard/settings`, `/dashboard/profile`) you can pass `matcher: "/dashboard/:path*"` to `config`.
For other patterns check out the [Next.js Middleware documentation](https://nextjs.org/docs/advanced-features/middleware#matcher).
:::
### Server Side
You can protect server side rendered pages using the `unstable_getServerSession` method. This is different from the old `getSession()` method, in that it does not do an extra fetch out over the internet to confirm data from itself, increasing performance significantly.

View File

@@ -7,7 +7,7 @@
"build:app": "turbo run build --filter=next-auth-app --include-dependencies",
"build": "turbo run build --filter=next-auth --filter=@next-auth/* --no-deps",
"lint": "turbo run lint --filter=!next-auth-docs --parallel",
"test": "turbo run test --concurrency=1 --filter=!@next-auth/pouchdb-adapter --filter=!@next-auth/upstash-redis-adapter --filter=!next-auth-* --filter=[HEAD^1]",
"test": "turbo run test --concurrency=1 --filter=!@next-auth/pouchdb-adapter --filter=!@next-auth/mikro-orm-adapter --filter=!@next-auth/upstash-redis-adapter --filter=!next-auth-* --filter=[HEAD^1]",
"clean": "turbo run clean --no-cache",
"dev:app": "turbo run dev --parallel --continue --filter=next-auth-app...",
"dev:docs": "turbo run dev --filter=next-auth-docs",
@@ -18,7 +18,7 @@
},
"devDependencies": {
"@actions/core": "^1.6.0",
"@balazsorban/monorepo-release": "0.0.5",
"@balazsorban/monorepo-release": "0.0.4",
"@types/jest": "^28.1.3",
"@types/node": "^17.0.25",
"@typescript-eslint/eslint-plugin": "^5.10.2",

View File

@@ -1,7 +1,7 @@
{
"name": "@next-auth/dynamodb-adapter",
"repository": "https://github.com/nextauthjs/next-auth",
"version": "1.0.5",
"version": "1.0.4",
"description": "AWS DynamoDB adapter for next-auth.",
"keywords": [
"next-auth",
@@ -43,4 +43,4 @@
"jest": "^27.4.3",
"next-auth": "workspace:*"
}
}
}

View File

@@ -4,10 +4,10 @@ import type {
BatchWriteCommandInput,
DynamoDBDocument,
} from "@aws-sdk/lib-dynamodb"
import type { Account } from "next-auth"
import type {
Adapter,
AdapterSession,
AdapterAccount,
AdapterUser,
VerificationToken,
} from "next-auth/adapters"
@@ -86,7 +86,7 @@ export function DynamoDBAdapter(
})
if (!data.Items?.length) return null
const accounts = data.Items[0] as AdapterAccount
const accounts = data.Items[0] as Account
const res = await client.get({
TableName,
Key: {
@@ -174,7 +174,7 @@ export function DynamoDBAdapter(
":gsi1sk": `ACCOUNT#${providerAccountId}`,
},
})
const account = format.from<AdapterAccount>(data.Items?.[0])
const account = format.from<Account>(data.Items?.[0])
if (!account) return
await client.delete({
TableName,

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/firebase-adapter",
"version": "1.0.2",
"version": "1.0.1",
"description": "Firebase adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -43,4 +43,4 @@
"jest": "^27.4.3",
"next-auth": "workspace:*"
}
}
}

View File

@@ -15,18 +15,17 @@ import {
where,
connectFirestoreEmulator,
} from "firebase/firestore"
import type { Account } from "next-auth"
import type {
Adapter,
AdapterUser,
AdapterAccount,
AdapterSession,
AdapterUser,
VerificationToken,
} from "next-auth/adapters"
import { getConverter } from "./converter"
export type IndexableObject = Record<string, unknown>
type IndexableObject = Record<string, unknown>
export interface FirestoreAdapterOptions {
emulator?: {
@@ -51,13 +50,13 @@ export function FirestoreAdapter({
}
const Users = collection(db, "users").withConverter(
getConverter<AdapterUser & IndexableObject>()
getConverter<AdapterUser>()
)
const Sessions = collection(db, "sessions").withConverter(
getConverter<AdapterSession & IndexableObject>()
)
const Accounts = collection(db, "accounts").withConverter(
getConverter<AdapterAccount>()
getConverter<Account>()
)
const VerificationTokens = collection(db, "verificationTokens").withConverter(
getConverter<VerificationToken & IndexableObject>({ excludeId: true })

View File

@@ -14,7 +14,7 @@ connectFirestoreEmulator(firestore, 'localhost', 8080);
type IndexableObject = Record<string, unknown>;
const Users = collection(firestore, 'users').withConverter(getConverter<AdapterUser & IndexableObject>());
const Users = collection(firestore, 'users').withConverter(getConverter<AdapterUser>());
const Sessions = collection(firestore, 'sessions').withConverter(getConverter<AdapterSession & IndexableObject>());
const Accounts = collection(firestore, 'accounts').withConverter(getConverter<Account>());
const VerificationTokens = collection(firestore, 'verificationTokens').withConverter(getConverter<VerificationToken & IndexableObject>({ excludeId: true }));

View File

@@ -0,0 +1,10 @@
<p align="center">
<br/>
<a href="https://next-auth.js.org" target="_blank"><img height="64px" src="https://next-auth.js.org/img/logo/logo-sm.png" /></a>&nbsp;&nbsp;&nbsp;&nbsp;<img height="64px" src="https://hasura.io/brand-assets/hasura-logo-primary-dark.svg" />
<h3 align="center"><b>Hasura Adapter</b> - NextAuth.js</h3>
<p align="center">
Open Source. Full Stack. Own Your Data.
</p>
</p>
TODO

View File

@@ -0,0 +1,44 @@
{
"name": "@next-auth/hasura-adapter",
"version": "1.0.0",
"description": "Hasura adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
"bugs": {
"url": "https://github.com/nextauthjs/next-auth/issues"
},
"author": "Balázs Orbán <info@balazsorban.com>",
"contributors": [],
"main": "dist/index.js",
"files": [
"dist",
"index.d.ts"
],
"license": "ISC",
"keywords": [
"next-auth",
"next.js",
"hasura",
"graphql"
],
"private": false,
"publishConfig": {
"access": "public"
},
"scripts": {
"dev": "tsc --watch",
"build": "tsc",
"test": "./tests/test.sh"
},
"peerDependencies": {
"next-auth": "^4"
},
"devDependencies": {
"@next-auth/adapter-test": "workspace:*",
"@next-auth/tsconfig": "workspace:*",
"next-auth": "workspace:*"
},
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}

View File

@@ -0,0 +1,49 @@
import type { Account } from "next-auth"
import type {
AdapterSession,
AdapterUser,
VerificationToken,
} from "next-auth/adapters"
export interface Model<T> {
name: string
fields: Array<keyof T>
}
export interface Models {
User: Model<AdapterUser>
Account: Model<Account>
Session: Model<AdapterSession>
VerificationToken: Model<VerificationToken>
}
export const models: Models = {
User: {
name: "User",
fields: ["email", "id", "image", "name", "emailVerified"],
},
Account: {
name: "Account",
fields: [
"id",
"type",
"provider",
"providerAccountId",
"expires_at",
"token_type",
"scope",
"access_token",
"refresh_token",
"id_token",
"session_state",
],
},
Session: {
name: "Session",
fields: ["expires", "id", "sessionToken"],
},
VerificationToken: {
name: "VerificationToken",
fields: ["identifier", "token", "expires"],
},
}

View File

@@ -0,0 +1 @@
# TODO

View File

@@ -0,0 +1,209 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-base-to-string */
import { format } from "./utils"
import { Models, Model, models } from "./graphql/models"
import type { Adapter, AdapterUser } from "next-auth/adapters"
import type { Account } from "next-auth"
export type { Models, Model }
export interface HasuraClientOptions {
/** GraphQL endpoint */
url: string
/**
* `X-Hasura-Admin-Secret` header value
*
* [Securing the GraphQL endpoint](https://hasura.io/docs/latest/deployment/securing-graphql-endpoint/)
*/
adminSecret: string
}
export interface HasuraAdapterOptions {
client: HasuraClientOptions
models?: Partial<Models>
}
export function HasuraAdapter(options: HasuraAdapterOptions): Adapter {
const c = client(options.client)
const m: Models = {
User: { ...models.User, ...options.models?.User },
Account: { ...models.Account, ...options.models?.Account },
Session: { ...models.Session, ...options.models?.Session },
VerificationToken: {
...models.VerificationToken,
...options.models?.VerificationToken,
},
}
return {
async createUser(data) {
const result = await c.run<AdapterUser[]>(
/* GraphQL */ `
mutation ($data: [${m.User.name}_insert_input!]!) {
insert_${m.User.name}(objects: $data) {
returning {
${m.User.fields.join(" ")}
}
}
}
`,
{ data }
)
return format.from(result?.[0])
},
async getUser(id) {
const result = await c.run<AdapterUser>(
/* GraphQL */ `
query ($id: String!) {
${m.User.name}(where: { id: { _eq: $id } }) {
${m.User.fields.join(" ")}
}
}
`,
{ id }
)
return format.from(result)
},
async getUserByEmail(email) {
const result = await c.run<AdapterUser[]>(
/* GraphQL */ `
query ($email: String!) {
${m.User.name}(where: { email: { _eq: $email } }) {
${m.User.fields.join(" ")}
}
}
`,
{ email }
)
return result?.[0] ?? null
},
async getUserByAccount(provider_providerAccountId) {
const result = await c.run<Array<AdapterUser & { Accounts: Account }>>(
/* GraphQL */ `
query getUserByAccount($providerAccountId: String!, $provider: String!) {
${m.User.name}(
where: {Accounts: {providerAccountId: {_eq: $providerAccountId}, provider: {_eq: $provider}}}
) {
${m.User.fields.join(" ")}
}
}
`,
provider_providerAccountId
)
if (!result?.length) return null
const { Accounts: _, ...user } = result[0]
return user
},
async updateUser({ id, ...input }) {
throw new HasuraAdapterError("`updateUser` not implemented")
},
async deleteUser(id) {
throw new HasuraAdapterError("`deleteUser` not implemented")
},
async linkAccount(data) {
throw new HasuraAdapterError("`linkAccount` not implemented")
},
async unlinkAccount(provider_providerAccountId) {
throw new HasuraAdapterError("`unlinkAccount` not implemented")
},
async getSessionAndUser(sessionToken) {
throw new HasuraAdapterError("`getSessionAndUser` not implemented")
},
async createSession(data) {
throw new HasuraAdapterError("`createSession` not implemented")
},
async updateSession({ sessionToken, ...input }) {
throw new HasuraAdapterError("`updateSession` not implemented")
},
async deleteSession(sessionToken) {
throw new HasuraAdapterError("`deleteSession` not implemented")
},
async createVerificationToken(input) {
throw new HasuraAdapterError("`createVerificationToken` not implemented")
},
async useVerificationToken(params) {
throw new HasuraAdapterError("`useVerificationToken` not implemented")
},
}
}
export class HasuraAdapterError extends Error {
name = "HasuraAdapterError"
query?: string
variables?: any
constructor(
e: Error | string | Array<Error | string>,
query?: string,
variables?: any
) {
// @ts-expect-error
super(Array.isArray(e) ? e.map((e) => e.message ?? e).join("\n") : e.m ?? e)
this.query = query
this.variables = variables
}
toJSON() {
return {
name: this.name,
message: this.message,
query: this.query,
variables: this.variables,
}
}
toString() {
const items = [
this.message,
this.query,
JSON.stringify(this.variables, null, 2),
].filter(Boolean)
return `${this.name}: ${items.join("\n")}`
}
}
export function client(options: HasuraClientOptions) {
if (!globalThis.fetch) {
throw new HasuraAdapterError("Please provide a `fetch` implementation")
}
const { url, adminSecret } = options
if (!adminSecret) {
throw new HasuraAdapterError("Please provide an API key")
}
if (!url) {
throw new HasuraAdapterError("Please provide a GraphQL endpoint")
}
return {
async run<T>(
query: string,
variables?: Record<string, any>
): Promise<T | null> {
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Hasura-Admin-Secret": adminSecret,
},
body: JSON.stringify({ query, variables }),
})
const { data = {}, errors } = await response.json()
if (errors?.length) {
throw new HasuraAdapterError(errors, query, variables)
}
return Object.values(data)[0] as any
} catch (error) {
if (error instanceof HasuraAdapterError) throw error
throw new HasuraAdapterError(error as Error, query, variables)
}
},
}
}
export { format, models }

View File

@@ -0,0 +1,26 @@
// https://github.com/honeinc/is-iso-date/blob/master/index.js
const isoDateRE =
/(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/
function isDate(value: any) {
return value && isoDateRE.test(value) && !isNaN(Date.parse(value))
}
export const format = {
from<T extends Record<string, any> | null>(
data?: T
): T extends null ? null : T {
if (!data) return null as any
const newObject: Record<string, unknown> = {}
for (const key in data) {
const value = data[key]
if (isDate(value)) {
newObject[key] = new Date(value)
} else {
newObject[key] = value
}
}
return newObject as any
},
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": ["tests", "src"],
"exclude": [
"./*.js",
"./*.d.ts",
]
}

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/mikro-orm-adapter",
"version": "3.0.1",
"version": "2.0.1",
"description": "MikroORM adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -32,22 +32,22 @@
"dist"
],
"peerDependencies": {
"@mikro-orm/core": "^5",
"@mikro-orm/core": "^5.0.2",
"next-auth": "^4"
},
"devDependencies": {
"@mikro-orm/core": "^5",
"@mikro-orm/sqlite": "^5",
"@mikro-orm/core": "^5.0.2",
"@mikro-orm/sqlite": "^5.0.2",
"@next-auth/adapter-test": "workspace:*",
"@next-auth/tsconfig": "workspace:*",
"@types/uuid": ">=8",
"jest": "^29",
"@types/uuid": "^8.3.3",
"jest": "^27.4.3",
"next-auth": "workspace:*"
},
"dependencies": {
"uuid": "^9"
},
"jest": {
"preset": "@next-auth/adapter-test/jest"
},
"dependencies": {
"uuid": "^8.3.2"
}
}

View File

@@ -5,16 +5,16 @@ import {
Unique,
PrimaryKey,
Entity,
Enum,
OneToMany,
Collection,
ManyToOne,
types,
} from "@mikro-orm/core"
import type { DefaultAccount } from "next-auth"
import type {
AdapterUser,
AdapterAccount,
AdapterSession,
AdapterUser,
VerificationToken as AdapterVerificationToken,
} from "next-auth/adapters"
import type { ProviderType } from "next-auth/providers"
@@ -29,105 +29,103 @@ export class User implements RemoveIndex<AdapterUser> {
@PrimaryKey()
id: string = randomUUID()
@Property({ type: types.string, nullable: true })
@Property({ nullable: true })
name?: string
@Property({ type: types.string, nullable: true })
@Property({ nullable: true })
@Unique()
email: string = ""
email?: string
@Property({ type: types.datetime, nullable: true })
@Property({ type: "Date", nullable: true })
emailVerified: Date | null = null
@Property({ type: types.string, nullable: true })
@Property({ nullable: true })
image?: string
@OneToMany({
entity: "Session",
mappedBy: (session: Session) => session.user,
entity: () => Session,
mappedBy: (session) => session.user,
hidden: true,
orphanRemoval: true,
})
sessions = new Collection<Session, object>(this)
sessions = new Collection<Session>(this)
@OneToMany({
entity: "Account",
mappedBy: (account: Account) => account.user,
entity: () => Account,
mappedBy: (account) => account.user,
hidden: true,
orphanRemoval: true,
})
accounts = new Collection<Account, object>(this)
accounts = new Collection<Account>(this)
}
@Entity()
export class Session implements AdapterSession {
@PrimaryKey()
@Property({ type: types.string })
id: string = randomUUID()
@ManyToOne({
entity: "User",
entity: () => User,
hidden: true,
onDelete: "cascade",
})
user!: User
@Property({ type: types.string, persist: false })
@Property({ persist: false })
userId!: string
@Property({ type: "Date" })
@Property()
expires!: Date
@Property({ type: types.string })
@Property()
@Unique()
sessionToken!: string
}
@Entity()
@Unique({ properties: ["provider", "providerAccountId"] })
export class Account implements RemoveIndex<AdapterAccount> {
export class Account implements RemoveIndex<DefaultAccount> {
@PrimaryKey()
@Property({ type: types.string })
id: string = randomUUID()
@ManyToOne({
entity: "User",
entity: () => User,
hidden: true,
onDelete: "cascade",
})
user!: User
@Property({ type: types.string, persist: false })
@Property({ persist: false })
userId!: string
@Property({ type: types.string })
@Enum()
type!: ProviderType
@Property({ type: types.string })
@Property()
provider!: string
@Property({ type: types.string })
@Property()
providerAccountId!: string
@Property({ type: types.string, nullable: true })
@Property({ nullable: true })
refresh_token?: string
@Property({ type: types.string, nullable: true })
@Property({ nullable: true })
access_token?: string
@Property({ type: types.integer, nullable: true })
@Property({ nullable: true })
expires_at?: number
@Property({ type: types.string, nullable: true })
@Property({ nullable: true })
token_type?: string
@Property({ type: types.string, nullable: true })
@Property({ nullable: true })
scope?: string
@Property({ type: types.text, nullable: true })
@Property({ nullable: true })
id_token?: string
@Property({ type: types.string, nullable: true })
@Property({ nullable: true })
session_state?: string
}
@@ -135,12 +133,12 @@ export class Account implements RemoveIndex<AdapterAccount> {
@Unique({ properties: ["token", "identifier"] })
export class VerificationToken implements AdapterVerificationToken {
@PrimaryKey()
@Property({ type: types.string })
@Property()
token!: string
@Property({ type: "Date" })
@Property()
expires!: Date
@Property({ type: types.string })
@Property()
identifier!: string
}

View File

@@ -10,7 +10,7 @@ import { MikroORM, wrap } from "@mikro-orm/core"
import * as defaultEntities from "./entities"
export { defaultEntities }
export * as defaultEntities from "./entities"
/**
* The MikroORM adapter accepts a MikroORM configuration and returns a NextAuth adapter.

View File

@@ -1,591 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`run migrations: createSchemaSQL 1`] = `
"pragma foreign_keys = off;
create table \`user\` (\`id\` text not null, \`name\` text null, \`email\` text null, \`email_verified\` datetime null, \`image\` text null, primary key (\`id\`));
create unique index \`user_email_unique\` on \`user\` (\`email\`);
create table \`session\` (\`id\` text not null, \`user_id\` text not null, \`expires\` datetime not null, \`session_token\` text not null, constraint \`session_user_id_foreign\` foreign key(\`user_id\`) references \`user\`(\`id\`) on delete cascade on update cascade, primary key (\`id\`));
create index \`session_user_id_index\` on \`session\` (\`user_id\`);
create unique index \`session_session_token_unique\` on \`session\` (\`session_token\`);
create table \`account\` (\`id\` text not null, \`user_id\` text not null, \`type\` text not null, \`provider\` text not null, \`provider_account_id\` text not null, \`refresh_token\` text null, \`access_token\` text null, \`expires_at\` integer null, \`token_type\` text null, \`scope\` text null, \`id_token\` text null, \`session_state\` text null, constraint \`account_user_id_foreign\` foreign key(\`user_id\`) references \`user\`(\`id\`) on delete cascade on update cascade, primary key (\`id\`));
create index \`account_user_id_index\` on \`account\` (\`user_id\`);
create unique index \`account_provider_provider_account_id_unique\` on \`account\` (\`provider\`, \`provider_account_id\`);
create table \`verification_token\` (\`token\` text not null, \`expires\` datetime not null, \`identifier\` text not null, primary key (\`token\`));
create unique index \`verification_token_token_identifier_unique\` on \`verification_token\` (\`token\`, \`identifier\`);
pragma foreign_keys = on;
"
`;
exports[`run migrations: targetSchema 1`] = `
{
"name": undefined,
"namespaces": [],
"tables": [
{
"checks": [],
"columns": {
"email": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "email",
"nullable": true,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"email_verified": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": 0,
"mappedType": "datetime",
"name": "email_verified",
"nullable": true,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "datetime",
"unsigned": false,
},
"id": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "id",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"image": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "image",
"nullable": true,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"name": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "name",
"nullable": true,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
},
"comment": undefined,
"foreignKeys": {},
"indexes": [
{
"columnNames": [
"email",
],
"composite": false,
"keyName": "user_email_unique",
"primary": false,
"unique": true,
},
{
"columnNames": [
"id",
],
"composite": false,
"expression": undefined,
"keyName": "primary",
"primary": true,
"type": undefined,
"unique": true,
},
],
"name": "user",
"schema": undefined,
},
{
"checks": [],
"columns": {
"expires": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": 0,
"mappedType": "datetime",
"name": "expires",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "datetime",
"unsigned": false,
},
"id": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "id",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"session_token": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "session_token",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"user_id": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "user_id",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
},
"comment": undefined,
"foreignKeys": {
"session_user_id_foreign": {
"columnNames": [
"user_id",
],
"constraintName": "session_user_id_foreign",
"deleteRule": "cascade",
"localTableName": "session",
"referencedColumnNames": [
"id",
],
"referencedTableName": "user",
"updateRule": "cascade",
},
},
"indexes": [
{
"columnNames": [
"user_id",
],
"composite": false,
"keyName": "session_user_id_index",
"primary": false,
"unique": false,
},
{
"columnNames": [
"session_token",
],
"composite": false,
"keyName": "session_session_token_unique",
"primary": false,
"unique": true,
},
{
"columnNames": [
"id",
],
"composite": false,
"expression": undefined,
"keyName": "primary",
"primary": true,
"type": undefined,
"unique": true,
},
],
"name": "session",
"schema": undefined,
},
{
"checks": [],
"columns": {
"access_token": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "access_token",
"nullable": true,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"expires_at": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "integer",
"name": "expires_at",
"nullable": true,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "integer",
"unsigned": false,
},
"id": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "id",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"id_token": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "id_token",
"nullable": true,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"provider": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "provider",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"provider_account_id": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "provider_account_id",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"refresh_token": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "refresh_token",
"nullable": true,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"scope": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "scope",
"nullable": true,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"session_state": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "session_state",
"nullable": true,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"token_type": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "token_type",
"nullable": true,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"type": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "type",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"user_id": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "user_id",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
},
"comment": undefined,
"foreignKeys": {
"account_user_id_foreign": {
"columnNames": [
"user_id",
],
"constraintName": "account_user_id_foreign",
"deleteRule": "cascade",
"localTableName": "account",
"referencedColumnNames": [
"id",
],
"referencedTableName": "user",
"updateRule": "cascade",
},
},
"indexes": [
{
"columnNames": [
"user_id",
],
"composite": false,
"keyName": "account_user_id_index",
"primary": false,
"unique": false,
},
{
"columnNames": [
"provider",
"provider_account_id",
],
"composite": true,
"expression": undefined,
"keyName": "account_provider_provider_account_id_unique",
"primary": false,
"type": undefined,
"unique": true,
},
{
"columnNames": [
"id",
],
"composite": false,
"expression": undefined,
"keyName": "primary",
"primary": true,
"type": undefined,
"unique": true,
},
],
"name": "account",
"schema": undefined,
},
{
"checks": [],
"columns": {
"expires": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": 0,
"mappedType": "datetime",
"name": "expires",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "datetime",
"unsigned": false,
},
"identifier": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "identifier",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
"token": {
"autoincrement": false,
"comment": undefined,
"default": undefined,
"enumItems": undefined,
"extra": undefined,
"length": undefined,
"mappedType": "text",
"name": "token",
"nullable": false,
"precision": undefined,
"primary": false,
"scale": undefined,
"type": "text",
"unsigned": false,
},
},
"comment": undefined,
"foreignKeys": {},
"indexes": [
{
"columnNames": [
"token",
"identifier",
],
"composite": true,
"expression": undefined,
"keyName": "verification_token_token_identifier_unique",
"primary": false,
"type": undefined,
"unique": true,
},
{
"columnNames": [
"token",
],
"composite": false,
"expression": undefined,
"keyName": "primary",
"primary": true,
"type": undefined,
"unique": true,
},
],
"name": "verification_token",
"schema": undefined,
},
],
}
`;

View File

@@ -1,70 +1,10 @@
import type { Options } from "@mikro-orm/core"
import type { SqliteDriver } from "@mikro-orm/sqlite"
import { MikroORM, wrap } from "@mikro-orm/core"
import { runBasicTests } from "@next-auth/adapter-test"
import { MikroOrmAdapter, defaultEntities } from "../src"
import {
Cascade,
Collection,
Entity,
OneToMany,
PrimaryKey,
Property,
Unique,
MikroORM,
wrap,
Options,
types,
} from "@mikro-orm/core"
import { randomUUID, runBasicTests } from "@next-auth/adapter-test"
@Entity()
export class User implements defaultEntities.User {
@PrimaryKey()
@Property({ type: types.string })
id: string = randomUUID()
@Property({ type: types.string, nullable: true })
name?: string
@Property({ type: types.string, nullable: true })
@Unique()
email: string = ""
@Property({ type: "Date", nullable: true })
emailVerified: Date | null = null
@Property({ type: types.string, nullable: true })
image?: string
@OneToMany({
entity: "Session",
mappedBy: (session: defaultEntities.Session) => session.user,
hidden: true,
orphanRemoval: true,
cascade: [Cascade.ALL],
})
sessions = new Collection<defaultEntities.Session>(this)
@OneToMany({
entity: "Account",
mappedBy: (account: defaultEntities.Account) => account.user,
hidden: true,
orphanRemoval: true,
cascade: [Cascade.ALL],
})
accounts = new Collection<defaultEntities.Account>(this)
@Property({ type: types.string, hidden: true })
role = "ADMIN"
}
@Entity()
export class VeryImportantEntity {
@PrimaryKey()
@Property({ type: types.string })
id: string = randomUUID()
@Property({ type: types.boolean })
important = true
}
import { User, VeryImportantEntity } from "./testEntities"
let _init: MikroORM

View File

@@ -1,28 +0,0 @@
import { MikroORM, Options } from "@mikro-orm/core";
import { SqliteDriver } from "@mikro-orm/sqlite";
import { defaultEntities } from "../src";
const config: Options<SqliteDriver> = {
dbName: "./db.sqlite",
type: "sqlite",
entities: [
defaultEntities.User,
defaultEntities.Account,
defaultEntities.Session,
defaultEntities.VerificationToken,
],
}
it("run migrations", async () => {
const orm = await MikroORM.init(config)
await orm.getSchemaGenerator().dropSchema()
const createSchemaSQL = await orm.getSchemaGenerator().getCreateSchemaSQL()
expect(createSchemaSQL).toMatchSnapshot('createSchemaSQL')
const targetSchema = await orm.getSchemaGenerator().getTargetSchema()
expect(targetSchema).toMatchSnapshot('targetSchema')
await orm.getSchemaGenerator().dropSchema()
await orm.close().catch(() => null)
})

View File

@@ -0,0 +1,61 @@
import {
Cascade,
Collection,
Entity,
OneToMany,
PrimaryKey,
Property,
Unique,
} from "@mikro-orm/core"
import { randomUUID } from "@next-auth/adapter-test"
import type { defaultEntities } from "../src"
import { Account, Session } from "../src/entities"
@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)
@Property({ hidden: true })
role = "ADMIN"
}
@Entity()
export class VeryImportantEntity {
@PrimaryKey()
id: string = randomUUID()
@Property()
important = true
}

View File

@@ -5,4 +5,4 @@
"./*.js",
"./*.d.ts",
]
}
}

View File

@@ -8,6 +8,5 @@
"outDir": "dist",
"stripInternal": true
},
"include": ["src"],
"exclude": ["dist", "test", "node_modules"]
"exclude": ["tests", "dist", "jest.config.js"]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/mongodb-adapter",
"version": "1.1.1",
"version": "1.0.4",
"description": "mongoDB adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -44,4 +44,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -3,12 +3,12 @@ import { ObjectId } from "mongodb"
import type {
Adapter,
AdapterUser,
AdapterAccount,
AdapterSession,
AdapterUser,
VerificationToken,
} from "next-auth/adapters"
import type { MongoClient } from "mongodb"
import type { Account } from "next-auth"
export interface MongoDBAdapterOptions {
collections?: {
@@ -56,7 +56,7 @@ export const format = {
else if (key === "id") continue
else newObject[key] = value
}
return newObject as T & { _id: ObjectId }
return newObject as T
},
}
@@ -78,7 +78,7 @@ export function MongoDBAdapter(
const c = { ...defaultCollections, ...collections }
return {
U: _db.collection<AdapterUser>(c.Users),
A: _db.collection<AdapterAccount>(c.Accounts),
A: _db.collection<Account>(c.Accounts),
S: _db.collection<AdapterSession>(c.Sessions),
V: _db.collection<VerificationToken>(c?.VerificationTokens),
}
@@ -128,7 +128,7 @@ export function MongoDBAdapter(
])
},
linkAccount: async (data) => {
const account = to<AdapterAccount>(data)
const account = to<Account>(data)
await (await db).A.insertOne(account)
return account
},
@@ -136,7 +136,7 @@ export function MongoDBAdapter(
const { value: account } = await (
await db
).A.findOneAndDelete(provider_providerAccountId)
return from<AdapterAccount>(account!)
return from<Account>(account!)
},
async getSessionAndUser(sessionToken) {
const session = await (await db).S.findOne({ sessionToken })
@@ -156,6 +156,7 @@ export function MongoDBAdapter(
return from<AdapterSession>(session)
},
async updateSession(data) {
// @ts-expect-error
const { _id, ...session } = to<AdapterSession>(data)
const result = await (

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/neo4j-adapter",
"version": "1.0.5",
"version": "1.0.4",
"description": "neo4j adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -50,4 +50,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -87,6 +87,8 @@ export function Neo4jAdapter(session: Session): Adapter {
)
},
// @ts-expect-error Property 'id' is missing in type
// We never use `session.id` anywhere in the core, so this is fine.
async createSession(data) {
const { userId, ...s } = format.to(data)
await write(

View File

@@ -38,7 +38,7 @@ runBasicTests({
return format.from(result?.records[0]?.get("u")?.properties)
},
async session(sessionToken: string) {
async session(sessionToken: any) {
const result = await neo4jSession.readTransaction((tx) =>
tx.run(
`MATCH (u:User)-[:HAS_SESSION]->(s:Session)

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env bash
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
NEO4J_USER=neo4j
NEO4J_PASS=password
CONTAINER_NAME=next-auth-neo4j-test-e
@@ -28,7 +29,7 @@ neo4j:4.2.0
# -e NEO4J_ACCEPT_LICENSE_AGREEMENT=yes \
# neo4j:4.2.0-enterprise
echo "Waiting 10 sec for db to start..." && sleep 10
echo "Waiting 5 sec for db to start..." && sleep 5
if $JEST_WATCH; then
# Run jest in watch mode

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/prisma-adapter",
"version": "1.0.5",
"version": "1.0.4",
"description": "Prisma adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -52,4 +52,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -20,6 +20,7 @@ model User {
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
@@ -34,10 +35,11 @@ model Account {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([provider, providerAccountId])
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
@@ -49,5 +51,5 @@ model VerificationToken {
token String @unique
expires DateTime
@@id([identifier, token])
@@unique([identifier, token])
}

View File

@@ -4,7 +4,8 @@ datasource db {
}
generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
previewFeatures = ["mongoDb"]
}
model Account {

View File

@@ -10,7 +10,7 @@ generator client {
model User {
id String @id @default(cuid())
name String?
email String @unique
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
@@ -18,6 +18,7 @@ model User {
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
@@ -32,10 +33,11 @@ model Account {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([provider, providerAccountId])
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
@@ -47,5 +49,5 @@ model VerificationToken {
token String @unique
expires DateTime
@@id([identifier, token])
@@unique([identifier, token])
}

View File

@@ -1,5 +1,5 @@
import type { PrismaClient, Prisma } from "@prisma/client"
import type { Adapter, AdapterAccount } from "next-auth/adapters"
import type { Adapter } from "next-auth/adapters"
export function PrismaAdapter(p: PrismaClient): Adapter {
return {
@@ -15,12 +15,9 @@ export function PrismaAdapter(p: PrismaClient): Adapter {
},
updateUser: ({ id, ...data }) => p.user.update({ where: { id }, data }),
deleteUser: (id) => p.user.delete({ where: { id } }),
linkAccount: (data) =>
p.account.create({ data }) as unknown as AdapterAccount,
linkAccount: (data) => p.account.create({ data }) as any,
unlinkAccount: (provider_providerAccountId) =>
p.account.delete({
where: { provider_providerAccountId },
}) as unknown as AdapterAccount,
p.account.delete({ where: { provider_providerAccountId } }) as any,
async getSessionAndUser(sessionToken) {
const userAndSession = await p.session.findUnique({
where: { sessionToken },
@@ -36,18 +33,17 @@ export function PrismaAdapter(p: PrismaClient): Adapter {
deleteSession: (sessionToken) =>
p.session.delete({ where: { sessionToken } }),
async createVerificationToken(data) {
const verificationToken = await p.verificationToken.create({ data })
// @ts-expect-errors // MongoDB needs an ID, but we don't
if (verificationToken.id) delete verificationToken.id
// @ts-ignore
const { id: _, ...verificationToken } = await p.verificationToken.create({
data,
})
return verificationToken
},
async useVerificationToken(identifier_token) {
try {
const verificationToken = await p.verificationToken.delete({
where: { identifier_token },
})
// @ts-expect-errors // MongoDB needs an ID, but we don't
if (verificationToken.id) delete verificationToken.id
// @ts-ignore
const { id: _, ...verificationToken } =
await p.verificationToken.delete({ where: { identifier_token } })
return verificationToken
} catch (error) {
// If token already used/deleted, just return null

View File

@@ -40,9 +40,9 @@ runBasicTests({
where: { identifier_token },
})
if (!result) return null
// @ts-ignore // MongoDB needs an ID, but we don't
delete result.id
return result
// @ts-ignore
const { id: _, ...verificationToken } = result
return verificationToken
},
},
})

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/sequelize-adapter",
"version": "1.0.6",
"version": "1.0.5",
"description": "Sequelize adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",

View File

@@ -1,7 +1,7 @@
import type { Account as AdapterAccount } from "next-auth"
import type {
Adapter,
AdapterUser,
AdapterAccount,
AdapterSession,
VerificationToken,
} from "next-auth/adapters"

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/typeorm-legacy-adapter",
"version": "2.0.1",
"version": "2.0.0",
"description": "TypeORM (legacy) adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",

View File

@@ -1,10 +1,6 @@
import type {
Adapter,
AdapterUser,
AdapterAccount,
AdapterSession,
} from "next-auth/adapters"
import type { Adapter, AdapterSession, AdapterUser } from "next-auth/adapters"
import { DataSourceOptions, DataSource, EntityManager } from "typeorm"
import type { Account } from "next-auth"
import * as defaultEntities from "./entities"
import { parseDataSourceConfig, updateConnectionEntities } from "./utils"
@@ -91,7 +87,7 @@ export function TypeORMLegacyAdapter(
},
async getUserByAccount(provider_providerAccountId) {
const m = await getManager(c)
const account = await m.findOne<AdapterAccount & { user: AdapterUser }>(
const account = await m.findOne<Account & { user: AdapterUser }>(
"AccountEntity",
{ where: provider_providerAccountId, relations: ["user"] }
)
@@ -119,8 +115,9 @@ export function TypeORMLegacyAdapter(
},
async unlinkAccount(providerAccountId) {
const m = await getManager(c)
await m.delete<AdapterAccount>("AccountEntity", providerAccountId)
await m.delete<Account>("AccountEntity", providerAccountId)
},
// @ts-expect-error
async createSession(data) {
const m = await getManager(c)
const session = await m.save("SessionEntity", data)

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/upstash-redis-adapter",
"version": "3.0.3",
"version": "3.0.1",
"description": "Upstash adapter for next-auth. It uses Upstash's connectionless (HTTP based) Redis client.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -49,4 +49,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -1,7 +1,7 @@
import type { Account as AdapterAccount } from "next-auth"
import type {
Adapter,
AdapterUser,
AdapterAccount,
AdapterSession,
VerificationToken,
} from "next-auth/adapters"
@@ -117,6 +117,7 @@ export function UpstashRedisAdapter(
const id = uuid()
// TypeScript thinks the emailVerified field is missing
// but all fields are copied directly from user, so it's there
// @ts-expect-error
return await setUser(id, { ...user, id })
},
getUser,
@@ -143,7 +144,10 @@ export function UpstashRedisAdapter(
const id = `${account.provider}:${account.providerAccountId}`
return await setAccount(id, { ...account, id })
},
createSession: (session) => setSession(session.sessionToken, session),
async createSession(session) {
const id = session.sessionToken
return await setSession(id, { ...session, id })
},
async getSessionAndUser(sessionToken) {
const session = await getSession(sessionToken)
if (!session) return null
@@ -161,20 +165,13 @@ export function UpstashRedisAdapter(
},
async createVerificationToken(verificationToken) {
await setObjectAsJson(
verificationTokenKeyPrefix +
verificationToken.identifier +
":" +
verificationToken.token,
verificationTokenKeyPrefix + verificationToken.identifier,
verificationToken
)
return verificationToken
},
async useVerificationToken(verificationToken) {
const tokenKey =
verificationTokenKeyPrefix +
verificationToken.identifier +
":" +
verificationToken.token
const tokenKey = verificationTokenKeyPrefix + verificationToken.identifier
const token = await client.get<VerificationToken>(tokenKey)
if (!token) return null

View File

@@ -11,14 +11,6 @@ if (!process.env.UPSTASH_REDIS_URL || !process.env.UPSTASH_REDIS_KEY) {
process.exit(0)
}
if (process.env.CI) {
// TODO: Fix this
test('Skipping UpstashRedisAdapter tests in CI because of "Request failed" errors. Should revisit', () => {
expect(true).toBe(true)
})
process.exit(0)
}
const client = new Redis({
url: process.env.UPSTASH_REDIS_URL,
token: process.env.UPSTASH_REDIS_KEY,

View File

@@ -1,6 +1,6 @@
{
"name": "next-auth",
"version": "4.13.0",
"version": "4.10.3",
"description": "Authentication for Next.js",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth.git",
@@ -69,8 +69,8 @@
"dependencies": {
"@babel/runtime": "^7.16.3",
"@panva/hkdf": "^1.0.1",
"cookie": "^0.5.0",
"jose": "^4.9.3",
"cookie": "^0.4.1",
"jose": "^4.3.7",
"oauth": "^0.9.15",
"openid-client": "^5.1.0",
"preact": "^10.6.3",
@@ -78,7 +78,6 @@
"uuid": "^8.3.2"
},
"peerDependencies": {
"next": "^12.2.5",
"nodemailer": "^6.6.5",
"react": "^17.0.2 || ^18",
"react-dom": "^17.0.2 || ^18"
@@ -119,7 +118,7 @@
"jest-environment-jsdom": "^28.1.1",
"jest-watch-typeahead": "^1.1.0",
"msw": "^0.42.3",
"next": "12.2.5",
"next": "12.2.0",
"postcss": "^8.4.14",
"postcss-cli": "^9.1.0",
"postcss-nested": "^5.0.6",

View File

@@ -2,15 +2,11 @@ import { Account, User, Awaitable } from "."
export interface AdapterUser extends User {
id: string
email: string
emailVerified: Date | null
}
export interface AdapterAccount extends Account {
userId: string
}
export interface AdapterSession {
id: string
/** A randomly generated value that is used to get hold of the session. */
sessionToken: string
/** Used to connect the session to a particular user */
@@ -59,30 +55,13 @@ 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>
/** Using the provider id and the id of the user for a specific account, get the user. */
getUserByAccount: (
providerAccountId: Pick<AdapterAccount, "provider" | "providerAccountId">
providerAccountId: Pick<Account, "provider" | "providerAccountId">
) => Awaitable<AdapterUser | null>
updateUser: (user: Partial<AdapterUser>) => Awaitable<AdapterUser>
/** @todo Implement */
@@ -90,12 +69,12 @@ export interface DefaultAdapter {
userId: string
) => Promise<void> | Awaitable<AdapterUser | null | undefined>
linkAccount: (
account: AdapterAccount
) => Promise<void> | Awaitable<AdapterAccount | null | undefined>
account: Account
) => Promise<void> | Awaitable<Account | null | undefined>
/** @todo Implement */
unlinkAccount?: (
providerAccountId: Pick<AdapterAccount, "provider" | "providerAccountId">
) => Promise<void> | Awaitable<AdapterAccount | undefined>
providerAccountId: Pick<Account, "provider" | "providerAccountId">
) => Promise<void> | Awaitable<Account | undefined>
/** Creates a session for the user and returns it. */
createSession: (session: {
sessionToken: string

View File

@@ -94,18 +94,10 @@ export function BroadcastChannel(name = "nextauth.message") {
/** Notify other tabs/windows. */
post(message: Record<string, unknown>) {
if (typeof window === "undefined") return
try {
localStorage.setItem(
name,
JSON.stringify({ ...message, timestamp: now() })
)
} catch {
/**
* The localStorage API isn't always available.
* It won't work in private mode prior to Safari 11 for example.
* Notifications are simply dropped if an error is encountered.
*/
}
localStorage.setItem(
name,
JSON.stringify({ ...message, timestamp: now() })
)
},
}
}

View File

@@ -1,4 +1,5 @@
import type { EventCallbacks, LoggerInstance } from ".."
import type { Adapter } from "../adapters"
/**
* Same as the default `Error`, but it is JSON serializable.
@@ -57,11 +58,6 @@ export class MissingAdapter extends UnknownError {
code = "EMAIL_REQUIRES_ADAPTER_ERROR"
}
export class MissingAdapterMethods extends UnknownError {
name = "MissingAdapterMethodsError"
code = "MISSING_ADAPTER_METHODS_ERROR"
}
export class UnsupportedStrategy extends UnknownError {
name = "UnsupportedStrategyError"
code = "CALLBACK_CREDENTIALS_JWT_ERROR"
@@ -103,10 +99,10 @@ export function eventsErrorHandler(
}
/** Handles adapter induced errors. */
export function adapterErrorHandler<TAdapter>(
adapter: TAdapter | undefined,
export function adapterErrorHandler(
adapter: Adapter | undefined,
logger: LoggerInstance
): TAdapter | undefined {
): Adapter | undefined {
if (!adapter) return
return Object.keys(adapter).reduce<any>((acc, name) => {

View File

@@ -94,21 +94,13 @@ export async function NextAuthHandler<
assertionResult.forEach(logger.warn)
} else if (assertionResult instanceof Error) {
// Bail out early if there's an error in the user config
const { pages, theme } = userOptions
logger.error(assertionResult.code, assertionResult)
const htmlPages = ["signin", "signout", "error", "verify-request"]
if (!htmlPages.includes(req.action) || req.method !== "GET") {
const message = `There is a problem with the server configuration. Check the server logs for more information.`
return {
status: 500,
headers: [{ key: "Content-Type", value: "application/json" }],
body: { message } as any,
}
}
const { pages, theme } = userOptions
const authOnErrorPage =
pages?.error && req.query?.callbackUrl?.startsWith(pages.error)
pages?.error &&
req.action === "signin" &&
req.query?.callbackUrl.startsWith(pages.error)
if (!pages?.error || authOnErrorPage) {
if (authOnErrorPage) {

View File

@@ -1,4 +1,3 @@
import { randomBytes, randomUUID } from "crypto"
import { NextAuthOptions } from ".."
import logger from "../utils/logger"
import parseUrl from "../utils/parse-url"
@@ -71,7 +70,6 @@ export async function init({
// and are request-specific.
url,
action,
// @ts-expect-errors
provider,
cookies: {
...cookie.defaultCookies(
@@ -88,10 +86,6 @@ export async function init({
strategy: userOptions.adapter ? "database" : "jwt",
maxAge,
updateAge: 24 * 60 * 60,
generateSessionToken: () => {
// Use `randomUUID` if available. (Node 15.6+)
return randomUUID?.() ?? randomBytes(32).toString("hex")
},
...userOptions.session,
},
// JWT options

View File

@@ -5,7 +5,6 @@ import {
MissingSecret,
UnsupportedStrategy,
InvalidCallbackUrl,
MissingAdapterMethods,
} from "../errors"
import parseUrl from "../../utils/parse-url"
import { defaultCookies } from "./cookie"
@@ -121,23 +120,8 @@ export function assertConfig(params: {
}
}
if (hasEmail) {
const { adapter } = options
if (!adapter) {
return new MissingAdapter("E-mail login requires an adapter.")
}
const missingMethods = [
"createVerificationToken",
"useVerificationToken",
"getUserByEmail",
].filter((method) => !adapter[method])
if (missingMethods.length) {
return new MissingAdapterMethods(
`Required adapter methods were missing: ${missingMethods.join(", ")}`
)
}
if (hasEmail && !options.adapter) {
return new MissingAdapter("E-mail login requires an adapter.")
}
if (!warned) {

View File

@@ -1,3 +1,4 @@
import { randomBytes, randomUUID } from "crypto"
import { AccountNotLinkedError } from "../errors"
import { fromDate } from "./utils"
@@ -21,11 +22,11 @@ import type { SessionToken } from "./cookie"
*/
export default async function callbackHandler(params: {
sessionToken?: SessionToken
profile: User | AdapterUser | { email: string }
account: Account | null
profile: User
account: Account
options: InternalOptions
}) {
const { sessionToken, profile: _profile, account, options } = params
const { sessionToken, profile, account, options } = params
// Input validation
if (!account?.providerAccountId || !account.type)
throw new Error("Missing or invalid provider account")
@@ -36,17 +37,15 @@ export default async function callbackHandler(params: {
adapter,
jwt,
events,
session: { strategy: sessionStrategy, generateSessionToken },
session: { strategy: sessionStrategy },
} = options
// If no adapter is configured then we don't have a database and cannot
// persist data; in this mode we just return a dummy session object.
if (!adapter) {
return { user: _profile as User, account }
return { user: profile, account, session: {} }
}
const profile = _profile as AdapterUser
const {
createUser,
updateUser,
@@ -86,7 +85,9 @@ export default async function callbackHandler(params: {
if (account.type === "email") {
// If signing in with an email, check if an account with the same email address exists already
const userByEmail = await getUserByEmail(profile.email)
const userByEmail = profile.email
? await getUserByEmail(profile.email)
: null
if (userByEmail) {
// If they are not already signed in as the same user, this flow will
// sign them out of the current session and sign them in as the new user
@@ -101,7 +102,8 @@ export default async function callbackHandler(params: {
user = await updateUser({ id: userByEmail.id, emailVerified: new Date() })
await events.updateUser?.({ user })
} else {
const { id: _, ...newUser } = { ...profile, emailVerified: new Date() }
const newUser = { ...profile, emailVerified: new Date() }
delete (newUser as Omit<AdapterUser, "id">).id
// Create user account if there isn't one for the email address already
user = await createUser(newUser)
await events.createUser?.({ user })
@@ -197,7 +199,8 @@ export default async function callbackHandler(params: {
// If no account matching the same [provider].id or .email exists, we can
// create a new account for the user, link it to the OAuth acccount and
// create a new session for them so they are signed in with it.
const { id: _, ...newUser } = { ...profile, emailVerified: null }
const newUser = { ...profile, emailVerified: null }
delete (newUser as Omit<AdapterUser, "id">).id
user = await createUser(newUser)
await events.createUser?.({ user })
@@ -215,6 +218,9 @@ export default async function callbackHandler(params: {
return { session, user, isNewUser: true }
}
}
throw new Error("Unsupported account type")
}
function generateSessionToken() {
// Use `randomUUID` if available. (Node 15.6++)
return randomUUID?.() ?? randomBytes(32).toString("hex")
}

View File

@@ -1,19 +0,0 @@
import type { InternalOptions } from "../../types"
export default async function getUserFromEmail({
email,
adapter,
withId = false,
}: {
email: string
adapter: InternalOptions<"email">["adapter"]
withId: boolean
}) {
const { getUserByEmail } = adapter
// If is an existing user return a user object (otherwise use placeholder)
return (email ? await getUserByEmail(email) : null) ?? withId
? { id: email, email }
: {
email,
}
}

View File

@@ -36,6 +36,7 @@ export default async function email(
theme,
}),
// Save in database
// @ts-expect-error // verified in `assertConfig`
adapter.createVerificationToken({
identifier,
token: hashToken(token, options),

View File

@@ -39,7 +39,10 @@ export default async function getAuthorizationUrl({
if (provider.version?.startsWith("1.")) {
const client = oAuth1Client(options)
const tokens = (await client.getOAuthRequestToken(params)) as any
const url = `${provider.authorization?.url}?${new URLSearchParams({
const url = `${
// @ts-expect-error
provider.authorization?.url ?? provider.authorization
}?${new URLSearchParams({
oauth_token: tokens.oauth_token,
oauth_token_secret: tokens.oauth_token_secret,
...tokens.params,
@@ -65,7 +68,7 @@ export default async function getAuthorizationUrl({
authorizationParams.nonce = nonce.value
cookies.push(nonce.cookie)
}
const pkce = await createPKCE(options)
if (pkce) {
authorizationParams.code_challenge = pkce.code_challenge

View File

@@ -7,10 +7,10 @@ import { useNonce } from "./nonce-handler"
import { OAuthCallbackError } from "../../errors"
import type { CallbackParamsType, OpenIDCallbackChecks } from "openid-client"
import type { LoggerInstance, Profile } from "../../.."
import type { Account, LoggerInstance, Profile } from "../../.."
import type { OAuthChecks, OAuthConfig } from "../../../providers"
import type { InternalOptions } from "../../types"
import type { RequestInternal } from "../.."
import type { RequestInternal, OutgoingResponse } from "../.."
import type { Cookie } from "../cookie"
export default async function oAuthCallback(params: {
@@ -19,7 +19,7 @@ export default async function oAuthCallback(params: {
body: RequestInternal["body"]
method: Required<RequestInternal>["method"]
cookies: RequestInternal["cookies"]
}) {
}): Promise<GetProfileResult & { cookies?: OutgoingResponse["cookies"] }> {
const { options, query, body, method, cookies } = params
const { logger, provider } = options
@@ -34,19 +34,23 @@ export default async function oAuthCallback(params: {
logger.debug("OAUTH_CALLBACK_HANDLER_ERROR", { body })
throw error
}
if (provider.version?.startsWith("1.")) {
try {
const client = await oAuth1Client(options)
// Handle OAuth v1.x
const { oauth_token, oauth_verifier } = query ?? {}
const tokens = (await (client as any).getOAuthAccessToken(
oauth_token,
// @ts-expect-error
const tokens: TokenSet = await client.getOAuthAccessToken(
oauth_token as string,
// @ts-expect-error
null,
oauth_verifier
)) as TokenSet
let profile: Profile = await (client as any).get(
provider.profileUrl,
)
// @ts-expect-error
let profile: Profile = await client.get(
(provider as any).profileUrl,
tokens.oauth_token,
tokens.oauth_token_secret
)
@@ -55,8 +59,7 @@ export default async function oAuthCallback(params: {
profile = JSON.parse(profile)
}
const newProfile = await getProfile({ profile, tokens, provider, logger })
return { ...newProfile, cookies: [] }
return await getProfile({ profile, tokens, provider, logger })
} catch (error) {
logger.error("OAUTH_V1_GET_ACCESS_TOKEN_ERROR", error as Error)
throw error
@@ -79,7 +82,7 @@ export default async function oAuthCallback(params: {
const nonce = await useNonce(cookies?.[options.cookies.nonce.name], options)
if (nonce && provider.idToken) {
;(checks as OpenIDCallbackChecks).nonce = nonce.value
(checks as OpenIDCallbackChecks).nonce = nonce.value
resCookies.push(nonce.cookie)
}
@@ -99,10 +102,13 @@ export default async function oAuthCallback(params: {
body,
method,
}),
// @ts-expect-error
...provider.token?.params,
}
// @ts-expect-error
if (provider.token?.request) {
// @ts-expect-error
const response = await provider.token.request({
provider,
params,
@@ -122,7 +128,9 @@ export default async function oAuthCallback(params: {
}
let profile: Profile
// @ts-expect-error
if (provider.userinfo?.request) {
// @ts-expect-error
profile = await provider.userinfo.request({
provider,
tokens,
@@ -132,6 +140,7 @@ export default async function oAuthCallback(params: {
profile = tokens.claims()
} else {
profile = await client.userinfo(tokens, {
// @ts-expect-error
params: provider.userinfo?.params,
})
}
@@ -155,22 +164,25 @@ export interface GetProfileParams {
logger: LoggerInstance
}
export interface GetProfileResult {
// @ts-expect-error
profile: ReturnType<OAuthConfig["profile"]> | null
account: Omit<Account, "userId"> | null
OAuthProfile: Profile
}
/** Returns profile, raw profile and auth provider details */
async function getProfile({
profile: OAuthProfile,
tokens,
provider,
logger,
}: GetProfileParams) {
}: GetProfileParams): Promise<GetProfileResult> {
try {
logger.debug("PROFILE_DATA", { OAuthProfile })
// @ts-expect-error
const profile = await provider.profile(OAuthProfile, tokens)
profile.email = profile.email?.toLowerCase()
if (!profile.id)
throw new TypeError(
`Profile id is missing in ${provider.name} OAuth profile response`
)
// Return profile, raw profile and auth provider details
return {
profile,
@@ -190,9 +202,11 @@ async function getProfile({
// all providers, so we return an empty object; the user should then be
// redirected back to the sign up page. We log the error to help developers
// who might be trying to debug this when configuring a new provider.
logger.error("OAUTH_PARSE_PROFILE_ERROR", {
error: error as Error,
logger.error("OAUTH_PARSE_PROFILE_ERROR", error as Error)
return {
profile: null,
account: null,
OAuthProfile,
})
}
}
}

View File

@@ -22,9 +22,13 @@ export async function openidClient(
} else {
issuer = new Issuer({
issuer: provider.issuer as string,
authorization_endpoint: provider.authorization?.url,
token_endpoint: provider.token?.url,
userinfo_endpoint: provider.userinfo?.url,
authorization_endpoint:
// @ts-expect-error
provider.authorization?.url ?? provider.authorization,
// @ts-expect-error
token_endpoint: provider.token?.url ?? provider.token,
// @ts-expect-error
userinfo_endpoint: provider.userinfo?.url ?? provider.userinfo,
})
}

View File

@@ -1,11 +1,7 @@
import { merge } from "../../utils/merge"
import type { InternalProvider } from "../types"
import type {
InternalOAuthConfig,
OAuthConfig,
Provider,
} from "../../providers"
import type { Provider } from "../../providers"
import type { InternalUrl } from "../../utils/parse-url"
/**
@@ -22,72 +18,52 @@ export default function parseProviders(params: {
} {
const { url, providerId } = params
const providers = params.providers.map<InternalProvider>(
({ options: userOptions, ...rest }) => {
if (rest.type === "oauth") {
const normalizedOptions = normalizeOAuthOptions(rest)
const normalizedUserOptions = normalizeOAuthOptions(userOptions, true)
return merge(normalizedOptions, {
...normalizedUserOptions,
signinUrl: `${url}/signin/${normalizedUserOptions?.id ?? rest.id}`,
callbackUrl: `${url}/callback/${
normalizedUserOptions?.id ?? rest.id
}`,
})
}
return merge(rest, {
...userOptions,
signinUrl: `${url}/signin/${userOptions?.id ?? rest.id}`,
callbackUrl: `${url}/callback/${userOptions?.id ?? rest.id}`,
})
}
)
const providers = params.providers.map(({ options, ...rest }) => {
const defaultOptions = normalizeProvider(rest as Provider)
const userOptions = normalizeProvider(options as Provider)
return {
providers,
provider: providers.find(({ id }) => id === providerId),
}
return merge(defaultOptions, {
...userOptions,
signinUrl: `${url}/signin/${userOptions?.id ?? rest.id}`,
callbackUrl: `${url}/callback/${userOptions?.id ?? rest.id}`,
})
})
const provider = providers.find(({ id }) => id === providerId)
return { providers, provider }
}
/**
* Transform OAuth options `authorization`, `token` and `profile` strings to `{ url: string; params: Record<string, string> }`
*/
function normalizeOAuthOptions(
oauthOptions?: Partial<OAuthConfig<any>> | Record<string, unknown>,
isUserOptions = false
) {
if (!oauthOptions) return
function normalizeProvider(provider?: Provider) {
if (!provider) return
const normalized = Object.entries(oauthOptions).reduce<
InternalOAuthConfig<Record<string, unknown>>
>(
(acc, [key, value]) => {
if (
["authorization", "token", "userinfo"].includes(key) &&
typeof value === "string"
) {
const url = new URL(value)
acc[key] = {
url: `${url.origin}${url.pathname}`,
params: Object.fromEntries(url.searchParams ?? []),
}
} else {
acc[key] = value
const normalized: InternalProvider = Object.entries(
provider
).reduce<InternalProvider>((acc, [key, value]) => {
if (
["authorization", "token", "userinfo"].includes(key) &&
typeof value === "string"
) {
const url = new URL(value)
acc[key] = {
url: `${url.origin}${url.pathname}`,
params: Object.fromEntries(url.searchParams ?? []),
}
} else {
acc[key] = value
}
return acc
},
// eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter
{} as any
)
return acc
// eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter, @typescript-eslint/consistent-type-assertions
}, {} as any)
if (!isUserOptions && !normalized.version?.startsWith("1.")) {
if (normalized.type === "oauth" && !normalized.version?.startsWith("1.")) {
// If provider has as an "openid-configuration" well-known endpoint
// or an "openid" scope request, it will also likely be able to receive an `id_token`
// Only do this if this function is not called with user options to avoid overriding in later stage.
normalized.idToken = Boolean(
normalized.idToken ??
normalized.wellKnown?.includes("openid-configuration") ??
// @ts-expect-error
normalized.authorization?.params?.scope?.includes("openid")
)

View File

@@ -1,17 +1,15 @@
import oAuthCallback from "../lib/oauth/callback"
import callbackHandler from "../lib/callback-handler"
import { hashToken } from "../lib/utils"
import getUserFromEmail from "../lib/email/getUserFromEmail"
import type { InternalOptions } from "../types"
import type { RequestInternal, OutgoingResponse } from ".."
import type { Cookie, SessionStore } from "../lib/cookie"
import type { User } from "../.."
import type { AdapterSession } from "../../adapters"
/** Handle callbacks from login services */
export default async function callback(params: {
options: InternalOptions
options: InternalOptions<"oauth" | "credentials" | "email">
query: RequestInternal["query"]
method: Required<RequestInternal>["method"]
body: RequestInternal["body"]
@@ -52,7 +50,7 @@ export default async function callback(params: {
cookies: params.cookies,
})
if (oauthCookies.length) cookies.push(...oauthCookies)
if (oauthCookies) cookies.push(...oauthCookies)
try {
// Make it easier to debug when adding a new provider
@@ -70,7 +68,7 @@ export default async function callback(params: {
// Note: In oAuthCallback an error is logged with debug info, so it
// should at least be visible to developers what happened if it is an
// error with the provider.
if (!profile || !account || !OAuthProfile) {
if (!profile) {
return { redirect: `${url}/signin`, cookies }
}
@@ -82,6 +80,7 @@ export default async function callback(params: {
if (adapter) {
const { getUserByAccount } = adapter
const userByAccount = await getUserByAccount({
// @ts-expect-error
providerAccountId: account.providerAccountId,
provider: provider.id,
})
@@ -92,6 +91,7 @@ export default async function callback(params: {
try {
const isAllowed = await callbacks.signIn({
user: userOrProfile,
// @ts-expect-error
account,
profile: OAuthProfile,
})
@@ -110,9 +110,11 @@ export default async function callback(params: {
}
// Sign user in
// @ts-expect-error
const { user, session, isNewUser } = await callbackHandler({
sessionToken: sessionStore.value,
profile,
// @ts-expect-error
account,
options,
})
@@ -127,6 +129,7 @@ export default async function callback(params: {
const token = await callbacks.jwt({
token: defaultToken,
user,
// @ts-expect-error
account,
profile: OAuthProfile,
isNewUser,
@@ -147,10 +150,10 @@ export default async function callback(params: {
// Save Session Token in cookie
cookies.push({
name: options.cookies.sessionToken.name,
value: (session as AdapterSession).sessionToken,
value: session.sessionToken,
options: {
...options.cookies.sessionToken.options,
expires: (session as AdapterSession).expires,
expires: session.expires,
},
})
}
@@ -198,16 +201,14 @@ export default async function callback(params: {
}
} else if (provider.type === "email") {
try {
const token = query?.token as string | undefined
const identifier = query?.email as string | undefined
// Verified in `assertConfig`
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { useVerificationToken, getUserByEmail } = adapter!
// If these are missing, the sign-in URL was manually opened without these params or the `sendVerificationRequest` method did not send the link correctly in the email.
if (!token || !identifier) {
return { redirect: `${url}/error?error=configuration`, cookies }
}
const token = query?.token
const identifier = query?.email
// @ts-expect-error -- Verified in `assertConfig`. adapter: Adapter<true>
const invite = await adapter.useVerificationToken({
const invite = await useVerificationToken?.({
identifier,
token: hashToken(token, options),
})
@@ -217,23 +218,29 @@ export default async function callback(params: {
return { redirect: `${url}/error?error=Verification`, cookies }
}
const profile = await getUserFromEmail({
// If it is an existing user, use that, otherwise use a placeholder
const profile = (identifier
? await getUserByEmail(identifier)
: null) ?? {
email: identifier,
// @ts-expect-error -- Verified in `assertConfig`. adapter: Adapter<true>
adapter,
})
}
/** @type {import("src").Account} */
const account = {
providerAccountId: profile.email,
type: "email" as const,
type: "email",
provider: provider.id,
}
// Check if user is allowed to sign in
try {
const signInCallbackResponse = await callbacks.signIn({
// @ts-expect-error
user: profile,
// @ts-expect-error
account,
// @ts-expect-error
email: { email: identifier },
})
if (!signInCallbackResponse) {
return { redirect: `${url}/error?error=AccessDenied`, cookies }
@@ -250,9 +257,12 @@ export default async function callback(params: {
}
// Sign user in
// @ts-expect-error
const { user, session, isNewUser } = await callbackHandler({
sessionToken: sessionStore.value,
// @ts-expect-error
profile,
// @ts-expect-error
account,
options,
})
@@ -267,6 +277,7 @@ export default async function callback(params: {
const token = await callbacks.jwt({
token: defaultToken,
user,
// @ts-expect-error
account,
isNewUser,
})
@@ -286,14 +297,15 @@ export default async function callback(params: {
// Save Session Token in cookie
cookies.push({
name: options.cookies.sessionToken.name,
value: (session as AdapterSession).sessionToken,
value: session.sessionToken,
options: {
...options.cookies.sessionToken.options,
expires: (session as AdapterSession).expires,
expires: session.expires,
},
})
}
// @ts-expect-error
await events.signIn?.({ user, account, isNewUser })
// Handle first logins on new accounts

View File

@@ -1,9 +1,8 @@
import getAuthorizationUrl from "../lib/oauth/authorization-url"
import emailSignin from "../lib/email/signin"
import getUserFromEmail from "../lib/email/getUserFromEmail"
import type { RequestInternal, OutgoingResponse } from ".."
import type { InternalOptions } from "../types"
import type { Account } from "../.."
import type { Account, User } from "../.."
/** Handle requests to /api/auth/signin */
export default async function signin(params: {
@@ -12,7 +11,7 @@ export default async function signin(params: {
body: RequestInternal["body"]
}): Promise<OutgoingResponse> {
const { options, query, body } = params
const { url, callbacks, logger, provider } = options
const { url, adapter, callbacks, logger, provider } = options
if (!provider.type) {
return {
@@ -55,12 +54,14 @@ export default async function signin(params: {
return { redirect: `${url}/error?error=EmailSignin` }
}
const user = await getUserFromEmail({
// Verified in `assertConfig`
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { getUserByEmail } = adapter!
// If is an existing user return a user object (otherwise use placeholder)
const user: User = (email ? await getUserByEmail(email) : null) ?? {
email,
// @ts-expect-error -- Verified in `assertConfig`. adapter: Adapter<true>
adapter: options.adapter,
withId: true,
})
id: email,
}
const account: Account = {
providerAccountId: email,
@@ -71,6 +72,7 @@ export default async function signin(params: {
// Check if user is allowed to sign in
try {
// @ts-expect-error
const signInCallbackResponse = await callbacks.signIn({
user,
account,

View File

@@ -1,11 +1,11 @@
import type { Adapter, AdapterUser } from "../adapters"
import type { Adapter } from "../adapters"
import type {
Provider,
CredentialInput,
ProviderType,
OAuthConfig,
EmailConfig,
CredentialsConfig,
InternalOAuthConfig,
} from "../providers"
import type { TokenSetParameters } from "openid-client"
import type { JWT, JWTOptions } from "../jwt"
@@ -231,7 +231,7 @@ export type TokenSet = TokenSetParameters
* Usually contains information about the provider being used
* and also extends `TokenSet`, which is different tokens returned by OAuth Providers.
*/
export interface Account extends Partial<TokenSet> {
export interface DefaultAccount extends Partial<TokenSet> {
/**
* This value depends on the type of the provider being used to create the account.
* - oauth: The OAuth account's id, returned from the `profile()` callback.
@@ -240,23 +240,30 @@ export interface Account extends Partial<TokenSet> {
*/
providerAccountId: string
/** id of the user this account belongs to. */
userId?: string
userId: string
/** id of the provider used for this account */
provider: string
/** Provider's type for this account */
type: ProviderType
}
/** The OAuth profile returned from your provider */
export interface Profile {
export interface Account extends Record<string, unknown>, DefaultAccount {}
export interface DefaultProfile {
sub?: string
name?: string
email?: string
image?: string
}
/** The OAuth profile returned from your provider */
export interface Profile extends Record<string, unknown>, DefaultProfile {}
/** [Documentation](https://next-auth.js.org/configuration/callbacks) */
export interface CallbacksOptions<P = Profile, A = Account> {
export interface CallbacksOptions<
P extends Record<string, unknown> = Profile,
A extends Record<string, unknown> = Account
> {
/**
* Use this callback to control if a user is allowed to sign in.
* Returning true will continue the sign-in flow.
@@ -265,13 +272,13 @@ export interface CallbacksOptions<P = Profile, A = Account> {
* [Documentation](https://next-auth.js.org/configuration/callbacks#sign-in-callback)
*/
signIn: (params: {
user: User | { email: string }
account: A | null
user: User
account: A
/**
* If OAuth provider is used, it contains the full
* OAuth profile returned by your provider.
*/
profile?: P
profile: P & Record<string, unknown>
/**
* If Email provider is used, on the first call, it contains a
* `verificationRequest: true` property to indicate it is being triggered in the verification request flow.
@@ -280,7 +287,7 @@ export interface CallbacksOptions<P = Profile, A = Account> {
* to avoid sending emails to addresses or domains on a blocklist or to only explicitly generate them
* for email address in an allow list.
*/
email?: {
email: {
verificationRequest?: boolean
}
/** If Credentials provider is used, it contains the user credentials */
@@ -334,8 +341,8 @@ export interface CallbacksOptions<P = Profile, A = Account> {
*/
jwt: (params: {
token: JWT
user?: User | AdapterUser
account?: A | null
user?: User
account?: A
profile?: P
isNewUser?: boolean
}) => Awaitable<JWT>
@@ -371,7 +378,7 @@ export interface EventCallbacks {
*/
signIn: (message: {
user: User
account: Account | null
account: Account
profile?: Profile
isNewUser?: boolean
}) => Awaitable<void>
@@ -385,9 +392,9 @@ export interface EventCallbacks {
createUser: (message: { user: User }) => Awaitable<void>
updateUser: (message: { user: User }) => Awaitable<void>
linkAccount: (message: {
user: User | AdapterUser | { email: string }
user: User
account: Account
profile: User | AdapterUser | { email: string }
profile: User
}) => Awaitable<void>
/**
* The message object will contain one of these depending on
@@ -413,7 +420,7 @@ export interface PagesOptions {
export type ISODateString = string
export interface DefaultSession {
export interface DefaultSession extends Record<string, unknown> {
user?: {
name?: string | null
email?: string | null
@@ -431,7 +438,7 @@ export interface DefaultSession {
* [`SessionProvider`](https://next-auth.js.org/getting-started/client#sessionprovider) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback)
*/
export interface Session extends DefaultSession {}
export interface Session extends Record<string, unknown>, DefaultSession {}
export type SessionStrategy = "jwt" | "database"
@@ -461,13 +468,6 @@ export interface SessionOptions {
* @default 86400 // 1 day
*/
updateAge: number
/**
* Generate a custom session token for database-based sessions.
* By default, a random UUID or string is generated depending on the Node.js version.
* However, you can specify your own custom string (such as CUID) to be used.
* @default `randomUUID` or `randomBytes.toHex` depending on the Node.js version
*/
generateSessionToken: () => string
}
export interface DefaultUser {
@@ -487,13 +487,13 @@ export interface DefaultUser {
* [`jwt` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`profile` OAuth provider callback](https://next-auth.js.org/configuration/providers#using-a-custom-provider)
*/
export interface User extends DefaultUser {}
export interface User extends Record<string, unknown>, DefaultUser {}
// Below are types that are only supposed be used by next-auth internally
/** @internal */
export type InternalProvider<T = ProviderType> = (T extends "oauth"
? InternalOAuthConfig<any>
export type InternalProvider<T extends ProviderType = any> = (T extends "oauth"
? OAuthConfig<any>
: T extends "email"
? EmailConfig
: T extends "credentials"
@@ -515,10 +515,7 @@ export type NextAuthAction =
| "_log"
/** @internal */
export interface InternalOptions<
TProviderType = ProviderType,
WithVerificationToken = TProviderType extends "email" ? true : false
> {
export interface InternalOptions<T extends ProviderType = any> {
providers: InternalProvider[]
/**
* Parsed from `NEXTAUTH_URL` or `x-forwarded-host` on Vercel.
@@ -526,7 +523,9 @@ export interface InternalOptions<
*/
url: InternalUrl
action: NextAuthAction
provider: InternalProvider<TProviderType>
provider: T extends string
? InternalProvider<T>
: InternalProvider<T> | undefined
csrfToken?: string
csrfTokenVerified?: boolean
secret: string
@@ -537,9 +536,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

View File

@@ -118,14 +118,12 @@ export async function unstable_getServerSession(
},
})
const { body, cookies, status = 200 } = session
const { body, cookies } = session
cookies?.forEach((cookie) => setCookie(res, cookie))
if (body && typeof body !== "string" && Object.keys(body).length) {
if (status === 200) return body as Session
throw new Error((body as any).message)
}
if (body && typeof body !== "string" && Object.keys(body).length)
return body as Session
return null
}

View File

@@ -80,7 +80,7 @@ export interface NextAuthMiddlewareOptions {
* ```
*
* ---
* [Documentation](https://next-auth.js.org/configuration/nextjs#middleware) | [`signIn` callback](configuration/callbacks#sign-in-callback)
* [Documentation](https://next-auth.js.org/getting-started/nextjs/middleware#api) | [`signIn` callback](configuration/callbacks#sign-in-callback)
*/
authorized?: AuthorizedCallback
}
@@ -101,18 +101,17 @@ async function handleMiddleware(
options: NextAuthMiddlewareOptions | undefined,
onSuccess?: (token: JWT | null) => Promise<NextMiddlewareResult>
) {
const { pathname, search, origin, basePath } = req.nextUrl
const { pathname, search, origin } = req.nextUrl
const signInPage = options?.pages?.signIn ?? "/api/auth/signin"
const errorPage = options?.pages?.error ?? "/api/auth/error"
const authPath = parseUrl(process.env.NEXTAUTH_URL).path
const publicPaths = ["/_next", "/favicon.ico"]
const basePath = parseUrl(process.env.NEXTAUTH_URL).path
const publicPaths = [signInPage, errorPage, "/_next", "/favicon.ico"]
// Avoid infinite redirects/invalid response
// on paths that never require authentication
if (
`${basePath}${pathname}`.startsWith(authPath) ||
[signInPage, errorPage].includes(pathname) ||
pathname.startsWith(basePath) ||
publicPaths.some((p) => pathname.startsWith(p))
) {
return
@@ -125,7 +124,7 @@ async function handleMiddleware(
`\nhttps://next-auth.js.org/errors#no_secret`
)
const errorUrl = new URL(`${basePath}${errorPage}`, origin)
const errorUrl = new URL(errorPage, origin)
errorUrl.searchParams.append("error", "Configuration")
return NextResponse.redirect(errorUrl)
@@ -145,8 +144,8 @@ async function handleMiddleware(
if (isAuthorized) return await onSuccess?.(token)
// the user is not logged in, redirect to the sign-in page
const signInUrl = new URL(`${basePath}${signInPage}`, origin)
signInUrl.searchParams.append("callbackUrl", `${basePath}${pathname}${search}`)
const signInUrl = new URL(signInPage, origin)
signInUrl.searchParams.append("callbackUrl", `${pathname}${search}`)
return NextResponse.redirect(signInUrl)
}

View File

@@ -1,25 +1,28 @@
import type { OAuthConfig, OAuthUserConfig } from "."
interface HubSpotProfile extends Record<string, any> {
// TODO: figure out additional fields, for now using
// TODO: figure out additional fields, for now using
// https://legacydocs.hubspot.com/docs/methods/oauth2/get-access-token-information
user: string
user_id: string
user: string,
user_id: string,
hub_domain: string
hub_id: string
hub_domain: string,
hub_id: string,
}
const HubSpotConfig = {
authorizationUrl: "https://app.hubspot.com/oauth/authorize",
tokenUrl: "https://api.hubapi.com/oauth/v1/token",
profileUrl: "https://api.hubapi.com/oauth/v1/access-tokens",
profileUrl: "https://api.hubapi.com/oauth/v1/access-tokens"
}
export default function HubSpot<P extends HubSpotProfile>(
options: OAuthUserConfig<P>
): OAuthConfig<P> {
return {
id: "hubspot",
name: "HubSpot",
@@ -33,6 +36,7 @@ export default function HubSpot<P extends HubSpotProfile>(
scope: "oauth",
client_id: options.clientId,
},
},
client: {
token_endpoint_auth_method: "client_secret_post",
@@ -41,27 +45,33 @@ export default function HubSpot<P extends HubSpotProfile>(
userinfo: {
url: HubSpotConfig.profileUrl,
async request(context) {
const url = `${HubSpotConfig.profileUrl}/${context.tokens.access_token}`
const url = `${HubSpotConfig.profileUrl}/${context.tokens.access_token}`;
const response = await fetch(url, {
headers: {
"Content-Type": "application/json",
},
method: "GET",
})
});
return await response.json()
},
const userInfo = await response.json();
return { userInfo }
}
},
profile(profile) {
return {
id: profile.user_id,
name: profile.user,
email: profile.user,
// TODO: get image from profile once it's available
const { userInfo } = profile
return {
id: userInfo.user_id,
name: userInfo.user,
email: userInfo.user,
// TODO: get image from profile once it's available
// Details available https://community.hubspot.com/t5/APIs-Integrations/Profile-photo-is-not-retrieved-with-User-API/m-p/325521
image: null,
image: null
}
},
options,

View File

@@ -110,7 +110,7 @@ export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
userinfo?: string | UserinfoEndpointHandler
type: "oauth"
version?: string
profile: (profile: P, tokens: TokenSet) => Awaitable<User>
profile?: (profile: P, tokens: TokenSet) => Awaitable<User & { id: string }>
checks?: ChecksType | ChecksType[]
client?: Partial<ClientMetadata>
jwks?: { keys: JWK[] }
@@ -147,14 +147,6 @@ export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
encoding?: string
}
/** @internal */
export interface InternalOAuthConfig<P>
extends Omit<OAuthConfig<P>, "authorization" | "token" | "userinfo"> {
authorization?: AuthorizationEndpointHandler
token?: TokenEndpointHandler
userinfo?: UserinfoEndpointHandler
}
export type OAuthUserConfig<P> = Omit<
Partial<OAuthConfig<P>>,
"options" | "type"

View File

@@ -1,51 +0,0 @@
import type { OAuthConfig, OAuthUserConfig } from "."
export interface ZitadelProfile extends Record<string, any> {
amr: string // Authentication Method References as defined in RFC8176
aud: string // The audience of the token, by default all client id's and the project id are included
auth_time: number // Unix time of the authentication
azp: string // Client id of the client who requested the token
email: string // Email Address of the subject
email_verified: boolean // if the email was verified by ZITADEL
exp: number // Time the token expires (as unix time)
family_name: string // The subjects family name
given_name: string // Given name of the subject
gender: string // Gender of the subject
iat: number // Time of the token was issued at (as unix time)
iss: string // Issuing domain of a token
jti: string // Unique id of the token
locale: string // Language from the subject
name: string // The subjects full name
nbf: number // Time the token must not be used before (as unix time)
picture: string // The subjects profile picture
phone: string // Phone number provided by the user
phone_verified: boolean // if the phonenumber was verified by ZITADEL
preferred_username: string // ZITADEL's login name of the user. Consist of username@primarydomain
sub: string // Subject ID of the user
}
export default function Zitadel<P extends ZitadelProfile>(
options: OAuthUserConfig<P>
): OAuthConfig<P> {
const { issuer } = options
return {
id: "zitadel",
name: "ZITADEL",
type: "oauth",
version: "2",
wellKnown: `${issuer}/.well-known/openid-configuration`,
authorization: { params: { scope: "openid email profile" } },
idToken: true,
checks: ["pkce", "state"],
async profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture,
}
},
options,
}
}

View File

@@ -74,7 +74,7 @@ export type SessionContextValue<R extends boolean = false> = R extends true
| { data: Session; status: "authenticated" }
| { data: null; status: "unauthenticated" | "loading" }
export const SessionContext = React.createContext<SessionContextValue | undefined>(
const SessionContext = React.createContext<SessionContextValue | undefined>(
undefined
)

View File

@@ -1,11 +1,5 @@
import {
InvalidCallbackUrl,
MissingAdapter,
MissingAdapterMethods,
MissingSecret,
} from "../src/core/errors"
import { InvalidCallbackUrl, MissingSecret } from "../src/core/errors"
import { handler } from "./lib"
import EmailProvider from "../src/providers/email"
it("Show error page if secret is not defined", async () => {
const { res, log } = await handler(
@@ -20,48 +14,6 @@ it("Show error page if secret is not defined", async () => {
expect(log.error).toBeCalledWith("NO_SECRET", expect.any(MissingSecret))
})
it("Show error page if adapter is missing functions when using with email", async () => {
const sendVerificationRequest = jest.fn()
const missingFunctionAdapter: any = {}
const { res, log } = await handler(
{
adapter: missingFunctionAdapter,
providers: [EmailProvider({ sendVerificationRequest })],
secret: "secret",
},
{ prod: true }
)
expect(res.status).toBe(500)
expect(res.html).toMatch(/there is a problem with the server configuration./i)
expect(res.html).toMatch(/check the server logs for more information./i)
expect(log.error).toBeCalledWith(
"MISSING_ADAPTER_METHODS_ERROR",
expect.any(MissingAdapterMethods)
)
})
it("Show error page if adapter is not configured when using with email", async () => {
const sendVerificationRequest = jest.fn()
const { res, log } = await handler(
{
providers: [EmailProvider({ sendVerificationRequest })],
secret: "secret",
},
{ prod: true }
)
expect(res.status).toBe(500)
expect(res.html).toMatch(/there is a problem with the server configuration./i)
expect(res.html).toMatch(/check the server logs for more information./i)
expect(log.error).toBeCalledWith(
"EMAIL_REQUIRES_ADAPTER_ERROR",
expect.any(MissingAdapter)
)
})
it("Should show configuration error page on invalid `callbackUrl`", async () => {
const { res, log } = await handler(
{ providers: [] },

View File

@@ -156,7 +156,6 @@ it("Redirect to error page if multiple addresses aren't allowed", async () => {
expect(signIn).toBeCalledTimes(0)
expect(sendVerificationRequest).toBeCalledTimes(0)
// @ts-expect-error
expect(log.error.mock.calls[0]).toEqual([
"SIGNIN_EMAIL_ERROR",
{ error, providerId: "email" },

View File

@@ -47,19 +47,17 @@ describe("Treat secret correctly", () => {
})
it("Error if missing NEXTAUTH_SECRET and secret", async () => {
const configError = new Error(
"There is a problem with the server configuration. Check the server logs for more information."
)
await expect(
unstable_getServerSession(req, res, { providers: [], logger })
).rejects.toThrowError(configError)
const session = await unstable_getServerSession(req, res, {
providers: [],
logger,
})
expect(session).toEqual(null)
expect(logger.error).toBeCalledTimes(1)
expect(logger.error).toBeCalledWith("NO_SECRET", expect.any(MissingSecret))
})
it("Only logs warning once and in development", async () => {
process.env.NEXTAUTH_SECRET = "secret"
// Expect console.warn to NOT be called due to NODE_ENV=production
await unstable_getServerSession(req, res, { providers: [], logger })
expect(console.warn).toBeCalledTimes(0)
@@ -73,7 +71,6 @@ describe("Treat secret correctly", () => {
// Expect console.warn to be still only be called ONCE
await unstable_getServerSession(req, res, { providers: [], logger })
expect(console.warn).toBeCalledTimes(1)
delete process.env.NEXTAUTH_SECRET
})
})

View File

@@ -59,10 +59,10 @@ export function createCSRF() {
}
export function mockAdapter(): Adapter {
// @ts-expect-error
const adapter: Adapter = {
createVerificationToken: jest.fn(() => {}),
useVerificationToken: jest.fn(() => {}),
getUserByEmail: jest.fn(() => {}),
}
return adapter
return adapter;
}

View File

@@ -1,95 +0,0 @@
import { NextMiddleware } from "next/server"
import { NextAuthMiddlewareOptions, withAuth } from "../src/next/middleware"
it("should not match pages as public paths", async () => {
const options: NextAuthMiddlewareOptions = {
pages: {
signIn: "/",
error: "/",
},
secret: "secret",
}
const nextUrl: any = {
pathname: "/protected/pathA",
search: "",
origin: "http://127.0.0.1",
}
const req: any = { nextUrl, headers: { authorization: "" } }
const handleMiddleware = withAuth(options) as NextMiddleware
const res = await handleMiddleware(req, null as any)
expect(res).toBeDefined()
expect(res?.status).toBe(307)
})
it("should not redirect on public paths", async () => {
const options: NextAuthMiddlewareOptions = {
secret: "secret",
}
const nextUrl: any = {
pathname: "/_next/foo",
search: "",
origin: "http://127.0.0.1",
}
const req: any = { nextUrl, headers: { authorization: "" } }
const handleMiddleware = withAuth(options) as NextMiddleware
const res = await handleMiddleware(req, null as any)
expect(res).toBeUndefined()
})
it("should redirect according to nextUrl basePath", async () => {
const options: NextAuthMiddlewareOptions = {
secret: "secret"
}
const nextUrl: any = {
pathname: "/protected/pathA",
search: "",
origin: "http://127.0.0.1",
basePath: "/custom-base-path",
}
const req: any = { nextUrl, headers: { authorization: "" } }
const handleMiddleware = withAuth(options) as NextMiddleware
const res = await handleMiddleware(req, null as any)
expect(res).toBeDefined()
expect(res.status).toEqual(307)
expect(res.headers.get('location')).toContain("http://127.0.0.1/custom-base-path/api/auth/signin?callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA")
})
it("should redirect according to nextUrl basePath", async () => {
// given
const options: NextAuthMiddlewareOptions = {
secret: "secret"
}
const handleMiddleware = withAuth(options) as NextMiddleware
// when
const res = await handleMiddleware({
nextUrl: {
pathname: "/protected/pathA",
search: "",
origin: "http://127.0.0.1",
basePath: "/custom-base-path"
}, headers: { authorization: "" }
} as any, null as any)
// then
expect(res).toBeDefined()
expect(res.status).toEqual(307)
expect(res.headers.get("location")).toContain("http://127.0.0.1/custom-base-path/api/auth/signin?callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA")
// and when follow redirect
const resFromRedirectedUrl = await handleMiddleware({
nextUrl: {
pathname: "/api/auth/signin",
search: "callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA",
origin: "http://127.0.0.1",
basePath: "/custom-base-path"
}, headers: { authorization: "" }
} as any, null as any)
// then return sign in page
expect(resFromRedirectedUrl).toBeUndefined()
})

View File

@@ -1,4 +1,5 @@
{
"private": true,
"name": "@next-auth/tsconfig",
"private": true,
"version": "0.0.0",

1238
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff