Compare commits

...

17 Commits

Author SHA1 Message Date
Balázs Orbán
f2a07932b9 chore(release): bump package version(s) [skip ci] 2022-12-03 12:38:32 +01:00
Balázs Orbán
25c7ce1d2b chore: revert sync-example action 2022-12-03 12:31:22 +01:00
Vedant
227a233bd8 chore(adapters): update firebase sdk version in firebase adapter (#5902)
* Update firebase SDK version

* Update pnpm-lock.yaml

* add `--debug`

* move flag

* add `FIREBASE_TOKEN`

* chore: upgrade `firebase-tools`

* revert peer dep change

* remove --debug flag

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-12-03 11:29:09 +00:00
Vedant
cf9f133aa3 fix(adapters): tsconfig for firebase and pouchdb (#5945)
* Update tsconfig.json

* Update tsconfig.json
2022-12-03 09:48:55 +01:00
Arnaud Zheng
2301c1be44 chore(types): fix typo in comment (#5815)
Co-authored-by: Nico Domino <yo@ndo.dev>
2022-12-02 16:39:28 +01:00
jintak0401
6e408e24bf fix(provider): modify response.name to response.nickname (Naver) (#5915)
fix(provider): modify response.name to response.nickname (Naver Provider)

Co-authored-by: Thang Vu <hi@thvu.dev>
2022-12-01 08:12:16 +07:00
Jason Brady
f277989c69 feat(core): make pkce and state maxAge configurable on the cookies (#4719)
* feat(cookies): make pkce and state maxAge configurable on the cookies (#4660)

* added tests for pkce and state handlers
2022-12-01 08:02:42 +07:00
Nico Domino
6146e93288 chore(docs): add new provider styling notes to CONTRIBUTING.md (#5900) 2022-11-30 15:44:50 +01:00
Nico Domino
1ff565da6c chore(docs): update oauth.md styling docs (#5899)
* chore(docs): update oauth.md styling docs

* chore: add note about styling object

* chore: typo

* chore: filename typos
2022-11-30 15:43:55 +01:00
Nico Domino
41f75cf870 chore(actions): version bump all deps (#5903) 2022-11-28 11:10:15 +00:00
Justin W Hall
dd591ed8d0 chore(docs): fix path Strava provider file (#5853)
Co-authored-by: Nico Domino <yo@ndo.dev>
2022-11-27 14:04:17 +01:00
koolskateguy89
297bc2317f chore(docs): fix spelling in docs (#5867)
Co-authored-by: Nico Domino <yo@ndo.dev>
2022-11-27 13:44:35 +01:00
Tormod Flesjø
b170138e70 chore: update mongodb.md with typescript filetypes (#5889) 2022-11-27 13:11:54 +01:00
Balázs Orbán
a307079e0f fix(ts): improve unstable_getServerSession return type (#5792)
Co-authored-by: Thang Vu <hi@thvu.dev>
2022-11-24 15:13:05 +01:00
Jacob Penny
d52b7a6b7d chore(adapters): fix typo on Firebase Adapter (#5813) 2022-11-24 14:31:43 +01:00
Jake Mulley
30b69a07eb chore: fix import statement for next-auth/providers/email (#5860) 2022-11-21 12:07:09 +01:00
Balázs Orbán
0d1757814f fix(next): improve dev environment variable handling (#5763)
* fix(next): HIDE `NEXTAUTH_URL` warning locally

* refactor: move out `process.env` from core

* fix tests

* simplify

* swap
2022-11-20 09:08:10 +00:00
31 changed files with 1371 additions and 620 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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 \

View File

@@ -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

View File

@@ -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!

View File

@@ -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'

View File

@@ -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: {

View File

@@ -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({

View File

@@ -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)>)

View File

@@ -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.

View File

@@ -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).
:::

View File

@@ -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:*"
}
}
}

View File

@@ -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

View File

@@ -1,5 +1,5 @@
{
"extends": "@next-auth/tsconfig/tsconfig.base.json",
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",

View File

@@ -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",

View File

@@ -1,5 +1,5 @@
{
"extends": "@next-auth/tsconfig/tsconfig.base.json",
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",

View File

@@ -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",

View File

@@ -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)

View File

@@ -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: {

View File

@@ -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 {

View File

@@ -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: {

View File

@@ -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
}
/**

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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,
}

View File

@@ -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
}

View File

@@ -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()

View 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())
}

View 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

File diff suppressed because it is too large Load Diff