Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/nonce check type #4100

Merged
merged 12 commits into from
Aug 16, 2022
9 changes: 9 additions & 0 deletions docs/docs/configuration/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,15 @@ cookies: {
secure: useSecureCookies,
},
},
nonce: {
name: `${cookiePrefix}next-auth.nonce`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: useSecureCookies,
},
},
}
```

Expand Down
9 changes: 9 additions & 0 deletions packages/next-auth/src/core/lib/cookie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ export function defaultCookies(useSecureCookies: boolean): CookiesOptions {
secure: useSecureCookies,
},
},
nonce: {
name: `${cookiePrefix}next-auth.nonce`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: useSecureCookies,
},
}
}
}

Expand Down
7 changes: 7 additions & 0 deletions packages/next-auth/src/core/lib/oauth/authorization-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { AuthorizationParameters } from "openid-client"
import type { InternalOptions } from "../../types"
import type { RequestInternal } from "../.."
import type { Cookie } from "../cookie"
import { createNonce } from "./nonce-handler"
james-bjss marked this conversation as resolved.
Show resolved Hide resolved

/**
*
Expand Down Expand Up @@ -62,6 +63,12 @@ export default async function getAuthorizationUrl({
cookies.push(state.cookie)
}

const nonce = await createNonce(options)
if (nonce) {
authorizationParams.nonce = nonce.value
cookies.push(nonce.cookie)
}

const pkce = await createPKCE(options)
if (pkce) {
authorizationParams.code_challenge = pkce.code_challenge
Expand Down
13 changes: 11 additions & 2 deletions packages/next-auth/src/core/lib/oauth/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { OAuthChecks, OAuthConfig } from "../../../providers"
import type { InternalOptions } from "../../types"
import type { RequestInternal, OutgoingResponse } from "../.."
import type { Cookie } from "../cookie"
import { useNonce } from "./nonce-handler"

export default async function oAuthCallback(params: {
options: InternalOptions<"oauth">
Expand All @@ -33,6 +34,7 @@ export default async function oAuthCallback(params: {
logger.debug("OAUTH_CALLBACK_HANDLER_ERROR", { body })
throw error
}


if (provider.version?.startsWith("1.")) {
try {
Expand Down Expand Up @@ -69,16 +71,23 @@ export default async function oAuthCallback(params: {

let tokens: TokenSet

const checks: OAuthChecks = {}
const checks: OAuthChecks = {
nonce: undefined,
ThangHuuVu marked this conversation as resolved.
Show resolved Hide resolved
}
const resCookies: Cookie[] = []

const state = await useState(cookies?.[options.cookies.state.name], options)

if (state) {
checks.state = state.value
resCookies.push(state.cookie)
}

const nonce = await useNonce(cookies?.[options.cookies.nonce.name], options)
if (nonce) {
james-bjss marked this conversation as resolved.
Show resolved Hide resolved
checks.nonce = nonce.value
resCookies.push(nonce.cookie)
}

const codeVerifier = cookies?.[options.cookies.pkceCodeVerifier.name]
const pkce = await usePKCECodeVerifier(codeVerifier, options)
if (pkce) {
Expand Down
74 changes: 74 additions & 0 deletions packages/next-auth/src/core/lib/oauth/nonce-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import * as jwt from "../../../jwt"
import { generators } from "openid-client"
import type { InternalOptions } from "../../types"
import type { Cookie } from "../cookie"

const NONCE_MAX_AGE = 60 * 15 // 15 minutes in seconds

/**
* Returns nonce if the provider supports it
* and saves it in a cookie */
export async function createNonce(options: InternalOptions<"oauth">): Promise<
| undefined
| {
value: string
cookie: Cookie
}
> {
const { cookies, logger, provider } = options
if (!provider.checks?.includes("nonce")) {
// Provider does not support nonce, return nothing.
return
}

const nonce = generators.nonce()

const expires = new Date()
expires.setTime(expires.getTime() + NONCE_MAX_AGE * 1000)

// Encrypt nonce and save it to an encrypted cookie
const encryptedNonce = await jwt.encode({
...options.jwt,
maxAge: NONCE_MAX_AGE,
token: { nonce },
})

logger.debug("CREATE_ENCRYPTED_NONCE", {
NONCE_MAX_AGE: NONCE_MAX_AGE,
james-bjss marked this conversation as resolved.
Show resolved Hide resolved
})

return {
cookie: {
name: cookies.nonce.name,
value: encryptedNonce,
options: { ...cookies.nonce.options, expires },
},
value: nonce,
}
}

/**
* Returns nonce from if the provider supports nonce,
* and clears the container cookie afterwards.
*/
export async function useNonce(
nonce: string | undefined,
options: InternalOptions<"oauth">
): Promise<{ value: string; cookie: Cookie } | undefined> {
const { cookies, provider } = options

if (!provider?.checks?.includes("nonce") || !nonce) {
return
}

const value = (await jwt.decode({...options.jwt, token: nonce })) as any

return {
value: value?.nonce ?? undefined,
cookie: {
name: cookies.nonce.name,
value: "",
options: { ...cookies.nonce.options, maxAge: 0 },
},
}
}
1 change: 1 addition & 0 deletions packages/next-auth/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ export interface CookiesOptions {
csrfToken: CookieOption
pkceCodeVerifier: CookieOption
state: CookieOption
nonce: CookieOption
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/next-auth/src/providers/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type Client = InstanceType<Issuer["Client"]>

export type { OAuthProviderType } from "./oauth-types"

type ChecksType = "pkce" | "state" | "none"
type ChecksType = "pkce" | "state" | "none" | "nonce"

export type OAuthChecks = OpenIDCallbackChecks | OAuthCallbackChecks

Expand Down