@atproto/oauth-provider 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/account/account-store.d.ts +2 -2
  3. package/dist/assets/app/bundle-manifest.json +3 -3
  4. package/dist/assets/app/main.css +1 -1
  5. package/dist/assets/app/main.js +3 -3
  6. package/dist/assets/app/main.js.map +1 -1
  7. package/dist/assets/assets-middleware.d.ts.map +1 -1
  8. package/dist/assets/assets-middleware.js +4 -2
  9. package/dist/assets/assets-middleware.js.map +1 -1
  10. package/dist/client/client-manager.d.ts.map +1 -1
  11. package/dist/client/client-manager.js +127 -118
  12. package/dist/client/client-manager.js.map +1 -1
  13. package/dist/client/client-utils.d.ts +1 -2
  14. package/dist/client/client-utils.d.ts.map +1 -1
  15. package/dist/client/client-utils.js +3 -12
  16. package/dist/client/client-utils.js.map +1 -1
  17. package/dist/client/client.d.ts +8 -3
  18. package/dist/client/client.d.ts.map +1 -1
  19. package/dist/client/client.js +70 -1
  20. package/dist/client/client.js.map +1 -1
  21. package/dist/constants.d.ts +0 -1
  22. package/dist/constants.d.ts.map +1 -1
  23. package/dist/constants.js +1 -2
  24. package/dist/constants.js.map +1 -1
  25. package/dist/errors/access-denied-error.d.ts +4 -4
  26. package/dist/errors/access-denied-error.d.ts.map +1 -1
  27. package/dist/errors/access-denied-error.js +2 -2
  28. package/dist/errors/access-denied-error.js.map +1 -1
  29. package/dist/errors/account-selection-required-error.d.ts +2 -2
  30. package/dist/errors/account-selection-required-error.d.ts.map +1 -1
  31. package/dist/errors/account-selection-required-error.js.map +1 -1
  32. package/dist/errors/consent-required-error.d.ts +2 -2
  33. package/dist/errors/consent-required-error.d.ts.map +1 -1
  34. package/dist/errors/consent-required-error.js.map +1 -1
  35. package/dist/errors/invalid-authorization-details-error.d.ts +2 -2
  36. package/dist/errors/invalid-authorization-details-error.d.ts.map +1 -1
  37. package/dist/errors/invalid-authorization-details-error.js.map +1 -1
  38. package/dist/errors/invalid-client-id-error.d.ts +1 -1
  39. package/dist/errors/invalid-client-id-error.d.ts.map +1 -1
  40. package/dist/errors/invalid-client-id-error.js +12 -6
  41. package/dist/errors/invalid-client-id-error.js.map +1 -1
  42. package/dist/errors/invalid-client-metadata-error.d.ts +1 -1
  43. package/dist/errors/invalid-client-metadata-error.d.ts.map +1 -1
  44. package/dist/errors/invalid-client-metadata-error.js +11 -3
  45. package/dist/errors/invalid-client-metadata-error.js.map +1 -1
  46. package/dist/errors/invalid-parameters-error.d.ts +2 -2
  47. package/dist/errors/invalid-parameters-error.d.ts.map +1 -1
  48. package/dist/errors/invalid-parameters-error.js.map +1 -1
  49. package/dist/errors/invalid-scope-error.d.ts +9 -0
  50. package/dist/errors/invalid-scope-error.d.ts.map +1 -0
  51. package/dist/errors/invalid-scope-error.js +14 -0
  52. package/dist/errors/invalid-scope-error.js.map +1 -0
  53. package/dist/errors/login-required-error.d.ts +2 -2
  54. package/dist/errors/login-required-error.d.ts.map +1 -1
  55. package/dist/errors/login-required-error.js.map +1 -1
  56. package/dist/lib/html/html.d.ts +1 -1
  57. package/dist/lib/html/html.d.ts.map +1 -1
  58. package/dist/lib/html/html.js +14 -11
  59. package/dist/lib/html/html.js.map +1 -1
  60. package/dist/lib/http/parser.d.ts +9 -2
  61. package/dist/lib/http/parser.d.ts.map +1 -1
  62. package/dist/lib/http/parser.js +15 -7
  63. package/dist/lib/http/parser.js.map +1 -1
  64. package/dist/lib/http/request.d.ts +0 -23
  65. package/dist/lib/http/request.d.ts.map +1 -1
  66. package/dist/lib/http/request.js +1 -11
  67. package/dist/lib/http/request.js.map +1 -1
  68. package/dist/lib/http/stream.d.ts +28 -6
  69. package/dist/lib/http/stream.d.ts.map +1 -1
  70. package/dist/lib/http/stream.js +21 -32
  71. package/dist/lib/http/stream.js.map +1 -1
  72. package/dist/lib/util/authorization-header.d.ts.map +1 -1
  73. package/dist/lib/util/authorization-header.js +1 -1
  74. package/dist/lib/util/authorization-header.js.map +1 -1
  75. package/dist/lib/util/hostname.d.ts +3 -2
  76. package/dist/lib/util/hostname.d.ts.map +1 -1
  77. package/dist/lib/util/hostname.js +12 -8
  78. package/dist/lib/util/hostname.js.map +1 -1
  79. package/dist/metadata/build-metadata.d.ts.map +1 -1
  80. package/dist/metadata/build-metadata.js +2 -1
  81. package/dist/metadata/build-metadata.js.map +1 -1
  82. package/dist/oauth-errors.d.ts +1 -0
  83. package/dist/oauth-errors.d.ts.map +1 -1
  84. package/dist/oauth-errors.js +3 -1
  85. package/dist/oauth-errors.js.map +1 -1
  86. package/dist/oauth-hooks.d.ts +3 -3
  87. package/dist/oauth-hooks.d.ts.map +1 -1
  88. package/dist/oauth-provider.d.ts +20 -22
  89. package/dist/oauth-provider.d.ts.map +1 -1
  90. package/dist/oauth-provider.js +234 -176
  91. package/dist/oauth-provider.js.map +1 -1
  92. package/dist/oauth-verifier.d.ts +2 -2
  93. package/dist/oauth-verifier.d.ts.map +1 -1
  94. package/dist/oauth-verifier.js.map +1 -1
  95. package/dist/output/build-authorize-data.d.ts +2 -2
  96. package/dist/output/build-authorize-data.d.ts.map +1 -1
  97. package/dist/output/send-authorize-redirect.d.ts +2 -4
  98. package/dist/output/send-authorize-redirect.d.ts.map +1 -1
  99. package/dist/output/send-authorize-redirect.js +5 -2
  100. package/dist/output/send-authorize-redirect.js.map +1 -1
  101. package/dist/request/request-data.d.ts +2 -2
  102. package/dist/request/request-data.d.ts.map +1 -1
  103. package/dist/request/request-info.d.ts +2 -2
  104. package/dist/request/request-info.d.ts.map +1 -1
  105. package/dist/request/request-manager.d.ts +4 -4
  106. package/dist/request/request-manager.d.ts.map +1 -1
  107. package/dist/request/request-manager.js +94 -60
  108. package/dist/request/request-manager.js.map +1 -1
  109. package/dist/signer/signed-token-payload.d.ts +122 -122
  110. package/dist/signer/signer.d.ts +41 -40
  111. package/dist/signer/signer.d.ts.map +1 -1
  112. package/dist/signer/signer.js +13 -15
  113. package/dist/signer/signer.js.map +1 -1
  114. package/dist/token/token-claims.d.ts +121 -121
  115. package/dist/token/token-data.d.ts +3 -3
  116. package/dist/token/token-data.d.ts.map +1 -1
  117. package/dist/token/token-manager.d.ts +4 -5
  118. package/dist/token/token-manager.d.ts.map +1 -1
  119. package/dist/token/token-manager.js +96 -72
  120. package/dist/token/token-manager.js.map +1 -1
  121. package/dist/token/verify-token-claims.d.ts +3 -3
  122. package/dist/token/verify-token-claims.d.ts.map +1 -1
  123. package/dist/token/verify-token-claims.js.map +1 -1
  124. package/package.json +7 -6
  125. package/src/assets/app/components/sign-in-form.tsx +31 -2
  126. package/src/assets/app/components/url-viewer.tsx +3 -3
  127. package/src/assets/assets-middleware.ts +4 -2
  128. package/src/client/client-manager.ts +163 -161
  129. package/src/client/client-utils.ts +7 -12
  130. package/src/client/client.ts +112 -3
  131. package/src/constants.ts +0 -2
  132. package/src/errors/access-denied-error.ts +10 -4
  133. package/src/errors/account-selection-required-error.ts +2 -2
  134. package/src/errors/consent-required-error.ts +2 -2
  135. package/src/errors/invalid-authorization-details-error.ts +2 -2
  136. package/src/errors/invalid-client-id-error.ts +15 -4
  137. package/src/errors/invalid-client-metadata-error.ts +15 -3
  138. package/src/errors/invalid-parameters-error.ts +2 -2
  139. package/src/errors/invalid-scope-error.ts +15 -0
  140. package/src/errors/login-required-error.ts +2 -2
  141. package/src/lib/html/html.ts +14 -12
  142. package/src/lib/http/parser.ts +21 -8
  143. package/src/lib/http/request.ts +1 -23
  144. package/src/lib/http/stream.ts +29 -60
  145. package/src/lib/util/authorization-header.ts +5 -2
  146. package/src/lib/util/hostname.ts +9 -5
  147. package/src/metadata/build-metadata.ts +3 -1
  148. package/src/oauth-errors.ts +1 -0
  149. package/src/oauth-hooks.ts +3 -3
  150. package/src/oauth-provider.ts +368 -269
  151. package/src/oauth-verifier.ts +2 -2
  152. package/src/output/build-authorize-data.ts +2 -2
  153. package/src/output/send-authorize-redirect.ts +7 -6
  154. package/src/request/request-data.ts +2 -2
  155. package/src/request/request-info.ts +2 -2
  156. package/src/request/request-manager.ts +129 -103
  157. package/src/signer/signer.ts +24 -25
  158. package/src/token/token-data.ts +3 -3
  159. package/src/token/token-manager.ts +141 -99
  160. package/src/token/verify-token-claims.ts +3 -3
  161. package/dist/request/types.d.ts +0 -328
  162. package/dist/request/types.d.ts.map +0 -1
  163. package/dist/request/types.js +0 -27
  164. package/dist/request/types.js.map +0 -1
  165. package/dist/token/types.d.ts +0 -250
  166. package/dist/token/types.d.ts.map +0 -1
  167. package/dist/token/types.js +0 -36
  168. package/dist/token/types.js.map +0 -1
  169. package/src/request/types.ts +0 -48
  170. package/src/token/types.ts +0 -86
