@@ -6,7 +6,7 @@ import { createError, eventHandler, getQuery, sendRedirect } from 'h3'
66import type { QueryObject } from 'ufo'
77import { withQuery } from 'ufo'
88import 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
1111export 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
0 commit comments