forked from panva/oauth4webapi
/
fapi2.diff
208 lines (195 loc) · 7.6 KB
/
fapi2.diff
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
diff --git a/examples/oauth.ts b/examples/fapi2.ts
index cc6d632..80ec0f4 100644
--- a/examples/oauth.ts
+++ b/examples/fapi2.ts
@@ -8,12 +8,22 @@ let algorithm!:
| 'oidc' /* For .well-known/openid-configuration discovery */
| undefined /* Defaults to 'oidc' */
let client_id!: string
-let client_secret!: string
/**
* Value used in the authorization request as redirect_uri pre-registered at the Authorization
* Server.
*/
let redirect_uri!: string
+/**
+ * In order to take full advantage of DPoP you shall generate a random private key for every
+ * session. In the browser environment you shall use IndexedDB to persist the generated
+ * CryptoKeyPair.
+ */
+let DPoP!: CryptoKeyPair
+/**
+ * A key that the client has pre-registered at the Authorization Server for use with Private Key JWT
+ * client authentication method.
+ */
+let clientPrivateKey!: CryptoKey
// End of prerequisites
@@ -23,38 +33,68 @@ const as = await oauth
const client: oauth.Client = {
client_id,
- client_secret,
- token_endpoint_auth_method: 'client_secret_basic',
+ token_endpoint_auth_method: 'private_key_jwt',
}
const code_challenge_method = 'S256'
/**
* The following MUST be generated for every redirect to the authorization_endpoint. You must store
- * the code_verifier and nonce in the end-user session such that it can be recovered as the user
- * gets redirected from the authorization server back to your application.
+ * the code_verifier in the end-user session such that it can be recovered as the user gets
+ * redirected from the authorization server back to your application.
*/
const code_verifier = oauth.generateRandomCodeVerifier()
const code_challenge = await oauth.calculatePKCECodeChallenge(code_verifier)
-let state: string | undefined
+
+// Pushed Authorization Request & Response (PAR)
+let request_uri: string
+{
+ const params = new URLSearchParams()
+ params.set('client_id', client.client_id)
+ params.set('code_challenge', code_challenge)
+ params.set('code_challenge_method', code_challenge_method)
+ params.set('redirect_uri', redirect_uri)
+ params.set('response_type', 'code')
+ params.set('scope', 'api:read')
+
+ const pushedAuthorizationRequest = () =>
+ oauth.pushedAuthorizationRequest(as, client, params, {
+ DPoP,
+ clientPrivateKey,
+ })
+ let response = await pushedAuthorizationRequest()
+ let challenges: oauth.WWWAuthenticateChallenge[] | undefined
+ if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) {
+ for (const challenge of challenges) {
+ console.error('WWW-Authenticate Challenge', challenge)
+ }
+ throw new Error() // Handle WWW-Authenticate Challenges as needed
+ }
+
+ const processPushedAuthorizationResponse = () =>
+ oauth.processPushedAuthorizationResponse(as, client, response)
+ let result = await processPushedAuthorizationResponse()
+ if (oauth.isOAuth2Error(result)) {
+ console.error('Error Response', result)
+ if (result.error === 'use_dpop_nonce') {
+ // the AS-signalled nonce is now cached, retrying
+ response = await pushedAuthorizationRequest()
+ result = await processPushedAuthorizationResponse()
+ if (oauth.isOAuth2Error(result)) {
+ throw new Error() // Handle OAuth 2.0 response body error
+ }
+ } else {
+ throw new Error() // Handle OAuth 2.0 response body error
+ }
+ }
+
+ ;({ request_uri } = result)
+}
{
// redirect user to as.authorization_endpoint
const authorizationUrl = new URL(as.authorization_endpoint!)
authorizationUrl.searchParams.set('client_id', client.client_id)
- authorizationUrl.searchParams.set('redirect_uri', redirect_uri)
- authorizationUrl.searchParams.set('response_type', 'code')
- authorizationUrl.searchParams.set('scope', 'api:read')
- authorizationUrl.searchParams.set('code_challenge', code_challenge)
- authorizationUrl.searchParams.set('code_challenge_method', code_challenge_method)
-
- /**
- * We cannot be sure the AS supports PKCE so we're going to use state too. Use of PKCE is
- * backwards compatible even if the AS doesn't support it which is why we're using it regardless.
- */
- if (as.code_challenge_methods_supported?.includes('S256') !== true) {
- state = oauth.generateRandomState()
- authorizationUrl.searchParams.set('state', state)
- }
+ authorizationUrl.searchParams.set('request_uri', request_uri)
// now redirect the user to authorizationUrl.href
}
@@ -65,19 +105,16 @@ let access_token: string
{
// @ts-expect-error
const currentUrl: URL = getCurrentUrl()
- const params = oauth.validateAuthResponse(as, client, currentUrl, state)
+ const params = oauth.validateAuthResponse(as, client, currentUrl)
if (oauth.isOAuth2Error(params)) {
console.error('Error Response', params)
throw new Error() // Handle OAuth 2.0 redirect error
}
- const response = await oauth.authorizationCodeGrantRequest(
- as,
- client,
- params,
- redirect_uri,
- code_verifier,
- )
+ const authorizationCodeGrantRequest = () =>
+ oauth.authorizationCodeGrantRequest(as, client, params, redirect_uri, code_verifier, { DPoP })
+
+ let response = await authorizationCodeGrantRequest()
let challenges: oauth.WWWAuthenticateChallenge[] | undefined
if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) {
@@ -87,10 +124,22 @@ let access_token: string
throw new Error() // Handle WWW-Authenticate Challenges as needed
}
- const result = await oauth.processAuthorizationCodeOAuth2Response(as, client, response)
+ const processAuthorizationCodeOAuth2Response = () =>
+ oauth.processAuthorizationCodeOAuth2Response(as, client, response)
+
+ let result = await processAuthorizationCodeOAuth2Response()
if (oauth.isOAuth2Error(result)) {
console.error('Error Response', result)
- throw new Error() // Handle OAuth 2.0 response body error
+ if (result.error === 'use_dpop_nonce') {
+ // the AS-signalled nonce is now cached, retrying
+ response = await authorizationCodeGrantRequest()
+ result = await processAuthorizationCodeOAuth2Response()
+ if (oauth.isOAuth2Error(result)) {
+ throw new Error() // Handle OAuth 2.0 response body error
+ }
+ } else {
+ throw new Error() // Handle OAuth 2.0 response body error
+ }
}
console.log('Access Token Response', result)
@@ -99,18 +148,33 @@ let access_token: string
// Protected Resource Request
{
- const response = await oauth.protectedResourceRequest(
- access_token,
- 'GET',
- new URL('https://rs.example.com/api'),
- )
+ const protectedResourceRequest = () =>
+ oauth.protectedResourceRequest(
+ access_token,
+ 'GET',
+ new URL('https://rs.example.com/api'),
+ undefined,
+ undefined,
+ { DPoP },
+ )
+ let response = await protectedResourceRequest()
let challenges: oauth.WWWAuthenticateChallenge[] | undefined
if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) {
- for (const challenge of challenges) {
- console.error('WWW-Authenticate Challenge', challenge)
+ const { 0: challenge, length } = challenges
+ if (
+ length === 1 &&
+ challenge.scheme === 'dpop' &&
+ challenge.parameters.error === 'use_dpop_nonce'
+ ) {
+ // the AS-signalled nonce is now cached, retrying
+ response = await protectedResourceRequest()
+ } else {
+ for (const challenge of challenges) {
+ console.error('WWW-Authenticate Challenge', challenge)
+ }
+ throw new Error() // Handle WWW-Authenticate Challenges as needed
}
- throw new Error() // Handle WWW-Authenticate Challenges as needed
}
console.log('Protected Resource Response', await response.json())