@@ -4,6 +4,7 @@ import { withQuery, parsePath } from 'ufo'
44import { ofetch } from 'ofetch'
55import { defu } from 'defu'
66import { useRuntimeConfig } from '#imports'
7+ import crypto from 'crypto'
78
89export interface OAuthAuth0Config {
910 /**
@@ -23,7 +24,7 @@ export interface OAuthAuth0Config {
2324 domain ?: string
2425 /**
2526 * Auth0 OAuth Audience
26- * @default process.env.NUXT_OAUTH_AUTH0_AUDIENCE
27+ * @default ''
2728 */
2829 audience ?: string
2930 /**
@@ -38,19 +39,40 @@ export interface OAuthAuth0Config {
3839 * @default false
3940 */
4041 emailRequired ?: boolean
42+ /**
43+ * checks
44+ * @default []
45+ * @see https://auth0.com/docs/flows/authorization-code-flow-with-proof-key-for-code-exchange-pkce
46+ * @see https://auth0.com/docs/protocols/oauth2/oauth-state
47+ */
48+ checks ?: OAuthChecks [ ]
4149}
4250
51+ type OAuthChecks = 'pkce' | 'state'
4352interface OAuthConfig {
4453 config ?: OAuthAuth0Config
4554 onSuccess : ( event : H3Event , result : { user : any , tokens : any } ) => Promise < void > | void
4655 onError ?: ( event : H3Event , error : H3Error ) => Promise < void > | void
4756}
4857
58+ function base64URLEncode ( str : string ) {
59+ return str . toString ( 'base64' )
60+ . replace ( / \+ / g, '-' )
61+ . replace ( / \/ / g, '_' )
62+ . replace ( / = / g, '' )
63+ }
64+ function randomBytes ( length : number ) {
65+ return crypto . randomBytes ( length ) . toString ( 'base64' )
66+ }
67+ function sha256 ( buffer : string ) {
68+ return crypto . createHash ( 'sha256' ) . update ( buffer ) . digest ( 'base64' )
69+ }
70+
4971export function auth0EventHandler ( { config, onSuccess, onError } : OAuthConfig ) {
5072 return eventHandler ( async ( event : H3Event ) => {
5173 // @ts -ignore
5274 config = defu ( config , useRuntimeConfig ( event ) . oauth ?. auth0 ) as OAuthAuth0Config
53- const { code } = getQuery ( event )
75+ const { code, state } = getQuery ( event )
5476
5577 if ( ! config . clientId || ! config . clientSecret || ! config . domain ) {
5678 const error = createError ( {
@@ -65,6 +87,19 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) {
6587
6688 const redirectUrl = getRequestURL ( event ) . href
6789 if ( ! code ) {
90+ // Initialize checks
91+ const checks : Record < string , string > = { }
92+ if ( config . checks ?. includes ( 'pkce' ) ) {
93+ const pkceVerifier = base64URLEncode ( randomBytes ( 32 ) )
94+ const pkceChallenge = base64URLEncode ( sha256 ( pkceVerifier ) )
95+ checks [ 'code_challenge' ] = pkceChallenge
96+ checks [ 'code_challenge_method' ] = 'S256'
97+ setCookie ( event , 'nuxt-auth-util-verifier' , pkceVerifier , { maxAge : 60 * 15 , secure : true , httpOnly : true } )
98+ }
99+ if ( config . checks ?. includes ( 'state' ) ) {
100+ checks [ 'state' ] = base64URLEncode ( randomBytes ( 32 ) )
101+ setCookie ( event , 'nuxt-auth-util-state' , checks [ 'state' ] , { maxAge : 60 * 15 , secure : true , httpOnly : true } )
102+ }
68103 config . scope = config . scope || [ 'openid' , 'offline_access' ]
69104 if ( config . emailRequired && ! config . scope . includes ( 'email' ) ) {
70105 config . scope . push ( 'email' )
@@ -78,10 +113,35 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) {
78113 redirect_uri : redirectUrl ,
79114 scope : config . scope . join ( ' ' ) ,
80115 audience : config . audience || '' ,
116+ ...checks
81117 } )
82118 )
83119 }
84120
121+ // Verify checks
122+ const pkceVerifier = getCookie ( event , 'nuxt-auth-util-verifier' )
123+ setCookie ( event , 'nuxt-auth-util-verifier' , '' , { maxAge : - 1 } )
124+ const stateInCookie = getCookie ( event , 'nuxt-auth-util-state' )
125+ setCookie ( event , 'nuxt-auth-util-state' , '' , { maxAge : - 1 } )
126+ if ( config . checks ?. includes ( 'state' ) ) {
127+ if ( ! state || ! stateInCookie ) {
128+ const error = createError ( {
129+ statusCode : 401 ,
130+ message : 'Auth0 login failed: state is missing'
131+ } )
132+ if ( ! onError ) throw error
133+ return onError ( event , error )
134+ }
135+ if ( state !== stateInCookie ) {
136+ const error = createError ( {
137+ statusCode : 401 ,
138+ message : 'Auth0 login failed: state does not match'
139+ } )
140+ if ( ! onError ) throw error
141+ return onError ( event , error )
142+ }
143+ }
144+
85145 const tokens : any = await ofetch (
86146 tokenURL as string ,
87147 {
@@ -95,6 +155,7 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) {
95155 client_secret : config . clientSecret ,
96156 redirect_uri : parsePath ( redirectUrl ) . pathname ,
97157 code,
158+ code_verifier : pkceVerifier
98159 }
99160 }
100161 ) . catch ( error => {
0 commit comments