@atproto/oauth-provider 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +36 -0
- package/dist/account/account-store.d.ts +2 -2
- package/dist/assets/app/bundle-manifest.json +3 -3
- package/dist/assets/app/main.css +1 -1
- package/dist/assets/app/main.js +3 -3
- package/dist/assets/app/main.js.map +1 -1
- package/dist/assets/assets-middleware.d.ts.map +1 -1
- package/dist/assets/assets-middleware.js +4 -2
- package/dist/assets/assets-middleware.js.map +1 -1
- package/dist/client/client-manager.d.ts.map +1 -1
- package/dist/client/client-manager.js +127 -118
- package/dist/client/client-manager.js.map +1 -1
- package/dist/client/client-utils.d.ts +1 -2
- package/dist/client/client-utils.d.ts.map +1 -1
- package/dist/client/client-utils.js +3 -12
- package/dist/client/client-utils.js.map +1 -1
- package/dist/client/client.d.ts +8 -3
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +70 -1
- package/dist/client/client.js.map +1 -1
- package/dist/constants.d.ts +0 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +1 -2
- package/dist/constants.js.map +1 -1
- package/dist/errors/access-denied-error.d.ts +4 -4
- package/dist/errors/access-denied-error.d.ts.map +1 -1
- package/dist/errors/access-denied-error.js +2 -2
- package/dist/errors/access-denied-error.js.map +1 -1
- package/dist/errors/account-selection-required-error.d.ts +2 -2
- package/dist/errors/account-selection-required-error.d.ts.map +1 -1
- package/dist/errors/account-selection-required-error.js.map +1 -1
- package/dist/errors/consent-required-error.d.ts +2 -2
- package/dist/errors/consent-required-error.d.ts.map +1 -1
- package/dist/errors/consent-required-error.js.map +1 -1
- package/dist/errors/invalid-authorization-details-error.d.ts +2 -2
- package/dist/errors/invalid-authorization-details-error.d.ts.map +1 -1
- package/dist/errors/invalid-authorization-details-error.js.map +1 -1
- package/dist/errors/invalid-client-id-error.d.ts +1 -1
- package/dist/errors/invalid-client-id-error.d.ts.map +1 -1
- package/dist/errors/invalid-client-id-error.js +12 -6
- package/dist/errors/invalid-client-id-error.js.map +1 -1
- package/dist/errors/invalid-client-metadata-error.d.ts +1 -1
- package/dist/errors/invalid-client-metadata-error.d.ts.map +1 -1
- package/dist/errors/invalid-client-metadata-error.js +11 -3
- package/dist/errors/invalid-client-metadata-error.js.map +1 -1
- package/dist/errors/invalid-parameters-error.d.ts +2 -2
- package/dist/errors/invalid-parameters-error.d.ts.map +1 -1
- package/dist/errors/invalid-parameters-error.js.map +1 -1
- package/dist/errors/invalid-scope-error.d.ts +9 -0
- package/dist/errors/invalid-scope-error.d.ts.map +1 -0
- package/dist/errors/invalid-scope-error.js +14 -0
- package/dist/errors/invalid-scope-error.js.map +1 -0
- package/dist/errors/login-required-error.d.ts +2 -2
- package/dist/errors/login-required-error.d.ts.map +1 -1
- package/dist/errors/login-required-error.js.map +1 -1
- package/dist/lib/html/html.d.ts +1 -1
- package/dist/lib/html/html.d.ts.map +1 -1
- package/dist/lib/html/html.js +14 -11
- package/dist/lib/html/html.js.map +1 -1
- package/dist/lib/http/parser.d.ts +9 -2
- package/dist/lib/http/parser.d.ts.map +1 -1
- package/dist/lib/http/parser.js +15 -7
- package/dist/lib/http/parser.js.map +1 -1
- package/dist/lib/http/request.d.ts +0 -23
- package/dist/lib/http/request.d.ts.map +1 -1
- package/dist/lib/http/request.js +1 -11
- package/dist/lib/http/request.js.map +1 -1
- package/dist/lib/http/stream.d.ts +28 -6
- package/dist/lib/http/stream.d.ts.map +1 -1
- package/dist/lib/http/stream.js +21 -32
- package/dist/lib/http/stream.js.map +1 -1
- package/dist/lib/util/authorization-header.d.ts.map +1 -1
- package/dist/lib/util/authorization-header.js +1 -1
- package/dist/lib/util/authorization-header.js.map +1 -1
- package/dist/lib/util/hostname.d.ts +3 -2
- package/dist/lib/util/hostname.d.ts.map +1 -1
- package/dist/lib/util/hostname.js +12 -8
- package/dist/lib/util/hostname.js.map +1 -1
- package/dist/metadata/build-metadata.d.ts.map +1 -1
- package/dist/metadata/build-metadata.js +2 -1
- package/dist/metadata/build-metadata.js.map +1 -1
- package/dist/oauth-errors.d.ts +1 -0
- package/dist/oauth-errors.d.ts.map +1 -1
- package/dist/oauth-errors.js +3 -1
- package/dist/oauth-errors.js.map +1 -1
- package/dist/oauth-hooks.d.ts +3 -3
- package/dist/oauth-hooks.d.ts.map +1 -1
- package/dist/oauth-provider.d.ts +20 -22
- package/dist/oauth-provider.d.ts.map +1 -1
- package/dist/oauth-provider.js +234 -176
- package/dist/oauth-provider.js.map +1 -1
- package/dist/oauth-verifier.d.ts +2 -2
- package/dist/oauth-verifier.d.ts.map +1 -1
- package/dist/oauth-verifier.js.map +1 -1
- package/dist/output/build-authorize-data.d.ts +2 -2
- package/dist/output/build-authorize-data.d.ts.map +1 -1
- package/dist/output/send-authorize-redirect.d.ts +2 -4
- package/dist/output/send-authorize-redirect.d.ts.map +1 -1
- package/dist/output/send-authorize-redirect.js +5 -2
- package/dist/output/send-authorize-redirect.js.map +1 -1
- package/dist/request/request-data.d.ts +2 -2
- package/dist/request/request-data.d.ts.map +1 -1
- package/dist/request/request-info.d.ts +2 -2
- package/dist/request/request-info.d.ts.map +1 -1
- package/dist/request/request-manager.d.ts +4 -4
- package/dist/request/request-manager.d.ts.map +1 -1
- package/dist/request/request-manager.js +94 -60
- package/dist/request/request-manager.js.map +1 -1
- package/dist/signer/signed-token-payload.d.ts +122 -122
- package/dist/signer/signer.d.ts +41 -40
- package/dist/signer/signer.d.ts.map +1 -1
- package/dist/signer/signer.js +13 -15
- package/dist/signer/signer.js.map +1 -1
- package/dist/token/token-claims.d.ts +121 -121
- package/dist/token/token-data.d.ts +3 -3
- package/dist/token/token-data.d.ts.map +1 -1
- package/dist/token/token-manager.d.ts +4 -5
- package/dist/token/token-manager.d.ts.map +1 -1
- package/dist/token/token-manager.js +96 -72
- package/dist/token/token-manager.js.map +1 -1
- package/dist/token/verify-token-claims.d.ts +3 -3
- package/dist/token/verify-token-claims.d.ts.map +1 -1
- package/dist/token/verify-token-claims.js.map +1 -1
- package/package.json +7 -6
- package/src/assets/app/components/sign-in-form.tsx +31 -2
- package/src/assets/assets-middleware.ts +4 -2
- package/src/client/client-manager.ts +163 -161
- package/src/client/client-utils.ts +7 -12
- package/src/client/client.ts +112 -3
- package/src/constants.ts +0 -2
- package/src/errors/access-denied-error.ts +10 -4
- package/src/errors/account-selection-required-error.ts +2 -2
- package/src/errors/consent-required-error.ts +2 -2
- package/src/errors/invalid-authorization-details-error.ts +2 -2
- package/src/errors/invalid-client-id-error.ts +15 -4
- package/src/errors/invalid-client-metadata-error.ts +15 -3
- package/src/errors/invalid-parameters-error.ts +2 -2
- package/src/errors/invalid-scope-error.ts +15 -0
- package/src/errors/login-required-error.ts +2 -2
- package/src/lib/html/html.ts +14 -12
- package/src/lib/http/parser.ts +21 -8
- package/src/lib/http/request.ts +1 -23
- package/src/lib/http/stream.ts +29 -60
- package/src/lib/util/authorization-header.ts +5 -2
- package/src/lib/util/hostname.ts +9 -5
- package/src/metadata/build-metadata.ts +3 -1
- package/src/oauth-errors.ts +1 -0
- package/src/oauth-hooks.ts +3 -3
- package/src/oauth-provider.ts +368 -269
- package/src/oauth-verifier.ts +2 -2
- package/src/output/build-authorize-data.ts +2 -2
- package/src/output/send-authorize-redirect.ts +7 -6
- package/src/request/request-data.ts +2 -2
- package/src/request/request-info.ts +2 -2
- package/src/request/request-manager.ts +129 -103
- package/src/signer/signer.ts +24 -25
- package/src/token/token-data.ts +3 -3
- package/src/token/token-manager.ts +141 -99
- package/src/token/verify-token-claims.ts +3 -3
- package/dist/request/types.d.ts +0 -328
- package/dist/request/types.d.ts.map +0 -1
- package/dist/request/types.js +0 -27
- package/dist/request/types.js.map +0 -1
- package/dist/token/types.d.ts +0 -250
- package/dist/token/types.d.ts.map +0 -1
- package/dist/token/types.js +0 -36
- package/dist/token/types.js.map +0 -1
- package/src/request/types.ts +0 -48
- package/src/token/types.ts +0 -86
package/dist/oauth-provider.js
CHANGED
@@ -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(
|
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
|
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.
|
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(
|
175
|
+
async pushedAuthorizationRequest(credentials, authorizationRequest, dpopJkt) {
|
163
176
|
try {
|
164
|
-
const client = await this.
|
165
|
-
const
|
166
|
-
|
167
|
-
|
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
|
188
|
-
|
189
|
-
|
190
|
-
|
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
|
-
|
193
|
-
|
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' },
|
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
|
-
|
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
|
-
|
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
|
223
|
-
|
224
|
-
const
|
225
|
-
if (
|
226
|
-
|
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
|
-
|
244
|
-
|
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
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
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
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
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
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
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 {
|
339
|
-
|
340
|
-
|
341
|
-
|
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
|
-
|
357
|
-
|
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
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
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(
|
383
|
-
const client = await this.
|
384
|
-
|
385
|
-
|
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 (
|
389
|
-
|
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 (
|
392
|
-
return this.
|
386
|
+
if (request.grant_type === 'authorization_code') {
|
387
|
+
return this.codeGrant(client, clientAuth, request, dpopJkt);
|
393
388
|
}
|
394
|
-
|
395
|
-
|
396
|
-
|
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
|
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(
|
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(
|
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(
|
448
|
-
const client = await this.
|
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,
|
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({
|
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
|
-
|
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
|
-
|
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
|
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(
|
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
|
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(
|
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
|
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(
|
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
|
683
|
+
const tokenIdentification = await oauth_types_1.oauthTokenIdentificationSchema
|
684
|
+
.parseAsync(query, { path: ['query'] })
|
685
|
+
.catch(throwInvalidRequest);
|
653
686
|
try {
|
654
|
-
await server.revoke(
|
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
|
665
|
-
|
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
|
673
|
-
path: ['
|
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
|
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
|
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
|
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
|
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
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
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
|