@atproto/oauth-provider 0.2.0 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
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