@@ -60,16 +60,15 @@ const build_error_payload_js_1 = require("./output/build-error-payload.js");
60
60
  const output_manager_js_1 = require("./output/output-manager.js");
61
61
  const send_authorize_redirect_js_1 = require("./output/send-authorize-redirect.js");
62
62
  const replay_store_js_1 = require("./replay/replay-store.js");
63
+ const code_js_1 = require("./request/code.js");
63
64
  const request_manager_js_1 = require("./request/request-manager.js");
64
65
  const request_store_memory_js_1 = require("./request/request-store-memory.js");
65
66
  const request_store_redis_js_1 = require("./request/request-store-redis.js");
66
67
  const request_store_js_1 = require("./request/request-store.js");
67
68
  const request_uri_js_1 = require("./request/request-uri.js");
68
- const types_js_1 = require("./request/types.js");
69
69
  const token_id_js_1 = require("./token/token-id.js");
70
70
  const token_manager_js_1 = require("./token/token-manager.js");
71
71
  const token_store_js_1 = require("./token/token-store.js");
72
- const types_js_2 = require("./token/types.js");
73
72
  class OAuthProvider extends oauth_verifier_js_1.OAuthVerifier {
74
73
  metadata;
75
74
  customization;
@@ -117,21 +116,35 @@ class OAuthProvider extends oauth_verifier_js_1.OAuthVerifier {
117
116
  }
118
117
  return authAge >= this.authenticationMaxAge;
119
118
  }
