@@ -4,6 +4,14 @@ import express from "express";
4
4
import { OAuthServerProvider } from "../provider.js" ;
5
5
import { rateLimit , Options as RateLimitOptions } from "express-rate-limit" ;
6
6
import { allowedMethods } from "../middleware/allowedMethods.js" ;
7
+ import {
8
+ InvalidRequestError ,
9
+ InvalidClientError ,
10
+ InvalidScopeError ,
11
+ ServerError ,
12
+ TooManyRequestsError ,
13
+ OAuthError
14
+ } from "../errors.js" ;
7
15
8
16
export type AuthorizationHandlerOptions = {
9
17
provider : OAuthServerProvider ;
@@ -42,81 +50,116 @@ export function authorizationHandler({ provider, rateLimit: rateLimitConfig }: A
42
50
max : 100 , // 100 requests per windowMs
43
51
standardHeaders : true ,
44
52
legacyHeaders : false ,
45
- message : {
46
- error : 'too_many_requests' ,
47
- error_description : 'You have exceeded the rate limit for authorization requests'
48
- } ,
53
+ message : new TooManyRequestsError ( 'You have exceeded the rate limit for authorization requests' ) . toResponseObject ( ) ,
49
54
...rateLimitConfig
50
55
} ) ) ;
51
56
}
52
57
53
- // Define the handler
54
58
router . all ( "/" , async ( req , res ) => {
55
59
res . setHeader ( 'Cache-Control' , 'no-store' ) ;
56
60
57
- let client_id , redirect_uri ;
61
+ // In the authorization flow, errors are split into two categories:
62
+ // 1. Pre-redirect errors (direct response with 400)
63
+ // 2. Post-redirect errors (redirect with error parameters)
64
+
65
+ // Phase 1: Validate client_id and redirect_uri. Any errors here must be direct responses.
66
+ let client_id , redirect_uri , client ;
58
67
try {
59
- const data = req . method === 'POST' ? req . body : req . query ;
60
- ( { client_id, redirect_uri } = ClientAuthorizationParamsSchema . parse ( data ) ) ;
61
- } catch ( error ) {
62
- res . status ( 400 ) . end ( `Bad Request: ${ error } ` ) ;
63
- return ;
64
- }
68
+ const result = ClientAuthorizationParamsSchema . safeParse ( req . method === 'POST' ? req . body : req . query ) ;
69
+ if ( ! result . success ) {
70
+ throw new InvalidRequestError ( result . error . message ) ;
71
+ }
65
72
66
- const client = await provider . clientsStore . getClient ( client_id ) ;
67
- if ( ! client ) {
68
- res . status ( 400 ) . end ( "Bad Request: invalid client_id" ) ;
69
- return ;
70
- }
73
+ client_id = result . data . client_id ;
74
+ redirect_uri = result . data . redirect_uri ;
71
75
72
- if ( redirect_uri !== undefined ) {
73
- if ( ! client . redirect_uris . includes ( redirect_uri ) ) {
74
- res . status ( 400 ) . end ( "Bad Request: unregistered redirect_uri" ) ;
75
- return ;
76
+ client = await provider . clientsStore . getClient ( client_id ) ;
77
+ if ( ! client ) {
78
+ throw new InvalidClientError ( "Invalid client_id" ) ;
76
79
}
77
- } else if ( client . redirect_uris . length === 1 ) {
78
- redirect_uri = client . redirect_uris [ 0 ] ;
79
- } else {
80
- res . status ( 400 ) . end ( "Bad Request: missing redirect_uri" ) ;
81
- return ;
82
- }
83
80
84
- let params ;
85
- try {
86
- const authData = req . method === 'POST' ? req . body : req . query ;
87
- params = RequestAuthorizationParamsSchema . parse ( authData ) ;
81
+ if ( redirect_uri !== undefined ) {
82
+ if ( ! client . redirect_uris . includes ( redirect_uri ) ) {
83
+ throw new InvalidRequestError ( "Unregistered redirect_uri" ) ;
84
+ }
85
+ } else if ( client . redirect_uris . length === 1 ) {
86
+ redirect_uri = client . redirect_uris [ 0 ] ;
87
+ } else {
88
+ throw new InvalidRequestError ( "redirect_uri must be specified when client has multiple registered URIs" ) ;
89
+ }
88
90
} catch ( error ) {
89
- const errorUrl = new URL ( redirect_uri ) ;
90
- errorUrl . searchParams . set ( "error" , "invalid_request" ) ;
91
- errorUrl . searchParams . set ( "error_description" , String ( error ) ) ;
92
- res . redirect ( 302 , errorUrl . href ) ;
91
+ // Pre-redirect errors - return direct response
92
+ if ( error instanceof OAuthError ) {
93
+ const status = error instanceof ServerError ? 500 : 400 ;
94
+ res . status ( status ) . end ( error . message ) ;
95
+ } else {
96
+ console . error ( "Unexpected error looking up client:" , error ) ;
97
+ res . status ( 500 ) . end ( "Internal Server Error" ) ;
98
+ }
99
+
93
100
return ;
94
101
}
95
102
96
- let requestedScopes : string [ ] = [ ] ;
97
- if ( params . scope !== undefined && client . scope !== undefined ) {
98
- requestedScopes = params . scope . split ( " " ) ;
99
- const allowedScopes = new Set ( client . scope . split ( " " ) ) ;
100
-
101
- // If any requested scope is not in the client's registered scopes, error out
102
- for ( const scope of requestedScopes ) {
103
- if ( ! allowedScopes . has ( scope ) ) {
104
- const errorUrl = new URL ( redirect_uri ) ;
105
- errorUrl . searchParams . set ( "error" , "invalid_scope" ) ;
106
- errorUrl . searchParams . set ( "error_description" , `Client was not registered with scope ${ scope } ` ) ;
107
- res . redirect ( 302 , errorUrl . href ) ;
108
- return ;
103
+ // Phase 2: Validate other parameters. Any errors here should go into redirect responses.
104
+ let state ;
105
+ try {
106
+ // Parse and validate authorization parameters
107
+ const parseResult = RequestAuthorizationParamsSchema . safeParse ( req . method === 'POST' ? req . body : req . query ) ;
108
+ if ( ! parseResult . success ) {
109
+ throw new InvalidRequestError ( parseResult . error . message ) ;
110
+ }
111
+
112
+ const { scope, code_challenge } = parseResult . data ;
113
+ state = parseResult . data . state ;
114
+
115
+ // Validate scopes
116
+ let requestedScopes : string [ ] = [ ] ;
117
+ if ( scope !== undefined && client . scope !== undefined ) {
118
+ requestedScopes = scope . split ( " " ) ;
119
+ const allowedScopes = new Set ( client . scope . split ( " " ) ) ;
120
+
121
+ // Check each requested scope against allowed scopes
122
+ for ( const scope of requestedScopes ) {
123
+ if ( ! allowedScopes . has ( scope ) ) {
124
+ throw new InvalidScopeError ( `Client was not registered with scope ${ scope } ` ) ;
125
+ }
109
126
}
110
127
}
111
- }
112
128
113
- await provider . authorize ( client , {
114
- state : params . state ,
115
- scopes : requestedScopes ,
116
- redirectUri : redirect_uri ,
117
- codeChallenge : params . code_challenge ,
118
- } , res ) ;
129
+ // All validation passed, proceed with authorization
130
+ await provider . authorize ( client , {
131
+ state,
132
+ scopes : requestedScopes ,
133
+ redirectUri : redirect_uri ,
134
+ codeChallenge : code_challenge ,
135
+ } , res ) ;
136
+ } catch ( error ) {
137
+ // Post-redirect errors - redirect with error parameters
138
+ if ( error instanceof OAuthError ) {
139
+ res . redirect ( 302 , createErrorRedirect ( redirect_uri , error , state ) ) ;
140
+ } else {
141
+ console . error ( "Unexpected error during authorization:" , error ) ;
142
+ const serverError = new ServerError ( "Internal Server Error" ) ;
143
+ res . redirect ( 302 , createErrorRedirect ( redirect_uri , serverError , state ) ) ;
144
+ }
145
+ }
119
146
} ) ;
120
147
121
148
return router ;
149
+ }
150
+
151
+ /**
152
+ * Helper function to create redirect URL with error parameters
153
+ */
154
+ function createErrorRedirect ( redirectUri : string , error : OAuthError , state ?: string ) : string {
155
+ const errorUrl = new URL ( redirectUri ) ;
156
+ errorUrl . searchParams . set ( "error" , error . errorCode ) ;
157
+ errorUrl . searchParams . set ( "error_description" , error . message ) ;
158
+ if ( error . errorUri ) {
159
+ errorUrl . searchParams . set ( "error_uri" , error . errorUri ) ;
160
+ }
161
+ if ( state ) {
162
+ errorUrl . searchParams . set ( "state" , state ) ;
163
+ }
164
+ return errorUrl . href ;
122
165
}
0 commit comments