mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
40 Commits
feat/hasur
...
@next-auth
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
777b7b2f23 | ||
|
|
6132c3fa75 | ||
|
|
94beef77e6 | ||
|
|
490d59dd17 | ||
|
|
26a8c5fc6d | ||
|
|
e26ec74720 | ||
|
|
d13997e140 | ||
|
|
d6efda077d | ||
|
|
0a4b99de3b | ||
|
|
2d2dfecc9d | ||
|
|
2a2c3d7a45 | ||
|
|
82786ac440 | ||
|
|
dfe3e02132 | ||
|
|
92b38ed740 | ||
|
|
97feae7916 | ||
|
|
24945895e9 | ||
|
|
6deccf610f | ||
|
|
f770b90219 | ||
|
|
87f4786917 | ||
|
|
191ef06471 | ||
|
|
75e6d8f0aa | ||
|
|
17999edd30 | ||
|
|
54b1845e58 | ||
|
|
879faf9fab | ||
|
|
3e3c36891e | ||
|
|
ac5d8a9795 | ||
|
|
965c6267e2 | ||
|
|
bfc429d20b | ||
|
|
2d8e910a19 | ||
|
|
d16e04848e | ||
|
|
ff3a52895b | ||
|
|
e6e03e8842 | ||
|
|
715aad9474 | ||
|
|
902bf92a85 | ||
|
|
44f2a47e6e | ||
|
|
a3b92dbaec | ||
|
|
bdd3ab2816 | ||
|
|
ba55f06585 | ||
|
|
d2b877fb28 | ||
|
|
658b22d9fb |
29
.eslintrc.js
29
.eslintrc.js
@@ -3,10 +3,27 @@ const path = require("path")
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: [path.resolve(__dirname, "./packages/**/tsconfig.eslint.json")],
|
||||
},
|
||||
extends: ["standard-with-typescript", "prettier"],
|
||||
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"],
|
||||
globals: {
|
||||
localStorage: "readonly",
|
||||
location: "readonly",
|
||||
@@ -14,10 +31,6 @@ 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: {
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/1_bug_framework.yml
vendored
1
.github/ISSUE_TEMPLATE/1_bug_framework.yml
vendored
@@ -5,6 +5,7 @@ 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
|
||||
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/2_bug_provider.yml
vendored
1
.github/ISSUE_TEMPLATE/2_bug_provider.yml
vendored
@@ -5,6 +5,7 @@ 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
|
||||
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/3_bug_adapter.yml
vendored
1
.github/ISSUE_TEMPLATE/3_bug_adapter.yml
vendored
@@ -5,6 +5,7 @@ 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
|
||||
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/5_feature_request.yml
vendored
1
.github/ISSUE_TEMPLATE/5_feature_request.yml
vendored
@@ -9,6 +9,7 @@ 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._
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/6_typescript.yml
vendored
1
.github/ISSUE_TEMPLATE/6_typescript.yml
vendored
@@ -17,6 +17,7 @@ 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
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/7_question.yml
vendored
1
.github/ISSUE_TEMPLATE/7_question.yml
vendored
@@ -9,6 +9,7 @@ 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
|
||||
|
||||
12
.github/PULL_REQUEST_TEMPLATE.md
vendored
12
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -5,9 +5,14 @@ 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
|
||||
|
||||
@@ -23,6 +28,7 @@ Fixes: INSERT_ISSUE_LINK_HERE
|
||||
|
||||
## 📌 Resources
|
||||
|
||||
- [Contributing guidelines](./CONTRIBUTING.md)
|
||||
- [Code of conduct](./CODE_OF_CONDUCT.md)
|
||||
- [Security guidelines](../Security.md)
|
||||
- [Contributing guidelines](../CONTRIBUTING.md)
|
||||
- [Code of conduct](../CODE_OF_CONDUCT.md)
|
||||
- [Contributing to Open Source](https://kcd.im/pull-request)
|
||||
|
||||
@@ -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 info@balazsorban.com, yo@ndo.dev, thvu@hey.com and me@iaincollins.com.
|
||||
reported by contacting hi@thvu.dev, info@balazsorban.com, yo@ndo.dev 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
|
||||
|
||||
@@ -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 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.)
|
||||
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.)
|
||||
|
||||
> 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.
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"scripts": {
|
||||
"clean": "rm -rf .next",
|
||||
"dev": "next dev",
|
||||
"lint": "next lint",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"email": "fake-smtp-server",
|
||||
|
||||
@@ -18,6 +18,7 @@ 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"
|
||||
@@ -35,6 +36,7 @@ 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"
|
||||
@@ -102,6 +104,7 @@ 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 }),
|
||||
@@ -120,6 +123,7 @@ 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 }),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,16 @@ 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 }: AppProps) {
|
||||
export default function App({
|
||||
Component,
|
||||
pageProps: { session, ...pageProps },
|
||||
}: AppProps<{ session: Session }>) {
|
||||
return (
|
||||
<SessionProvider session={pageProps.session} refetchInterval={0}>
|
||||
<SessionProvider session={session}>
|
||||
<Component {...pageProps} />
|
||||
</SessionProvider>
|
||||
)
|
||||
|
||||
@@ -4,8 +4,7 @@ import Layout from "../components/layout"
|
||||
import AccessDenied from "../components/access-denied"
|
||||
|
||||
export default function ProtectedPage() {
|
||||
const { data: session, status } = useSession()
|
||||
const loading = status === "loading"
|
||||
const { data: session } = useSession()
|
||||
const [content, setContent] = useState()
|
||||
|
||||
// Fetch content from protected route
|
||||
@@ -19,9 +18,7 @@ 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) {
|
||||
|
||||
@@ -1161,9 +1161,9 @@ isexe@^2.0.0:
|
||||
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
|
||||
|
||||
jose@^4.1.4, jose@^4.3.7:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/jose/-/jose-4.5.0.tgz#92829d8cf846351eb55aaaf94f252fb1d191f2d5"
|
||||
integrity sha512-GFcVFQwYQKbQTUOo2JlpFGXTkgBw26uzDsRMD2q1WgSKNSnpKS9Ug7bdQ8dS+p4sZHNH6iRPu6WK2jLIjspaMA==
|
||||
version "4.9.3"
|
||||
resolved "https://registry.yarnpkg.com/jose/-/jose-4.9.3.tgz#890abd3f26725fe0f2aa720bc2f7835702b624db"
|
||||
integrity sha512-f8E/z+T3Q0kA9txzH2DKvH/ds2uggcw0m3vVPSB9HrSkrQ7mojjifvS7aR8cw+lQl2Fcmx9npwaHpM/M3GD8UQ==
|
||||
|
||||
js-yaml@^4.1.0:
|
||||
version "4.1.0"
|
||||
|
||||
@@ -12,15 +12,28 @@ 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 { PrismaClient } from "@prisma/client"
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
import prisma from "../../../lib/prismadb"
|
||||
|
||||
export default NextAuth({
|
||||
adapter: PrismaAdapter(prisma),
|
||||
|
||||
@@ -112,15 +112,16 @@ 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 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.
|
||||
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.
|
||||
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
...
|
||||
callbacks: {
|
||||
async jwt({ token, account }) {
|
||||
// Persist the OAuth access_token to the token right after signin
|
||||
async jwt({ token, account, profile }) {
|
||||
// Persist the OAuth access_token and or the user id to the token right after signin
|
||||
if (account) {
|
||||
token.accessToken = account.access_token
|
||||
token.id = profile.id
|
||||
}
|
||||
return token
|
||||
}
|
||||
@@ -134,7 +135,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 through 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 (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.
|
||||
|
||||
e.g. `getSession()`, `useSession()`, `/api/auth/session`
|
||||
|
||||
@@ -145,8 +146,10 @@ e.g. `getSession()`, `useSession()`, `/api/auth/session`
|
||||
...
|
||||
callbacks: {
|
||||
async session({ session, token, user }) {
|
||||
// Send properties to the client, like an access_token from a provider.
|
||||
// Send properties to the client, like an access_token and user id from a provider.
|
||||
session.accessToken = token.accessToken
|
||||
session.user.id = token.id
|
||||
|
||||
return session
|
||||
}
|
||||
}
|
||||
@@ -155,7 +158,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` from a provider.
|
||||
JSON Web Token will be immediately available in the session callback, like for example an `access_token` or `id` from a provider.
|
||||
:::
|
||||
|
||||
:::warning
|
||||
|
||||
@@ -114,6 +114,12 @@ 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")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ interface OAuthConfig {
|
||||
*/
|
||||
id: string
|
||||
version: string
|
||||
profile(profile: P, tokens: TokenSet): Awaitable<User & { id: string }>
|
||||
profile(profile: P, tokens: TokenSet): Awaitable<User>
|
||||
checks?: ChecksType | ChecksType[]
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
|
||||
@@ -136,7 +136,7 @@ The `callbackUrl` provided was either invalid or not defined. See [specifying a
|
||||
|
||||
#### JWT_SESSION_ERROR
|
||||
|
||||
JWKKeySupport: the key does not support HS512 verify algorithm
|
||||
JWTKeySupport: 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
|
||||
|
||||
|
||||
@@ -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 `onFail()` 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 `onUnauthenticated()` callback, if you would like to do something else:
|
||||
|
||||
#### Example
|
||||
|
||||
|
||||
@@ -24,7 +24,11 @@ providers: [
|
||||
AtlassianProvider({
|
||||
clientId: process.env.ATLASSIAN_CLIENT_ID,
|
||||
clientSecret: process.env.ATLASSIAN_CLIENT_SECRET,
|
||||
scope: "write:jira-work read:jira-work read:jira-user offline_access read:me"
|
||||
authorization: {
|
||||
params: {
|
||||
scope: "write:jira-work read:jira-work read:jira-user offline_access read:me"
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
...
|
||||
|
||||
@@ -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
|
||||
|
||||
87
docs/docs/providers/zitadel.md
Normal file
87
docs/docs/providers/zitadel.md
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
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`
|
||||
},
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -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 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.)
|
||||
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.)
|
||||
|
||||
:::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.
|
||||
|
||||
@@ -105,6 +105,11 @@ 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.
|
||||
|
||||
@@ -42,18 +42,30 @@ 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"
|
||||
```
|
||||
|
||||
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`.
|
||||
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"] }
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -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/mikro-orm-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/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.4",
|
||||
"@balazsorban/monorepo-release": "0.0.5",
|
||||
"@types/jest": "^28.1.3",
|
||||
"@types/node": "^17.0.25",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@next-auth/dynamodb-adapter",
|
||||
"repository": "https://github.com/nextauthjs/next-auth",
|
||||
"version": "1.0.4",
|
||||
"version": "1.0.5",
|
||||
"description": "AWS DynamoDB adapter for next-auth.",
|
||||
"keywords": [
|
||||
"next-auth",
|
||||
@@ -43,4 +43,4 @@
|
||||
"jest": "^27.4.3",
|
||||
"next-auth": "workspace:*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 Account
|
||||
const accounts = data.Items[0] as AdapterAccount
|
||||
const res = await client.get({
|
||||
TableName,
|
||||
Key: {
|
||||
@@ -174,7 +174,7 @@ export function DynamoDBAdapter(
|
||||
":gsi1sk": `ACCOUNT#${providerAccountId}`,
|
||||
},
|
||||
})
|
||||
const account = format.from<Account>(data.Items?.[0])
|
||||
const account = format.from<AdapterAccount>(data.Items?.[0])
|
||||
if (!account) return
|
||||
await client.delete({
|
||||
TableName,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@next-auth/firebase-adapter",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"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:*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,17 +15,18 @@ import {
|
||||
where,
|
||||
connectFirestoreEmulator,
|
||||
} from "firebase/firestore"
|
||||
import type { Account } from "next-auth"
|
||||
|
||||
import type {
|
||||
Adapter,
|
||||
AdapterSession,
|
||||
AdapterUser,
|
||||
AdapterAccount,
|
||||
AdapterSession,
|
||||
VerificationToken,
|
||||
} from "next-auth/adapters"
|
||||
|
||||
import { getConverter } from "./converter"
|
||||
|
||||
type IndexableObject = Record<string, unknown>
|
||||
export type IndexableObject = Record<string, unknown>
|
||||
|
||||
export interface FirestoreAdapterOptions {
|
||||
emulator?: {
|
||||
@@ -50,13 +51,13 @@ export function FirestoreAdapter({
|
||||
}
|
||||
|
||||
const Users = collection(db, "users").withConverter(
|
||||
getConverter<AdapterUser>()
|
||||
getConverter<AdapterUser & IndexableObject>()
|
||||
)
|
||||
const Sessions = collection(db, "sessions").withConverter(
|
||||
getConverter<AdapterSession & IndexableObject>()
|
||||
)
|
||||
const Accounts = collection(db, "accounts").withConverter(
|
||||
getConverter<Account>()
|
||||
getConverter<AdapterAccount>()
|
||||
)
|
||||
const VerificationTokens = collection(db, "verificationTokens").withConverter(
|
||||
getConverter<VerificationToken & IndexableObject>({ excludeId: true })
|
||||
|
||||
@@ -14,7 +14,7 @@ connectFirestoreEmulator(firestore, 'localhost', 8080);
|
||||
|
||||
type IndexableObject = Record<string, unknown>;
|
||||
|
||||
const Users = collection(firestore, 'users').withConverter(getConverter<AdapterUser>());
|
||||
const Users = collection(firestore, 'users').withConverter(getConverter<AdapterUser & IndexableObject>());
|
||||
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 }));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@next-auth/mikro-orm-adapter",
|
||||
"version": "2.0.1",
|
||||
"version": "3.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.0.2",
|
||||
"@mikro-orm/core": "^5",
|
||||
"next-auth": "^4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mikro-orm/core": "^5.0.2",
|
||||
"@mikro-orm/sqlite": "^5.0.2",
|
||||
"@mikro-orm/core": "^5",
|
||||
"@mikro-orm/sqlite": "^5",
|
||||
"@next-auth/adapter-test": "workspace:*",
|
||||
"@next-auth/tsconfig": "workspace:*",
|
||||
"@types/uuid": "^8.3.3",
|
||||
"jest": "^27.4.3",
|
||||
"@types/uuid": ">=8",
|
||||
"jest": "^29",
|
||||
"next-auth": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": "^9"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "@next-auth/adapter-test/jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
AdapterSession,
|
||||
AdapterUser,
|
||||
AdapterAccount,
|
||||
AdapterSession,
|
||||
VerificationToken as AdapterVerificationToken,
|
||||
} from "next-auth/adapters"
|
||||
import type { ProviderType } from "next-auth/providers"
|
||||
@@ -29,103 +29,105 @@ export class User implements RemoveIndex<AdapterUser> {
|
||||
@PrimaryKey()
|
||||
id: string = randomUUID()
|
||||
|
||||
@Property({ nullable: true })
|
||||
@Property({ type: types.string, nullable: true })
|
||||
name?: string
|
||||
|
||||
@Property({ nullable: true })
|
||||
@Property({ type: types.string, nullable: true })
|
||||
@Unique()
|
||||
email?: string
|
||||
email: string = ""
|
||||
|
||||
@Property({ type: "Date", nullable: true })
|
||||
@Property({ type: types.datetime, nullable: true })
|
||||
emailVerified: Date | null = null
|
||||
|
||||
@Property({ nullable: true })
|
||||
@Property({ type: types.string, nullable: true })
|
||||
image?: string
|
||||
|
||||
@OneToMany({
|
||||
entity: () => Session,
|
||||
mappedBy: (session) => session.user,
|
||||
entity: "Session",
|
||||
mappedBy: (session: Session) => session.user,
|
||||
hidden: true,
|
||||
orphanRemoval: true,
|
||||
})
|
||||
sessions = new Collection<Session>(this)
|
||||
sessions = new Collection<Session, object>(this)
|
||||
|
||||
@OneToMany({
|
||||
entity: () => Account,
|
||||
mappedBy: (account) => account.user,
|
||||
entity: "Account",
|
||||
mappedBy: (account: Account) => account.user,
|
||||
hidden: true,
|
||||
orphanRemoval: true,
|
||||
})
|
||||
accounts = new Collection<Account>(this)
|
||||
accounts = new Collection<Account, object>(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({ persist: false })
|
||||
@Property({ type: types.string, persist: false })
|
||||
userId!: string
|
||||
|
||||
@Property()
|
||||
@Property({ type: "Date" })
|
||||
expires!: Date
|
||||
|
||||
@Property()
|
||||
@Property({ type: types.string })
|
||||
@Unique()
|
||||
sessionToken!: string
|
||||
}
|
||||
|
||||
@Entity()
|
||||
@Unique({ properties: ["provider", "providerAccountId"] })
|
||||
export class Account implements RemoveIndex<DefaultAccount> {
|
||||
export class Account implements RemoveIndex<AdapterAccount> {
|
||||
@PrimaryKey()
|
||||
@Property({ type: types.string })
|
||||
id: string = randomUUID()
|
||||
|
||||
@ManyToOne({
|
||||
entity: () => User,
|
||||
entity: "User",
|
||||
hidden: true,
|
||||
onDelete: "cascade",
|
||||
})
|
||||
user!: User
|
||||
|
||||
@Property({ persist: false })
|
||||
@Property({ type: types.string, persist: false })
|
||||
userId!: string
|
||||
|
||||
@Enum()
|
||||
@Property({ type: types.string })
|
||||
type!: ProviderType
|
||||
|
||||
@Property()
|
||||
@Property({ type: types.string })
|
||||
provider!: string
|
||||
|
||||
@Property()
|
||||
@Property({ type: types.string })
|
||||
providerAccountId!: string
|
||||
|
||||
@Property({ nullable: true })
|
||||
@Property({ type: types.string, nullable: true })
|
||||
refresh_token?: string
|
||||
|
||||
@Property({ nullable: true })
|
||||
@Property({ type: types.string, nullable: true })
|
||||
access_token?: string
|
||||
|
||||
@Property({ nullable: true })
|
||||
@Property({ type: types.integer, nullable: true })
|
||||
expires_at?: number
|
||||
|
||||
@Property({ nullable: true })
|
||||
@Property({ type: types.string, nullable: true })
|
||||
token_type?: string
|
||||
|
||||
@Property({ nullable: true })
|
||||
@Property({ type: types.string, nullable: true })
|
||||
scope?: string
|
||||
|
||||
@Property({ nullable: true })
|
||||
@Property({ type: types.text, nullable: true })
|
||||
id_token?: string
|
||||
|
||||
@Property({ nullable: true })
|
||||
@Property({ type: types.string, nullable: true })
|
||||
session_state?: string
|
||||
}
|
||||
|
||||
@@ -133,12 +135,12 @@ export class Account implements RemoveIndex<DefaultAccount> {
|
||||
@Unique({ properties: ["token", "identifier"] })
|
||||
export class VerificationToken implements AdapterVerificationToken {
|
||||
@PrimaryKey()
|
||||
@Property()
|
||||
@Property({ type: types.string })
|
||||
token!: string
|
||||
|
||||
@Property()
|
||||
@Property({ type: "Date" })
|
||||
expires!: Date
|
||||
|
||||
@Property()
|
||||
@Property({ type: types.string })
|
||||
identifier!: string
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { MikroORM, wrap } from "@mikro-orm/core"
|
||||
|
||||
import * as defaultEntities from "./entities"
|
||||
|
||||
export * as defaultEntities from "./entities"
|
||||
export { defaultEntities }
|
||||
|
||||
/**
|
||||
* The MikroORM adapter accepts a MikroORM configuration and returns a NextAuth adapter.
|
||||
|
||||
@@ -0,0 +1,591 @@
|
||||
// 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,
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
@@ -1,10 +1,70 @@
|
||||
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 { User, VeryImportantEntity } from "./testEntities"
|
||||
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
|
||||
}
|
||||
|
||||
let _init: MikroORM
|
||||
|
||||
28
packages/adapter-mikro-orm/tests/schema.test.ts
Normal file
28
packages/adapter-mikro-orm/tests/schema.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
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)
|
||||
})
|
||||
@@ -1,61 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -5,4 +5,4 @@
|
||||
"./*.js",
|
||||
"./*.d.ts",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,6 @@
|
||||
"outDir": "dist",
|
||||
"stripInternal": true
|
||||
},
|
||||
"exclude": ["tests", "dist", "jest.config.js"]
|
||||
"include": ["src"],
|
||||
"exclude": ["dist", "test", "node_modules"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@next-auth/mongodb-adapter",
|
||||
"version": "1.0.4",
|
||||
"version": "1.1.1",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ import { ObjectId } from "mongodb"
|
||||
|
||||
import type {
|
||||
Adapter,
|
||||
AdapterSession,
|
||||
AdapterUser,
|
||||
AdapterAccount,
|
||||
AdapterSession,
|
||||
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
|
||||
return newObject as T & { _id: ObjectId }
|
||||
},
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export function MongoDBAdapter(
|
||||
const c = { ...defaultCollections, ...collections }
|
||||
return {
|
||||
U: _db.collection<AdapterUser>(c.Users),
|
||||
A: _db.collection<Account>(c.Accounts),
|
||||
A: _db.collection<AdapterAccount>(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<Account>(data)
|
||||
const account = to<AdapterAccount>(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<Account>(account!)
|
||||
return from<AdapterAccount>(account!)
|
||||
},
|
||||
async getSessionAndUser(sessionToken) {
|
||||
const session = await (await db).S.findOne({ sessionToken })
|
||||
@@ -156,7 +156,6 @@ export function MongoDBAdapter(
|
||||
return from<AdapterSession>(session)
|
||||
},
|
||||
async updateSession(data) {
|
||||
// @ts-expect-error
|
||||
const { _id, ...session } = to<AdapterSession>(data)
|
||||
|
||||
const result = await (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@next-auth/neo4j-adapter",
|
||||
"version": "1.0.4",
|
||||
"version": "1.0.5",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,8 +87,6 @@ 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(
|
||||
|
||||
@@ -38,7 +38,7 @@ runBasicTests({
|
||||
return format.from(result?.records[0]?.get("u")?.properties)
|
||||
},
|
||||
|
||||
async session(sessionToken: any) {
|
||||
async session(sessionToken: string) {
|
||||
const result = await neo4jSession.readTransaction((tx) =>
|
||||
tx.run(
|
||||
`MATCH (u:User)-[:HAS_SESSION]->(s:Session)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/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
|
||||
@@ -29,7 +28,7 @@ neo4j:4.2.0
|
||||
# -e NEO4J_ACCEPT_LICENSE_AGREEMENT=yes \
|
||||
# neo4j:4.2.0-enterprise
|
||||
|
||||
echo "Waiting 5 sec for db to start..." && sleep 5
|
||||
echo "Waiting 10 sec for db to start..." && sleep 10
|
||||
|
||||
if $JEST_WATCH; then
|
||||
# Run jest in watch mode
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@next-auth/prisma-adapter",
|
||||
"version": "1.0.4",
|
||||
"version": "1.0.5",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ model User {
|
||||
}
|
||||
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
type String
|
||||
provider String
|
||||
@@ -35,11 +34,10 @@ model Account {
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([provider, providerAccountId])
|
||||
@@id([provider, providerAccountId])
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
sessionToken String @unique
|
||||
userId String
|
||||
expires DateTime
|
||||
@@ -51,5 +49,5 @@ model VerificationToken {
|
||||
token String @unique
|
||||
expires DateTime
|
||||
|
||||
@@unique([identifier, token])
|
||||
@@id([identifier, token])
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@ datasource db {
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["mongoDb"]
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
model Account {
|
||||
|
||||
@@ -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,7 +18,6 @@ model User {
|
||||
}
|
||||
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
type String
|
||||
provider String
|
||||
@@ -33,11 +32,10 @@ model Account {
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([provider, providerAccountId])
|
||||
@@id([provider, providerAccountId])
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
sessionToken String @unique
|
||||
userId String
|
||||
expires DateTime
|
||||
@@ -49,5 +47,5 @@ model VerificationToken {
|
||||
token String @unique
|
||||
expires DateTime
|
||||
|
||||
@@unique([identifier, token])
|
||||
@@id([identifier, token])
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PrismaClient, Prisma } from "@prisma/client"
|
||||
import type { Adapter } from "next-auth/adapters"
|
||||
import type { Adapter, AdapterAccount } from "next-auth/adapters"
|
||||
|
||||
export function PrismaAdapter(p: PrismaClient): Adapter {
|
||||
return {
|
||||
@@ -15,9 +15,12 @@ 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 any,
|
||||
linkAccount: (data) =>
|
||||
p.account.create({ data }) as unknown as AdapterAccount,
|
||||
unlinkAccount: (provider_providerAccountId) =>
|
||||
p.account.delete({ where: { provider_providerAccountId } }) as any,
|
||||
p.account.delete({
|
||||
where: { provider_providerAccountId },
|
||||
}) as unknown as AdapterAccount,
|
||||
async getSessionAndUser(sessionToken) {
|
||||
const userAndSession = await p.session.findUnique({
|
||||
where: { sessionToken },
|
||||
@@ -33,17 +36,18 @@ export function PrismaAdapter(p: PrismaClient): Adapter {
|
||||
deleteSession: (sessionToken) =>
|
||||
p.session.delete({ where: { sessionToken } }),
|
||||
async createVerificationToken(data) {
|
||||
// @ts-ignore
|
||||
const { id: _, ...verificationToken } = await p.verificationToken.create({
|
||||
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
|
||||
return verificationToken
|
||||
},
|
||||
async useVerificationToken(identifier_token) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
const { id: _, ...verificationToken } =
|
||||
await p.verificationToken.delete({ where: { identifier_token } })
|
||||
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
|
||||
return verificationToken
|
||||
} catch (error) {
|
||||
// If token already used/deleted, just return null
|
||||
|
||||
@@ -40,9 +40,9 @@ runBasicTests({
|
||||
where: { identifier_token },
|
||||
})
|
||||
if (!result) return null
|
||||
// @ts-ignore
|
||||
const { id: _, ...verificationToken } = result
|
||||
return verificationToken
|
||||
// @ts-ignore // MongoDB needs an ID, but we don't
|
||||
delete result.id
|
||||
return result
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@next-auth/sequelize-adapter",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.6",
|
||||
"description": "Sequelize adapter for next-auth.",
|
||||
"homepage": "https://next-auth.js.org",
|
||||
"repository": "https://github.com/nextauthjs/next-auth",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Account as AdapterAccount } from "next-auth"
|
||||
import type {
|
||||
Adapter,
|
||||
AdapterUser,
|
||||
AdapterAccount,
|
||||
AdapterSession,
|
||||
VerificationToken,
|
||||
} from "next-auth/adapters"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@next-auth/typeorm-legacy-adapter",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"description": "TypeORM (legacy) adapter for next-auth.",
|
||||
"homepage": "https://next-auth.js.org",
|
||||
"repository": "https://github.com/nextauthjs/next-auth",
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { Adapter, AdapterSession, AdapterUser } from "next-auth/adapters"
|
||||
import type {
|
||||
Adapter,
|
||||
AdapterUser,
|
||||
AdapterAccount,
|
||||
AdapterSession,
|
||||
} 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"
|
||||
|
||||
@@ -87,7 +91,7 @@ export function TypeORMLegacyAdapter(
|
||||
},
|
||||
async getUserByAccount(provider_providerAccountId) {
|
||||
const m = await getManager(c)
|
||||
const account = await m.findOne<Account & { user: AdapterUser }>(
|
||||
const account = await m.findOne<AdapterAccount & { user: AdapterUser }>(
|
||||
"AccountEntity",
|
||||
{ where: provider_providerAccountId, relations: ["user"] }
|
||||
)
|
||||
@@ -115,9 +119,8 @@ export function TypeORMLegacyAdapter(
|
||||
},
|
||||
async unlinkAccount(providerAccountId) {
|
||||
const m = await getManager(c)
|
||||
await m.delete<Account>("AccountEntity", providerAccountId)
|
||||
await m.delete<AdapterAccount>("AccountEntity", providerAccountId)
|
||||
},
|
||||
// @ts-expect-error
|
||||
async createSession(data) {
|
||||
const m = await getManager(c)
|
||||
const session = await m.save("SessionEntity", data)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@next-auth/upstash-redis-adapter",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.3",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Account as AdapterAccount } from "next-auth"
|
||||
import type {
|
||||
Adapter,
|
||||
AdapterUser,
|
||||
AdapterAccount,
|
||||
AdapterSession,
|
||||
VerificationToken,
|
||||
} from "next-auth/adapters"
|
||||
@@ -117,7 +117,6 @@ 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,
|
||||
@@ -144,10 +143,7 @@ export function UpstashRedisAdapter(
|
||||
const id = `${account.provider}:${account.providerAccountId}`
|
||||
return await setAccount(id, { ...account, id })
|
||||
},
|
||||
async createSession(session) {
|
||||
const id = session.sessionToken
|
||||
return await setSession(id, { ...session, id })
|
||||
},
|
||||
createSession: (session) => setSession(session.sessionToken, session),
|
||||
async getSessionAndUser(sessionToken) {
|
||||
const session = await getSession(sessionToken)
|
||||
if (!session) return null
|
||||
@@ -165,13 +161,20 @@ export function UpstashRedisAdapter(
|
||||
},
|
||||
async createVerificationToken(verificationToken) {
|
||||
await setObjectAsJson(
|
||||
verificationTokenKeyPrefix + verificationToken.identifier,
|
||||
verificationTokenKeyPrefix +
|
||||
verificationToken.identifier +
|
||||
":" +
|
||||
verificationToken.token,
|
||||
verificationToken
|
||||
)
|
||||
return verificationToken
|
||||
},
|
||||
async useVerificationToken(verificationToken) {
|
||||
const tokenKey = verificationTokenKeyPrefix + verificationToken.identifier
|
||||
const tokenKey =
|
||||
verificationTokenKeyPrefix +
|
||||
verificationToken.identifier +
|
||||
":" +
|
||||
verificationToken.token
|
||||
|
||||
const token = await client.get<VerificationToken>(tokenKey)
|
||||
if (!token) return null
|
||||
|
||||
@@ -11,6 +11,14 @@ 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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next-auth",
|
||||
"version": "4.10.3",
|
||||
"version": "4.13.0",
|
||||
"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.4.1",
|
||||
"jose": "^4.3.7",
|
||||
"cookie": "^0.5.0",
|
||||
"jose": "^4.9.3",
|
||||
"oauth": "^0.9.15",
|
||||
"openid-client": "^5.1.0",
|
||||
"preact": "^10.6.3",
|
||||
@@ -78,6 +78,7 @@
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"next": "^12.2.5",
|
||||
"nodemailer": "^6.6.5",
|
||||
"react": "^17.0.2 || ^18",
|
||||
"react-dom": "^17.0.2 || ^18"
|
||||
@@ -118,7 +119,7 @@
|
||||
"jest-environment-jsdom": "^28.1.1",
|
||||
"jest-watch-typeahead": "^1.1.0",
|
||||
"msw": "^0.42.3",
|
||||
"next": "12.2.0",
|
||||
"next": "12.2.5",
|
||||
"postcss": "^8.4.14",
|
||||
"postcss-cli": "^9.1.0",
|
||||
"postcss-nested": "^5.0.6",
|
||||
|
||||
@@ -2,11 +2,15 @@ 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 */
|
||||
@@ -55,13 +59,30 @@ 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 interface 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 {
|
||||
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<Account, "provider" | "providerAccountId">
|
||||
providerAccountId: Pick<AdapterAccount, "provider" | "providerAccountId">
|
||||
) => Awaitable<AdapterUser | null>
|
||||
updateUser: (user: Partial<AdapterUser>) => Awaitable<AdapterUser>
|
||||
/** @todo Implement */
|
||||
@@ -69,12 +90,12 @@ export interface Adapter {
|
||||
userId: string
|
||||
) => Promise<void> | Awaitable<AdapterUser | null | undefined>
|
||||
linkAccount: (
|
||||
account: Account
|
||||
) => Promise<void> | Awaitable<Account | null | undefined>
|
||||
account: AdapterAccount
|
||||
) => Promise<void> | Awaitable<AdapterAccount | null | undefined>
|
||||
/** @todo Implement */
|
||||
unlinkAccount?: (
|
||||
providerAccountId: Pick<Account, "provider" | "providerAccountId">
|
||||
) => Promise<void> | Awaitable<Account | undefined>
|
||||
providerAccountId: Pick<AdapterAccount, "provider" | "providerAccountId">
|
||||
) => Promise<void> | Awaitable<AdapterAccount | undefined>
|
||||
/** Creates a session for the user and returns it. */
|
||||
createSession: (session: {
|
||||
sessionToken: string
|
||||
|
||||
@@ -94,10 +94,18 @@ export function BroadcastChannel(name = "nextauth.message") {
|
||||
/** Notify other tabs/windows. */
|
||||
post(message: Record<string, unknown>) {
|
||||
if (typeof window === "undefined") return
|
||||
localStorage.setItem(
|
||||
name,
|
||||
JSON.stringify({ ...message, timestamp: now() })
|
||||
)
|
||||
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.
|
||||
*/
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { EventCallbacks, LoggerInstance } from ".."
|
||||
import type { Adapter } from "../adapters"
|
||||
|
||||
/**
|
||||
* Same as the default `Error`, but it is JSON serializable.
|
||||
@@ -58,6 +57,11 @@ 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"
|
||||
@@ -99,10 +103,10 @@ export function eventsErrorHandler(
|
||||
}
|
||||
|
||||
/** Handles adapter induced errors. */
|
||||
export function adapterErrorHandler(
|
||||
adapter: Adapter | undefined,
|
||||
export function adapterErrorHandler<TAdapter>(
|
||||
adapter: TAdapter | undefined,
|
||||
logger: LoggerInstance
|
||||
): Adapter | undefined {
|
||||
): TAdapter | undefined {
|
||||
if (!adapter) return
|
||||
|
||||
return Object.keys(adapter).reduce<any>((acc, name) => {
|
||||
|
||||
@@ -94,13 +94,21 @@ 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.action === "signin" &&
|
||||
req.query?.callbackUrl.startsWith(pages.error)
|
||||
pages?.error && req.query?.callbackUrl?.startsWith(pages.error)
|
||||
|
||||
if (!pages?.error || authOnErrorPage) {
|
||||
if (authOnErrorPage) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { randomBytes, randomUUID } from "crypto"
|
||||
import { NextAuthOptions } from ".."
|
||||
import logger from "../utils/logger"
|
||||
import parseUrl from "../utils/parse-url"
|
||||
@@ -70,6 +71,7 @@ export async function init({
|
||||
// and are request-specific.
|
||||
url,
|
||||
action,
|
||||
// @ts-expect-errors
|
||||
provider,
|
||||
cookies: {
|
||||
...cookie.defaultCookies(
|
||||
@@ -86,6 +88,10 @@ 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
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
MissingSecret,
|
||||
UnsupportedStrategy,
|
||||
InvalidCallbackUrl,
|
||||
MissingAdapterMethods,
|
||||
} from "../errors"
|
||||
import parseUrl from "../../utils/parse-url"
|
||||
import { defaultCookies } from "./cookie"
|
||||
@@ -120,8 +121,23 @@ export function assertConfig(params: {
|
||||
}
|
||||
}
|
||||
|
||||
if (hasEmail && !options.adapter) {
|
||||
return new MissingAdapter("E-mail login requires an adapter.")
|
||||
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 (!warned) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { randomBytes, randomUUID } from "crypto"
|
||||
import { AccountNotLinkedError } from "../errors"
|
||||
import { fromDate } from "./utils"
|
||||
|
||||
@@ -22,11 +21,11 @@ import type { SessionToken } from "./cookie"
|
||||
*/
|
||||
export default async function callbackHandler(params: {
|
||||
sessionToken?: SessionToken
|
||||
profile: User
|
||||
account: Account
|
||||
profile: User | AdapterUser | { email: string }
|
||||
account: Account | null
|
||||
options: InternalOptions
|
||||
}) {
|
||||
const { sessionToken, profile, account, options } = params
|
||||
const { sessionToken, profile: _profile, account, options } = params
|
||||
// Input validation
|
||||
if (!account?.providerAccountId || !account.type)
|
||||
throw new Error("Missing or invalid provider account")
|
||||
@@ -37,15 +36,17 @@ export default async function callbackHandler(params: {
|
||||
adapter,
|
||||
jwt,
|
||||
events,
|
||||
session: { strategy: sessionStrategy },
|
||||
session: { strategy: sessionStrategy, generateSessionToken },
|
||||
} = 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, account, session: {} }
|
||||
return { user: _profile as User, account }
|
||||
}
|
||||
|
||||
const profile = _profile as AdapterUser
|
||||
|
||||
const {
|
||||
createUser,
|
||||
updateUser,
|
||||
@@ -85,9 +86,7 @@ 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 = profile.email
|
||||
? await getUserByEmail(profile.email)
|
||||
: null
|
||||
const userByEmail = await getUserByEmail(profile.email)
|
||||
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
|
||||
@@ -102,8 +101,7 @@ export default async function callbackHandler(params: {
|
||||
user = await updateUser({ id: userByEmail.id, emailVerified: new Date() })
|
||||
await events.updateUser?.({ user })
|
||||
} else {
|
||||
const newUser = { ...profile, emailVerified: new Date() }
|
||||
delete (newUser as Omit<AdapterUser, "id">).id
|
||||
const { id: _, ...newUser } = { ...profile, emailVerified: new Date() }
|
||||
// Create user account if there isn't one for the email address already
|
||||
user = await createUser(newUser)
|
||||
await events.createUser?.({ user })
|
||||
@@ -199,8 +197,7 @@ 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 newUser = { ...profile, emailVerified: null }
|
||||
delete (newUser as Omit<AdapterUser, "id">).id
|
||||
const { id: _, ...newUser } = { ...profile, emailVerified: null }
|
||||
user = await createUser(newUser)
|
||||
await events.createUser?.({ user })
|
||||
|
||||
@@ -218,9 +215,6 @@ export default async function callbackHandler(params: {
|
||||
return { session, user, isNewUser: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateSessionToken() {
|
||||
// Use `randomUUID` if available. (Node 15.6++)
|
||||
return randomUUID?.() ?? randomBytes(32).toString("hex")
|
||||
throw new Error("Unsupported account type")
|
||||
}
|
||||
|
||||
19
packages/next-auth/src/core/lib/email/getUserFromEmail.ts
Normal file
19
packages/next-auth/src/core/lib/email/getUserFromEmail.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,6 @@ export default async function email(
|
||||
theme,
|
||||
}),
|
||||
// Save in database
|
||||
// @ts-expect-error // verified in `assertConfig`
|
||||
adapter.createVerificationToken({
|
||||
identifier,
|
||||
token: hashToken(token, options),
|
||||
|
||||
@@ -39,10 +39,7 @@ export default async function getAuthorizationUrl({
|
||||
if (provider.version?.startsWith("1.")) {
|
||||
const client = oAuth1Client(options)
|
||||
const tokens = (await client.getOAuthRequestToken(params)) as any
|
||||
const url = `${
|
||||
// @ts-expect-error
|
||||
provider.authorization?.url ?? provider.authorization
|
||||
}?${new URLSearchParams({
|
||||
const url = `${provider.authorization?.url}?${new URLSearchParams({
|
||||
oauth_token: tokens.oauth_token,
|
||||
oauth_token_secret: tokens.oauth_token_secret,
|
||||
...tokens.params,
|
||||
@@ -68,7 +65,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
|
||||
|
||||
@@ -7,10 +7,10 @@ import { useNonce } from "./nonce-handler"
|
||||
import { OAuthCallbackError } from "../../errors"
|
||||
|
||||
import type { CallbackParamsType, OpenIDCallbackChecks } from "openid-client"
|
||||
import type { Account, LoggerInstance, Profile } from "../../.."
|
||||
import type { LoggerInstance, Profile } from "../../.."
|
||||
import type { OAuthChecks, OAuthConfig } from "../../../providers"
|
||||
import type { InternalOptions } from "../../types"
|
||||
import type { RequestInternal, OutgoingResponse } from "../.."
|
||||
import type { RequestInternal } 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,23 +34,19 @@ 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 ?? {}
|
||||
// @ts-expect-error
|
||||
const tokens: TokenSet = await client.getOAuthAccessToken(
|
||||
oauth_token as string,
|
||||
// @ts-expect-error
|
||||
const tokens = (await (client as any).getOAuthAccessToken(
|
||||
oauth_token,
|
||||
null,
|
||||
oauth_verifier
|
||||
)
|
||||
// @ts-expect-error
|
||||
let profile: Profile = await client.get(
|
||||
(provider as any).profileUrl,
|
||||
)) as TokenSet
|
||||
let profile: Profile = await (client as any).get(
|
||||
provider.profileUrl,
|
||||
tokens.oauth_token,
|
||||
tokens.oauth_token_secret
|
||||
)
|
||||
@@ -59,7 +55,8 @@ export default async function oAuthCallback(params: {
|
||||
profile = JSON.parse(profile)
|
||||
}
|
||||
|
||||
return await getProfile({ profile, tokens, provider, logger })
|
||||
const newProfile = await getProfile({ profile, tokens, provider, logger })
|
||||
return { ...newProfile, cookies: [] }
|
||||
} catch (error) {
|
||||
logger.error("OAUTH_V1_GET_ACCESS_TOKEN_ERROR", error as Error)
|
||||
throw error
|
||||
@@ -82,7 +79,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)
|
||||
}
|
||||
|
||||
@@ -102,13 +99,10 @@ 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,
|
||||
@@ -128,9 +122,7 @@ 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,
|
||||
@@ -140,7 +132,6 @@ export default async function oAuthCallback(params: {
|
||||
profile = tokens.claims()
|
||||
} else {
|
||||
profile = await client.userinfo(tokens, {
|
||||
// @ts-expect-error
|
||||
params: provider.userinfo?.params,
|
||||
})
|
||||
}
|
||||
@@ -164,25 +155,22 @@ 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): Promise<GetProfileResult> {
|
||||
}: GetProfileParams) {
|
||||
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,
|
||||
@@ -202,11 +190,9 @@ 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 as Error)
|
||||
return {
|
||||
profile: null,
|
||||
account: null,
|
||||
logger.error("OAUTH_PARSE_PROFILE_ERROR", {
|
||||
error: error as Error,
|
||||
OAuthProfile,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,13 +22,9 @@ export async function openidClient(
|
||||
} else {
|
||||
issuer = new Issuer({
|
||||
issuer: provider.issuer as string,
|
||||
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,
|
||||
authorization_endpoint: provider.authorization?.url,
|
||||
token_endpoint: provider.token?.url,
|
||||
userinfo_endpoint: provider.userinfo?.url,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { merge } from "../../utils/merge"
|
||||
|
||||
import type { InternalProvider } from "../types"
|
||||
import type { Provider } from "../../providers"
|
||||
import type {
|
||||
InternalOAuthConfig,
|
||||
OAuthConfig,
|
||||
Provider,
|
||||
} from "../../providers"
|
||||
import type { InternalUrl } from "../../utils/parse-url"
|
||||
|
||||
/**
|
||||
@@ -18,52 +22,72 @@ export default function parseProviders(params: {
|
||||
} {
|
||||
const { url, providerId } = params
|
||||
|
||||
const providers = params.providers.map(({ options, ...rest }) => {
|
||||
const defaultOptions = normalizeProvider(rest as Provider)
|
||||
const userOptions = normalizeProvider(options as Provider)
|
||||
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}`,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
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 }
|
||||
return {
|
||||
providers,
|
||||
provider: providers.find(({ id }) => id === providerId),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeProvider(provider?: Provider) {
|
||||
if (!provider) return
|
||||
/**
|
||||
* 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
|
||||
|
||||
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 ?? []),
|
||||
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
|
||||
}
|
||||
} else {
|
||||
acc[key] = value
|
||||
}
|
||||
|
||||
return acc
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter, @typescript-eslint/consistent-type-assertions
|
||||
}, {} as any)
|
||||
return acc
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter
|
||||
{} as any
|
||||
)
|
||||
|
||||
if (normalized.type === "oauth" && !normalized.version?.startsWith("1.")) {
|
||||
if (!isUserOptions && !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")
|
||||
)
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
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<"oauth" | "credentials" | "email">
|
||||
options: InternalOptions
|
||||
query: RequestInternal["query"]
|
||||
method: Required<RequestInternal>["method"]
|
||||
body: RequestInternal["body"]
|
||||
@@ -50,7 +52,7 @@ export default async function callback(params: {
|
||||
cookies: params.cookies,
|
||||
})
|
||||
|
||||
if (oauthCookies) cookies.push(...oauthCookies)
|
||||
if (oauthCookies.length) cookies.push(...oauthCookies)
|
||||
|
||||
try {
|
||||
// Make it easier to debug when adding a new provider
|
||||
@@ -68,7 +70,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) {
|
||||
if (!profile || !account || !OAuthProfile) {
|
||||
return { redirect: `${url}/signin`, cookies }
|
||||
}
|
||||
|
||||
@@ -80,7 +82,6 @@ export default async function callback(params: {
|
||||
if (adapter) {
|
||||
const { getUserByAccount } = adapter
|
||||
const userByAccount = await getUserByAccount({
|
||||
// @ts-expect-error
|
||||
providerAccountId: account.providerAccountId,
|
||||
provider: provider.id,
|
||||
})
|
||||
@@ -91,7 +92,6 @@ export default async function callback(params: {
|
||||
try {
|
||||
const isAllowed = await callbacks.signIn({
|
||||
user: userOrProfile,
|
||||
// @ts-expect-error
|
||||
account,
|
||||
profile: OAuthProfile,
|
||||
})
|
||||
@@ -110,11 +110,9 @@ 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,
|
||||
})
|
||||
@@ -129,7 +127,6 @@ export default async function callback(params: {
|
||||
const token = await callbacks.jwt({
|
||||
token: defaultToken,
|
||||
user,
|
||||
// @ts-expect-error
|
||||
account,
|
||||
profile: OAuthProfile,
|
||||
isNewUser,
|
||||
@@ -150,10 +147,10 @@ export default async function callback(params: {
|
||||
// Save Session Token in cookie
|
||||
cookies.push({
|
||||
name: options.cookies.sessionToken.name,
|
||||
value: session.sessionToken,
|
||||
value: (session as AdapterSession).sessionToken,
|
||||
options: {
|
||||
...options.cookies.sessionToken.options,
|
||||
expires: session.expires,
|
||||
expires: (session as AdapterSession).expires,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -201,14 +198,16 @@ export default async function callback(params: {
|
||||
}
|
||||
} else if (provider.type === "email") {
|
||||
try {
|
||||
// Verified in `assertConfig`
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const { useVerificationToken, getUserByEmail } = adapter!
|
||||
const token = query?.token as string | undefined
|
||||
const identifier = query?.email as string | undefined
|
||||
|
||||
const token = query?.token
|
||||
const identifier = query?.email
|
||||
// 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 invite = await useVerificationToken?.({
|
||||
// @ts-expect-error -- Verified in `assertConfig`. adapter: Adapter<true>
|
||||
const invite = await adapter.useVerificationToken({
|
||||
identifier,
|
||||
token: hashToken(token, options),
|
||||
})
|
||||
@@ -218,29 +217,23 @@ export default async function callback(params: {
|
||||
return { redirect: `${url}/error?error=Verification`, cookies }
|
||||
}
|
||||
|
||||
// If it is an existing user, use that, otherwise use a placeholder
|
||||
const profile = (identifier
|
||||
? await getUserByEmail(identifier)
|
||||
: null) ?? {
|
||||
const profile = await getUserFromEmail({
|
||||
email: identifier,
|
||||
}
|
||||
// @ts-expect-error -- Verified in `assertConfig`. adapter: Adapter<true>
|
||||
adapter,
|
||||
})
|
||||
|
||||
/** @type {import("src").Account} */
|
||||
const account = {
|
||||
providerAccountId: profile.email,
|
||||
type: "email",
|
||||
type: "email" as const,
|
||||
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 }
|
||||
@@ -257,12 +250,9 @@ 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,
|
||||
})
|
||||
@@ -277,7 +267,6 @@ export default async function callback(params: {
|
||||
const token = await callbacks.jwt({
|
||||
token: defaultToken,
|
||||
user,
|
||||
// @ts-expect-error
|
||||
account,
|
||||
isNewUser,
|
||||
})
|
||||
@@ -297,15 +286,14 @@ export default async function callback(params: {
|
||||
// Save Session Token in cookie
|
||||
cookies.push({
|
||||
name: options.cookies.sessionToken.name,
|
||||
value: session.sessionToken,
|
||||
value: (session as AdapterSession).sessionToken,
|
||||
options: {
|
||||
...options.cookies.sessionToken.options,
|
||||
expires: session.expires,
|
||||
expires: (session as AdapterSession).expires,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
await events.signIn?.({ user, account, isNewUser })
|
||||
|
||||
// Handle first logins on new accounts
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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, User } from "../.."
|
||||
import type { Account } from "../.."
|
||||
|
||||
/** Handle requests to /api/auth/signin */
|
||||
export default async function signin(params: {
|
||||
@@ -11,7 +12,7 @@ export default async function signin(params: {
|
||||
body: RequestInternal["body"]
|
||||
}): Promise<OutgoingResponse> {
|
||||
const { options, query, body } = params
|
||||
const { url, adapter, callbacks, logger, provider } = options
|
||||
const { url, callbacks, logger, provider } = options
|
||||
|
||||
if (!provider.type) {
|
||||
return {
|
||||
@@ -54,14 +55,12 @@ export default async function signin(params: {
|
||||
return { redirect: `${url}/error?error=EmailSignin` }
|
||||
}
|
||||
|
||||
// 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) ?? {
|
||||
const user = await getUserFromEmail({
|
||||
email,
|
||||
id: email,
|
||||
}
|
||||
// @ts-expect-error -- Verified in `assertConfig`. adapter: Adapter<true>
|
||||
adapter: options.adapter,
|
||||
withId: true,
|
||||
})
|
||||
|
||||
const account: Account = {
|
||||
providerAccountId: email,
|
||||
@@ -72,7 +71,6 @@ 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,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { Adapter } from "../adapters"
|
||||
import type { Adapter, AdapterUser } 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 DefaultAccount extends Partial<TokenSet> {
|
||||
export interface Account 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,30 +240,23 @@ export interface DefaultAccount 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
|
||||
}
|
||||
|
||||
export interface Account extends Record<string, unknown>, DefaultAccount {}
|
||||
|
||||
export interface DefaultProfile {
|
||||
/** The OAuth profile returned from your provider */
|
||||
export interface Profile {
|
||||
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 extends Record<string, unknown> = Profile,
|
||||
A extends Record<string, unknown> = Account
|
||||
> {
|
||||
export interface CallbacksOptions<P = Profile, A = Account> {
|
||||
/**
|
||||
* Use this callback to control if a user is allowed to sign in.
|
||||
* Returning true will continue the sign-in flow.
|
||||
@@ -272,13 +265,13 @@ export interface CallbacksOptions<
|
||||
* [Documentation](https://next-auth.js.org/configuration/callbacks#sign-in-callback)
|
||||
*/
|
||||
signIn: (params: {
|
||||
user: User
|
||||
account: A
|
||||
user: User | { email: string }
|
||||
account: A | null
|
||||
/**
|
||||
* If OAuth provider is used, it contains the full
|
||||
* OAuth profile returned by your provider.
|
||||
*/
|
||||
profile: P & Record<string, unknown>
|
||||
profile?: P
|
||||
/**
|
||||
* 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.
|
||||
@@ -287,7 +280,7 @@ export interface CallbacksOptions<
|
||||
* 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 */
|
||||
@@ -341,8 +334,8 @@ export interface CallbacksOptions<
|
||||
*/
|
||||
jwt: (params: {
|
||||
token: JWT
|
||||
user?: User
|
||||
account?: A
|
||||
user?: User | AdapterUser
|
||||
account?: A | null
|
||||
profile?: P
|
||||
isNewUser?: boolean
|
||||
}) => Awaitable<JWT>
|
||||
@@ -378,7 +371,7 @@ export interface EventCallbacks {
|
||||
*/
|
||||
signIn: (message: {
|
||||
user: User
|
||||
account: Account
|
||||
account: Account | null
|
||||
profile?: Profile
|
||||
isNewUser?: boolean
|
||||
}) => Awaitable<void>
|
||||
@@ -392,9 +385,9 @@ export interface EventCallbacks {
|
||||
createUser: (message: { user: User }) => Awaitable<void>
|
||||
updateUser: (message: { user: User }) => Awaitable<void>
|
||||
linkAccount: (message: {
|
||||
user: User
|
||||
user: User | AdapterUser | { email: string }
|
||||
account: Account
|
||||
profile: User
|
||||
profile: User | AdapterUser | { email: string }
|
||||
}) => Awaitable<void>
|
||||
/**
|
||||
* The message object will contain one of these depending on
|
||||
@@ -420,7 +413,7 @@ export interface PagesOptions {
|
||||
|
||||
export type ISODateString = string
|
||||
|
||||
export interface DefaultSession extends Record<string, unknown> {
|
||||
export interface DefaultSession {
|
||||
user?: {
|
||||
name?: string | null
|
||||
email?: string | null
|
||||
@@ -438,7 +431,7 @@ export interface DefaultSession extends Record<string, unknown> {
|
||||
* [`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 Record<string, unknown>, DefaultSession {}
|
||||
export interface Session extends DefaultSession {}
|
||||
|
||||
export type SessionStrategy = "jwt" | "database"
|
||||
|
||||
@@ -468,6 +461,13 @@ 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 Record<string, unknown>, DefaultUser {}
|
||||
export interface User extends DefaultUser {}
|
||||
|
||||
// Below are types that are only supposed be used by next-auth internally
|
||||
|
||||
/** @internal */
|
||||
export type InternalProvider<T extends ProviderType = any> = (T extends "oauth"
|
||||
? OAuthConfig<any>
|
||||
export type InternalProvider<T = ProviderType> = (T extends "oauth"
|
||||
? InternalOAuthConfig<any>
|
||||
: T extends "email"
|
||||
? EmailConfig
|
||||
: T extends "credentials"
|
||||
@@ -515,7 +515,10 @@ export type NextAuthAction =
|
||||
| "_log"
|
||||
|
||||
/** @internal */
|
||||
export interface InternalOptions<T extends ProviderType = any> {
|
||||
export interface InternalOptions<
|
||||
TProviderType = ProviderType,
|
||||
WithVerificationToken = TProviderType extends "email" ? true : false
|
||||
> {
|
||||
providers: InternalProvider[]
|
||||
/**
|
||||
* Parsed from `NEXTAUTH_URL` or `x-forwarded-host` on Vercel.
|
||||
@@ -523,9 +526,7 @@ export interface InternalOptions<T extends ProviderType = any> {
|
||||
*/
|
||||
url: InternalUrl
|
||||
action: NextAuthAction
|
||||
provider: T extends string
|
||||
? InternalProvider<T>
|
||||
: InternalProvider<T> | undefined
|
||||
provider: InternalProvider<TProviderType>
|
||||
csrfToken?: string
|
||||
csrfTokenVerified?: boolean
|
||||
secret: string
|
||||
@@ -536,7 +537,9 @@ export interface InternalOptions<T extends ProviderType = any> {
|
||||
pages: Partial<PagesOptions>
|
||||
jwt: JWTOptions
|
||||
events: Partial<EventCallbacks>
|
||||
adapter?: Adapter
|
||||
adapter: WithVerificationToken extends true
|
||||
? Adapter<WithVerificationToken>
|
||||
: Adapter<WithVerificationToken> | undefined
|
||||
callbacks: CallbacksOptions
|
||||
cookies: CookiesOptions
|
||||
callbackUrl: string
|
||||
|
||||
@@ -118,12 +118,14 @@ export async function unstable_getServerSession(
|
||||
},
|
||||
})
|
||||
|
||||
const { body, cookies } = session
|
||||
const { body, cookies, status = 200 } = session
|
||||
|
||||
cookies?.forEach((cookie) => setCookie(res, cookie))
|
||||
|
||||
if (body && typeof body !== "string" && Object.keys(body).length)
|
||||
return body as Session
|
||||
if (body && typeof body !== "string" && Object.keys(body).length) {
|
||||
if (status === 200) return body as Session
|
||||
throw new Error((body as any).message)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ export interface NextAuthMiddlewareOptions {
|
||||
* ```
|
||||
*
|
||||
* ---
|
||||
* [Documentation](https://next-auth.js.org/getting-started/nextjs/middleware#api) | [`signIn` callback](configuration/callbacks#sign-in-callback)
|
||||
* [Documentation](https://next-auth.js.org/configuration/nextjs#middleware) | [`signIn` callback](configuration/callbacks#sign-in-callback)
|
||||
*/
|
||||
authorized?: AuthorizedCallback
|
||||
}
|
||||
@@ -101,17 +101,18 @@ async function handleMiddleware(
|
||||
options: NextAuthMiddlewareOptions | undefined,
|
||||
onSuccess?: (token: JWT | null) => Promise<NextMiddlewareResult>
|
||||
) {
|
||||
const { pathname, search, origin } = req.nextUrl
|
||||
const { pathname, search, origin, basePath } = req.nextUrl
|
||||
|
||||
const signInPage = options?.pages?.signIn ?? "/api/auth/signin"
|
||||
const errorPage = options?.pages?.error ?? "/api/auth/error"
|
||||
const basePath = parseUrl(process.env.NEXTAUTH_URL).path
|
||||
const publicPaths = [signInPage, errorPage, "/_next", "/favicon.ico"]
|
||||
const authPath = parseUrl(process.env.NEXTAUTH_URL).path
|
||||
const publicPaths = ["/_next", "/favicon.ico"]
|
||||
|
||||
// Avoid infinite redirects/invalid response
|
||||
// on paths that never require authentication
|
||||
if (
|
||||
pathname.startsWith(basePath) ||
|
||||
`${basePath}${pathname}`.startsWith(authPath) ||
|
||||
[signInPage, errorPage].includes(pathname) ||
|
||||
publicPaths.some((p) => pathname.startsWith(p))
|
||||
) {
|
||||
return
|
||||
@@ -124,7 +125,7 @@ async function handleMiddleware(
|
||||
`\nhttps://next-auth.js.org/errors#no_secret`
|
||||
)
|
||||
|
||||
const errorUrl = new URL(errorPage, origin)
|
||||
const errorUrl = new URL(`${basePath}${errorPage}`, origin)
|
||||
errorUrl.searchParams.append("error", "Configuration")
|
||||
|
||||
return NextResponse.redirect(errorUrl)
|
||||
@@ -144,8 +145,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(signInPage, origin)
|
||||
signInUrl.searchParams.append("callbackUrl", `${pathname}${search}`)
|
||||
const signInUrl = new URL(`${basePath}${signInPage}`, origin)
|
||||
signInUrl.searchParams.append("callbackUrl", `${basePath}${pathname}${search}`)
|
||||
return NextResponse.redirect(signInUrl)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,25 @@
|
||||
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",
|
||||
@@ -36,7 +33,6 @@ export default function HubSpot<P extends HubSpotProfile>(
|
||||
scope: "oauth",
|
||||
client_id: options.clientId,
|
||||
},
|
||||
|
||||
},
|
||||
client: {
|
||||
token_endpoint_auth_method: "client_secret_post",
|
||||
@@ -45,33 +41,27 @@ 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",
|
||||
});
|
||||
})
|
||||
|
||||
const userInfo = await response.json();
|
||||
|
||||
return { userInfo }
|
||||
}
|
||||
return await response.json()
|
||||
},
|
||||
},
|
||||
profile(profile) {
|
||||
|
||||
const { userInfo } = profile
|
||||
|
||||
return {
|
||||
id: userInfo.user_id,
|
||||
name: userInfo.user,
|
||||
email: userInfo.user,
|
||||
id: profile.user_id,
|
||||
name: profile.user,
|
||||
email: profile.user,
|
||||
|
||||
// TODO: get image from profile once it's available
|
||||
// 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,
|
||||
|
||||
@@ -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 & { id: string }>
|
||||
profile: (profile: P, tokens: TokenSet) => Awaitable<User>
|
||||
checks?: ChecksType | ChecksType[]
|
||||
client?: Partial<ClientMetadata>
|
||||
jwks?: { keys: JWK[] }
|
||||
@@ -147,6 +147,14 @@ 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"
|
||||
|
||||
51
packages/next-auth/src/providers/zitadel.ts
Normal file
51
packages/next-auth/src/providers/zitadel.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ export type SessionContextValue<R extends boolean = false> = R extends true
|
||||
| { data: Session; status: "authenticated" }
|
||||
| { data: null; status: "unauthenticated" | "loading" }
|
||||
|
||||
const SessionContext = React.createContext<SessionContextValue | undefined>(
|
||||
export const SessionContext = React.createContext<SessionContextValue | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { InvalidCallbackUrl, MissingSecret } from "../src/core/errors"
|
||||
import {
|
||||
InvalidCallbackUrl,
|
||||
MissingAdapter,
|
||||
MissingAdapterMethods,
|
||||
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(
|
||||
@@ -14,6 +20,48 @@ 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: [] },
|
||||
|
||||
@@ -156,6 +156,7 @@ 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" },
|
||||
|
||||
@@ -47,17 +47,19 @@ describe("Treat secret correctly", () => {
|
||||
})
|
||||
|
||||
it("Error if missing NEXTAUTH_SECRET and secret", async () => {
|
||||
const session = await unstable_getServerSession(req, res, {
|
||||
providers: [],
|
||||
logger,
|
||||
})
|
||||
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)
|
||||
|
||||
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)
|
||||
@@ -71,6 +73,7 @@ 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
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
95
packages/next-auth/tests/middleware.test.ts
Normal file
95
packages/next-auth/tests/middleware.test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
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()
|
||||
})
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "@next-auth/tsconfig",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
|
||||
1238
pnpm-lock.yaml
generated
1238
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user