@@ -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,37 @@ 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 . replace ( / \+ / g, '-' ) . replace ( / \/ / g, '_' ) . replace ( / = / g, '' )
60+ }
61+ function randomBytes ( length : number ) {
62+ return crypto . randomBytes ( length ) . toString ( 'base64' )
63+ }
64+ function sha256 ( buffer : string ) {
65+ return crypto . createHash ( 'sha256' ) . update ( buffer ) . digest ( 'base64' )
66+ }
67+
4968export function auth0EventHandler ( { config, onSuccess, onError } : OAuthConfig ) {
5069 return eventHandler ( async ( event : H3Event ) => {
5170 // @ts -ignore
5271 config = defu ( config , useRuntimeConfig ( event ) . oauth ?. auth0 ) as OAuthAuth0Config
53- const { code } = getQuery ( event )
72+ const { code, state } = getQuery ( event )
5473
5574 if ( ! config . clientId || ! config . clientSecret || ! config . domain ) {
5675 const error = createError ( {
@@ -65,6 +84,19 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) {
6584
6685 const redirectUrl = getRequestURL ( event ) . href
6786 if ( ! code ) {
87+ // Initialize checks
88+ const checks : Record < string , string > = { }
89+ if ( config . checks ?. includes ( 'pkce' ) ) {
90+ const pkceVerifier = base64URLEncode ( randomBytes ( 32 ) )
91+ const pkceChallenge = base64URLEncode ( sha256 ( pkceVerifier ) )
92+ checks [ 'code_challenge' ] = pkceChallenge
93+ checks [ 'code_challenge_method' ] = 'S256'
94+ setCookie ( event , 'nuxt-auth-util-verifier' , pkceVerifier , { maxAge : 60 * 15 , secure : true , httpOnly : true } )
95+ }
96+ if ( config . checks ?. includes ( 'state' ) ) {
97+ checks [ 'state' ] = base64URLEncode ( randomBytes ( 32 ) )
98+ setCookie ( event , 'nuxt-auth-util-state' , checks [ 'state' ] , { maxAge : 60 * 15 , secure : true , httpOnly : true } )
99+ }
68100 config . scope = config . scope || [ 'openid' , 'offline_access' ]
69101 if ( config . emailRequired && ! config . scope . includes ( 'email' ) ) {
70102 config . scope . push ( 'email' )
@@ -78,10 +110,35 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) {
78110 redirect_uri : redirectUrl ,
79111 scope : config . scope . join ( ' ' ) ,
80112 audience : config . audience || '' ,
113+ ...checks
81114 } )
82115 )
83116 }
84117
118+ // Verify checks
119+ const pkceVerifier = getCookie ( event , 'nuxt-auth-util-verifier' )
120+ setCookie ( event , 'nuxt-auth-util-verifier' , '' , { maxAge : - 1 } )
121+ const stateInCookie = getCookie ( event , 'nuxt-auth-util-state' )
122+ setCookie ( event , 'nuxt-auth-util-state' , '' , { maxAge : - 1 } )
123+ if ( config . checks ?. includes ( 'state' ) ) {
124+ if ( ! state || ! stateInCookie ) {
125+ const error = createError ( {
126+ statusCode : 401 ,
127+ message : 'Auth0 login failed: state is missing'
128+ } )
129+ if ( ! onError ) throw error
130+ return onError ( event , error )
131+ }
132+ if ( state !== stateInCookie ) {
133+ const error = createError ( {
134+ statusCode : 401 ,
135+ message : 'Auth0 login failed: state does not match'
136+ } )
137+ if ( ! onError ) throw error
138+ return onError ( event , error )
139+ }
140+ }
141+
85142 const tokens : any = await ofetch (
86143 tokenURL as string ,
87144 {
@@ -95,6 +152,7 @@ export function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig) {
95152 client_secret : config . clientSecret ,
96153 redirect_uri : parsePath ( redirectUrl ) . pathname ,
97154 code,
155+ code_verifier : pkceVerifier
98156 }
99157 }
100158 ) . catch ( error => {
0 commit comments