120
- async authenticateClient(client, credentials) {
119
+ async authenticateClient(credentials) {
120
+ const client = await this.clientManager.getClient(credentials.client_id);
121
121
  const { clientAuth, nonce } = await client.verifyCredentials(credentials, {
122
122
  audience: this.issuer,
123
123
  });
124
+ if (client.metadata.application_type === 'native' &&
125
+ clientAuth.method !== 'none') {
126
+ // https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
127
+ //
128
+ // > Except when using a mechanism like Dynamic Client Registration
129
+ // > [RFC7591] to provision per-instance secrets, native apps are
130
+ // > classified as public clients, as defined by Section 2.1 of OAuth 2.0
131
+ // > [RFC6749]; they MUST be registered with the authorization server as
132
+ // > such. Authorization servers MUST record the client type in the client
133
+ // > registration details in order to identify and process requests
134
+ // > accordingly.
135
+ throw new invalid_grant_error_js_1.InvalidGrantError('Native clients must authenticate using "none" method');
136
+ }
124
137
  if (nonce != null) {
125
138
  const unique = await this.replayManager.uniqueAuth(nonce, client.id);
126
139
  if (!unique) {
127
- throw new invalid_client_error_js_1.InvalidClientError(`${clientAuth.method} jti reused`);
140
+ throw new invalid_grant_error_js_1.InvalidGrantError(`${clientAuth.method} jti reused`);
128
141
  }
129
142
  }
130
- return clientAuth;
143
+ return [client, clientAuth];
131
144
  }
132
145
  async decodeJAR(client, input) {
133
146
  const result = await client.decodeRequestObject(input.request);
134
- const payload = oauth_types_1.oauthAuthenticationRequestParametersSchema.parse(result.payload);
147
+ const payload = oauth_types_1.oauthAuthorizationRequestParametersSchema.parse(result.payload);
135
148
  if (!result.payload.jti) {
136
149
  throw new invalid_parameters_error_js_1.InvalidParametersError(payload, 'Request object must contain a jti claim');
137
150
  }
@@ -159,13 +172,12 @@ class OAuthProvider extends oauth_verifier_js_1.OAuthVerifier {
159
172
  /**
160
173
  * @see {@link https://datatracker.ietf.org/doc/html/rfc9126}
161
174
  */
162
- async pushedAuthorizationRequest(input, dpopJkt) {
175
+ async pushedAuthorizationRequest(credentials, authorizationRequest, dpopJkt) {
163
176
  try {
164
- const client = await this.clientManager.getClient(input.client_id);
165
- const clientAuth = await this.authenticateClient(client, input);
166
- const { payload: parameters } = 'request' in input // Handle JAR
167
- ? await this.decodeJAR(client, input)
168
- : { payload: input };
177
+ const [client, clientAuth] = await this.authenticateClient(credentials);
178
+ const { payload: parameters } = 'request' in authorizationRequest // Handle JAR
179
+ ? await this.decodeJAR(client, authorizationRequest)
180
+ : { payload: authorizationRequest };
169
181
  const { uri, expiresAt } = await this.requestManager.createAuthorizationRequest(client, clientAuth, parameters, null, dpopJkt);
170
182
  return {
171
183
  request_uri: uri,
@@ -184,14 +196,15 @@ class OAuthProvider extends oauth_verifier_js_1.OAuthVerifier {
184
196
  throw err;
185
197
  }
186
198
  }
187
- async loadAuthorizationRequest(client, deviceId, input) {
188
- // Load PAR
189
- if ('request_uri' in input) {
190
- return this.requestManager.get(input.request_uri, client.id, deviceId);
199
+ async processAuthorizationRequest(client, deviceId, query) {
200
+ if ('request_uri' in query) {
201
+ const requestUri = await request_uri_js_1.requestUriSchema
202
+ .parseAsync(query.request_uri, { path: ['query', 'request_uri'] })
203
+ .catch(throwInvalidRequest);
204
+ return this.requestManager.get(requestUri, client.id, deviceId);
191
205
  }
192
- // Handle JAR
193
- if ('request' in input) {
194
- const requestObject = await this.decodeJAR(client, input);
206
+ if ('request' in query) {
207
+ const requestObject = await this.decodeJAR(client, query);
195
208
  if ('protectedHeader' in requestObject && requestObject.protectedHeader) {
196
209
  // Allow using signed JAR during "/authorize" as client authentication.
197
210
  // This allows clients to skip PAR to initiate trusted sessions.
@@ -205,7 +218,7 @@ class OAuthProvider extends oauth_verifier_js_1.OAuthVerifier {
205
218
  }
206
219
  return this.requestManager.createAuthorizationRequest(client, { method: 'none' }, requestObject.payload, deviceId, null);
207
220
  }
208
- return this.requestManager.createAuthorizationRequest(client, { method: 'none' }, input, deviceId, null);
221
+ return this.requestManager.createAuthorizationRequest(client, { method: 'none' }, query, deviceId, null);
209
222
  }
210
223
  async deleteRequest(uri, parameters) {
211
224
  try {
@@ -215,79 +228,79 @@ class OAuthProvider extends oauth_verifier_js_1.OAuthVerifier {
215
228
  throw access_denied_error_js_1.AccessDeniedError.from(parameters, err);
216
229
  }
217
230
  }
218
- async authorize(deviceId, input) {
231
+ /**
232
+ * @see {@link https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-4.1.1}
233
+ */
234
+ async authorize(deviceId, credentials, query) {
219
235
  const { issuer } = this;
220
- const client = await this.clientManager.getClient(input.client_id);
236
+ // If there is a chance to redirect the user to the client, let's do
237
+ // it by wrapping the error in an AccessDeniedError.
238
+ const accessDeniedCatcher = 'redirect_uri' in query
239
+ ? (err) => {
240
+ // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-4.1.2.1
241
+ throw access_denied_error_js_1.AccessDeniedError.from(query, err, 'invalid_request');
242
+ }
243
+ : null;
244
+ const client = await this.clientManager
245
+ .getClient(credentials.client_id)
246
+ .catch(accessDeniedCatcher);
247
+ const { clientAuth, parameters, uri } = await this.processAuthorizationRequest(client, deviceId, query).catch(accessDeniedCatcher);
221
248
  try {
222
- const { uri, parameters, clientAuth } = await this.loadAuthorizationRequest(client, deviceId, input);
223
- try {
224
- const sessions = await this.getSessions(client, clientAuth, deviceId, parameters);
225
- if (parameters.prompt === 'none') {
226
- const ssoSessions = sessions.filter((s) => s.matchesHint);
227
- if (ssoSessions.length > 1) {
228
- throw new account_selection_required_error_js_1.AccountSelectionRequiredError(parameters);
229
- }
230
- if (ssoSessions.length < 1) {
231
- throw new login_required_error_js_1.LoginRequiredError(parameters);
232
- }
233
- const ssoSession = ssoSessions[0];
234
- if (ssoSession.loginRequired) {
235
- throw new login_required_error_js_1.LoginRequiredError(parameters);
236
- }
237
- if (ssoSession.consentRequired) {
238
- throw new consent_required_error_js_1.ConsentRequiredError(parameters);
239
- }
240
- const code = await this.requestManager.setAuthorized(client, uri, deviceId, ssoSession.account);
241
- return { issuer, client, parameters, redirect: { code } };
249
+ const sessions = await this.getSessions(client, clientAuth, deviceId, parameters);
250
+ if (parameters.prompt === 'none') {
251
+ const ssoSessions = sessions.filter((s) => s.matchesHint);
252
+ if (ssoSessions.length > 1) {
253
+ throw new account_selection_required_error_js_1.AccountSelectionRequiredError(parameters);
242
254
  }
243
- // Automatic SSO when a did was provided
244
- if (parameters.prompt == null && parameters.login_hint != null) {
245
- const ssoSessions = sessions.filter((s) => s.matchesHint);
246
- if (ssoSessions.length === 1) {
247
- const ssoSession = ssoSessions[0];
248
- if (!ssoSession.loginRequired && !ssoSession.consentRequired) {
249
- const code = await this.requestManager.setAuthorized(client, uri, deviceId, ssoSession.account);
250
- return { issuer, client, parameters, redirect: { code } };
251
- }
252
- }
255
+ if (ssoSessions.length < 1) {
256
+ throw new login_required_error_js_1.LoginRequiredError(parameters);
253
257
  }
254
- return {
255
- issuer,
256
- client,
257
- parameters,
258
- authorize: {
259
- uri,
260
- sessions,
261
- scopeDetails: parameters.scope
262
- ?.split(/\s+/)
263
- .filter(Boolean)
264
- .sort((a, b) => a.localeCompare(b))
265
- .map((scope) => ({
266
- scope,
267
- // @TODO Allow to customize the scope descriptions (e.g.
268
- // using a hook)
269
- description: undefined,
270
- })),
271
- },
272
- };
258
+ const ssoSession = ssoSessions[0];
259
+ if (ssoSession.loginRequired) {
260
+ throw new login_required_error_js_1.LoginRequiredError(parameters);
261
+ }
262
+ if (ssoSession.consentRequired) {
263
+ throw new consent_required_error_js_1.ConsentRequiredError(parameters);
264
+ }
265
+ const code = await this.requestManager.setAuthorized(client, uri, deviceId, ssoSession.account);
266
+ return { issuer, client, parameters, redirect: { code } };
273
267
  }
274
- catch (err) {
275
- await this.deleteRequest(uri, parameters);
276
- // Transform into an AccessDeniedError to allow redirecting the user
277
- // to the client with the error details.
278
- throw access_denied_error_js_1.AccessDeniedError.from(parameters, err);
268
+ // Automatic SSO when a did was provided
269
+ if (parameters.prompt == null && parameters.login_hint != null) {
270
+ const ssoSessions = sessions.filter((s) => s.matchesHint);
271
+ if (ssoSessions.length === 1) {
272
+ const ssoSession = ssoSessions[0];
273
+ if (!ssoSession.loginRequired && !ssoSession.consentRequired) {
274
+ const code = await this.requestManager.setAuthorized(client, uri, deviceId, ssoSession.account);
275
+ return { issuer, client, parameters, redirect: { code } };
276
+ }
277
+ }
279
278
  }
279
+ return {
280
+ issuer,
281
+ client,
282
+ parameters,
283
+ authorize: {
284
+ uri,
285
+ sessions,
286
+ scopeDetails: parameters.scope
287
+ ?.split(/\s+/)
288
+ .filter(Boolean)
289
+ .sort((a, b) => a.localeCompare(b))
290
+ .map((scope) => ({
291
+ scope,
292
+ // @TODO Allow to customize the scope descriptions (e.g.
293
+ // using a hook)
294
+ description: undefined,
295
+ })),
296
+ },
297
+ };
280
298
  }
281
299
  catch (err) {
282
- if (err instanceof access_denied_error_js_1.AccessDeniedError) {
283
- return {
284
- issuer,
285
- client,
286
- parameters: err.parameters,
287
- redirect: err.toJSON(),
288
- };
289
- }
290
- throw err;
300
+ await this.deleteRequest(uri, parameters);
301
+ // Not using accessDeniedCatcher here because "parameters" will most
302
+ // likely contain the redirect_uri (using the client default).
303
+ throw access_denied_error_js_1.AccessDeniedError.from(parameters, err);
291
304
  }
292
305
  }
293
306
  async getSessions(client, clientAuth, deviceId, parameters) {
@@ -334,70 +347,54 @@ class OAuthProvider extends oauth_verifier_js_1.OAuthVerifier {
334
347
  async acceptRequest(deviceId, uri, clientId, sub) {
335
348
  const { issuer } = this;
336
349
  const client = await this.clientManager.getClient(clientId);
350
+ const { parameters, clientAuth } = await this.requestManager.get(uri, clientId, deviceId);
337
351
  try {
338
- const { parameters, clientAuth } = await this.requestManager.get(uri, clientId, deviceId);
339
- try {
340
- const { account, info } = await this.accountManager.get(deviceId, sub);
341
- // The user is trying to authorize without a fresh login
342
- if (this.loginRequired(client, parameters, info)) {
343
- throw new login_required_error_js_1.LoginRequiredError(parameters, 'Account authentication required.');
344
- }
345
- const code = await this.requestManager.setAuthorized(client, uri, deviceId, account);
346
- await this.accountManager.addAuthorizedClient(deviceId, account, client, clientAuth);
347
- return { issuer, client, parameters, redirect: { code } };
348
- }
349
- catch (err) {
350
- await this.deleteRequest(uri, parameters);
351
- // throw AccessDeniedError.from(parameters, err)
352
- throw err;
352
+ const { account, info } = await this.accountManager.get(deviceId, sub);
353
+ // The user is trying to authorize without a fresh login
354
+ if (this.loginRequired(client, parameters, info)) {
355
+ throw new login_required_error_js_1.LoginRequiredError(parameters, 'Account authentication required.');
353
356
  }
357
+ const code = await this.requestManager.setAuthorized(client, uri, deviceId, account);
358
+ await this.accountManager.addAuthorizedClient(deviceId, account, client, clientAuth);
359
+ return { issuer, parameters, redirect: { code } };
354
360
  }
355
361
  catch (err) {
356
- if (err instanceof access_denied_error_js_1.AccessDeniedError) {
357
- const { parameters } = err;
358
- return { issuer, client, parameters, redirect: err.toJSON() };
359
- }
360
- throw err;
362
+ await this.deleteRequest(uri, parameters);
363
+ throw access_denied_error_js_1.AccessDeniedError.from(parameters, err);
361
364
  }
362
365
  }
363
366
  async rejectRequest(deviceId, uri, clientId) {
364
- try {
365
- const { parameters } = await this.requestManager.get(uri, clientId, deviceId);
366
- await this.deleteRequest(uri, parameters);
367
- // Trigger redirect (see catch block)
368
- throw new access_denied_error_js_1.AccessDeniedError(parameters, 'Access denied');
369
- }
370
- catch (err) {
371
- if (err instanceof access_denied_error_js_1.AccessDeniedError) {
372
- return {
373
- issuer: this.issuer,
374
- client: await this.clientManager.getClient(clientId),
375
- parameters: err.parameters,
376
- redirect: err.toJSON(),
377
- };
378
- }
379
- throw err;
380
- }
367
+ const { parameters } = await this.requestManager.get(uri, clientId, deviceId);
368
+ await this.deleteRequest(uri, parameters);
369
+ return {
370
+ issuer: this.issuer,
371
+ parameters: parameters,
372
+ redirect: {
373
+ error: 'access_denied',
374
+ error_description: 'Access denied',
375
+ },
376
+ };
381
377
  }
382
- async token(input, dpopJkt) {
383
- const client = await this.clientManager.getClient(input.client_id);
384
- const clientAuth = await this.authenticateClient(client, input);
385
- if (!client.metadata.grant_types.includes(input.grant_type)) {
386
- throw new invalid_grant_error_js_1.InvalidGrantError(`"${input.grant_type}" grant type is not allowed for this client`);
378
+ async token(credentials, request, dpopJkt) {
379
+ const [client, clientAuth] = await this.authenticateClient(credentials);
380
+ if (!this.metadata.grant_types_supported?.includes(request.grant_type)) {
381
+ throw new invalid_grant_error_js_1.InvalidGrantError(`Grant type "${request.grant_type}" is not supported by the server`);
387
382
  }
388
- if (input.grant_type === 'authorization_code') {
389
- return this.codeGrant(client, clientAuth, input, dpopJkt);
383
+ if (!client.metadata.grant_types.includes(request.grant_type)) {
384
+ throw new invalid_grant_error_js_1.InvalidGrantError(`"${request.grant_type}" grant type is not allowed for this client`);
390
385
  }
391
- if (input.grant_type === 'refresh_token') {
392
- return this.refreshTokenGrant(client, clientAuth, input, dpopJkt);
386
+ if (request.grant_type === 'authorization_code') {
387
+ return this.codeGrant(client, clientAuth, request, dpopJkt);
393
388
  }
394
- throw new invalid_grant_error_js_1.InvalidGrantError(
395
- // @ts-expect-error: fool proof
396
- `Grant type "${input.grant_type}" not supported`);
389
+ if (request.grant_type === 'refresh_token') {
390
+ return this.refreshTokenGrant(client, clientAuth, request, dpopJkt);
391
+ }
392
+ throw new invalid_grant_error_js_1.InvalidGrantError(`Grant type "${request.grant_type}" not supported`);
397
393
  }
398
394
  async codeGrant(client, clientAuth, input, dpopJkt) {
399
395
  try {
400
- const { sub, deviceId, parameters } = await this.requestManager.findCode(client, clientAuth, input.code);
396
+ const code = code_js_1.codeSchema.parse(input.code);
397
+ const { sub, deviceId, parameters } = await this.requestManager.findCode(client, clientAuth, code);
401
398
  // the following check prevents re-use of PKCE challenges, enforcing the
402
399
  // clients to generate a new challenge for each authorization request. The
403
400
  // replay manager typically prevents replay over a certain time frame,
@@ -436,17 +433,16 @@ class OAuthProvider extends oauth_verifier_js_1.OAuthVerifier {
436
433
  /**
437
434
  * @see {@link https://datatracker.ietf.org/doc/html/rfc7009#section-2.1 rfc7009}
438
435
  */
439
- async revoke(input) {
436
+ async revoke({ token }) {
440
437
  // @TODO this should also remove the account-device association (or, at
441
438
  // least, mark it as expired)
442
- await this.tokenManager.revoke(input.token);
439
+ await this.tokenManager.revoke(token);
443
440
  }
444
441
  /**
445
442
  * @see {@link https://datatracker.ietf.org/doc/html/rfc7662#section-2.1 rfc7662}
446
443
  */
447
- async introspect(input) {
448
- const client = await this.clientManager.getClient(input.client_id);
449
- const clientAuth = await this.authenticateClient(client, input);
444
+ async introspect(credentials, { token }) {
445
+ const [client, clientAuth] = await this.authenticateClient(credentials);
450
446
  // RFC7662 states the following:
451
447
  //
452
448
  // > To prevent token scanning attacks, the endpoint MUST also require some
@@ -460,7 +456,7 @@ class OAuthProvider extends oauth_verifier_js_1.OAuthVerifier {
460
456
  }
461
457
  const start = Date.now();
462
458
  try {
463
- const tokenInfo = await this.tokenManager.clientTokenInfo(client, clientAuth, input.token);
459
+ const tokenInfo = await this.tokenManager.clientTokenInfo(client, clientAuth, token);
464
460
  return {
465
461
  active: true,
466
462
  scope: tokenInfo.data.parameters.scope,
@@ -499,9 +495,7 @@ class OAuthProvider extends oauth_verifier_js_1.OAuthVerifier {
499
495
  const router = this.buildRouter(options);
500
496
  return router.buildHandler();
501
497
  }
502
- buildRouter({ onError = process.env['NODE_ENV'] === 'development'
503
- ? (req, res, err, msg) => console.error(`OAuthProvider error (${msg}):`, err)
504
- : undefined, } = {}) {
498
+ buildRouter(options) {
505
499
  const deviceManager = new device_manager_js_1.DeviceManager(this.deviceStore);
506
500
  const outputManager = new output_manager_js_1.OutputManager(this.customization);
507
501
  // eslint-disable-next-line @typescript-eslint/no-this-alias
@@ -511,6 +505,10 @@ class OAuthProvider extends oauth_verifier_js_1.OAuthVerifier {
511
505
  const router = new index_js_1.Router(issuerUrl);
512
506
  // Utils
513
507
  const csrfCookie = (uri) => `csrf-${uri}`;
508
+ const onError = options?.onError ??
509
+ (process.env['NODE_ENV'] === 'development'
510
+ ? (req, res, err, msg) => console.error(`OAuthProvider error (${msg}):`, err)
511
+ : undefined);
514
512
  /**
515
513
  * Creates a middleware that will serve static JSON content.
516
514
  */
@@ -562,7 +560,7 @@ class OAuthProvider extends oauth_verifier_js_1.OAuthVerifier {
562
560
  // OAuthError are used to build expected responses, so we don't log
563
561
  // them as errors.
564
562
  if (!(err instanceof oauth_error_js_1.OAuthError) || err.statusCode >= 500) {
565
- await onError?.(req, res, err, 'Unexpected error');
563
+ onError?.(req, res, err, 'Unexpected error');
566
564
  }
567
565
  }
568
566
  };
@@ -582,12 +580,30 @@ class OAuthProvider extends oauth_verifier_js_1.OAuthVerifier {
582
580
  }
583
581
  }
584
582
  catch (err) {
585
- await onError?.(req, res, err, `Failed to handle navigation request to "${req.url}"`);
583
+ onError?.(req, res, err, `Failed to handle navigation request to "${req.url}"`);
586
584
  if (!res.headersSent) {
587
585
  await outputManager.sendErrorPage(res, err);
588
586
  }
589
587
  }
590
588
  };
589
+ /**
590
+ * Provides a better UX when a request is denied by redirecting to the
591
+ * client with the error details. This will also log any error that caused
592
+ * the access to be denied (such as system errors).
593
+ */
594
+ const accessDeniedToRedirectCatcher = (req, res, err) => {
595
+ if (err instanceof access_denied_error_js_1.AccessDeniedError && err.parameters.redirect_uri) {
596
+ const { cause } = err;
597
+ if (cause)
598
+ onError?.(req, res, cause, 'Access denied');
599
+ return {
600
+ issuer: server.issuer,
601
+ parameters: err.parameters,
602
+ redirect: err.toJSON(),
603
+ };
604
+ }
605
+ throw err;
606
+ };
591
607
  //- Public OAuth endpoints
592
608
  router.get('/.well-known/oauth-authorization-server', staticJson(server.metadata));
593
609
  // CORS preflight
@@ -619,9 +635,15 @@ class OAuthProvider extends oauth_verifier_js_1.OAuthVerifier {
619
635
  router.get('/oauth/jwks', staticJson(server.jwks));
620
636
  router.options('/oauth/par', corsPreflight);
621
637
  router.post('/oauth/par', jsonHandler(async function (req, _res) {
622
- const input = await validateRequest(req, types_js_1.pushedAuthorizationRequestSchema);
638
+ const payload = await (0, index_js_1.parseHttpRequest)(req, ['json', 'urlencoded']);
639
+ const credentials = await oauth_types_1.oauthClientCredentialsSchema
640
+ .parseAsync(payload, { path: ['body'] })
641
+ .catch(throwInvalidRequest);
642
+ const authorizationRequest = await oauth_types_1.oauthAuthorizationRequestParSchema
643
+ .parseAsync(payload, { path: ['body'] })
644
+ .catch(throwInvalidRequest);
623
645
  const dpopJkt = await server.checkDpopProof(req.headers['dpop'], req.method, this.url);
624
- return server.pushedAuthorizationRequest(input, dpopJkt);
646
+ return server.pushedAuthorizationRequest(credentials, authorizationRequest, dpopJkt);
625
647
  }, 201));
626
648
  // https://datatracker.ietf.org/doc/html/rfc9126#section-2.3
627
649
  // > If the request did not use the POST method, the authorization server
@@ -632,15 +654,24 @@ class OAuthProvider extends oauth_verifier_js_1.OAuthVerifier {
632
654
  });
633
655
  router.options('/oauth/token', corsPreflight);
634
656
  router.post('/oauth/token', jsonHandler(async function (req, _res) {
635
- const input = await validateRequest(req, types_js_2.tokenRequestSchema);
657
+ const payload = await (0, index_js_1.parseHttpRequest)(req, ['json', 'urlencoded']);
658
+ const credentials = await oauth_types_1.oauthClientCredentialsSchema
659
+ .parseAsync(payload, { path: ['body'] })
660
+ .catch(throwInvalidClient);
661
+ const tokenRequest = await oauth_types_1.oauthTokenRequestSchema
662
+ .parseAsync(payload, { path: ['body'] })
663
+ .catch(throwInvalidGrant);
636
664
  const dpopJkt = await server.checkDpopProof(req.headers['dpop'], req.method, this.url);
637
- return server.token(input, dpopJkt);
665
+ return server.token(credentials, tokenRequest, dpopJkt);
638
666
  }));
639
667
  router.options('/oauth/revoke', corsPreflight);
640
668
  router.post('/oauth/revoke', jsonHandler(async function (req, res) {
641
- const input = await validateRequest(req, types_js_2.revokeSchema);
669
+ const payload = await (0, index_js_1.parseHttpRequest)(req, ['json', 'urlencoded']);
670
+ const tokenIdentification = await oauth_types_1.oauthTokenIdentificationSchema
671
+ .parseAsync(payload, { path: ['body'] })
672
+ .catch(throwInvalidRequest);
642
673
  try {
643
- await server.revoke(input);
674
+ await server.revoke(tokenIdentification);
644
675
  }
645
676
  catch (err) {
646
677
  onError?.(req, res, err, 'Failed to revoke token');
@@ -649,9 +680,11 @@ class OAuthProvider extends oauth_verifier_js_1.OAuthVerifier {
649
680
  router.options('/oauth/revoke', corsPreflight);
650
681
  router.get('/oauth/revoke', navigationHandler(async function (req, res) {
651
682
  const query = Object.fromEntries(this.url.searchParams);
652
- const input = types_js_2.revokeSchema.parse(query, { path: ['query'] });
683
+ const tokenIdentification = await oauth_types_1.oauthTokenIdentificationSchema
684
+ .parseAsync(query, { path: ['query'] })
685
+ .catch(throwInvalidRequest);
653
686
  try {
654
- await server.revoke(input);
687
+ await server.revoke(tokenIdentification);
655
688
  }
656
689
  catch (err) {
657
690
  onError?.(req, res, err, 'Failed to revoke token');
@@ -661,19 +694,33 @@ class OAuthProvider extends oauth_verifier_js_1.OAuthVerifier {
661
694
  throw new Error('You are successfully logged out. Redirect not implemented');
662
695
  }));
663
696
  router.post('/oauth/introspect', jsonHandler(async function (req, _res) {
664
- const input = await validateRequest(req, types_js_2.introspectSchema);
665
- return server.introspect(input);
697
+ const payload = await (0, index_js_1.parseHttpRequest)(req, ['json', 'urlencoded']);
698
+ const credentials = await oauth_types_1.oauthClientCredentialsSchema
699
+ .parseAsync(payload, { path: ['body'] })
700
+ .catch(throwInvalidRequest);
701
+ const tokenIdentification = await oauth_types_1.oauthTokenIdentificationSchema
702
+ .parseAsync(payload, { path: ['body'] })
703
+ .catch(throwInvalidRequest);
704
+ return server.introspect(credentials, tokenIdentification);
666
705
  }));
667
706
  //- Private authorization endpoints
668
707
  router.use((0, assets_middleware_js_1.authorizeAssetsMiddleware)());
669
708
  router.get('/oauth/authorize', navigationHandler(async function (req, res) {
670
709
  (0, index_js_1.validateFetchSite)(req, res, ['cross-site', 'none']);
671
710
  const query = Object.fromEntries(this.url.searchParams);
672
- const input = await types_js_1.authorizationRequestQuerySchema.parseAsync(query, {
673
- path: ['query'],
674
- });
711
+ const credentials = await oauth_types_1.oauthClientCredentialsSchema
712
+ .parseAsync(query, { path: ['body'] })
713
+ .catch(throwInvalidRequest);
714
+ if ('client_secret' in credentials) {
715
+ throw new invalid_request_error_js_1.InvalidRequestError('Client secret must not be provided');
716
+ }
717
+ const authorizationRequest = await oauth_types_1.oauthAuthorizationRequestQuerySchema
718
+ .parseAsync(query, { path: ['query'] })
719
+ .catch(throwInvalidRequest);
675
720
  const { deviceId } = await deviceManager.load(req, res);
676
- const data = await server.authorize(deviceId, input);
721
+ const data = await server
722
+ .authorize(deviceId, credentials, authorizationRequest)
723
+ .catch((err) => accessDeniedToRedirectCatcher(req, res, err));
677
724
  switch (true) {
678
725
  case 'redirect' in data: {
679
726
  return (0, send_authorize_redirect_js_1.sendAuthorizeRedirect)(res, data);
@@ -699,7 +746,10 @@ class OAuthProvider extends oauth_verifier_js_1.OAuthVerifier {
699
746
  (0, index_js_1.validateFetchMode)(req, res, ['same-origin']);
700
747
  (0, index_js_1.validateFetchSite)(req, res, ['same-origin']);
701
748
  (0, index_js_1.validateSameOrigin)(req, res, issuerOrigin);
702
- const input = await validateRequest(req, signInPayloadSchema);
749
+ const payload = await (0, index_js_1.parseHttpRequest)(req, ['json']);
750
+ const input = await signInPayloadSchema.parseAsync(payload, {
751
+ path: ['body'],
752
+ });
703
753
  (0, index_js_1.validateReferer)(req, res, {
704
754
  origin: issuerOrigin,
705
755
  pathname: '/oauth/authorize',
@@ -739,7 +789,9 @@ class OAuthProvider extends oauth_verifier_js_1.OAuthVerifier {
739
789
  });
740
790
  (0, index_js_1.validateCsrfToken)(req, res, input.csrf_token, csrfCookie(input.request_uri), true);
741
791
  const { deviceId } = await deviceManager.load(req, res);
742
- const data = await server.acceptRequest(deviceId, input.request_uri, input.client_id, input.account_sub);
792
+ const data = await server
793
+ .acceptRequest(deviceId, input.request_uri, input.client_id, input.account_sub)
794
+ .catch((err) => accessDeniedToRedirectCatcher(req, res, err));
743
795
  return await (0, send_authorize_redirect_js_1.sendAuthorizeRedirect)(res, data);
744
796
  }));
745
797
  const rejectQuerySchema = zod_1.default.object({
@@ -772,27 +824,33 @@ class OAuthProvider extends oauth_verifier_js_1.OAuthVerifier {
772
824
  });
773
825
  (0, index_js_1.validateCsrfToken)(req, res, input.csrf_token, csrfCookie(input.request_uri), true);
774
826
  const { deviceId } = await deviceManager.load(req, res);
775
- const data = await server.rejectRequest(deviceId, input.request_uri, input.client_id);
827
+ const data = await server
828
+ .rejectRequest(deviceId, input.request_uri, input.client_id)
829
+ .catch((err) => accessDeniedToRedirectCatcher(req, res, err));
776
830
  return await (0, send_authorize_redirect_js_1.sendAuthorizeRedirect)(res, data);
777
831
  }));
778
832
  return router;
779
833
  }
780
834
  }
781
835
  exports.OAuthProvider = OAuthProvider;
782
- async function validateRequest(req, schema) {
783
- try {
784
- return await (0, index_js_1.validateRequestPayload)(req, schema);
785
- }
786
- catch (err) {
787
- if (err instanceof zod_1.ZodError) {
788
- const issue = err.issues[0];
789
- if (issue?.path.length) {
790
- // "part" will typically be
791
- const [part, ...path] = issue.path;
792
- throw new invalid_request_error_js_1.InvalidRequestError(`Validation of ${part}'s "${path.join('.')}" with error: ${issue.message}`, err);
793
- }
836
+ function throwInvalidGrant(err) {
837
+ throw new invalid_grant_error_js_1.InvalidGrantError(extractZodErrorMessage(err) || 'Invalid grant', err);
838
+ }
839
+ function throwInvalidClient(err) {
840
+ throw new invalid_client_error_js_1.InvalidClientError(extractZodErrorMessage(err) || 'Client authentication failed', err);
841
+ }
842
+ function throwInvalidRequest(err) {
843
+ throw new invalid_request_error_js_1.InvalidRequestError(extractZodErrorMessage(err) || 'Input validation error', err);
844
+ }
845
+ function extractZodErrorMessage(err) {
846
+ if (err instanceof zod_1.ZodError) {
847
+ const issue = err.issues[0];
848
+ if (issue?.path.length) {
849
+ // "part" will typically be "body" or "query"
850
+ const [part, ...path] = issue.path;
851
+ return `Validation of "${path.join('.')}" ${part} parameter failed: ${issue.message}`;
794
852
  }
795
- throw new invalid_request_error_js_1.InvalidRequestError('Input validation error', err);
796
853
  }
854
+ return undefined;
797
855
  }
798
856
  //# sourceMappingURL=oauth-provider.js.map