Skip to content

Commit e6590ff

Browse files
committed
fix: unify checks
1 parent 26c8465 commit e6590ff

File tree

6 files changed

+190
-270
lines changed

6 files changed

+190
-270
lines changed

packages/next-auth/src/core/lib/oauth/authorization-url.ts

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { openidClient } from "./client"
22
import { oAuth1Client, oAuth1TokenStore } from "./client-legacy"
3-
import { createState } from "./state-handler"
4-
import { createNonce } from "./nonce-handler"
5-
import { createPKCE } from "./pkce-handler"
3+
import * as checks from "./checks"
64

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

57-
const state = await createState(options)
58-
if (state) {
59-
authorizationParams.state = state.value
60-
cookies.push(state.cookie)
61-
}
62-
63-
const nonce = await createNonce(options)
64-
if (nonce) {
65-
authorizationParams.nonce = nonce.value
66-
cookies.push(nonce.cookie)
67-
}
68-
69-
const pkce = await createPKCE(options)
70-
if (pkce) {
71-
authorizationParams.code_challenge = pkce.code_challenge
72-
authorizationParams.code_challenge_method = pkce.code_challenge_method
73-
cookies.push(pkce.cookie)
74-
}
55+
await checks.state.create(options, cookies, authorizationParams)
56+
await checks.pkce.create(options, cookies, authorizationParams)
57+
await checks.nonce.create(options, cookies, authorizationParams)
7558

7659
const url = client.authorizationUrl(authorizationParams)
7760

packages/next-auth/src/core/lib/oauth/callback.ts

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import { TokenSet } from "openid-client"
22
import { openidClient } from "./client"
33
import { oAuth1Client, oAuth1TokenStore } from "./client-legacy"
4-
import { useState } from "./state-handler"
5-
import { usePKCECodeVerifier } from "./pkce-handler"
6-
import { useNonce } from "./nonce-handler"
4+
import * as _checks from "./checks"
75
import { OAuthCallbackError } from "../../errors"
86

