@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.
- package/CHANGELOG.md +42 -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/app/components/url-viewer.tsx +3 -3
- 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
|