@@ -19,6 +19,21 @@ import type { Cookie } from "../../../utils/cookie.js"
19
19
import { isOIDCProvider } from "../../../utils/providers.js"
20
20
import { fetchOpt } from "../../../utils/custom-fetch.js"
21
21
22
+ function formUrlEncode ( token : string ) {
23
+ return encodeURIComponent ( token ) . replace ( / % 2 0 / g, "+" )
24
+ }
25
+
26
+ /**
27
+ * Formats client_id and client_secret as an HTTP Basic Authentication header as per the OAuth 2.0
28
+ * specified in RFC6749.
29
+ */
30
+ function clientSecretBasic ( clientId : string , clientSecret : string ) {
31
+ const username = formUrlEncode ( clientId )
32
+ const password = formUrlEncode ( clientSecret )
33
+ const credentials = btoa ( `${ username } :${ password } ` )
34
+ return `Basic ${ credentials } `
35
+ }
36
+
22
37
/**
23
38
* Handles the following OAuth steps.
24
39
* https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1
@@ -46,10 +61,11 @@ export async function handleOAuth(
46
61
// We assume that issuer is always defined as this has been asserted earlier
47
62
48
63
const issuer = new URL ( provider . issuer ! )
49
- const discoveryResponse = await o . discoveryRequest (
50
- issuer ,
51
- fetchOpt ( provider )
52
- )
64
+ // TODO: move away from allowing insecure HTTP requests
65
+ const discoveryResponse = await o . discoveryRequest ( issuer , {
66
+ ...fetchOpt ( provider ) ,
67
+ [ o . allowInsecureRequests ] : true ,
68
+ } )
53
69
const discoveredAs = await o . processDiscoveryResponse (
54
70
issuer ,
55
71
discoveryResponse
@@ -76,26 +92,63 @@ export async function handleOAuth(
76
92
77
93
const client : o . Client = {
78
94
client_id : provider . clientId ,
79
- client_secret : provider . clientSecret ,
80
95
...provider . client ,
81
96
}
82
97
98
+ let clientAuth : o . ClientAuth
99
+
100
+ switch ( client . token_endpoint_auth_method ) {
101
+ // TODO: in the next breaking major version have undefined be `client_secret_post`
102
+ case undefined :
103
+ case "client_secret_basic" :
104
+ // TODO: in the next breaking major version use o.ClientSecretBasic() here
105
+ clientAuth = ( _as , _client , _body , headers ) => {
106
+ headers . set (
107
+ "authorization" ,
108
+ clientSecretBasic ( provider . clientId , provider . clientSecret ! )
109
+ )
110
+ }
111
+ break
112
+ case "client_secret_post" :
113
+ clientAuth = o . ClientSecretPost ( provider . clientSecret ! )
114
+ break
115
+ case "client_secret_jwt" :
116
+ clientAuth = o . ClientSecretJwt ( provider . clientSecret ! )
117
+ break
118
+ case "private_key_jwt" :
119
+ clientAuth = o . PrivateKeyJwt ( provider . token ! . clientPrivateKey ! , {
120
+ // TODO: review in the next breaking change
121
+ [ o . modifyAssertion ] ( _header , payload ) {
122
+ payload . aud = [ as . issuer , as . token_endpoint ! ]
123
+ } ,
124
+ } )
125
+ break
126
+ default :
127
+ throw new Error ( "unsupported client authentication method" )
128
+ }
129
+
83
130
const resCookies : Cookie [ ] = [ ]
84
131
85
132
const state = await checks . state . use ( cookies , resCookies , options )
86
133
87
- const codeGrantParams = o . validateAuthResponse (
88
- as ,
89
- client ,
90
- new URLSearchParams ( params ) ,
91
- provider . checks . includes ( "state" ) ? state : o . skipStateCheck
92
- )
93
-
94
- /** https://www.rfc-editor.org/rfc/rfc6749#section-4.1.2.1 */
95
- if ( o . isOAuth2Error ( codeGrantParams ) ) {
96
- const cause = { providerId : provider . id , ...codeGrantParams }
97
- logger . debug ( "OAuthCallbackError" , cause )
98
- throw new OAuthCallbackError ( "OAuth Provider returned an error" , cause )
134
+ let codeGrantParams : URLSearchParams
135
+ try {
136
+ codeGrantParams = o . validateAuthResponse (
137
+ as ,
138
+ client ,
139
+ new URLSearchParams ( params ) ,
140
+ provider . checks . includes ( "state" ) ? state : o . skipStateCheck
141
+ )
142
+ } catch ( err ) {
143
+ if ( err instanceof o . AuthorizationResponseError ) {
144
+ const cause = {
145
+ providerId : provider . id ,
146
+ ...Object . fromEntries ( err . cause . entries ( ) ) ,
147
+ }
148
+ logger . debug ( "OAuthCallbackError" , cause )
149
+ throw new OAuthCallbackError ( "OAuth Provider returned an error" , cause )
150
+ }
151
+ throw err
99
152
}
100
153
101
154
const codeVerifier = await checks . pkce . use ( cookies , resCookies , options )
@@ -108,20 +161,19 @@ export async function handleOAuth(
108
161
let codeGrantResponse = await o . authorizationCodeGrantRequest (
109
162
as ,
110
163
client ,
164
+ clientAuth ,
111
165
codeGrantParams ,
112
166
redirect_uri ,
113
- codeVerifier ?? "auth" , // TODO: review fallback code verifier ,
167
+ codeVerifier ?? "decoy" ,
114
168
{
169
+ // TODO: move away from allowing insecure HTTP requests
170
+ [ o . allowInsecureRequests ] : true ,
115
171
[ o . customFetch ] : ( ...args ) => {
116
- if (
117
- ! provider . checks . includes ( "pkce" ) &&
118
- args [ 1 ] ?. body instanceof URLSearchParams
119
- ) {
172
+ if ( ! provider . checks . includes ( "pkce" ) ) {
120
173
args [ 1 ] . body . delete ( "code_verifier" )
121
174
}
122
175
return fetchOpt ( provider ) [ o . customFetch ] ( ...args )
123
176
} ,
124
- clientPrivateKey : provider . token ?. clientPrivateKey ,
125
177
}
126
178
)
127
179
@@ -131,41 +183,35 @@ export async function handleOAuth(
131
183
codeGrantResponse
132
184
}
133
185
134
- let challenges : o . WWWAuthenticateChallenge [ ] | undefined
135
- if ( ( challenges = o . parseWwwAuthenticateChallenges ( codeGrantResponse ) ) ) {
136
- for ( const challenge of challenges ) {
137
- console . log ( "challenge" , challenge )
138
- }
139
- throw new Error ( "TODO: Handle www-authenticate challenges as needed" )
140
- }
141
-
142
186
let profile : Profile = { }
143
- let tokens : TokenSet & Pick < Account , "expires_at" >
144
187
145
- if ( isOIDCProvider ( provider ) ) {
146
- const nonce = await checks . nonce . use ( cookies , resCookies , options )
147
- const processedCodeResponse =
148
- await o . processAuthorizationCodeOpenIDResponse (
149
- as ,
150
- client ,
151
- codeGrantResponse ,
152
- nonce ?? o . expectNoNonce
153
- )
154
-
155
- if ( o . isOAuth2Error ( processedCodeResponse ) ) {
156
- console . log ( "error" , processedCodeResponse )
157
- throw new Error ( "TODO: Handle OIDC response body error" )
188
+ const isOidc = isOIDCProvider ( provider )
189
+ const processedCodeResponse = await o . processAuthorizationCodeResponse (
190
+ as ,
191
+ client ,
192
+ codeGrantResponse ,
193
+ {
194
+ expectedNonce : await checks . nonce . use ( cookies , resCookies , options ) ,
195
+ requireIdToken : isOidc ,
158
196
}
197
+ )
198
+
199
+ const tokens : TokenSet & Pick < Account , "expires_at" > = processedCodeResponse
159
200
160
- const idTokenClaims = o . getValidatedIdTokenClaims ( processedCodeResponse )
201
+ if ( isOidc ) {
202
+ const idTokenClaims = o . getValidatedIdTokenClaims ( processedCodeResponse ) !
161
203
profile = idTokenClaims
162
204
163
205
if ( provider . idToken === false ) {
164
206
const userinfoResponse = await o . userInfoRequest (
165
207
as ,
166
208
client ,
167
209
processedCodeResponse . access_token ,
168
- fetchOpt ( provider )
210
+ {
211
+ ...fetchOpt ( provider ) ,
212
+ // TODO: move away from allowing insecure HTTP requests
213
+ [ o . allowInsecureRequests ] : true ,
214
+ }
169
215
)
170
216
171
217
profile = await o . processUserInfoResponse (
@@ -175,20 +221,7 @@ export async function handleOAuth(
175
221
userinfoResponse
176
222
)
177
223
}
178
- tokens = processedCodeResponse
179
224
} else {
180
- const processedCodeResponse =
181
- await o . processAuthorizationCodeOAuth2Response (
182
- as ,
183
- client ,
184
- codeGrantResponse
185
- )
186
- tokens = processedCodeResponse
187
- if ( o . isOAuth2Error ( processedCodeResponse ) ) {
188
- console . log ( "error" , processedCodeResponse )
189
- throw new Error ( "TODO: Handle OAuth 2.0 response body error" )
190
- }
191
-
192
225
if ( userinfo ?. request ) {
193
226
const _profile = await userinfo . request ( { tokens, provider } )
194
227
if ( _profile instanceof Object ) profile = _profile
0 commit comments