mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
17 Commits
@next-auth
...
@next-auth
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2a07932b9 | ||
|
|
25c7ce1d2b | ||
|
|
227a233bd8 | ||
|
|
cf9f133aa3 | ||
|
|
2301c1be44 | ||
|
|
6e408e24bf | ||
|
|
f277989c69 | ||
|
|
6146e93288 | ||
|
|
1ff565da6c | ||
|
|
41f75cf870 | ||
|
|
dd591ed8d0 | ||
|
|
297bc2317f | ||
|
|
b170138e70 | ||
|
|
a307079e0f | ||
|
|
d52b7a6b7d | ||
|
|
30b69a07eb | ||
|
|
0d1757814f |
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -18,10 +18,10 @@ jobs:
|
||||
language: ["javascript"]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
||||
2
.github/workflows/label-issue.yml
vendored
2
.github/workflows/label-issue.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
name: Triage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: github/issue-labeler@v2.4.1
|
||||
- uses: github/issue-labeler@v2.5
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
configuration-path: ".github/issue-labeler.yml"
|
||||
|
||||
2
.github/workflows/label-pr.yml
vendored
2
.github/workflows/label-pr.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
name: Triage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v3
|
||||
- uses: actions/labeler@v4
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
configuration-path: ".github/pr-labeler.yml"
|
||||
|
||||
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@@ -15,11 +15,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Init
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.2.1
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7.5.1
|
||||
- name: Setup Node
|
||||
@@ -49,11 +49,11 @@ jobs:
|
||||
environment: Production
|
||||
steps:
|
||||
- name: Init
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.2.1
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7.5.1
|
||||
- name: Setup Node
|
||||
@@ -81,9 +81,9 @@ jobs:
|
||||
environment: Preview
|
||||
steps:
|
||||
- name: Init
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v2.2.1
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7.5.1
|
||||
- name: Setup Node
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN_PKG }}
|
||||
- name: Comment version on PR
|
||||
uses: NejcZdovc/comment-pr@v1
|
||||
uses: NejcZdovc/comment-pr@v2
|
||||
with:
|
||||
message:
|
||||
"🎉 Experimental release [published 📦️ on npm](https://npmjs.com/package/next-auth/v/${{ env.VERSION }})!\n \
|
||||
|
||||
2
.github/workflows/sync-examples.yml
vendored
2
.github/workflows/sync-examples.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Run GitHub File Sync
|
||||
# Can update to v1 when https://github.com/BetaHuhn/repo-file-sync-action/issues/168 is resolved
|
||||
uses: BetaHuhn/repo-file-sync-action@v1.16.5
|
||||
|
||||
@@ -87,6 +87,7 @@ If you think your custom provider might be useful to others, we encourage you to
|
||||
|
||||
1. Add your config: [`src/providers/{provider}.js`](https://github.com/nextauthjs/next-auth/tree/main/packages/next-auth/src/providers) (Make sure you use a named default export, like `export default function YourProvider`!)
|
||||
2. Add provider documentation: [`www/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/www/docs/providers)
|
||||
3. Add provider logo svgs, like `google-dark.svg` (dark mode) and `google.svg` (light mode) to the `/packages/next-auth/provider-logos/` directory. Don't forget to set the provider's styling options in the `provider.style` config object.
|
||||
|
||||
That's it! 🎉 Others will be able to discover this provider much more easily now!
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@ The MongoDB adapter does not handle connections automatically, so you will have
|
||||
npm install next-auth @next-auth/mongodb-adapter mongodb
|
||||
```
|
||||
|
||||
2. Add `lib/mongodb.js`
|
||||
2. Add `lib/mongodb.ts`
|
||||
|
||||
```js
|
||||
```ts
|
||||
// This approach is taken from https://github.com/vercel/next.js/tree/canary/examples/with-mongodb
|
||||
import { MongoClient } from 'mongodb'
|
||||
|
||||
|
||||
@@ -472,7 +472,8 @@ cookies: {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
secure: useSecureCookies
|
||||
secure: useSecureCookies,
|
||||
maxAge: 900
|
||||
}
|
||||
},
|
||||
state: {
|
||||
@@ -482,6 +483,7 @@ cookies: {
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
secure: useSecureCookies,
|
||||
maxAge: 900
|
||||
},
|
||||
},
|
||||
nonce: {
|
||||
|
||||
@@ -13,7 +13,7 @@ Adding support for signing in via email in addition to one or more OAuth service
|
||||
Configuration is similar to other providers, but the options are different:
|
||||
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
import EmailProvider from `next-auth/providers/email`
|
||||
import EmailProvider from "next-auth/providers/email"
|
||||
...
|
||||
providers: [
|
||||
EmailProvider({
|
||||
|
||||
@@ -174,6 +174,10 @@ interface OAuthConfig {
|
||||
issuer?: string
|
||||
client?: Partial<ClientMetadata>
|
||||
allowDangerousEmailAccountLinking?: boolean
|
||||
/**
|
||||
* Object containing the settings for the styling of the providers sign-in buttons
|
||||
*/
|
||||
style: ProviderStyleType
|
||||
}
|
||||
```
|
||||
|
||||
@@ -428,7 +432,8 @@ If you think your custom provider might be useful to others, we encourage you to
|
||||
You only need to add three changes:
|
||||
|
||||
1. Add your config: [`src/providers/{provider}.ts`](https://github.com/nextauthjs/next-auth/tree/main/packages/next-auth/src/providers)<br />
|
||||
• make sure you use a named default export, like this: `export default function YourProvider`
|
||||
- Make sure you use a named default export, like this: `export default function YourProvider`
|
||||
- Add two SVG's of the provider logo, like `google-dark.svg` (dark mode) and `google.svg` (light mode), to the `/packages/next-auth/provider-logos/` directory as well as the styling config to the provider config object. See existing provider for example
|
||||
2. Add provider documentation: [`/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/docs/docs/providers)
|
||||
3. Add the new provider name to the `Provider type` dropdown options in [`the provider issue template`](<[http](https://github.com/nextauthjs/next-auth/edit/main/.github/ISSUE_TEMPLATE/2_bug_provider.yml)>)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ http://developers.strava.com/docs/reference/
|
||||
|
||||
The **Strava Provider** comes with a set of default options:
|
||||
|
||||
- [Strava Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/strava.js)
|
||||
- [Strava Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/strava.ts)
|
||||
|
||||
You can override any of the options to suit your own use case. Ensure the redirect_uri configuration fits your needs accordingly.
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ For the time being, the `withAuth` middleware only supports `"jwt"` as [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`.
|
||||
To include 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).
|
||||
:::
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@next-auth/firebase-adapter",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"description": "Firebase adapter for next-auth.",
|
||||
"homepage": "https://next-auth.js.org",
|
||||
"repository": "https://github.com/nextauthjs/next-auth",
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "FIRESTORE_EMULATOR_HOST=localhost:8080 firebase emulators:exec --only firestore --project next-auth-test jest"
|
||||
"test": "FIRESTORE_EMULATOR_HOST=localhost:8080 firebase --token '$FIREBASE_TOKEN' emulators:exec --only firestore --project next-auth-test jest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"firebase": "^9.7.0",
|
||||
@@ -38,9 +38,9 @@
|
||||
"devDependencies": {
|
||||
"@next-auth/adapter-test": "workspace:*",
|
||||
"@next-auth/tsconfig": "workspace:*",
|
||||
"firebase": "^9.7.0",
|
||||
"firebase-tools": "^10.7.2",
|
||||
"firebase": "^9.14.0",
|
||||
"firebase-tools": "^11.16.1",
|
||||
"jest": "^27.4.3",
|
||||
"next-auth": "workspace:*"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,10 +86,10 @@ export function FirestoreAdapter({
|
||||
async getUserByEmail(email) {
|
||||
const userQuery = query(Users, where("email", "==", email), limit(1))
|
||||
const userSnapshots = await getDocs(userQuery)
|
||||
const userSnpashot = userSnapshots.docs[0]
|
||||
const userSnapshot = userSnapshots.docs[0]
|
||||
|
||||
if (userSnpashot?.exists() && Users.converter) {
|
||||
return Users.converter.fromFirestore(userSnpashot)
|
||||
if (userSnapshot?.exists() && Users.converter) {
|
||||
return Users.converter.fromFirestore(userSnapshot)
|
||||
}
|
||||
|
||||
return null
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "@next-auth/tsconfig/tsconfig.base.json",
|
||||
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@next-auth/pouchdb-adapter",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.5",
|
||||
"description": "PouchDB adapter for next-auth.",
|
||||
"homepage": "https://next-auth.js.org",
|
||||
"repository": "https://github.com/nextauthjs/next-auth",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "@next-auth/tsconfig/tsconfig.base.json",
|
||||
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next-auth",
|
||||
"version": "4.17.0",
|
||||
"version": "4.18.0",
|
||||
"description": "Authentication for Next.js",
|
||||
"homepage": "https://next-auth.js.org",
|
||||
"repository": "https://github.com/nextauthjs/next-auth.git",
|
||||
|
||||
@@ -52,7 +52,8 @@ async function getBody(req: Request): Promise<Record<string, any> | undefined> {
|
||||
|
||||
// TODO:
|
||||
async function toInternalRequest(
|
||||
req: RequestInternal | Request
|
||||
req: RequestInternal | Request,
|
||||
trustHost: boolean = false
|
||||
): Promise<RequestInternal> {
|
||||
if (req instanceof Request) {
|
||||
const url = new URL(req.url)
|
||||
@@ -70,7 +71,11 @@ async function toInternalRequest(
|
||||
cookies: parseCookie(req.headers.get("cookie") ?? ""),
|
||||
providerId: nextauth[1],
|
||||
error: url.searchParams.get("error") ?? nextauth[1],
|
||||
host: detectHost(headers["x-forwarded-host"] ?? headers.host),
|
||||
host: detectHost(
|
||||
trustHost,
|
||||
headers["x-forwarded-host"] ?? headers.host,
|
||||
"http://localhost:3000"
|
||||
),
|
||||
query,
|
||||
}
|
||||
}
|
||||
@@ -82,7 +87,7 @@ export async function NextAuthHandler<
|
||||
>(params: NextAuthHandlerParams): Promise<OutgoingResponse<Body>> {
|
||||
const { options: userOptions, req: incomingRequest } = params
|
||||
|
||||
const req = await toInternalRequest(incomingRequest)
|
||||
const req = await toInternalRequest(incomingRequest, userOptions.trustHost)
|
||||
|
||||
setLogger(userOptions.logger, userOptions.debug)
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ export function defaultCookies(useSecureCookies: boolean): CookiesOptions {
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
secure: useSecureCookies,
|
||||
maxAge: 60 * 15, // 15 minutes in seconds
|
||||
},
|
||||
},
|
||||
state: {
|
||||
@@ -102,6 +103,7 @@ export function defaultCookies(useSecureCookies: boolean): CookiesOptions {
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
secure: useSecureCookies,
|
||||
maxAge: 60 * 15, // 15 minutes in seconds
|
||||
},
|
||||
},
|
||||
nonce: {
|
||||
|
||||
@@ -26,13 +26,15 @@ export async function createPKCE(options: InternalOptions<"oauth">): Promise<
|
||||
const code_verifier = generators.codeVerifier()
|
||||
const code_challenge = generators.codeChallenge(code_verifier)
|
||||
|
||||
const maxAge = cookies.pkceCodeVerifier.options.maxAge ?? PKCE_MAX_AGE
|
||||
|
||||
const expires = new Date()
|
||||
expires.setTime(expires.getTime() + PKCE_MAX_AGE * 1000)
|
||||
expires.setTime(expires.getTime() + maxAge * 1000)
|
||||
|
||||
// Encrypt code_verifier and save it to an encrypted cookie
|
||||
const encryptedCodeVerifier = await jwt.encode({
|
||||
...options.jwt,
|
||||
maxAge: PKCE_MAX_AGE,
|
||||
maxAge,
|
||||
token: { code_verifier },
|
||||
})
|
||||
|
||||
@@ -40,7 +42,7 @@ export async function createPKCE(options: InternalOptions<"oauth">): Promise<
|
||||
code_challenge,
|
||||
code_challenge_method: PKCE_CODE_CHALLENGE_METHOD,
|
||||
code_verifier,
|
||||
PKCE_MAX_AGE,
|
||||
maxAge,
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -17,17 +17,18 @@ export async function createState(
|
||||
}
|
||||
|
||||
const state = generators.state()
|
||||
const maxAge = cookies.state.options.maxAge ?? STATE_MAX_AGE
|
||||
|
||||
const encodedState = await jwt.encode({
|
||||
...jwt,
|
||||
maxAge: STATE_MAX_AGE,
|
||||
maxAge,
|
||||
token: { state },
|
||||
})
|
||||
|
||||
logger.debug("CREATE_STATE", { state, maxAge: STATE_MAX_AGE })
|
||||
logger.debug("CREATE_STATE", { state, maxAge })
|
||||
|
||||
const expires = new Date()
|
||||
expires.setTime(expires.getTime() + STATE_MAX_AGE * 1000)
|
||||
expires.setTime(expires.getTime() + maxAge * 1000)
|
||||
return {
|
||||
value: state,
|
||||
cookie: {
|
||||
|
||||
@@ -38,10 +38,10 @@ export interface NextAuthOptions {
|
||||
providers: Provider[]
|
||||
/**
|
||||
* A random string used to hash tokens, sign cookies and generate cryptographic keys.
|
||||
* If not specified, it falls back to `jwt.secret` or `NEXTAUTH_SECRET` from environment vairables.
|
||||
* Otherwise it will use a hash of all configuration options, including Client ID / Secrets for entropy.
|
||||
* If not specified, it falls back to `jwt.secret` or `NEXTAUTH_SECRET` from environment variables.
|
||||
* Otherwise, it will use a hash of all configuration options, including Client ID / Secrets for entropy.
|
||||
*
|
||||
* NOTE: The last behavior is extrmely volatile, and will throw an error in production.
|
||||
* NOTE: The last behavior is extremely volatile, and will throw an error in production.
|
||||
* * **Default value**: `string` (SHA hash of the "options" object)
|
||||
* * **Required**: No - **but strongly recommended**!
|
||||
*
|
||||
@@ -203,6 +203,16 @@ export interface NextAuthOptions {
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#cookies) | [Usage example](https://next-auth.js.org/configuration/options#example)
|
||||
*/
|
||||
cookies?: Partial<CookiesOptions>
|
||||
/**
|
||||
* If set to `true`, NextAuth.js will use either the `x-forwarded-host` or `host` headers,
|
||||
* instead of `NEXTAUTH_URL`
|
||||
* Make sure that reading `x-forwarded-host` on your hosting platform can be trusted.
|
||||
* - ⚠ **This is an advanced option.** Advanced options are passed the same way as basic options,
|
||||
* but **may have complex implications** or side effects.
|
||||
* You should **try to avoid using advanced options** unless you are very comfortable using them.
|
||||
* @default Boolean(process.env.AUTH_TRUST_HOST ?? process.env.VERCEL)
|
||||
*/
|
||||
trustHost?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
} from "next"
|
||||
import type { NextAuthOptions, Session } from ".."
|
||||
import type {
|
||||
CallbacksOptions,
|
||||
NextAuthAction,
|
||||
NextAuthRequest,
|
||||
NextAuthResponse,
|
||||
@@ -21,12 +22,17 @@ async function NextAuthNextHandler(
|
||||
) {
|
||||
const { nextauth, ...query } = req.query
|
||||
|
||||
options.secret =
|
||||
options.secret ?? options.jwt?.secret ?? process.env.NEXTAUTH_SECRET
|
||||
options.secret ??= options.jwt?.secret ?? process.env.NEXTAUTH_SECRET
|
||||
options.trustHost ??= !!(process.env.AUTH_TRUST_HOST ?? process.env.VERCEL)
|
||||
|
||||
const handler = await NextAuthHandler({
|
||||
req: {
|
||||
host: detectHost(req.headers["x-forwarded-host"]),
|
||||
host: detectHost(
|
||||
options.trustHost,
|
||||
req.headers["x-forwarded-host"],
|
||||
process.env.NEXTAUTH_URL ??
|
||||
(process.env.NODE_ENV !== "production" && "http://localhost:3000")
|
||||
),
|
||||
body: req.body,
|
||||
query,
|
||||
cookies: req.cookies,
|
||||
@@ -85,17 +91,25 @@ export default NextAuth
|
||||
|
||||
let experimentalWarningShown = false
|
||||
let experimentalRSCWarningShown = false
|
||||
export async function unstable_getServerSession(
|
||||
|
||||
type GetServerSessionOptions = Partial<Omit<NextAuthOptions, "callbacks">> & {
|
||||
callbacks?: Omit<NextAuthOptions['callbacks'], "session"> & {
|
||||
session?: (...args: Parameters<CallbacksOptions["session"]>) => any
|
||||
}
|
||||
}
|
||||
|
||||
export async function unstable_getServerSession<
|
||||
O extends GetServerSessionOptions,
|
||||
R = O["callbacks"] extends { session: (...args: any[]) => infer U }
|
||||
? U
|
||||
: Session
|
||||
>(
|
||||
...args:
|
||||
| [
|
||||
GetServerSidePropsContext["req"],
|
||||
GetServerSidePropsContext["res"],
|
||||
NextAuthOptions
|
||||
]
|
||||
| [NextApiRequest, NextApiResponse, NextAuthOptions]
|
||||
| [NextAuthOptions]
|
||||
| [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"], O]
|
||||
| [NextApiRequest, NextApiResponse, O]
|
||||
| [O]
|
||||
| []
|
||||
): Promise<Session | null> {
|
||||
): Promise<R | null> {
|
||||
if (!experimentalWarningShown && process.env.NODE_ENV !== "production") {
|
||||
console.warn(
|
||||
"[next-auth][warn][EXPERIMENTAL_API]",
|
||||
@@ -123,7 +137,8 @@ export async function unstable_getServerSession(
|
||||
|
||||
let req, res, options: NextAuthOptions
|
||||
if (isRSC) {
|
||||
options = args[0] ?? { providers: [] }
|
||||
options = Object.assign({}, args[0], { providers: [] })
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { headers, cookies } = require("next/headers")
|
||||
req = {
|
||||
@@ -138,15 +153,21 @@ export async function unstable_getServerSession(
|
||||
} else {
|
||||
req = args[0]
|
||||
res = args[1]
|
||||
options = args[2]
|
||||
options = Object.assign(args[2], { providers: [] })
|
||||
}
|
||||
|
||||
options.secret = options.secret ?? process.env.NEXTAUTH_SECRET
|
||||
options.secret ??= process.env.NEXTAUTH_SECRET
|
||||
options.trustHost ??= !!(process.env.AUTH_TRUST_HOST ?? process.env.VERCEL)
|
||||
|
||||
const session = await NextAuthHandler<Session | {} | string>({
|
||||
options,
|
||||
req: {
|
||||
host: detectHost(req.headers["x-forwarded-host"]),
|
||||
host: detectHost(
|
||||
options.trustHost,
|
||||
req.headers["x-forwarded-host"],
|
||||
process.env.NEXTAUTH_URL ??
|
||||
(process.env.NODE_ENV !== "production" && "http://localhost:3000")
|
||||
),
|
||||
action: "session",
|
||||
method: "GET",
|
||||
cookies: req.cookies,
|
||||
@@ -162,7 +183,7 @@ export async function unstable_getServerSession(
|
||||
if (status === 200) {
|
||||
// @ts-expect-error
|
||||
if (isRSC) delete body.expires
|
||||
return body as Session
|
||||
return body as R
|
||||
}
|
||||
throw new Error((body as any).message)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { NextResponse, NextRequest } from "next/server"
|
||||
|
||||
import { getToken } from "../jwt"
|
||||
import parseUrl from "../utils/parse-url"
|
||||
import { detectHost } from "../utils/detect-host"
|
||||
|
||||
type AuthorizedCallback = (params: {
|
||||
token: JWT | null
|
||||
@@ -89,7 +90,17 @@ export interface NextAuthMiddlewareOptions {
|
||||
* The same `secret` used in the `NextAuth` configuration.
|
||||
* Defaults to the `NEXTAUTH_SECRET` environment variable.
|
||||
*/
|
||||
secret?: string
|
||||
secret?: NextAuthOptions["secret"]
|
||||
/**
|
||||
* If set to `true`, NextAuth.js will use either the `x-forwarded-host` or `host` headers,
|
||||
* instead of `NEXTAUTH_URL`
|
||||
* Make sure that reading `x-forwarded-host` on your hosting platform can be trusted.
|
||||
* - ⚠ **This is an advanced option.** Advanced options are passed the same way as basic options,
|
||||
* but **may have complex implications** or side effects.
|
||||
* You should **try to avoid using advanced options** unless you are very comfortable using them.
|
||||
* @default Boolean(process.env.VERCEL ?? process.env.AUTH_TRUST_HOST)
|
||||
*/
|
||||
trustHost?: NextAuthOptions["trustHost"]
|
||||
}
|
||||
|
||||
// TODO: `NextMiddleware` should allow returning `void`
|
||||
@@ -98,14 +109,25 @@ type NextMiddlewareResult = ReturnType<NextMiddleware> | void // eslint-disable-
|
||||
|
||||
async function handleMiddleware(
|
||||
req: NextRequest,
|
||||
options: NextAuthMiddlewareOptions | undefined,
|
||||
options: NextAuthMiddlewareOptions | undefined = {},
|
||||
onSuccess?: (token: JWT | null) => Promise<NextMiddlewareResult>
|
||||
) {
|
||||
const { pathname, search, origin, basePath } = req.nextUrl
|
||||
|
||||
const signInPage = options?.pages?.signIn ?? "/api/auth/signin"
|
||||
const errorPage = options?.pages?.error ?? "/api/auth/error"
|
||||
const authPath = parseUrl(process.env.NEXTAUTH_URL).path
|
||||
|
||||
options.trustHost = Boolean(
|
||||
options.trustHost ?? process.env.VERCEL ?? process.env.AUTH_TRUST_HOST
|
||||
)
|
||||
|
||||
const host = detectHost(
|
||||
options.trustHost,
|
||||
req.headers.get("x-forwarded-host"),
|
||||
process.env.NEXTAUTH_URL ??
|
||||
(process.env.NODE_ENV !== "production" && "http://localhost:3000")
|
||||
)
|
||||
const authPath = parseUrl(host).path
|
||||
const publicPaths = ["/_next", "/favicon.ico"]
|
||||
|
||||
// Avoid infinite redirects/invalid response
|
||||
@@ -146,7 +168,10 @@ async function handleMiddleware(
|
||||
|
||||
// the user is not logged in, redirect to the sign-in page
|
||||
const signInUrl = new URL(`${basePath}${signInPage}`, origin)
|
||||
signInUrl.searchParams.append("callbackUrl", `${basePath}${pathname}${search}`)
|
||||
signInUrl.searchParams.append(
|
||||
"callbackUrl",
|
||||
`${basePath}${pathname}${search}`
|
||||
)
|
||||
return NextResponse.redirect(signInUrl)
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function Naver<P extends NaverProfile>(
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.response.id,
|
||||
name: profile.response.name,
|
||||
name: profile.response.nickname,
|
||||
email: profile.response.email,
|
||||
image: profile.response.profile_image,
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
/** Extract the host from the environment */
|
||||
export function detectHost(forwardedHost: any) {
|
||||
// If we detect a Vercel environment, we can trust the host
|
||||
if (process.env.VERCEL ?? process.env.AUTH_TRUST_HOST)
|
||||
return forwardedHost
|
||||
// If `NEXTAUTH_URL` is `undefined` we fall back to "http://localhost:3000"
|
||||
return process.env.NEXTAUTH_URL
|
||||
export function detectHost(
|
||||
trusted: boolean,
|
||||
forwardedValue: string | string[] | undefined | null,
|
||||
defaultValue: string | false
|
||||
): string | undefined {
|
||||
if (trusted && forwardedValue) {
|
||||
return Array.isArray(forwardedValue) ? forwardedValue[0] : forwardedValue
|
||||
}
|
||||
|
||||
return defaultValue || undefined
|
||||
}
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import { NextMiddleware } from "next/server"
|
||||
import { NextMiddleware, NextRequest } 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: "/",
|
||||
},
|
||||
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 req = new NextRequest("http://127.0.0.1/protected/pathA", {
|
||||
headers: { authorization: "" },
|
||||
})
|
||||
|
||||
const handleMiddleware = withAuth(options) as NextMiddleware
|
||||
const res = await handleMiddleware(req, null as any)
|
||||
@@ -24,15 +18,11 @@ it("should not match pages as public paths", async () => {
|
||||
})
|
||||
|
||||
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 options: NextAuthMiddlewareOptions = { secret: "secret" }
|
||||
|
||||
const req = new NextRequest("http://127.0.0.1/_next/foo", {
|
||||
headers: { authorization: "" },
|
||||
})
|
||||
|
||||
const handleMiddleware = withAuth(options) as NextMiddleware
|
||||
const res = await handleMiddleware(req, null as any)
|
||||
@@ -40,55 +30,66 @@ it("should not redirect on public paths", async () => {
|
||||
})
|
||||
|
||||
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 options: NextAuthMiddlewareOptions = { secret: "secret" }
|
||||
|
||||
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({
|
||||
const req = {
|
||||
nextUrl: {
|
||||
pathname: "/protected/pathA",
|
||||
search: "",
|
||||
origin: "http://127.0.0.1",
|
||||
basePath: "/custom-base-path"
|
||||
}, headers: { authorization: "" }
|
||||
} as any, null as any)
|
||||
basePath: "/custom-base-path",
|
||||
},
|
||||
headers: new Headers({ authorization: "" }),
|
||||
}
|
||||
|
||||
const handleMiddleware = withAuth(options) as NextMiddleware
|
||||
const res = await handleMiddleware(req as NextRequest, 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
|
||||
|
||||
const req1 = {
|
||||
nextUrl: {
|
||||
pathname: "/protected/pathA",
|
||||
search: "",
|
||||
origin: "http://127.0.0.1",
|
||||
basePath: "/custom-base-path",
|
||||
},
|
||||
headers: new Headers({ authorization: "" }),
|
||||
}
|
||||
// when
|
||||
const res = await handleMiddleware(req1 as NextRequest, 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")
|
||||
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({
|
||||
const req2 = {
|
||||
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)
|
||||
basePath: "/custom-base-path",
|
||||
},
|
||||
headers: new Headers({ authorization: "" }),
|
||||
}
|
||||
// and when follow redirect
|
||||
const resFromRedirectedUrl = await handleMiddleware(
|
||||
req2 as NextRequest,
|
||||
null as any
|
||||
)
|
||||
|
||||
// then return sign in page
|
||||
expect(resFromRedirectedUrl).toBeUndefined()
|
||||
|
||||
139
packages/next-auth/tests/pkce-handler.test.ts
Normal file
139
packages/next-auth/tests/pkce-handler.test.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { mockLogger } from "./lib"
|
||||
import type { InternalOptions, LoggerInstance, InternalProvider, CallbacksOptions, Account, Awaitable, Profile, Session, User, CookiesOptions } from "../src"
|
||||
import { createPKCE } from "../src/core/lib/oauth/pkce-handler"
|
||||
import { InternalUrl } from "../src/utils/parse-url"
|
||||
import { JWT, JWTDecodeParams, JWTEncodeParams, JWTOptions } from "../src/jwt"
|
||||
import { CredentialInput } from "../src/providers"
|
||||
|
||||
let logger: LoggerInstance
|
||||
let url: InternalUrl
|
||||
let provider: InternalProvider<"oauth">
|
||||
let jwt: JWTOptions
|
||||
let callbacks: CallbacksOptions
|
||||
let cookies: CookiesOptions
|
||||
let options: InternalOptions<"oauth">
|
||||
|
||||
beforeEach(() => {
|
||||
logger = mockLogger()
|
||||
|
||||
url = {
|
||||
origin: "http://localhost:3000",
|
||||
host: "localhost:3000",
|
||||
path: "/api/auth",
|
||||
base: "http://localhost:3000/api/auth",
|
||||
toString: () => "http://localhost:3000/api/auth"
|
||||
}
|
||||
|
||||
provider = {
|
||||
type: "oauth",
|
||||
id: "testId",
|
||||
name: "testName",
|
||||
signinUrl: "/",
|
||||
callbackUrl: "/",
|
||||
checks: ["pkce", "state"]
|
||||
}
|
||||
|
||||
jwt = {
|
||||
secret: "secret",
|
||||
maxAge: 0,
|
||||
encode: function (params: JWTEncodeParams): Awaitable<string> {
|
||||
throw new Error("Function not implemented.")
|
||||
},
|
||||
decode: function (params: JWTDecodeParams): Awaitable<JWT | null> {
|
||||
throw new Error("Function not implemented.")
|
||||
}
|
||||
}
|
||||
|
||||
callbacks = {
|
||||
signIn: function (params: { user: User; account: Account; profile: Profile & Record<string, unknown>; email: { verificationRequest?: boolean | undefined }; credentials?: Record<string, CredentialInput> | undefined }): Awaitable<string | boolean> {
|
||||
throw new Error("Function not implemented.")
|
||||
},
|
||||
redirect: function (params: { url: string; baseUrl: string }): Awaitable<string> {
|
||||
throw new Error("Function not implemented.")
|
||||
},
|
||||
session: function (params: { session: Session; user: User; token: JWT }): Awaitable<Session> {
|
||||
throw new Error("Function not implemented.")
|
||||
},
|
||||
jwt: function (params: { token: JWT; user?: User | undefined; account?: Account | undefined; profile?: Profile | undefined; isNewUser?: boolean | undefined }): Awaitable<JWT> {
|
||||
throw new Error("Function not implemented.")
|
||||
}
|
||||
}
|
||||
|
||||
cookies = {
|
||||
sessionToken: { name: "", options: undefined },
|
||||
callbackUrl: { name: "", options: undefined },
|
||||
csrfToken: { name: "", options: undefined },
|
||||
pkceCodeVerifier: { name: "", options: {} },
|
||||
state: { name: "", options: undefined },
|
||||
nonce: { name: "", options: undefined }
|
||||
}
|
||||
|
||||
options = {
|
||||
url,
|
||||
action: "session",
|
||||
provider,
|
||||
secret: "",
|
||||
debug: false,
|
||||
logger,
|
||||
session: { strategy: "jwt", maxAge: 0, updateAge: 0 },
|
||||
pages: {},
|
||||
jwt,
|
||||
events: {},
|
||||
callbacks,
|
||||
cookies,
|
||||
callbackUrl: '',
|
||||
providers: [],
|
||||
theme: {}
|
||||
}
|
||||
})
|
||||
|
||||
describe("createPKCE", () => {
|
||||
it("returns a code challenge, code challenge method, and cookie", async () => {
|
||||
const pkce = await createPKCE(options)
|
||||
|
||||
expect(pkce?.code_challenge).not.toBeNull()
|
||||
expect(pkce?.code_challenge_method).toEqual("S256")
|
||||
expect(pkce?.cookie).not.toBeNull()
|
||||
})
|
||||
it("does not return a pkce when the provider does not support pkce", async () => {
|
||||
options.provider.checks = ["state"]
|
||||
|
||||
const pkce = await createPKCE(options)
|
||||
|
||||
expect(pkce).toBeUndefined()
|
||||
})
|
||||
it("sets the cookie expiration to a default of 15 minutes when the max age option is not provided", async () => {
|
||||
const pkce = await createPKCE(options)
|
||||
|
||||
const defaultMaxAge = 60 * 15 // 15 minutes in seconds
|
||||
const expires = new Date()
|
||||
expires.setTime(expires.getTime() + defaultMaxAge * 1000)
|
||||
|
||||
validateCookieExpiration({pkce, expires})
|
||||
expect(pkce?.cookie.options.maxAge).toBeUndefined()
|
||||
})
|
||||
|
||||
it("sets the cookie expiration and max age to the provided max age from the options", async () => {
|
||||
const maxAge = 60 * 20 // 20 minutes
|
||||
cookies.pkceCodeVerifier.options.maxAge = maxAge
|
||||
|
||||
const pkce = await createPKCE(options)
|
||||
|
||||
const expires = new Date()
|
||||
expires.setTime(expires.getTime() + maxAge * 1000)
|
||||
|
||||
validateCookieExpiration({pkce, expires})
|
||||
expect(pkce?.cookie.options.maxAge).toEqual(maxAge)
|
||||
})
|
||||
})
|
||||
|
||||
// comparing the parts instead of getTime() because the milliseconds
|
||||
// will not match since the two Date objects are created milliseconds apart
|
||||
const validateCookieExpiration = ({pkce, expires}) => {
|
||||
const cookieExpires = pkce?.cookie.options.expires
|
||||
expect(cookieExpires.getFullYear()).toEqual(expires.getFullYear())
|
||||
expect(cookieExpires.getMonth()).toEqual(expires.getMonth())
|
||||
expect(cookieExpires.getFullYear()).toEqual(expires.getFullYear())
|
||||
expect(cookieExpires.getHours()).toEqual(expires.getHours())
|
||||
expect(cookieExpires.getMinutes()).toEqual(expires.getMinutes())
|
||||
}
|
||||
134
packages/next-auth/tests/state-handler.test.ts
Normal file
134
packages/next-auth/tests/state-handler.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { mockLogger } from "./lib"
|
||||
import type { InternalOptions, LoggerInstance, InternalProvider, CallbacksOptions, Account, Awaitable, Profile, Session, User, CookiesOptions } from "../src"
|
||||
import { createState } from "../src/core/lib/oauth/state-handler"
|
||||
import { InternalUrl } from "../src/utils/parse-url"
|
||||
import { JWT, JWTOptions, encode, decode } from "../src/jwt"
|
||||
import { CredentialInput } from "../src/providers"
|
||||
|
||||
let logger: LoggerInstance
|
||||
let url: InternalUrl
|
||||
let provider: InternalProvider<"oauth">
|
||||
let jwt: JWTOptions
|
||||
let callbacks: CallbacksOptions
|
||||
let cookies: CookiesOptions
|
||||
let options: InternalOptions<"oauth">
|
||||
|
||||
beforeEach(() => {
|
||||
logger = mockLogger()
|
||||
|
||||
url = {
|
||||
origin: "http://localhost:3000",
|
||||
host: "localhost:3000",
|
||||
path: "/api/auth",
|
||||
base: "http://localhost:3000/api/auth",
|
||||
toString: () => "http://localhost:3000/api/auth"
|
||||
}
|
||||
|
||||
provider = {
|
||||
type: "oauth",
|
||||
id: "testId",
|
||||
name: "testName",
|
||||
signinUrl: "/",
|
||||
callbackUrl: "/",
|
||||
checks: ["pkce", "state"]
|
||||
}
|
||||
|
||||
jwt = {
|
||||
secret: "secret",
|
||||
maxAge: 0,
|
||||
encode,
|
||||
decode
|
||||
}
|
||||
|
||||
callbacks = {
|
||||
signIn: function (params: { user: User; account: Account; profile: Profile & Record<string, unknown>; email: { verificationRequest?: boolean | undefined }; credentials?: Record<string, CredentialInput> | undefined }): Awaitable<string | boolean> {
|
||||
throw new Error("Function not implemented.")
|
||||
},
|
||||
redirect: function (params: { url: string; baseUrl: string }): Awaitable<string> {
|
||||
throw new Error("Function not implemented.")
|
||||
},
|
||||
session: function (params: { session: Session; user: User; token: JWT }): Awaitable<Session> {
|
||||
throw new Error("Function not implemented.")
|
||||
},
|
||||
jwt: function (params: { token: JWT; user?: User | undefined; account?: Account | undefined; profile?: Profile | undefined; isNewUser?: boolean | undefined }): Awaitable<JWT> {
|
||||
throw new Error("Function not implemented.")
|
||||
}
|
||||
}
|
||||
|
||||
cookies = {
|
||||
sessionToken: { name: "", options: undefined },
|
||||
callbackUrl: { name: "", options: undefined },
|
||||
csrfToken: { name: "", options: undefined },
|
||||
pkceCodeVerifier: { name: "", options: undefined },
|
||||
state: { name: "", options: {} },
|
||||
nonce: { name: "", options: undefined }
|
||||
}
|
||||
|
||||
options = {
|
||||
url,
|
||||
action: "session",
|
||||
provider,
|
||||
secret: "",
|
||||
debug: false,
|
||||
logger,
|
||||
session: { strategy: "jwt", maxAge: 0, updateAge: 0 },
|
||||
pages: {},
|
||||
jwt,
|
||||
events: {},
|
||||
callbacks,
|
||||
cookies,
|
||||
callbackUrl: '',
|
||||
providers: [],
|
||||
theme: {}
|
||||
}
|
||||
})
|
||||
|
||||
describe("createState", () => {
|
||||
it("returns a state, and cookie", async () => {
|
||||
const state = await createState(options)
|
||||
|
||||
expect(state?.value).not.toBeNull()
|
||||
expect(state?.cookie).not.toBeNull()
|
||||
})
|
||||
it("does not return a state when the provider does not support state", async () => {
|
||||
options.provider.checks = ["pkce"]
|
||||
|
||||
const state = await createState(options)
|
||||
|
||||
expect(state).toBeUndefined()
|
||||
})
|
||||
it("sets the cookie expiration to a default of 15 minutes when the max age option is not provided", async () => {
|
||||
const state = await createState(options)
|
||||
|
||||
const defaultMaxAge = 60 * 15 // 15 minutes in seconds
|
||||
const expires = new Date()
|
||||
expires.setTime(expires.getTime() + defaultMaxAge * 1000)
|
||||
|
||||
validateCookieExpiration({state, expires})
|
||||
expect(state?.cookie.options.maxAge).toBeUndefined()
|
||||
})
|
||||
|
||||
it("sets the cookie expiration and max age to the provided max age from the options", async () => {
|
||||
const maxAge = 60 * 20 // 20 minutes
|
||||
cookies.state.options.maxAge = maxAge
|
||||
|
||||
const state = await createState(options)
|
||||
|
||||
const expires = new Date()
|
||||
expires.setTime(expires.getTime() + maxAge * 1000)
|
||||
|
||||
validateCookieExpiration({state, expires})
|
||||
expect(state?.cookie.options.maxAge).toEqual(maxAge)
|
||||
})
|
||||
})
|
||||
|
||||
// comparing the parts instead of getTime() because the milliseconds
|
||||
// will not match since the two Date objects are created milliseconds apart
|
||||
const validateCookieExpiration = ({state, expires}) => {
|
||||
const cookieExpires = state?.cookie.options.expires
|
||||
expect(cookieExpires.getFullYear()).toEqual(expires.getFullYear())
|
||||
expect(cookieExpires.getMonth()).toEqual(expires.getMonth())
|
||||
expect(cookieExpires.getFullYear()).toEqual(expires.getFullYear())
|
||||
expect(cookieExpires.getHours()).toEqual(expires.getHours())
|
||||
expect(cookieExpires.getMinutes()).toEqual(expires.getMinutes())
|
||||
}
|
||||
1383
pnpm-lock.yaml
generated
1383
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user