Skip to content

Commit

Permalink
fix: unify checks
Browse files Browse the repository at this point in the history
  • Loading branch information
balazsorban44 committed Mar 2, 2023
1 parent 26c8465 commit e6590ff
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 270 deletions.
25 changes: 4 additions & 21 deletions packages/next-auth/src/core/lib/oauth/authorization-url.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { openidClient } from "./client"
import { oAuth1Client, oAuth1TokenStore } from "./client-legacy"
import { createState } from "./state-handler"
import { createNonce } from "./nonce-handler"
import { createPKCE } from "./pkce-handler"
import * as checks from "./checks"

import type { AuthorizationParameters } from "openid-client"
import type { InternalOptions } from "../../types"
Expand Down Expand Up @@ -54,24 +52,9 @@ export default async function getAuthorizationUrl({
const authorizationParams: AuthorizationParameters = params
const cookies: Cookie[] = []

const state = await createState(options)
if (state) {
authorizationParams.state = state.value
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
authorizationParams.code_challenge_method = pkce.code_challenge_method
cookies.push(pkce.cookie)
}
await checks.state.create(options, cookies, authorizationParams)
await checks.pkce.create(options, cookies, authorizationParams)
await checks.nonce.create(options, cookies, authorizationParams)

const url = client.authorizationUrl(authorizationParams)

Expand Down
27 changes: 5 additions & 22 deletions packages/next-auth/src/core/lib/oauth/callback.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { TokenSet } from "openid-client"
import { openidClient } from "./client"
import { oAuth1Client, oAuth1TokenStore } from "./client-legacy"
import { useState } from "./state-handler"
import { usePKCECodeVerifier } from "./pkce-handler"
import { useNonce } from "./nonce-handler"
import * as _checks from "./checks"
import { OAuthCallbackError } from "../../errors"

import type { CallbackParamsType, OpenIDCallbackChecks } from "openid-client"
import type { CallbackParamsType } from "openid-client"
import type { LoggerInstance, Profile } from "../../.."
import type { OAuthChecks, OAuthConfig } from "../../../providers"
import type { InternalOptions } from "../../types"
Expand Down Expand Up @@ -73,24 +71,9 @@ export default async function oAuthCallback(params: {
const checks: OAuthChecks = {}
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) {
checks.code_verifier = pkce.codeVerifier
resCookies.push(pkce.cookie)
}
await _checks.state.use(cookies, resCookies, options, checks)
await _checks.pkce.use(cookies, resCookies, options, checks)
await _checks.nonce.use(cookies, resCookies, options, checks)

const params: CallbackParamsType = {
...client.callbackParams({
Expand Down
181 changes: 181 additions & 0 deletions packages/next-auth/src/core/lib/oauth/checks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import {
AuthorizationParameters,
generators,
OpenIDCallbackChecks,
} from "openid-client"
import * as jwt from "../../../jwt"

import type { RequestInternal } from "../.."
import type { CookiesOptions, InternalOptions } from "../../types"
import type { Cookie } from "../cookie"
import { OAuthChecks } from "src/providers"

/** Returns a signed cookie. */
export async function signCookie(
type: keyof CookiesOptions,
value: string,
maxAge: number,
options: InternalOptions<"oauth">
): Promise<Cookie> {
const { cookies, logger } = options

logger.debug(`CREATE_${type.toUpperCase()}`, { value, maxAge })

const expires = new Date()
expires.setTime(expires.getTime() + maxAge * 1000)
return {
name: cookies[type].name,
value: await jwt.encode({ ...options.jwt, maxAge, token: { value } }),
options: { ...cookies[type].options, expires },
}
}

const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
export const PKCE_CODE_CHALLENGE_METHOD = "S256"
export const pkce = {
async create(
options: InternalOptions<"oauth">,
cookies: Cookie[],
resParams: AuthorizationParameters
) {
if (!options.provider?.checks?.includes("pkce")) return
const code_verifier = generators.codeVerifier()
const value = generators.codeChallenge(code_verifier)
resParams.code_challenge = value
resParams.code_challenge_method = PKCE_CODE_CHALLENGE_METHOD

const maxAge =
options.cookies.pkceCodeVerifier.options.maxAge ?? PKCE_MAX_AGE

cookies.push(
await signCookie("pkceCodeVerifier", code_verifier, maxAge, options)
)
},
/**
* Returns code_verifier if the provider is configured to use PKCE,
* and clears the container cookie afterwards.
* An error is thrown if the code_verifier is missing or invalid.
* @see https://www.rfc-editor.org/rfc/rfc7636
* @see https://danielfett.de/2020/05/16/pkce-vs-nonce-equivalent-or-not/#pkce
*/
async use(
cookies: RequestInternal["cookies"],
resCookies: Cookie[],
options: InternalOptions<"oauth">,
checks: OAuthChecks
): Promise<string | undefined> {
if (!options.provider?.checks?.includes("pkce")) return

const codeVerifier = cookies?.[options.cookies.pkceCodeVerifier.name]

if (!codeVerifier)
throw new TypeError("PKCE code_verifier cookie was missing.")

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

if (!value?.value)
throw new TypeError("PKCE code_verifier value could not be parsed.")

resCookies.push({
name: options.cookies.pkceCodeVerifier.name,
value: "",
options: { ...options.cookies.pkceCodeVerifier.options, maxAge: 0 },
})

checks.code_verifier = value.value
},
}

const STATE_MAX_AGE = 60 * 15 // 15 minutes in seconds
export const state = {
async create(
options: InternalOptions<"oauth">,
cookies: Cookie[],
resParams: AuthorizationParameters
) {
if (!options.provider.checks?.includes("state")) return
const value = generators.state()
resParams.state = value
const maxAge = options.cookies.state.options.maxAge ?? STATE_MAX_AGE
cookies.push(await signCookie("state", value, maxAge, options))
},
/**
* Returns state if the provider is configured to use state,
* and clears the container cookie afterwards.
* An error is thrown if the state is missing or invalid.
* @see https://www.rfc-editor.org/rfc/rfc6749#section-10.12
* @see https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1
*/
async use(
cookies: RequestInternal["cookies"],
resCookies: Cookie[],
options: InternalOptions<"oauth">,
checks: OAuthChecks
) {
if (!options.provider.checks?.includes("state")) return

const state = cookies?.[options.cookies.state.name]

if (!state) throw new TypeError("State cookie was missing.")

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

if (!value?.value) throw new TypeError("State value could not be parsed.")

resCookies.push({
name: options.cookies.state.name,
value: "",
options: { ...options.cookies.state.options, maxAge: 0 },
})

checks.state = value.value
},
}

const NONCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
export const nonce = {
async create(
options: InternalOptions<"oauth">,
cookies: Cookie[],
resParams: AuthorizationParameters
) {
if (!options.provider.checks?.includes("nonce")) return
const value = generators.nonce()
resParams.nonce = value
const maxAge = options.cookies.nonce.options.maxAge ?? NONCE_MAX_AGE
cookies.push(await signCookie("nonce", value, maxAge, options))
},
/**
* Returns nonce if the provider is configured to use nonce,
* and clears the container cookie afterwards.
* An error is thrown if the nonce is missing or invalid.
* @see https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes
* @see https://danielfett.de/2020/05/16/pkce-vs-nonce-equivalent-or-not/#nonce
*/
async use(
cookies: RequestInternal["cookies"],
resCookies: Cookie[],
options: InternalOptions<"oauth">,
checks: OpenIDCallbackChecks
): Promise<string | undefined> {
if (!options.provider?.checks?.includes("nonce")) return

const nonce = cookies?.[options.cookies.nonce.name]
if (!nonce) throw new TypeError("Nonce cookie was missing.")

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

if (!value?.value) throw new TypeError("Nonce value could not be parsed.")

resCookies.push({
name: options.cookies.nonce.name,
value: "",
options: { ...options.cookies.nonce.options, maxAge: 0 },
})

checks.nonce = value.value
},
}
75 changes: 0 additions & 75 deletions packages/next-auth/src/core/lib/oauth/nonce-handler.ts

This file was deleted.

Loading

0 comments on commit e6590ff

Please sign in to comment.