Skip to content

Commit 6f3a1a2

Browse files
sandros94atinux
andauthored
fix(OIDC): typo + make PKCE and nonce mandatory as per specs (#491)
Co-authored-by: Sébastien Chopin <seb@nuxt.com>
1 parent 9890998 commit 6f3a1a2

File tree

2 files changed

+45
-18
lines changed

2 files changed

+45
-18
lines changed

src/runtime/server/lib/oauth/oidc.ts

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { createError, eventHandler, getQuery, sendRedirect } from 'h3'
66
import type { QueryObject } from 'ufo'
77
import { withQuery } from 'ufo'
88
import type { RequestAccessTokenBody } from '../utils'
9-
import { getOAuthRedirectURL, handleAccessTokenErrorResponse, handleInvalidState, handleMissingConfiguration, handlePkceVerifier, handleState, requestAccessToken } from '../utils'
9+
import { getOAuthRedirectURL, handleAccessTokenErrorResponse, handleInvalidState, handleMissingConfiguration, handlePkceVerifier, handleState, requestAccessToken, handleNonce, parseJwt } from '../utils'
1010

1111
export interface OAuthOidcConfig {
1212
/**
@@ -17,7 +17,6 @@ export interface OAuthOidcConfig {
1717
clientId?: string
1818
/**
1919
* OAuth Client secret.
20-
* If unset, PKCE will be used where no client secret is needed.
2120
*
2221
* @default process.env.NUXT_OAUTH_OIDC_CLIENT_SECRET
2322
*/
@@ -269,8 +268,9 @@ export function defineOAuthOidcEventHandler<TUser = OidcUser>({ config, onSucces
269268
const redirectURL = config.redirectURL || getOAuthRedirectURL(event)
270269
const state = await handleState(event)
271270

272-
// if no client secret is provided, we will use PKCE so no client secret is needed
273-
const verifier = !config.clientSecret ? await handlePkceVerifier(event) : undefined
271+
// We always use PKCE to mitigate man-in-the-middle attacks
272+
const verifier = await handlePkceVerifier(event)
273+
const nonce = handleNonce(event)
274274

275275
if (!query.code) {
276276
config.scope = config.scope || []
@@ -280,17 +280,13 @@ export function defineOAuthOidcEventHandler<TUser = OidcUser>({ config, onSucces
280280
redirect_uri: redirectURL,
281281
scope: config.scope.join(' '),
282282
state,
283+
nonce,
283284
response_type: 'code',
284285
...config.params?.authorization_endpoint,
285286
}
286287

287-
// when using PKCE, we need to set the code_challenge in the request
288-
// since some OIDC providers fail with an error if those params are set with "undefined" value
289-
// we make sure to only include them at all if they are set
290-
if (verifier) {
291-
authQuery.code_challenge = verifier.code_challenge
292-
authQuery.code_challenge_method = verifier.code_challenge_method
293-
}
288+
authQuery.code_challenge = verifier.code_challenge
289+
authQuery.code_challenge_method = verifier.code_challenge_method
294290

295291
return sendRedirect(event, withQuery(oidcConfig.authorization_endpoint, authQuery),
296292
)
@@ -306,16 +302,10 @@ export function defineOAuthOidcEventHandler<TUser = OidcUser>({ config, onSucces
306302
client_secret: config.clientSecret,
307303
redirect_uri: redirectURL,
308304
code: query.code,
305+
code_verifier: verifier.code_verifier,
309306
...config.params?.token_endpoint,
310307
}
311308

312-
// when using PKCE, we need to set the code_challenge in the request
313-
// since some OIDC providers fail with an error if those params are set with "undefined" value
314-
// we make sure to only include them at all if they are set
315-
if (verifier) {
316-
tokenQuery.code_verifier = verifier.code_verifier
317-
}
318-
319309
const tokens = await requestAccessToken<OidcTokens & { error?: unknown }>(oidcConfig.token_endpoint, {
320310
body: tokenQuery,
321311
})
@@ -324,6 +314,18 @@ export function defineOAuthOidcEventHandler<TUser = OidcUser>({ config, onSucces
324314
return handleAccessTokenErrorResponse(event, 'oidc', tokens, onError)
325315
}
326316

317+
if (tokens.id_token) {
318+
const claims = parseJwt(tokens.id_token)
319+
if (claims.nonce !== nonce) {
320+
const error = createError({
321+
statusCode: 401,
322+
message: 'OIDC login failed: nonce mismatch',
323+
})
324+
if (!onError) throw error
325+
return onError(event, error)
326+
}
327+
}
328+
327329
let user = {} as TUser
328330

329331
// some OIDC providers do not support a userinfo endpoint so we only call it when its defined inside the OIDC config

src/runtime/server/lib/utils.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,28 @@ export async function handleState(event: H3Event) {
239239
})
240240
return state
241241
}
242+
243+
export function parseJwt(token: string) {
244+
return jose.decodeJwt(token)
245+
}
246+
247+
export function handleNonce(event: H3Event) {
248+
const query = getQuery<{ code?: string }>(event)
249+
// If the code is in the query, get the nonce from the cookie and delete the cookie
250+
if (query.code) {
251+
const nonce = getCookie(event, 'nuxt-auth-nonce')
252+
deleteCookie(event, 'nuxt-auth-nonce')
253+
return nonce
254+
}
255+
256+
// If the code is not in the query, generate a new nonce and set it in the cookie
257+
const nonce = encodeBase64Url(getRandomBytes(8))
258+
setCookie(event, 'nuxt-auth-nonce', nonce, {
259+
httpOnly: true,
260+
secure: !isDevelopment,
261+
sameSite: 'lax',
262+
maxAge: OAUTH_COOKIE_MAX_AGE,
263+
path: '/',
264+
})
265+
return nonce
266+
}

0 commit comments

Comments
 (0)