9-
import type { CallbackParamsType, OpenIDCallbackChecks } from "openid-client"
7+
import type { CallbackParamsType } from "openid-client"
108
import type { LoggerInstance, Profile } from "../../.."
119
import type { OAuthChecks, OAuthConfig } from "../../../providers"
1210
import type { InternalOptions } from "../../types"
@@ -73,24 +71,9 @@ export default async function oAuthCallback(params: {
7371
const checks: OAuthChecks = {}
7472
const resCookies: Cookie[] = []
7573

76-
const state = await useState(cookies?.[options.cookies.state.name], options)
77-
if (state) {
78-
checks.state = state.value
79-
resCookies.push(state.cookie)
80-
}
81-
82-
const nonce = await useNonce(cookies?.[options.cookies.nonce.name], options)
83-
if (nonce && provider.idToken) {
84-
;(checks as OpenIDCallbackChecks).nonce = nonce.value
85-
resCookies.push(nonce.cookie)
86-
}
87-
88-
const codeVerifier = cookies?.[options.cookies.pkceCodeVerifier.name]
89-
const pkce = await usePKCECodeVerifier(codeVerifier, options)
90-
if (pkce) {
91-
checks.code_verifier = pkce.codeVerifier
92-
resCookies.push(pkce.cookie)
93-
}
74+
await _checks.state.use(cookies, resCookies, options, checks)
75+
await _checks.pkce.use(cookies, resCookies, options, checks)
76+
await _checks.nonce.use(cookies, resCookies, options, checks)
9477

9578
const params: CallbackParamsType = {
9679
...client.callbackParams({
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import {
2+
AuthorizationParameters,
3+
generators,
4+
OpenIDCallbackChecks,
5+
} from "openid-client"
6+
import * as jwt from "../../../jwt"
7+
8+
import type { RequestInternal } from "../.."
9+
import type { CookiesOptions, InternalOptions } from "../../types"
10+
import type { Cookie } from "../cookie"
11+
import { OAuthChecks } from "src/providers"
12+
13+
/** Returns a signed cookie. */
14+
export async function signCookie(
15+
type: keyof CookiesOptions,
16+
value: string,
17+
maxAge: number,
18+
options: InternalOptions<"oauth">
19+
): Promise<Cookie> {
20+
const { cookies, logger } = options
21+
22+
logger.debug(`CREATE_${type.toUpperCase()}`, { value, maxAge })
23+
24+
const expires = new Date()
25+
expires.setTime(expires.getTime() + maxAge * 1000)
26+
return {
27+
name: cookies[type].name,
28+
value: await jwt.encode({ ...options.jwt, maxAge, token: { value } }),
29+
options: { ...cookies[type].options, expires },
30+
}
31+
}
32+
33+
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
34+
export const PKCE_CODE_CHALLENGE_METHOD = "S256"
35+
export const pkce = {
36+
async create(
37+
options: InternalOptions<"oauth">,
38+
cookies: Cookie[],
39+
resParams: AuthorizationParameters
40+
) {
41+
if (!options.provider?.checks?.includes("pkce")) return
42+
const code_verifier = generators.codeVerifier()
43+
const value = generators.codeChallenge(code_verifier)
44+
resParams.code_challenge = value
45+
resParams.code_challenge_method = PKCE_CODE_CHALLENGE_METHOD
46+
47+
const maxAge =
48+
options.cookies.pkceCodeVerifier.options.maxAge ?? PKCE_MAX_AGE
49+
50+
cookies.push(
51+
await signCookie("pkceCodeVerifier", code_verifier, maxAge, options)
52+
)
53+
},
54+
/**
55+
* Returns code_verifier if the provider is configured to use PKCE,
56+
* and clears the container cookie afterwards.
57+
* An error is thrown if the code_verifier is missing or invalid.
58+
* @see https://www.rfc-editor.org/rfc/rfc7636
59+
* @see https://danielfett.de/2020/05/16/pkce-vs-nonce-equivalent-or-not/#pkce
60+
*/
61+
async use(
62+
cookies: RequestInternal["cookies"],
63+
resCookies: Cookie[],
64+
options: InternalOptions<"oauth">,
65+
checks: OAuthChecks
66+
): Promise<string | undefined> {
67+
if (!options.provider?.checks?.includes("pkce")) return
68+
69+
const codeVerifier = cookies?.[options.cookies.pkceCodeVerifier.name]
70+
71+
if (!codeVerifier)
72+
throw new TypeError("PKCE code_verifier cookie was missing.")
73+
74+
const value = (await jwt.decode({
75+
...options.jwt,
76+
token: codeVerifier,
77+
})) as any
78+
79+
if (!value?.value)
80+
throw new TypeError("PKCE code_verifier value could not be parsed.")
81+
82+
resCookies.push({
83+
name: options.cookies.pkceCodeVerifier.name,
84+
value: "",
85+
options: { ...options.cookies.pkceCodeVerifier.options, maxAge: 0 },
86+
})
87+
88+
checks.code_verifier = value.value
89+
},
90+
}
91+
92+
const STATE_MAX_AGE = 60 * 15 // 15 minutes in seconds
93+
export const state = {
94+
async create(
95+
options: InternalOptions<"oauth">,
96+
cookies: Cookie[],
97+
resParams: AuthorizationParameters
98+
) {
99+
if (!options.provider.checks?.includes("state")) return
100+
const value = generators.state()
101+
resParams.state = value
102+
const maxAge = options.cookies.state.options.maxAge ?? STATE_MAX_AGE
103+
cookies.push(await signCookie("state", value, maxAge, options))
104+
},
105+
/**
106+
* Returns state if the provider is configured to use state,
107+
* and clears the container cookie afterwards.
108+
* An error is thrown if the state is missing or invalid.
109+
* @see https://www.rfc-editor.org/rfc/rfc6749#section-10.12
110+
* @see https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1
111+
*/
112+
async use(
113+
cookies: RequestInternal["cookies"],
114+
resCookies: Cookie[],
115+
options: InternalOptions<"oauth">,
116+
checks: OAuthChecks
117+
) {
118+
if (!options.provider.checks?.includes("state")) return
119+
120+
const state = cookies?.[options.cookies.state.name]
121+
122+
if (!state) throw new TypeError("State cookie was missing.")
123+
124+
const value = (await jwt.decode({ ...options.jwt, token: state })) as any
125+
126+
if (!value?.value) throw new TypeError("State value could not be parsed.")
127+
128+
resCookies.push({
129+
name: options.cookies.state.name,
130+
value: "",
131+
options: { ...options.cookies.state.options, maxAge: 0 },
132+
})
133+
134+
checks.state = value.value
135+
},
136+
}
137+
138+
const NONCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
139+
export const nonce = {
140+
async create(
141+
options: InternalOptions<"oauth">,
142+
cookies: Cookie[],
143+
resParams: AuthorizationParameters
144+
) {
145+
if (!options.provider.checks?.includes("nonce")) return
146+
const value = generators.nonce()
147+
resParams.nonce = value
148+
const maxAge = options.cookies.nonce.options.maxAge ?? NONCE_MAX_AGE
149+
cookies.push(await signCookie("nonce", value, maxAge, options))
150+
},
151+
/**
152+
* Returns nonce if the provider is configured to use nonce,
153+
* and clears the container cookie afterwards.
154+
* An error is thrown if the nonce is missing or invalid.
155+
* @see https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes
156+
* @see https://danielfett.de/2020/05/16/pkce-vs-nonce-equivalent-or-not/#nonce
157+
*/
158+
async use(
159+
cookies: RequestInternal["cookies"],
160+
resCookies: Cookie[],
161+
options: InternalOptions<"oauth">,
162+
checks: OpenIDCallbackChecks
163+
): Promise<string | undefined> {
164+
if (!options.provider?.checks?.includes("nonce")) return
165+
166+
const nonce = cookies?.[options.cookies.nonce.name]
167+
if (!nonce) throw new TypeError("Nonce cookie was missing.")
168+
169+
const value = (await jwt.decode({ ...options.jwt, token: nonce })) as any
170+
171+
if (!value?.value) throw new TypeError("Nonce value could not be parsed.")
172+
173+
resCookies.push({
174+
name: options.cookies.nonce.name,
175+
value: "",
176+
options: { ...options.cookies.nonce.options, maxAge: 0 },
177+
})
178+
179+
checks.nonce = value.value
180+
},
181+
}

packages/next-auth/src/core/lib/oauth/nonce-handler.ts

Lines changed: 0 additions & 75 deletions
This file was deleted.

0 commit comments

Comments
 (0)