From d349ae2b1bf1f2e6ba8e24cd7fab087592743bcf Mon Sep 17 00:00:00 2001 From: James Date: Tue, 16 Aug 2022 11:07:42 +0100 Subject: [PATCH] Feature/nonce check type (#4100) * feat: add nonce check type * Update types import for nonce-handler.ts * Update packages/next-auth/src/core/lib/oauth/callback.ts Co-authored-by: Thang Vu * Add further info to debug msg as per PR suggestion * Cast OauthChecks as OpenIDCallbackChecks * Update order of imports as per PR suggestion Co-authored-by: Hamid Adelyar Co-authored-by: hamidbjss <98807568+hamidbjss@users.noreply.github.com> Co-authored-by: Thang Vu --- docs/docs/configuration/options.md | 9 +++ packages/next-auth/src/core/lib/cookie.ts | 9 +++ .../src/core/lib/oauth/authorization-url.ts | 7 ++ .../next-auth/src/core/lib/oauth/callback.ts | 11 ++- .../src/core/lib/oauth/nonce-handler.ts | 75 +++++++++++++++++++ packages/next-auth/src/core/types.ts | 1 + packages/next-auth/src/providers/oauth.ts | 2 +- 7 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 packages/next-auth/src/core/lib/oauth/nonce-handler.ts diff --git a/docs/docs/configuration/options.md b/docs/docs/configuration/options.md index 4f11080c4e..8644ef6f5b 100644 --- a/docs/docs/configuration/options.md +++ b/docs/docs/configuration/options.md @@ -478,6 +478,15 @@ cookies: { secure: useSecureCookies, }, }, + nonce: { + name: `${cookiePrefix}next-auth.nonce`, + options: { + httpOnly: true, + sameSite: "lax", + path: "/", + secure: useSecureCookies, + }, + }, } ``` diff --git a/packages/next-auth/src/core/lib/cookie.ts b/packages/next-auth/src/core/lib/cookie.ts index 3458aa8509..3c75e001a6 100644 --- a/packages/next-auth/src/core/lib/cookie.ts +++ b/packages/next-auth/src/core/lib/cookie.ts @@ -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, + }, + } } } diff --git a/packages/next-auth/src/core/lib/oauth/authorization-url.ts b/packages/next-auth/src/core/lib/oauth/authorization-url.ts index 1f74acf630..05ad90b4b3 100644 --- a/packages/next-auth/src/core/lib/oauth/authorization-url.ts +++ b/packages/next-auth/src/core/lib/oauth/authorization-url.ts @@ -1,6 +1,7 @@ import { openidClient } from "./client" import { oAuth1Client } from "./client-legacy" import { createState } from "./state-handler" +import { createNonce } from "./nonce-handler" import { createPKCE } from "./pkce-handler" import type { AuthorizationParameters } from "openid-client" @@ -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 diff --git a/packages/next-auth/src/core/lib/oauth/callback.ts b/packages/next-auth/src/core/lib/oauth/callback.ts index 0904216d7e..5fdd7f0ec1 100644 --- a/packages/next-auth/src/core/lib/oauth/callback.ts +++ b/packages/next-auth/src/core/lib/oauth/callback.ts @@ -3,9 +3,10 @@ import { openidClient } from "./client" import { oAuth1Client } from "./client-legacy" import { useState } from "./state-handler" import { usePKCECodeVerifier } from "./pkce-handler" +import { useNonce } from "./nonce-handler" import { OAuthCallbackError } from "../../errors" -import type { CallbackParamsType } from "openid-client" +import type { CallbackParamsType, OpenIDCallbackChecks } from "openid-client" import type { Account, LoggerInstance, Profile } from "../../.." import type { OAuthChecks, OAuthConfig } from "../../../providers" import type { InternalOptions } from "../../types" @@ -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 { @@ -73,12 +75,17 @@ export default async function oAuthCallback(params: { 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 && provider.idToken) { + (checks as OpenIDCallbackChecks).nonce = nonce.value + resCookies.push(nonce.cookie) + } + const codeVerifier = cookies?.[options.cookies.pkceCodeVerifier.name] const pkce = await usePKCECodeVerifier(codeVerifier, options) if (pkce) { diff --git a/packages/next-auth/src/core/lib/oauth/nonce-handler.ts b/packages/next-auth/src/core/lib/oauth/nonce-handler.ts new file mode 100644 index 0000000000..cc7aab362f --- /dev/null +++ b/packages/next-auth/src/core/lib/oauth/nonce-handler.ts @@ -0,0 +1,75 @@ +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, + maxAge: NONCE_MAX_AGE, + }) + + 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 }, + }, + } +} diff --git a/packages/next-auth/src/core/types.ts b/packages/next-auth/src/core/types.ts index f75fcf8b6b..1f3843dc8f 100644 --- a/packages/next-auth/src/core/types.ts +++ b/packages/next-auth/src/core/types.ts @@ -361,6 +361,7 @@ export interface CookiesOptions { csrfToken: CookieOption pkceCodeVerifier: CookieOption state: CookieOption + nonce: CookieOption } /** diff --git a/packages/next-auth/src/providers/oauth.ts b/packages/next-auth/src/providers/oauth.ts index f2bb1d26e5..9bac59c986 100644 --- a/packages/next-auth/src/providers/oauth.ts +++ b/packages/next-auth/src/providers/oauth.ts @@ -17,7 +17,7 @@ type Client = InstanceType export type { OAuthProviderType } from "./oauth-types" -type ChecksType = "pkce" | "state" | "none" +type ChecksType = "pkce" | "state" | "none" | "nonce" export type OAuthChecks = OpenIDCallbackChecks | OAuthCallbackChecks