@telecrypt-io/storage 0.6.1 → 0.7.1
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/README.md +7 -10
- package/dist/TeleCryptIOStorage.d.ts +1 -1
- package/dist/TeleCryptIOStorage.js +39 -83
- package/dist/core/constants.d.ts +4 -4
- package/dist/core/constants.js +20 -14
- package/dist/core/http.d.ts +2 -2
- package/dist/core/http.js +24 -95
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +1 -0
- package/dist/core/oidc.d.ts +4 -6
- package/dist/core/oidc.js +111 -338
- package/dist/core/operations.js +82 -184
- package/package.json +2 -4
package/dist/core/oidc.js
CHANGED
|
@@ -11,35 +11,13 @@ import { StorageError } from "./errors.js";
|
|
|
11
11
|
import { cancelResponseBody, raceWithAbort, readResponseBody, ResponseBodyReadError, } from "./http.js";
|
|
12
12
|
import { validateCanonicalMatrixUserId, validateMatrixDeviceId, } from "./constants.js";
|
|
13
13
|
const AUTHORIZATION_CONTEXT_PREFIX = "telecrypt:oauth2:pkce:v1:";
|
|
14
|
-
const AUTHORIZATION_CONTEXT_VERSION = 1;
|
|
15
|
-
// The callback context is a short-lived transaction, not a durable login
|
|
16
|
-
// session. Ten minutes covers normal redirects while limiting replay after a
|
|
17
|
-
// stale browser tab or session-storage copy is recovered.
|
|
18
|
-
const AUTHORIZATION_CONTEXT_MAX_AGE_MS = 10 * 60 * 1000;
|
|
19
|
-
const AUTHORIZATION_CONTEXT_MAX_FUTURE_SKEW_MS = 30 * 1000;
|
|
20
|
-
const MAX_AUTHORIZATION_CONTEXT_LENGTH = 64 * 1024;
|
|
21
14
|
const PKCE_VERIFIER_PATTERN = /^[A-Za-z0-9._~-]{43,128}$/;
|
|
22
|
-
const DEVICE_ID_PATTERN = /^[A-Za-z0-9._~-]{1,128}$/;
|
|
23
|
-
const STATE_PATTERN = /^[A-Za-z0-9._~-]{32,128}$/;
|
|
24
15
|
const OIDC_REQUEST_TIMEOUT_MS = 30_000;
|
|
25
16
|
const OIDC_REFRESH_TIMEOUT_MS = 30_000;
|
|
26
17
|
const OIDC_CLEANUP_TIMEOUT_MS = 30_000;
|
|
27
|
-
const MAX_OIDC_URL_LENGTH = 2048;
|
|
28
|
-
const MAX_OIDC_METADATA_STRING_LENGTH = 4096;
|
|
29
|
-
const MAX_OIDC_CLIENT_ID_LENGTH = 512;
|
|
30
|
-
const MAX_OIDC_CODE_LENGTH = 4096;
|
|
31
|
-
const MAX_OIDC_TOKEN_LENGTH = 8192;
|
|
32
|
-
const MAX_OIDC_SCOPE_LENGTH = 4096;
|
|
33
|
-
const MAX_OIDC_CONTACTS = 16;
|
|
34
|
-
const MAX_OIDC_REDIRECT_URIS = 8;
|
|
35
18
|
const LEGACY_MATRIX_SCOPE_PREFIX = "urn:matrix:client:";
|
|
36
19
|
const MSC2967_MATRIX_SCOPE_PREFIX = "urn:matrix:org.matrix.msc2967.client:";
|
|
37
20
|
const MATRIX_SCOPE_PREFIXES = [LEGACY_MATRIX_SCOPE_PREFIX, MSC2967_MATRIX_SCOPE_PREFIX];
|
|
38
|
-
// MAS advertises a 20-minute device-code lifetime. Keep that provider window
|
|
39
|
-
// as the hard upper bound while retaining a finite polling budget.
|
|
40
|
-
const MAX_DEVICE_EXPIRES_IN_SECONDS = 20 * 60;
|
|
41
|
-
const MAX_DEVICE_INTERVAL_SECONDS = 5 * 60;
|
|
42
|
-
const MAX_DEVICE_POLL_DURATION_MS = MAX_DEVICE_EXPIRES_IN_SECONDS * 1000;
|
|
43
21
|
const deviceAuthorizationDeviceIds = new WeakMap();
|
|
44
22
|
const SAFE_OAUTH_ERROR_CODES = new Set([
|
|
45
23
|
"invalid_request",
|
|
@@ -82,7 +60,7 @@ class OidcValidationError extends StorageError {
|
|
|
82
60
|
this.name = "OidcValidationError";
|
|
83
61
|
}
|
|
84
62
|
}
|
|
85
|
-
const DIAGNOSTIC_SECRET_FIELD =
|
|
63
|
+
const DIAGNOSTIC_SECRET_FIELD = /(?:access[_-]?token|refresh[_-]?token|id[_-]?token|client[_-]?id|client[_-]?secret|user[_-]?id|device[_-]?id|authorization(?:[_-]?code)?|device[_-]?code|user[_-]?code|code[_-]?verifier|token|credential|private[_-]?key|encryption|signing|password|secret|api[_-]?key|recovery[_-]?key|cookie|session)/iu;
|
|
86
64
|
const DIAGNOSTIC_EMAIL = /[A-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Z0-9-]+(?:\.[A-Z0-9-]+)+/giu;
|
|
87
65
|
const DIAGNOSTIC_MXID = /@[A-Z0-9._=+\-/]+:[A-Z0-9.-]+/giu;
|
|
88
66
|
const DIAGNOSTIC_ULID = /\b[0-9A-HJKMNP-TV-Z]{26}\b/giu;
|
|
@@ -112,194 +90,19 @@ class OidcResponseError extends Error {
|
|
|
112
90
|
this.status = status;
|
|
113
91
|
}
|
|
114
92
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const failures = [];
|
|
127
|
-
let keys;
|
|
128
|
-
try {
|
|
129
|
-
keys = Reflect.ownKeys(value);
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
failures.push(`own properties unavailable: ${diagnosticFailureText(error, seen)}`);
|
|
133
|
-
return { properties, failures };
|
|
134
|
-
}
|
|
135
|
-
for (const key of keys) {
|
|
136
|
-
let descriptor;
|
|
137
|
-
try {
|
|
138
|
-
descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
139
|
-
}
|
|
140
|
-
catch (error) {
|
|
141
|
-
failures.push(`${diagnosticDisplayKey(key)} unavailable: ${diagnosticPropertyIsSecret(key) ? "<redacted>" : diagnosticFailureText(error, seen)}`);
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
if (!descriptor)
|
|
145
|
-
continue;
|
|
146
|
-
try {
|
|
147
|
-
if ("value" in descriptor) {
|
|
148
|
-
properties.push({ key, value: descriptor.value });
|
|
149
|
-
}
|
|
150
|
-
else if (typeof descriptor.get === "function") {
|
|
151
|
-
properties.push({ key, value: Reflect.get(value, key) });
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
failures.push(`${diagnosticDisplayKey(key)} unavailable: ${diagnosticPropertyIsSecret(key) ? "<redacted>" : "accessor has no getter"}`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
catch (error) {
|
|
158
|
-
failures.push(`${diagnosticDisplayKey(key)} unavailable: ${diagnosticPropertyIsSecret(key) ? "<redacted>" : diagnosticFailureText(error, seen)}`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return { properties, failures };
|
|
162
|
-
}
|
|
163
|
-
function diagnosticObjectText(value, seen) {
|
|
164
|
-
const inspection = readDiagnosticProperties(value, seen);
|
|
165
|
-
const parts = inspection.failures.slice();
|
|
166
|
-
for (const property of inspection.properties) {
|
|
167
|
-
const key = diagnosticDisplayKey(property.key);
|
|
168
|
-
parts.push(`${key}: ${diagnosticPropertyIsSecret(property.key) ? "<redacted>" : diagnosticValueText(property.value, seen)}`);
|
|
169
|
-
}
|
|
170
|
-
if (parts.length === 0) {
|
|
171
|
-
return "object { }";
|
|
172
|
-
}
|
|
173
|
-
return `object { ${parts.join(", ")} }`;
|
|
174
|
-
}
|
|
175
|
-
function diagnosticValueText(value, seen) {
|
|
176
|
-
if (value !== null && (typeof value === "object" || typeof value === "function")) {
|
|
177
|
-
if (seen.has(value))
|
|
178
|
-
return "[cyclic diagnostic]";
|
|
179
|
-
seen.add(value);
|
|
180
|
-
}
|
|
181
|
-
if (value instanceof AggregateError) {
|
|
182
|
-
const parts = [`${sanitizeDiagnosticText(value.name)}: ${sanitizeDiagnosticText(value.message)}`];
|
|
183
|
-
if (typeof value.stack === "string")
|
|
184
|
-
parts.push(`stack: ${sanitizeDiagnosticText(value.stack)}`);
|
|
185
|
-
try {
|
|
186
|
-
parts.push(`children: ${value.errors.map((child) => diagnosticValueText(child, seen)).join(", ")}`);
|
|
187
|
-
}
|
|
188
|
-
catch (error) {
|
|
189
|
-
parts.push(`children unavailable: ${diagnosticFailureText(error, seen)}`);
|
|
190
|
-
}
|
|
191
|
-
const inspection = readDiagnosticProperties(value, seen);
|
|
192
|
-
for (const property of inspection.properties) {
|
|
193
|
-
if (["name", "message", "stack", "cause", "errors"].includes(diagnosticKey(property.key)))
|
|
194
|
-
continue;
|
|
195
|
-
const key = diagnosticDisplayKey(property.key);
|
|
196
|
-
parts.push(`${key}: ${diagnosticPropertyIsSecret(property.key) ? "<redacted>" : diagnosticValueText(property.value, seen)}`);
|
|
197
|
-
}
|
|
198
|
-
parts.push(...inspection.failures);
|
|
199
|
-
return parts.join("; ");
|
|
200
|
-
}
|
|
201
|
-
if (value instanceof Error) {
|
|
202
|
-
const parts = [`${sanitizeDiagnosticText(value.name)}: ${sanitizeDiagnosticText(value.message)}`];
|
|
203
|
-
if (typeof value.stack === "string")
|
|
204
|
-
parts.push(`stack: ${sanitizeDiagnosticText(value.stack)}`);
|
|
205
|
-
try {
|
|
206
|
-
if (value.cause !== undefined)
|
|
207
|
-
parts.push(`cause: ${diagnosticValueText(value.cause, seen)}`);
|
|
208
|
-
}
|
|
209
|
-
catch (error) {
|
|
210
|
-
parts.push(`cause unavailable: ${diagnosticFailureText(error, seen)}`);
|
|
211
|
-
}
|
|
212
|
-
const inspection = readDiagnosticProperties(value, seen);
|
|
213
|
-
for (const property of inspection.properties) {
|
|
214
|
-
if (["name", "message", "stack", "cause", "bytes"].includes(diagnosticKey(property.key)))
|
|
215
|
-
continue;
|
|
216
|
-
const key = diagnosticDisplayKey(property.key);
|
|
217
|
-
parts.push(`${key}: ${diagnosticPropertyIsSecret(property.key) ? "<redacted>" : diagnosticValueText(property.value, seen)}`);
|
|
218
|
-
}
|
|
219
|
-
parts.push(...inspection.failures);
|
|
220
|
-
return parts.join("; ");
|
|
221
|
-
}
|
|
222
|
-
if (value !== null && (typeof value === "object" || typeof value === "function")) {
|
|
223
|
-
return diagnosticObjectText(value, seen);
|
|
224
|
-
}
|
|
225
|
-
try {
|
|
226
|
-
return sanitizeDiagnosticText(String(value));
|
|
227
|
-
}
|
|
228
|
-
catch {
|
|
229
|
-
return "unknown failure";
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
function diagnosticFailureText(error, seen) {
|
|
233
|
-
try {
|
|
234
|
-
return diagnosticValueText(error, seen);
|
|
235
|
-
}
|
|
236
|
-
catch {
|
|
237
|
-
return "unknown property failure";
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
/** Copy a transport error's useful details across the SDK boundary after the
|
|
241
|
-
* same secret/control-character sanitization used for provider responses. */
|
|
242
|
-
export function diagnosticCause(error, seen = new WeakSet()) {
|
|
243
|
-
if (error !== null && (typeof error === "object" || typeof error === "function")) {
|
|
244
|
-
if (seen.has(error))
|
|
245
|
-
return new Error("[cyclic diagnostic]");
|
|
93
|
+
/** Copy only a native transport error's message across the SDK boundary. */
|
|
94
|
+
function diagnosticCause(error, seen = new Set()) {
|
|
95
|
+
if (error instanceof Error && seen.has(error))
|
|
96
|
+
return new Error("cyclic failure cause");
|
|
97
|
+
const detail = error instanceof Error
|
|
98
|
+
? `${error.name}: ${error.message}`
|
|
99
|
+
: typeof error === "string"
|
|
100
|
+
? error
|
|
101
|
+
: "request failed";
|
|
102
|
+
const cause = error instanceof Error ? error.cause : undefined;
|
|
103
|
+
if (error instanceof Error)
|
|
246
104
|
seen.add(error);
|
|
247
|
-
}
|
|
248
|
-
if (error instanceof AggregateError) {
|
|
249
|
-
let children = [];
|
|
250
|
-
try {
|
|
251
|
-
if (Array.isArray(error.errors))
|
|
252
|
-
children = error.errors;
|
|
253
|
-
}
|
|
254
|
-
catch {
|
|
255
|
-
children = [];
|
|
256
|
-
}
|
|
257
|
-
const copy = new AggregateError(children.map((child) => diagnosticCause(child, seen)), diagnosticValueText(error, new WeakSet()));
|
|
258
|
-
copy.name = sanitizeDiagnosticText(error.name) || "AggregateError";
|
|
259
|
-
if (typeof error.stack === "string")
|
|
260
|
-
copy.stack = sanitizeDiagnosticText(error.stack);
|
|
261
|
-
let cause;
|
|
262
|
-
try {
|
|
263
|
-
cause = error.cause;
|
|
264
|
-
}
|
|
265
|
-
catch {
|
|
266
|
-
cause = undefined;
|
|
267
|
-
}
|
|
268
|
-
if (cause !== undefined) {
|
|
269
|
-
Object.defineProperty(copy, "cause", {
|
|
270
|
-
configurable: true,
|
|
271
|
-
enumerable: false,
|
|
272
|
-
value: diagnosticCause(cause, seen),
|
|
273
|
-
writable: true,
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
return copy;
|
|
277
|
-
}
|
|
278
|
-
if (error instanceof Error) {
|
|
279
|
-
const copy = new Error(diagnosticValueText(error, new WeakSet()));
|
|
280
|
-
copy.name = sanitizeDiagnosticText(error.name) || "Error";
|
|
281
|
-
if (typeof error.stack === "string")
|
|
282
|
-
copy.stack = sanitizeDiagnosticText(error.stack);
|
|
283
|
-
let cause;
|
|
284
|
-
try {
|
|
285
|
-
cause = error.cause;
|
|
286
|
-
}
|
|
287
|
-
catch {
|
|
288
|
-
cause = undefined;
|
|
289
|
-
}
|
|
290
|
-
if (cause !== undefined) {
|
|
291
|
-
Object.defineProperty(copy, "cause", {
|
|
292
|
-
configurable: true,
|
|
293
|
-
enumerable: false,
|
|
294
|
-
value: diagnosticCause(cause, seen),
|
|
295
|
-
writable: true,
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
return copy;
|
|
299
|
-
}
|
|
300
|
-
const copy = new Error(diagnosticValueText(error, new WeakSet()));
|
|
301
|
-
copy.name = "ThrownObject";
|
|
302
|
-
return copy;
|
|
105
|
+
return new Error(sanitizeDiagnosticText(detail) || "request failed", cause instanceof Error ? { cause: diagnosticCause(cause, seen) } : undefined);
|
|
303
106
|
}
|
|
304
107
|
function isRecord(value) {
|
|
305
108
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -311,7 +114,6 @@ function randomHex(byteLength) {
|
|
|
311
114
|
function requireString(value, name, pattern) {
|
|
312
115
|
if (typeof value !== "string" ||
|
|
313
116
|
value.length === 0 ||
|
|
314
|
-
value.length > MAX_OIDC_METADATA_STRING_LENGTH ||
|
|
315
117
|
/[\u0000-\u001f\u007f]/.test(value) ||
|
|
316
118
|
(pattern && !pattern.test(value))) {
|
|
317
119
|
throw new StorageError(`OIDC authorization context has an invalid ${name}`);
|
|
@@ -320,27 +122,32 @@ function requireString(value, name, pattern) {
|
|
|
320
122
|
}
|
|
321
123
|
function requireClientId(value) {
|
|
322
124
|
const clientId = requireString(value, "client ID");
|
|
323
|
-
if (
|
|
125
|
+
if (/\s/.test(clientId)) {
|
|
324
126
|
throw new StorageError("OIDC authorization context has an invalid client ID");
|
|
325
127
|
}
|
|
326
128
|
return clientId;
|
|
327
129
|
}
|
|
328
|
-
function
|
|
329
|
-
|
|
330
|
-
value
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
throw new StorageError(
|
|
130
|
+
function requireDeviceId(value) {
|
|
131
|
+
try {
|
|
132
|
+
return validateMatrixDeviceId(value);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
throw new StorageError("OIDC authorization context has an invalid device ID", { cause: error });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function isDeviceId(value) {
|
|
139
|
+
try {
|
|
140
|
+
validateMatrixDeviceId(value);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return false;
|
|
334
145
|
}
|
|
335
|
-
return value;
|
|
336
146
|
}
|
|
337
|
-
function
|
|
147
|
+
function requireOpaqueString(value, name) {
|
|
338
148
|
if (typeof value !== "string" ||
|
|
339
149
|
value.length === 0 ||
|
|
340
|
-
value
|
|
341
|
-
/[\u0000-\u001f\u007f]/.test(value) ||
|
|
342
|
-
value.trim() !== value ||
|
|
343
|
-
/\s{2,}/.test(value)) {
|
|
150
|
+
/[\s\u0000-\u001f\u007f]/.test(value)) {
|
|
344
151
|
throw new StorageError(`OIDC authorization context has an invalid ${name}`);
|
|
345
152
|
}
|
|
346
153
|
return value;
|
|
@@ -350,7 +157,7 @@ function isLoopbackHostname(hostname) {
|
|
|
350
157
|
return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1";
|
|
351
158
|
}
|
|
352
159
|
function parseSafeRedirectUri(value, name) {
|
|
353
|
-
const text =
|
|
160
|
+
const text = requireOpaqueString(value, name);
|
|
354
161
|
let parsed;
|
|
355
162
|
try {
|
|
356
163
|
parsed = new URL(text);
|
|
@@ -369,10 +176,9 @@ function parseSafeRedirectUri(value, name) {
|
|
|
369
176
|
}
|
|
370
177
|
return parsed;
|
|
371
178
|
}
|
|
372
|
-
function requireMetadataString(value, name
|
|
179
|
+
function requireMetadataString(value, name) {
|
|
373
180
|
if (typeof value !== "string" ||
|
|
374
181
|
value.length === 0 ||
|
|
375
|
-
value.length > maxLength ||
|
|
376
182
|
/[\u0000-\u001f\u007f]/.test(value)) {
|
|
377
183
|
throw new StorageError(`OIDC dynamic client registration has an invalid ${name}`);
|
|
378
184
|
}
|
|
@@ -388,30 +194,23 @@ function validateRegistrationMetadata(metadata) {
|
|
|
388
194
|
["policy URI", metadata.policyUri],
|
|
389
195
|
]) {
|
|
390
196
|
if (value !== undefined) {
|
|
391
|
-
|
|
392
|
-
if (uri.origin !== clientUri.origin) {
|
|
393
|
-
throw new StorageError("OIDC dynamic client registration has an untrusted metadata origin");
|
|
394
|
-
}
|
|
197
|
+
parseHttpUrl(value, name);
|
|
395
198
|
}
|
|
396
199
|
}
|
|
397
200
|
if (metadata.contacts !== undefined) {
|
|
398
|
-
if (!Array.isArray(metadata.contacts)
|
|
399
|
-
throw new StorageError("OIDC dynamic client registration has
|
|
201
|
+
if (!Array.isArray(metadata.contacts)) {
|
|
202
|
+
throw new StorageError("OIDC dynamic client registration has invalid contacts");
|
|
400
203
|
}
|
|
401
|
-
metadata.contacts.forEach((contact) => requireMetadataString(contact, "contact"
|
|
204
|
+
metadata.contacts.forEach((contact) => requireMetadataString(contact, "contact"));
|
|
402
205
|
}
|
|
403
206
|
if (metadata.redirectUris === undefined)
|
|
404
207
|
return { clientUri };
|
|
405
208
|
if (!Array.isArray(metadata.redirectUris) ||
|
|
406
|
-
metadata.redirectUris.length === 0
|
|
407
|
-
|
|
408
|
-
throw new StorageError("OIDC dynamic client registration has too many redirect URIs");
|
|
209
|
+
metadata.redirectUris.length === 0) {
|
|
210
|
+
throw new StorageError("OIDC dynamic client registration has invalid redirect URIs");
|
|
409
211
|
}
|
|
410
212
|
const redirectUris = metadata.redirectUris.map((value) => {
|
|
411
213
|
const uri = parseSafeRedirectUri(value, "redirect URI");
|
|
412
|
-
if (uri.origin !== clientUri.origin) {
|
|
413
|
-
throw new StorageError("OIDC dynamic client registration has an untrusted redirect origin");
|
|
414
|
-
}
|
|
415
214
|
return uri.toString();
|
|
416
215
|
});
|
|
417
216
|
return { clientUri, redirectUris };
|
|
@@ -451,7 +250,7 @@ async function assertNoRedirect(response, endpoint, operation) {
|
|
|
451
250
|
(response.url !== "" && response.url !== endpoint) ||
|
|
452
251
|
response.redirected) {
|
|
453
252
|
const failure = new StorageError(`OIDC ${operation} rejected an untrusted redirect`);
|
|
454
|
-
|
|
253
|
+
cancelResponseBody(response);
|
|
455
254
|
throw failure;
|
|
456
255
|
}
|
|
457
256
|
}
|
|
@@ -563,20 +362,28 @@ function abortableDelay(milliseconds, signal) {
|
|
|
563
362
|
return Promise.reject(new OidcRequestCancelledError("device authorization"));
|
|
564
363
|
return new Promise((resolve, reject) => {
|
|
565
364
|
let timer;
|
|
365
|
+
let remaining = milliseconds;
|
|
566
366
|
const abort = () => {
|
|
567
367
|
clearTimeout(timer);
|
|
568
368
|
signal?.removeEventListener("abort", abort);
|
|
569
369
|
reject(new OidcRequestCancelledError("device authorization"));
|
|
570
370
|
};
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
371
|
+
const schedule = () => {
|
|
372
|
+
if (remaining <= 0) {
|
|
373
|
+
signal?.removeEventListener("abort", abort);
|
|
374
|
+
resolve();
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const delay = Math.min(remaining, 2_147_483_647);
|
|
378
|
+
remaining -= delay;
|
|
379
|
+
timer = setTimeout(schedule, delay);
|
|
380
|
+
};
|
|
575
381
|
signal?.addEventListener("abort", abort, { once: true });
|
|
382
|
+
schedule();
|
|
576
383
|
});
|
|
577
384
|
}
|
|
578
|
-
function parseHttpUrl(value, name) {
|
|
579
|
-
const text =
|
|
385
|
+
function parseHttpUrl(value, name, allowQuery = false) {
|
|
386
|
+
const text = requireOpaqueString(value, name);
|
|
580
387
|
let parsed;
|
|
581
388
|
try {
|
|
582
389
|
parsed = new URL(text);
|
|
@@ -589,7 +396,7 @@ function parseHttpUrl(value, name) {
|
|
|
589
396
|
(parsed.protocol === "http:" && !isLoopbackHostname(parsed.hostname)) ||
|
|
590
397
|
parsed.username !== "" ||
|
|
591
398
|
parsed.password !== "" ||
|
|
592
|
-
parsed.search !== "" ||
|
|
399
|
+
(!allowQuery && parsed.search !== "") ||
|
|
593
400
|
parsed.hash !== "" ||
|
|
594
401
|
(parsed.toString() !== text && !isRootWithoutSlash)) {
|
|
595
402
|
throw new StorageError(`OIDC authorization context has an invalid ${name}`);
|
|
@@ -602,6 +409,15 @@ function isWithinIssuerPath(issuer, endpoint) {
|
|
|
602
409
|
const prefix = issuer.pathname.endsWith("/") ? issuer.pathname : `${issuer.pathname}/`;
|
|
603
410
|
return endpoint.pathname === issuer.pathname || endpoint.pathname.startsWith(prefix);
|
|
604
411
|
}
|
|
412
|
+
/** Validates an endpoint against the configured homeserver and optional issuer path. */
|
|
413
|
+
export function assertOidcEndpoint(value, trustedHomeserver, name, issuer, allowQuery = false) {
|
|
414
|
+
const homeserver = parseHttpUrl(trustedHomeserver, "homeserver URL");
|
|
415
|
+
const endpoint = parseHttpUrl(value, name, allowQuery);
|
|
416
|
+
if (endpoint.origin !== homeserver.origin || (issuer && !isWithinIssuerPath(issuer, endpoint))) {
|
|
417
|
+
throw new StorageError(`${name} must remain on the configured OIDC origin and issuer path`);
|
|
418
|
+
}
|
|
419
|
+
return endpoint.toString();
|
|
420
|
+
}
|
|
605
421
|
function matrixAuthMetadataEndpoint(homeserver) {
|
|
606
422
|
const basePath = homeserver.pathname.replace(/\/+$/u, "");
|
|
607
423
|
return new URL(`${basePath}/_matrix/client/v1/auth_metadata`, homeserver.origin).toString();
|
|
@@ -645,7 +461,6 @@ function validateAuthMetadata(value) {
|
|
|
645
461
|
if (field !== undefined &&
|
|
646
462
|
(typeof field !== "string" ||
|
|
647
463
|
field.length === 0 ||
|
|
648
|
-
field.length > MAX_OIDC_URL_LENGTH ||
|
|
649
464
|
/[\u0000-\u001f\u007f]/.test(field))) {
|
|
650
465
|
throw new StorageError("OIDC authorization context has invalid auth metadata");
|
|
651
466
|
}
|
|
@@ -664,7 +479,6 @@ function validateAuthMetadata(value) {
|
|
|
664
479
|
if (field !== undefined &&
|
|
665
480
|
(typeof field !== "string" ||
|
|
666
481
|
field.length === 0 ||
|
|
667
|
-
field.length > MAX_OIDC_URL_LENGTH ||
|
|
668
482
|
/[\u0000-\u001f\u007f]/.test(field))) {
|
|
669
483
|
throw new StorageError("OIDC authorization context has invalid auth metadata");
|
|
670
484
|
}
|
|
@@ -679,9 +493,8 @@ function validateAuthMetadata(value) {
|
|
|
679
493
|
]) {
|
|
680
494
|
const field = metadata[name];
|
|
681
495
|
if (field !== undefined &&
|
|
682
|
-
(!Array.isArray(field) || field.
|
|
496
|
+
(!Array.isArray(field) || field.some((item) => typeof item !== "string" ||
|
|
683
497
|
item.length === 0 ||
|
|
684
|
-
item.length > MAX_OIDC_METADATA_STRING_LENGTH ||
|
|
685
498
|
/[\u0000-\u001f\u007f]/.test(item)))) {
|
|
686
499
|
throw new StorageError("OIDC authorization context has invalid auth metadata");
|
|
687
500
|
}
|
|
@@ -724,21 +537,18 @@ function validateDeviceAuthorizationSession(value) {
|
|
|
724
537
|
throw new StorageError("OIDC device authorization returned an invalid response", { cause: error });
|
|
725
538
|
}
|
|
726
539
|
const session = value;
|
|
727
|
-
|
|
728
|
-
|
|
540
|
+
requireOpaqueString(session.device_code, "device code");
|
|
541
|
+
requireOpaqueString(session.user_code, "user code");
|
|
729
542
|
parseSafeRedirectUri(session.verification_uri, "verification URI");
|
|
730
543
|
if (session.verification_uri_complete !== undefined) {
|
|
731
544
|
parseSafeRedirectUri(session.verification_uri_complete, "complete verification URI");
|
|
732
545
|
}
|
|
733
|
-
if (!Number.
|
|
734
|
-
session.expires_in <= 0
|
|
735
|
-
session.expires_in > MAX_DEVICE_EXPIRES_IN_SECONDS) {
|
|
546
|
+
if (!Number.isSafeInteger(session.expires_in) ||
|
|
547
|
+
session.expires_in <= 0) {
|
|
736
548
|
throw new StorageError("OIDC device authorization returned an invalid expiry");
|
|
737
549
|
}
|
|
738
550
|
if (session.interval !== undefined &&
|
|
739
|
-
(!Number.
|
|
740
|
-
session.interval <= 0 ||
|
|
741
|
-
session.interval > MAX_DEVICE_INTERVAL_SECONDS)) {
|
|
551
|
+
(!Number.isSafeInteger(session.interval) || session.interval <= 0)) {
|
|
742
552
|
throw new StorageError("OIDC device authorization returned an invalid polling interval");
|
|
743
553
|
}
|
|
744
554
|
return session;
|
|
@@ -756,18 +566,16 @@ function validateDeviceAccessTokenResponse(value) {
|
|
|
756
566
|
}
|
|
757
567
|
function validateTokenBounds(value, operation) {
|
|
758
568
|
try {
|
|
759
|
-
|
|
569
|
+
requireOpaqueString(value.access_token, "access token");
|
|
760
570
|
if (value.refresh_token !== undefined) {
|
|
761
|
-
|
|
571
|
+
requireOpaqueString(value.refresh_token, "refresh token");
|
|
762
572
|
}
|
|
763
|
-
if (value.scope !== undefined)
|
|
764
|
-
requireScope(value.scope, "scope");
|
|
765
573
|
}
|
|
766
574
|
catch (error) {
|
|
767
575
|
throw new StorageError(`OIDC ${operation} returned an invalid token response`, { cause: error });
|
|
768
576
|
}
|
|
769
577
|
if (value.expires_in !== undefined &&
|
|
770
|
-
(!Number.
|
|
578
|
+
(!Number.isSafeInteger(value.expires_in) || value.expires_in < 0)) {
|
|
771
579
|
throw new StorageError(`OIDC ${operation} returned an invalid token expiry`);
|
|
772
580
|
}
|
|
773
581
|
}
|
|
@@ -787,26 +595,16 @@ function validateAuthorizationInputs(opts) {
|
|
|
787
595
|
}
|
|
788
596
|
requireClientId(opts.clientId);
|
|
789
597
|
const deviceId = opts.deviceId ?? randomHex(5).toUpperCase();
|
|
790
|
-
|
|
598
|
+
requireDeviceId(deviceId);
|
|
791
599
|
return { authMetadata, homeserver, redirect, deviceId };
|
|
792
600
|
}
|
|
793
601
|
function parseAuthorizationContext(value, expectedState) {
|
|
794
|
-
if (!isRecord(value)
|
|
602
|
+
if (!isRecord(value)) {
|
|
795
603
|
throw new StorageError("OIDC authorization context is missing or invalid");
|
|
796
604
|
}
|
|
797
605
|
if (value.state !== expectedState) {
|
|
798
606
|
throw new StorageError("OIDC authorization context state does not match callback state");
|
|
799
607
|
}
|
|
800
|
-
if (typeof value.createdAtMs !== "number" || !Number.isSafeInteger(value.createdAtMs)) {
|
|
801
|
-
throw new StorageError("OIDC authorization context has an invalid creation time");
|
|
802
|
-
}
|
|
803
|
-
const ageMs = Date.now() - value.createdAtMs;
|
|
804
|
-
if (ageMs < -AUTHORIZATION_CONTEXT_MAX_FUTURE_SKEW_MS) {
|
|
805
|
-
throw new StorageError("OIDC authorization context is from the future");
|
|
806
|
-
}
|
|
807
|
-
if (ageMs > AUTHORIZATION_CONTEXT_MAX_AGE_MS) {
|
|
808
|
-
throw new StorageError("OIDC authorization context is expired");
|
|
809
|
-
}
|
|
810
608
|
const authMetadata = validateTrustedAuthMetadata(value.authMetadata);
|
|
811
609
|
const homeserver = parseHttpUrl(value.homeserverUrl, "homeserver URL");
|
|
812
610
|
const redirect = parseHttpUrl(value.redirectUri, "redirect URI");
|
|
@@ -814,14 +612,12 @@ function parseAuthorizationContext(value, expectedState) {
|
|
|
814
612
|
throw new StorageError("OIDC authorization context origins are not trusted");
|
|
815
613
|
}
|
|
816
614
|
return {
|
|
817
|
-
version: AUTHORIZATION_CONTEXT_VERSION,
|
|
818
|
-
createdAtMs: value.createdAtMs,
|
|
819
615
|
state: expectedState,
|
|
820
616
|
authMetadata,
|
|
821
617
|
clientId: requireClientId(value.clientId),
|
|
822
618
|
redirectUri: redirect.toString(),
|
|
823
619
|
homeserverUrl: homeserver.toString(),
|
|
824
|
-
deviceId:
|
|
620
|
+
deviceId: requireDeviceId(value.deviceId),
|
|
825
621
|
codeVerifier: requireString(value.codeVerifier, "PKCE verifier", PKCE_VERIFIER_PATTERN),
|
|
826
622
|
};
|
|
827
623
|
}
|
|
@@ -853,7 +649,12 @@ export async function discoverOidcIssuer(homeserverBaseUrl, signal) {
|
|
|
853
649
|
throw failure;
|
|
854
650
|
}
|
|
855
651
|
ensureOidcNotCancelled(signal, "discovery");
|
|
856
|
-
|
|
652
|
+
try {
|
|
653
|
+
return validateTrustedAuthMetadata(body);
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
throw responseValidationFailure("discovery", response, responseText ?? "", error);
|
|
657
|
+
}
|
|
857
658
|
}
|
|
858
659
|
catch (err) {
|
|
859
660
|
if (err instanceof AggregateError)
|
|
@@ -862,9 +663,8 @@ export async function discoverOidcIssuer(homeserverBaseUrl, signal) {
|
|
|
862
663
|
}
|
|
863
664
|
}
|
|
864
665
|
/**
|
|
865
|
-
* Dynamic client registration (DCR).
|
|
866
|
-
*
|
|
867
|
-
* does; a production issuer may not).
|
|
666
|
+
* Dynamic client registration (DCR). The issuer validates the supplied
|
|
667
|
+
* metadata; the PKCE flow separately binds its callback to the current app.
|
|
868
668
|
*/
|
|
869
669
|
export async function registerClient(authMetadata, metadata, signal) {
|
|
870
670
|
try {
|
|
@@ -944,7 +744,7 @@ export async function startDeviceCodeLogin(authMetadata, clientId, deviceId, sig
|
|
|
944
744
|
try {
|
|
945
745
|
const safeAuthMetadata = validateTrustedAuthMetadata(authMetadata);
|
|
946
746
|
const safeClientId = requireClientId(clientId);
|
|
947
|
-
|
|
747
|
+
requireDeviceId(deviceId);
|
|
948
748
|
const scope = generateScope(deviceId);
|
|
949
749
|
const endpoint = safeAuthMetadata.device_authorization_endpoint;
|
|
950
750
|
if (!endpoint)
|
|
@@ -1006,10 +806,10 @@ export async function waitForDeviceCodeLogin(authMetadata, clientId, session, si
|
|
|
1006
806
|
if (requestedDeviceId === undefined) {
|
|
1007
807
|
throw new StorageError("OIDC device authorization requires an expected device ID");
|
|
1008
808
|
}
|
|
1009
|
-
|
|
809
|
+
requireDeviceId(requestedDeviceId);
|
|
1010
810
|
const endpoint = exactEndpoint(safeAuthMetadata.token_endpoint, "token endpoint");
|
|
1011
811
|
let interval = (safeSession.interval ?? 5) * 1000;
|
|
1012
|
-
const expiration = Date.now() +
|
|
812
|
+
const expiration = Date.now() + safeSession.expires_in * 1000;
|
|
1013
813
|
do {
|
|
1014
814
|
const remaining = expiration - Date.now();
|
|
1015
815
|
if (remaining <= 0) {
|
|
@@ -1038,9 +838,6 @@ export async function waitForDeviceCodeLogin(authMetadata, clientId, session, si
|
|
|
1038
838
|
throw providerResponseFailure("device authorization", response, responseText ?? "", "OIDC device authorization returned an invalid response");
|
|
1039
839
|
}
|
|
1040
840
|
if (response.ok) {
|
|
1041
|
-
if (!isValidDeviceAccessTokenResponse(body)) {
|
|
1042
|
-
throw providerResponseFailure("device authorization", response, responseText ?? "", "OIDC device authorization returned an invalid response");
|
|
1043
|
-
}
|
|
1044
841
|
try {
|
|
1045
842
|
const tokenResponse = validateDeviceAccessTokenResponse(body);
|
|
1046
843
|
if (!requestedDeviceId ||
|
|
@@ -1101,7 +898,6 @@ export async function beginAuthorizationCodeFlow(opts) {
|
|
|
1101
898
|
if (opts.signal?.aborted)
|
|
1102
899
|
throw new StorageError("OIDC authorization cancelled");
|
|
1103
900
|
const { authMetadata, homeserver, redirect, deviceId } = validateAuthorizationInputs(opts);
|
|
1104
|
-
const createdAtMs = Date.now();
|
|
1105
901
|
const state = randomHex(32);
|
|
1106
902
|
const codeVerifier = randomHex(64);
|
|
1107
903
|
const oauth = new OAuth2(authMetadata, {
|
|
@@ -1134,8 +930,6 @@ export async function beginAuthorizationCodeFlow(opts) {
|
|
|
1134
930
|
throw new StorageError("OIDC authorization URL is not bound to the validated request");
|
|
1135
931
|
}
|
|
1136
932
|
const context = {
|
|
1137
|
-
version: AUTHORIZATION_CONTEXT_VERSION,
|
|
1138
|
-
createdAtMs,
|
|
1139
933
|
state,
|
|
1140
934
|
authMetadata,
|
|
1141
935
|
clientId: opts.clientId,
|
|
@@ -1149,43 +943,27 @@ export async function beginAuthorizationCodeFlow(opts) {
|
|
|
1149
943
|
}
|
|
1150
944
|
/** Completes the authorization-code exchange after the `?code&state` redirect. */
|
|
1151
945
|
export async function completeAuthorizationCodeFlow(code, state, signal) {
|
|
1152
|
-
requireString(state, "authorization state", STATE_PATTERN);
|
|
1153
946
|
const store = sessionStorage();
|
|
1154
947
|
const key = `${AUTHORIZATION_CONTEXT_PREFIX}${state}`;
|
|
1155
948
|
const serialized = store.getItem(key);
|
|
1156
949
|
if (!serialized)
|
|
1157
|
-
throw new StorageError("OIDC authorization context is missing
|
|
950
|
+
throw new StorageError("OIDC authorization context is missing");
|
|
1158
951
|
let context;
|
|
1159
952
|
let validationError;
|
|
1160
|
-
if (serialized.length > MAX_AUTHORIZATION_CONTEXT_LENGTH) {
|
|
1161
|
-
validationError = new StorageError("OIDC authorization context is too large");
|
|
1162
|
-
}
|
|
1163
|
-
else {
|
|
1164
|
-
try {
|
|
1165
|
-
context = parseAuthorizationContext(JSON.parse(serialized), state);
|
|
1166
|
-
}
|
|
1167
|
-
catch (error) {
|
|
1168
|
-
validationError = error instanceof StorageError
|
|
1169
|
-
? error
|
|
1170
|
-
: new StorageError("OIDC authorization context is missing or invalid", { cause: error });
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
// Consume the state before any network exchange. A retry must not replay
|
|
1174
|
-
// the authorization code, even if validation fails. Preserve a validation
|
|
1175
|
-
// failure when storage cleanup fails as well.
|
|
1176
953
|
try {
|
|
1177
|
-
|
|
954
|
+
context = parseAuthorizationContext(JSON.parse(serialized), state);
|
|
1178
955
|
}
|
|
1179
956
|
catch (error) {
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
throw error;
|
|
957
|
+
validationError = error instanceof StorageError
|
|
958
|
+
? error
|
|
959
|
+
: new StorageError("OIDC authorization context is missing or invalid", { cause: error });
|
|
1184
960
|
}
|
|
961
|
+
// Consume the state before any network exchange so a retry cannot replay it.
|
|
962
|
+
store.removeItem(key);
|
|
1185
963
|
if (validationError !== undefined)
|
|
1186
964
|
throw validationError;
|
|
1187
965
|
try {
|
|
1188
|
-
|
|
966
|
+
requireOpaqueString(code, "authorization code");
|
|
1189
967
|
const endpoint = exactEndpoint(context.authMetadata.token_endpoint, "token endpoint");
|
|
1190
968
|
const params = new URLSearchParams({
|
|
1191
969
|
grant_type: "authorization_code",
|
|
@@ -1250,7 +1028,6 @@ export async function completeAuthorizationCodeFlow(code, state, signal) {
|
|
|
1250
1028
|
* OAuth scope. */
|
|
1251
1029
|
export function extractDeviceIdFromScope(scope) {
|
|
1252
1030
|
if (typeof scope !== "string" ||
|
|
1253
|
-
scope.length > MAX_OIDC_SCOPE_LENGTH ||
|
|
1254
1031
|
/[\u0000-\u001f\u007f]/.test(scope) ||
|
|
1255
1032
|
scope.trim() !== scope) {
|
|
1256
1033
|
return null;
|
|
@@ -1264,7 +1041,7 @@ export function extractDeviceIdFromScope(scope) {
|
|
|
1264
1041
|
if (!prefix)
|
|
1265
1042
|
return null;
|
|
1266
1043
|
const deviceId = deviceToken.slice(`${prefix}device:`.length);
|
|
1267
|
-
return
|
|
1044
|
+
return isDeviceId(deviceId) ? deviceId : null;
|
|
1268
1045
|
}
|
|
1269
1046
|
/**
|
|
1270
1047
|
* Accepts the legacy Matrix OAuth scope and the current MSC2967 namespace
|
|
@@ -1275,9 +1052,8 @@ export function extractDeviceIdFromScope(scope) {
|
|
|
1275
1052
|
function scopeMatchesDevice(scope, expectedDeviceId) {
|
|
1276
1053
|
if (typeof scope !== "string" ||
|
|
1277
1054
|
typeof expectedDeviceId !== "string" ||
|
|
1278
|
-
!
|
|
1055
|
+
!isDeviceId(expectedDeviceId) ||
|
|
1279
1056
|
scope.length === 0 ||
|
|
1280
|
-
scope.length > MAX_OIDC_SCOPE_LENGTH ||
|
|
1281
1057
|
/[\u0000-\u001f\u007f]/.test(scope) ||
|
|
1282
1058
|
scope.trim() !== scope ||
|
|
1283
1059
|
/\s{2,}/.test(scope)) {
|
|
@@ -1317,7 +1093,7 @@ function validateMatrixUserId(userId, serverName) {
|
|
|
1317
1093
|
export async function whoAmI(homeserverUrl, accessToken, serverName, signal) {
|
|
1318
1094
|
try {
|
|
1319
1095
|
const homeserver = parseHttpUrl(homeserverUrl, "homeserver URL");
|
|
1320
|
-
|
|
1096
|
+
requireOpaqueString(accessToken, "access token");
|
|
1321
1097
|
const endpoint = matrixWhoAmIEndpoint(homeserver);
|
|
1322
1098
|
const { response, body, responseText } = await requestWithTimeout(endpoint, {
|
|
1323
1099
|
method: "GET",
|
|
@@ -1338,14 +1114,20 @@ export async function whoAmI(homeserverUrl, accessToken, serverName, signal) {
|
|
|
1338
1114
|
if (!isRecord(body)) {
|
|
1339
1115
|
throw responseValidationFailure("identity confirmation", response, responseText ?? "", new StorageError("OIDC identity confirmation returned an invalid response"));
|
|
1340
1116
|
}
|
|
1341
|
-
|
|
1117
|
+
let userId;
|
|
1118
|
+
try {
|
|
1119
|
+
userId = validateMatrixUserId(body.user_id, serverName);
|
|
1120
|
+
}
|
|
1121
|
+
catch (error) {
|
|
1122
|
+
throw responseValidationFailure("identity confirmation", response, responseText ?? "", error);
|
|
1123
|
+
}
|
|
1342
1124
|
let deviceId = null;
|
|
1343
1125
|
if (body.device_id !== undefined && body.device_id !== null) {
|
|
1344
1126
|
try {
|
|
1345
1127
|
deviceId = validateMatrixDeviceId(body.device_id);
|
|
1346
1128
|
}
|
|
1347
1129
|
catch (error) {
|
|
1348
|
-
throw new StorageError("OIDC identity confirmation returned an invalid device ID", { cause: error });
|
|
1130
|
+
throw responseValidationFailure("identity confirmation", response, responseText ?? "", new StorageError("OIDC identity confirmation returned an invalid device ID", { cause: error }));
|
|
1349
1131
|
}
|
|
1350
1132
|
}
|
|
1351
1133
|
if (signal?.aborted)
|
|
@@ -1374,7 +1156,7 @@ export async function whoAmI(homeserverUrl, accessToken, serverName, signal) {
|
|
|
1374
1156
|
async function refreshOidcToken(tokenEndpoint, clientId, refreshToken, expectedDeviceId, signal) {
|
|
1375
1157
|
const endpoint = exactEndpoint(tokenEndpoint, "token endpoint");
|
|
1376
1158
|
const safeClientId = requireClientId(clientId);
|
|
1377
|
-
const safeRefreshToken =
|
|
1159
|
+
const safeRefreshToken = requireOpaqueString(refreshToken, "refresh token");
|
|
1378
1160
|
const controller = new AbortController();
|
|
1379
1161
|
const timeout = setTimeout(() => controller.abort(), OIDC_REFRESH_TIMEOUT_MS);
|
|
1380
1162
|
const abortExternal = () => controller.abort(signal?.reason);
|
|
@@ -1438,7 +1220,6 @@ async function refreshOidcToken(tokenEndpoint, clientId, refreshToken, expectedD
|
|
|
1438
1220
|
const expiresIn = record.expires_in;
|
|
1439
1221
|
if (typeof accessToken !== "string" ||
|
|
1440
1222
|
accessToken.trim() === "" ||
|
|
1441
|
-
accessToken.length > MAX_OIDC_TOKEN_LENGTH ||
|
|
1442
1223
|
/[\s\u0000-\u001f\u007f]/.test(accessToken)) {
|
|
1443
1224
|
throw invalidResponse("OIDC token refresh returned no access token");
|
|
1444
1225
|
}
|
|
@@ -1449,7 +1230,6 @@ async function refreshOidcToken(tokenEndpoint, clientId, refreshToken, expectedD
|
|
|
1449
1230
|
if (nextRefreshToken !== undefined &&
|
|
1450
1231
|
(typeof nextRefreshToken !== "string" ||
|
|
1451
1232
|
nextRefreshToken.trim() === "" ||
|
|
1452
|
-
nextRefreshToken.length > MAX_OIDC_TOKEN_LENGTH ||
|
|
1453
1233
|
/[\s\u0000-\u001f\u007f]/.test(nextRefreshToken))) {
|
|
1454
1234
|
throw invalidResponse("OIDC token refresh returned an invalid refresh token");
|
|
1455
1235
|
}
|
|
@@ -1457,12 +1237,6 @@ async function refreshOidcToken(tokenEndpoint, clientId, refreshToken, expectedD
|
|
|
1457
1237
|
if (typeof grantedScope !== "string") {
|
|
1458
1238
|
throw invalidResponse("OIDC token refresh returned an invalid scope");
|
|
1459
1239
|
}
|
|
1460
|
-
try {
|
|
1461
|
-
requireScope(grantedScope, "scope");
|
|
1462
|
-
}
|
|
1463
|
-
catch (error) {
|
|
1464
|
-
throw invalidResponse("OIDC token refresh returned an invalid scope", { cause: error });
|
|
1465
|
-
}
|
|
1466
1240
|
if (!scopeMatchesDevice(grantedScope, expectedDeviceId)) {
|
|
1467
1241
|
throw invalidResponse("OIDC token refresh returned an unexpected granted scope");
|
|
1468
1242
|
}
|
|
@@ -1472,9 +1246,8 @@ async function refreshOidcToken(tokenEndpoint, clientId, refreshToken, expectedD
|
|
|
1472
1246
|
}
|
|
1473
1247
|
if (expiresIn !== undefined &&
|
|
1474
1248
|
(typeof expiresIn !== "number" ||
|
|
1475
|
-
!Number.
|
|
1476
|
-
expiresIn < 0
|
|
1477
|
-
expiresIn > 86400)) {
|
|
1249
|
+
!Number.isSafeInteger(expiresIn) ||
|
|
1250
|
+
expiresIn < 0)) {
|
|
1478
1251
|
throw invalidResponse("OIDC token refresh returned an invalid expiry");
|
|
1479
1252
|
}
|
|
1480
1253
|
ensureOidcNotCancelled(signal, "token refresh");
|
|
@@ -1508,7 +1281,7 @@ async function refreshOidcToken(tokenEndpoint, clientId, refreshToken, expectedD
|
|
|
1508
1281
|
async function revokeOidcToken(revocationEndpoint, clientId, token, tokenTypeHint = "access_token", signal) {
|
|
1509
1282
|
const endpoint = exactEndpoint(revocationEndpoint, "revocation endpoint");
|
|
1510
1283
|
const safeClientId = requireClientId(clientId);
|
|
1511
|
-
const safeToken =
|
|
1284
|
+
const safeToken = requireOpaqueString(token, "token");
|
|
1512
1285
|
try {
|
|
1513
1286
|
const { response, responseText } = await requestWithTimeout(endpoint, {
|
|
1514
1287
|
method: "POST",
|
|
@@ -1591,7 +1364,7 @@ export function buildTokenRefreshFunction(metadata, clientId, onPersist, expecte
|
|
|
1591
1364
|
// endpoint and cause a refresh or revocation token to leave that issuer.
|
|
1592
1365
|
const { tokenEndpoint: endpoint, revocationEndpoint } = validateTokenEndpointMetadata(metadata);
|
|
1593
1366
|
const safeClientId = requireClientId(clientId);
|
|
1594
|
-
const safeExpectedDeviceId =
|
|
1367
|
+
const safeExpectedDeviceId = requireDeviceId(expectedDeviceId);
|
|
1595
1368
|
return async (refreshToken, signal) => {
|
|
1596
1369
|
const tokens = await refreshOidcToken(endpoint, safeClientId, refreshToken, safeExpectedDeviceId, signal);
|
|
1597
1370
|
try {
|