@lvce-editor/auth-worker 1.13.0 → 1.15.0
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/dist/authWorkerMain.js +869 -10
- package/package.json +1 -1
package/dist/authWorkerMain.js
CHANGED
|
@@ -1188,6 +1188,7 @@ const toBackendAuthState = value => {
|
|
|
1188
1188
|
return {
|
|
1189
1189
|
authAccessToken: getString(value.accessToken),
|
|
1190
1190
|
authErrorMessage: getString(value.error),
|
|
1191
|
+
authRefreshToken: getString(value.refreshToken),
|
|
1191
1192
|
userName: getString(value.userName),
|
|
1192
1193
|
userState: value.accessToken ? 'loggedIn' : 'loggedOut',
|
|
1193
1194
|
userSubscriptionPlan: getString(value.subscriptionPlan),
|
|
@@ -1261,7 +1262,22 @@ const waitForBackendLogin = async (backendUrl, timeoutMs = 30_000, pollIntervalM
|
|
|
1261
1262
|
return getLoggedOutBackendAuthState(lastErrorMessage);
|
|
1262
1263
|
};
|
|
1263
1264
|
|
|
1264
|
-
|
|
1265
|
+
let USER_AGENT;
|
|
1266
|
+
if (typeof navigator === 'undefined' || !navigator.userAgent?.startsWith?.('Mozilla/5.0 ')) {
|
|
1267
|
+
const NAME = 'oauth4webapi';
|
|
1268
|
+
const VERSION = 'v3.8.6';
|
|
1269
|
+
USER_AGENT = `${NAME}/${VERSION}`;
|
|
1270
|
+
}
|
|
1271
|
+
function looseInstanceOf(input, expected) {
|
|
1272
|
+
if (input == null) {
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
try {
|
|
1276
|
+
return input instanceof expected || Object.getPrototypeOf(input)[Symbol.toStringTag] === expected.prototype[Symbol.toStringTag];
|
|
1277
|
+
} catch {
|
|
1278
|
+
return false;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1265
1281
|
const ERR_INVALID_ARG_VALUE = 'ERR_INVALID_ARG_VALUE';
|
|
1266
1282
|
const ERR_INVALID_ARG_TYPE = 'ERR_INVALID_ARG_TYPE';
|
|
1267
1283
|
function CodedTypeError(message, code, cause) {
|
|
@@ -1273,6 +1289,11 @@ function CodedTypeError(message, code, cause) {
|
|
|
1273
1289
|
});
|
|
1274
1290
|
return err;
|
|
1275
1291
|
}
|
|
1292
|
+
const allowInsecureRequests = Symbol();
|
|
1293
|
+
const clockSkew = Symbol();
|
|
1294
|
+
const clockTolerance = Symbol();
|
|
1295
|
+
const customFetch = Symbol();
|
|
1296
|
+
const jweDecrypt = Symbol();
|
|
1276
1297
|
const encoder = new TextEncoder();
|
|
1277
1298
|
const decoder = new TextDecoder();
|
|
1278
1299
|
function buf(input) {
|
|
@@ -1336,6 +1357,83 @@ function b64u(input) {
|
|
|
1336
1357
|
}
|
|
1337
1358
|
return encodeBase64Url(input);
|
|
1338
1359
|
}
|
|
1360
|
+
class UnsupportedOperationError extends Error {
|
|
1361
|
+
code;
|
|
1362
|
+
constructor(message, options) {
|
|
1363
|
+
super(message, options);
|
|
1364
|
+
this.name = this.constructor.name;
|
|
1365
|
+
this.code = UNSUPPORTED_OPERATION;
|
|
1366
|
+
Error.captureStackTrace?.(this, this.constructor);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
class OperationProcessingError extends Error {
|
|
1370
|
+
code;
|
|
1371
|
+
constructor(message, options) {
|
|
1372
|
+
super(message, options);
|
|
1373
|
+
this.name = this.constructor.name;
|
|
1374
|
+
if (options?.code) {
|
|
1375
|
+
this.code = options?.code;
|
|
1376
|
+
}
|
|
1377
|
+
Error.captureStackTrace?.(this, this.constructor);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
function OPE(message, code, cause) {
|
|
1381
|
+
return new OperationProcessingError(message, {
|
|
1382
|
+
code,
|
|
1383
|
+
cause
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
function isJsonObject(input) {
|
|
1387
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
1388
|
+
return false;
|
|
1389
|
+
}
|
|
1390
|
+
return true;
|
|
1391
|
+
}
|
|
1392
|
+
function prepareHeaders(input) {
|
|
1393
|
+
if (looseInstanceOf(input, Headers)) {
|
|
1394
|
+
input = Object.fromEntries(input.entries());
|
|
1395
|
+
}
|
|
1396
|
+
const headers = new Headers(input ?? {});
|
|
1397
|
+
if (USER_AGENT && !headers.has('user-agent')) {
|
|
1398
|
+
headers.set('user-agent', USER_AGENT);
|
|
1399
|
+
}
|
|
1400
|
+
if (headers.has('authorization')) {
|
|
1401
|
+
throw CodedTypeError('"options.headers" must not include the "authorization" header name', ERR_INVALID_ARG_VALUE);
|
|
1402
|
+
}
|
|
1403
|
+
return headers;
|
|
1404
|
+
}
|
|
1405
|
+
function signal(url, value) {
|
|
1406
|
+
if (value !== undefined) {
|
|
1407
|
+
if (typeof value === 'function') {
|
|
1408
|
+
value = value(url.href);
|
|
1409
|
+
}
|
|
1410
|
+
if (!(value instanceof AbortSignal)) {
|
|
1411
|
+
throw CodedTypeError('"options.signal" must return or be an instance of AbortSignal', ERR_INVALID_ARG_TYPE);
|
|
1412
|
+
}
|
|
1413
|
+
return value;
|
|
1414
|
+
}
|
|
1415
|
+
return undefined;
|
|
1416
|
+
}
|
|
1417
|
+
function assertNumber(input, allow0, it, code, cause) {
|
|
1418
|
+
try {
|
|
1419
|
+
if (typeof input !== 'number' || !Number.isFinite(input)) {
|
|
1420
|
+
throw CodedTypeError(`${it} must be a number`, ERR_INVALID_ARG_TYPE, cause);
|
|
1421
|
+
}
|
|
1422
|
+
if (input > 0) return;
|
|
1423
|
+
if (allow0) {
|
|
1424
|
+
if (input !== 0) {
|
|
1425
|
+
throw CodedTypeError(`${it} must be a non-negative number`, ERR_INVALID_ARG_VALUE, cause);
|
|
1426
|
+
}
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
throw CodedTypeError(`${it} must be a positive number`, ERR_INVALID_ARG_VALUE, cause);
|
|
1430
|
+
} catch (err) {
|
|
1431
|
+
if (code) {
|
|
1432
|
+
throw OPE(err.message, code, cause);
|
|
1433
|
+
}
|
|
1434
|
+
throw err;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1339
1437
|
function assertString(input, it, code, cause) {
|
|
1340
1438
|
try {
|
|
1341
1439
|
if (typeof input !== 'string') {
|
|
@@ -1345,9 +1443,32 @@ function assertString(input, it, code, cause) {
|
|
|
1345
1443
|
throw CodedTypeError(`${it} must not be empty`, ERR_INVALID_ARG_VALUE, cause);
|
|
1346
1444
|
}
|
|
1347
1445
|
} catch (err) {
|
|
1446
|
+
if (code) {
|
|
1447
|
+
throw OPE(err.message, code, cause);
|
|
1448
|
+
}
|
|
1348
1449
|
throw err;
|
|
1349
1450
|
}
|
|
1350
1451
|
}
|
|
1452
|
+
function assertApplicationJson(response) {
|
|
1453
|
+
assertContentType(response, 'application/json');
|
|
1454
|
+
}
|
|
1455
|
+
function notJson(response, ...types) {
|
|
1456
|
+
let msg = '"response" content-type must be ';
|
|
1457
|
+
if (types.length > 2) {
|
|
1458
|
+
const last = types.pop();
|
|
1459
|
+
msg += `${types.join(', ')}, or ${last}`;
|
|
1460
|
+
} else if (types.length === 2) {
|
|
1461
|
+
msg += `${types[0]} or ${types[1]}`;
|
|
1462
|
+
} else {
|
|
1463
|
+
msg += types[0];
|
|
1464
|
+
}
|
|
1465
|
+
return OPE(msg, RESPONSE_IS_NOT_JSON, response);
|
|
1466
|
+
}
|
|
1467
|
+
function assertContentType(response, contentType) {
|
|
1468
|
+
if (getContentType(response) !== contentType) {
|
|
1469
|
+
throw notJson(response, contentType);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1351
1472
|
function randomBytes() {
|
|
1352
1473
|
return b64u(crypto.getRandomValues(new Uint8Array(32)));
|
|
1353
1474
|
}
|
|
@@ -1358,6 +1479,708 @@ async function calculatePKCECodeChallenge(codeVerifier) {
|
|
|
1358
1479
|
assertString(codeVerifier, 'codeVerifier');
|
|
1359
1480
|
return b64u(await crypto.subtle.digest('SHA-256', buf(codeVerifier)));
|
|
1360
1481
|
}
|
|
1482
|
+
function getClockSkew(client) {
|
|
1483
|
+
const skew = client?.[clockSkew];
|
|
1484
|
+
return typeof skew === 'number' && Number.isFinite(skew) ? skew : 0;
|
|
1485
|
+
}
|
|
1486
|
+
function getClockTolerance(client) {
|
|
1487
|
+
const tolerance = client?.[clockTolerance];
|
|
1488
|
+
return typeof tolerance === 'number' && Number.isFinite(tolerance) && Math.sign(tolerance) !== -1 ? tolerance : 30;
|
|
1489
|
+
}
|
|
1490
|
+
function epochTime() {
|
|
1491
|
+
return Math.floor(Date.now() / 1000);
|
|
1492
|
+
}
|
|
1493
|
+
function assertAs(as) {
|
|
1494
|
+
if (typeof as !== 'object' || as === null) {
|
|
1495
|
+
throw CodedTypeError('"as" must be an object', ERR_INVALID_ARG_TYPE);
|
|
1496
|
+
}
|
|
1497
|
+
assertString(as.issuer, '"as.issuer"');
|
|
1498
|
+
}
|
|
1499
|
+
function assertClient(client) {
|
|
1500
|
+
if (typeof client !== 'object' || client === null) {
|
|
1501
|
+
throw CodedTypeError('"client" must be an object', ERR_INVALID_ARG_TYPE);
|
|
1502
|
+
}
|
|
1503
|
+
assertString(client.client_id, '"client.client_id"');
|
|
1504
|
+
}
|
|
1505
|
+
function None() {
|
|
1506
|
+
return (_as, client, body, _headers) => {
|
|
1507
|
+
body.set('client_id', client.client_id);
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
const URLParse = URL.parse ? (url, base) => URL.parse(url, base) : (url, base) => {
|
|
1511
|
+
try {
|
|
1512
|
+
return new URL(url, base);
|
|
1513
|
+
} catch {
|
|
1514
|
+
return null;
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1517
|
+
function checkProtocol(url, enforceHttps) {
|
|
1518
|
+
if (enforceHttps && url.protocol !== 'https:') {
|
|
1519
|
+
throw OPE('only requests to HTTPS are allowed', HTTP_REQUEST_FORBIDDEN, url);
|
|
1520
|
+
}
|
|
1521
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
1522
|
+
throw OPE('only HTTP and HTTPS requests are allowed', REQUEST_PROTOCOL_FORBIDDEN, url);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
function validateEndpoint(value, endpoint, useMtlsAlias, enforceHttps) {
|
|
1526
|
+
let url;
|
|
1527
|
+
if (typeof value !== 'string' || !(url = URLParse(value))) {
|
|
1528
|
+
throw OPE(`authorization server metadata does not contain a valid ${useMtlsAlias ? `"as.mtls_endpoint_aliases.${endpoint}"` : `"as.${endpoint}"`}`, value === undefined ? MISSING_SERVER_METADATA : INVALID_SERVER_METADATA, {
|
|
1529
|
+
attribute: useMtlsAlias ? `mtls_endpoint_aliases.${endpoint}` : endpoint
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
checkProtocol(url, enforceHttps);
|
|
1533
|
+
return url;
|
|
1534
|
+
}
|
|
1535
|
+
function resolveEndpoint(as, endpoint, useMtlsAlias, enforceHttps) {
|
|
1536
|
+
if (useMtlsAlias && as.mtls_endpoint_aliases && endpoint in as.mtls_endpoint_aliases) {
|
|
1537
|
+
return validateEndpoint(as.mtls_endpoint_aliases[endpoint], endpoint, useMtlsAlias, enforceHttps);
|
|
1538
|
+
}
|
|
1539
|
+
return validateEndpoint(as[endpoint], endpoint, useMtlsAlias, enforceHttps);
|
|
1540
|
+
}
|
|
1541
|
+
class ResponseBodyError extends Error {
|
|
1542
|
+
cause;
|
|
1543
|
+
code;
|
|
1544
|
+
error;
|
|
1545
|
+
status;
|
|
1546
|
+
error_description;
|
|
1547
|
+
response;
|
|
1548
|
+
constructor(message, options) {
|
|
1549
|
+
super(message, options);
|
|
1550
|
+
this.name = this.constructor.name;
|
|
1551
|
+
this.code = RESPONSE_BODY_ERROR;
|
|
1552
|
+
this.cause = options.cause;
|
|
1553
|
+
this.error = options.cause.error;
|
|
1554
|
+
this.status = options.response.status;
|
|
1555
|
+
this.error_description = options.cause.error_description;
|
|
1556
|
+
Object.defineProperty(this, 'response', {
|
|
1557
|
+
enumerable: false,
|
|
1558
|
+
value: options.response
|
|
1559
|
+
});
|
|
1560
|
+
Error.captureStackTrace?.(this, this.constructor);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
class WWWAuthenticateChallengeError extends Error {
|
|
1564
|
+
cause;
|
|
1565
|
+
code;
|
|
1566
|
+
response;
|
|
1567
|
+
status;
|
|
1568
|
+
constructor(message, options) {
|
|
1569
|
+
super(message, options);
|
|
1570
|
+
this.name = this.constructor.name;
|
|
1571
|
+
this.code = WWW_AUTHENTICATE_CHALLENGE;
|
|
1572
|
+
this.cause = options.cause;
|
|
1573
|
+
this.status = options.response.status;
|
|
1574
|
+
this.response = options.response;
|
|
1575
|
+
Object.defineProperty(this, 'response', {
|
|
1576
|
+
enumerable: false
|
|
1577
|
+
});
|
|
1578
|
+
Error.captureStackTrace?.(this, this.constructor);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
const tokenMatch = "[a-zA-Z0-9!#$%&\\'\\*\\+\\-\\.\\^_`\\|~]+";
|
|
1582
|
+
const token68Match = '[a-zA-Z0-9\\-\\._\\~\\+\\/]+={0,2}';
|
|
1583
|
+
const quotedMatch = '"((?:[^"\\\\]|\\\\[\\s\\S])*)"';
|
|
1584
|
+
const quotedParamMatcher = '(' + tokenMatch + ')\\s*=\\s*' + quotedMatch;
|
|
1585
|
+
const paramMatcher = '(' + tokenMatch + ')\\s*=\\s*(' + tokenMatch + ')';
|
|
1586
|
+
const schemeRE = new RegExp('^[,\\s]*(' + tokenMatch + ')');
|
|
1587
|
+
const quotedParamRE = new RegExp('^[,\\s]*' + quotedParamMatcher + '[,\\s]*(.*)');
|
|
1588
|
+
const unquotedParamRE = new RegExp('^[,\\s]*' + paramMatcher + '[,\\s]*(.*)');
|
|
1589
|
+
const token68ParamRE = new RegExp('^(' + token68Match + ')(?:$|[,\\s])(.*)');
|
|
1590
|
+
function parseWwwAuthenticateChallenges(response) {
|
|
1591
|
+
if (!looseInstanceOf(response, Response)) {
|
|
1592
|
+
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
|
|
1593
|
+
}
|
|
1594
|
+
const header = response.headers.get('www-authenticate');
|
|
1595
|
+
if (header === null) {
|
|
1596
|
+
return undefined;
|
|
1597
|
+
}
|
|
1598
|
+
const challenges = [];
|
|
1599
|
+
let rest = header;
|
|
1600
|
+
while (rest) {
|
|
1601
|
+
let match = rest.match(schemeRE);
|
|
1602
|
+
const scheme = match?.['1'].toLowerCase();
|
|
1603
|
+
if (!scheme) {
|
|
1604
|
+
return undefined;
|
|
1605
|
+
}
|
|
1606
|
+
const afterScheme = rest.substring(match[0].length);
|
|
1607
|
+
if (afterScheme && !afterScheme.match(/^[\s,]/)) {
|
|
1608
|
+
return undefined;
|
|
1609
|
+
}
|
|
1610
|
+
const spaceMatch = afterScheme.match(/^\s+(.*)$/);
|
|
1611
|
+
const hasParameters = !!spaceMatch;
|
|
1612
|
+
rest = spaceMatch ? spaceMatch[1] : undefined;
|
|
1613
|
+
const parameters = {};
|
|
1614
|
+
let token68;
|
|
1615
|
+
if (hasParameters) {
|
|
1616
|
+
while (rest) {
|
|
1617
|
+
let key;
|
|
1618
|
+
let value;
|
|
1619
|
+
if (match = rest.match(quotedParamRE)) {
|
|
1620
|
+
[, key, value, rest] = match;
|
|
1621
|
+
if (value.includes('\\')) {
|
|
1622
|
+
try {
|
|
1623
|
+
value = JSON.parse(`"${value}"`);
|
|
1624
|
+
} catch {}
|
|
1625
|
+
}
|
|
1626
|
+
parameters[key.toLowerCase()] = value;
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1629
|
+
if (match = rest.match(unquotedParamRE)) {
|
|
1630
|
+
[, key, value, rest] = match;
|
|
1631
|
+
parameters[key.toLowerCase()] = value;
|
|
1632
|
+
continue;
|
|
1633
|
+
}
|
|
1634
|
+
if (match = rest.match(token68ParamRE)) {
|
|
1635
|
+
if (Object.keys(parameters).length) {
|
|
1636
|
+
break;
|
|
1637
|
+
}
|
|
1638
|
+
[, token68, rest] = match;
|
|
1639
|
+
break;
|
|
1640
|
+
}
|
|
1641
|
+
return undefined;
|
|
1642
|
+
}
|
|
1643
|
+
} else {
|
|
1644
|
+
rest = afterScheme || undefined;
|
|
1645
|
+
}
|
|
1646
|
+
const challenge = {
|
|
1647
|
+
scheme,
|
|
1648
|
+
parameters
|
|
1649
|
+
};
|
|
1650
|
+
if (token68) {
|
|
1651
|
+
challenge.token68 = token68;
|
|
1652
|
+
}
|
|
1653
|
+
challenges.push(challenge);
|
|
1654
|
+
}
|
|
1655
|
+
if (!challenges.length) {
|
|
1656
|
+
return undefined;
|
|
1657
|
+
}
|
|
1658
|
+
return challenges;
|
|
1659
|
+
}
|
|
1660
|
+
async function parseOAuthResponseErrorBody(response) {
|
|
1661
|
+
if (response.status > 399 && response.status < 500) {
|
|
1662
|
+
assertReadableResponse(response);
|
|
1663
|
+
assertApplicationJson(response);
|
|
1664
|
+
try {
|
|
1665
|
+
const json = await response.clone().json();
|
|
1666
|
+
if (isJsonObject(json) && typeof json.error === 'string' && json.error.length) {
|
|
1667
|
+
return json;
|
|
1668
|
+
}
|
|
1669
|
+
} catch {}
|
|
1670
|
+
}
|
|
1671
|
+
return undefined;
|
|
1672
|
+
}
|
|
1673
|
+
async function checkOAuthBodyError(response, expected, label) {
|
|
1674
|
+
if (response.status !== expected) {
|
|
1675
|
+
checkAuthenticationChallenges(response);
|
|
1676
|
+
let err;
|
|
1677
|
+
if (err = await parseOAuthResponseErrorBody(response)) {
|
|
1678
|
+
await response.body?.cancel();
|
|
1679
|
+
throw new ResponseBodyError('server responded with an error in the response body', {
|
|
1680
|
+
cause: err,
|
|
1681
|
+
response
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
throw OPE(`"response" is not a conform ${label} response (unexpected HTTP status code)`, RESPONSE_IS_NOT_CONFORM, response);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
function assertDPoP(option) {
|
|
1688
|
+
if (!branded.has(option)) {
|
|
1689
|
+
throw CodedTypeError('"options.DPoP" is not a valid DPoPHandle', ERR_INVALID_ARG_VALUE);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
function getContentType(input) {
|
|
1693
|
+
return input.headers.get('content-type')?.split(';')[0];
|
|
1694
|
+
}
|
|
1695
|
+
async function authenticatedRequest(as, client, clientAuthentication, url, body, headers, options) {
|
|
1696
|
+
await clientAuthentication(as, client, body, headers);
|
|
1697
|
+
headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8');
|
|
1698
|
+
return (options?.[customFetch] || fetch)(url.href, {
|
|
1699
|
+
body,
|
|
1700
|
+
headers: Object.fromEntries(headers.entries()),
|
|
1701
|
+
method: 'POST',
|
|
1702
|
+
redirect: 'manual',
|
|
1703
|
+
signal: signal(url, options?.signal)
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
async function tokenEndpointRequest(as, client, clientAuthentication, grantType, parameters, options) {
|
|
1707
|
+
const url = resolveEndpoint(as, 'token_endpoint', client.use_mtls_endpoint_aliases, options?.[allowInsecureRequests] !== true);
|
|
1708
|
+
parameters.set('grant_type', grantType);
|
|
1709
|
+
const headers = prepareHeaders(options?.headers);
|
|
1710
|
+
headers.set('accept', 'application/json');
|
|
1711
|
+
if (options?.DPoP !== undefined) {
|
|
1712
|
+
assertDPoP(options.DPoP);
|
|
1713
|
+
await options.DPoP.addProof(url, headers, 'POST');
|
|
1714
|
+
}
|
|
1715
|
+
const response = await authenticatedRequest(as, client, clientAuthentication, url, parameters, headers, options);
|
|
1716
|
+
options?.DPoP?.cacheNonce(response, url);
|
|
1717
|
+
return response;
|
|
1718
|
+
}
|
|
1719
|
+
const idTokenClaims = new WeakMap();
|
|
1720
|
+
const jwtRefs = new WeakMap();
|
|
1721
|
+
function getValidatedIdTokenClaims(ref) {
|
|
1722
|
+
if (!ref.id_token) {
|
|
1723
|
+
return undefined;
|
|
1724
|
+
}
|
|
1725
|
+
const claims = idTokenClaims.get(ref);
|
|
1726
|
+
if (!claims) {
|
|
1727
|
+
throw CodedTypeError('"ref" was already garbage collected or did not resolve from the proper sources', ERR_INVALID_ARG_VALUE);
|
|
1728
|
+
}
|
|
1729
|
+
return claims;
|
|
1730
|
+
}
|
|
1731
|
+
async function processGenericAccessTokenResponse(as, client, response, additionalRequiredIdTokenClaims, decryptFn, recognizedTokenTypes) {
|
|
1732
|
+
assertAs(as);
|
|
1733
|
+
assertClient(client);
|
|
1734
|
+
if (!looseInstanceOf(response, Response)) {
|
|
1735
|
+
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
|
|
1736
|
+
}
|
|
1737
|
+
await checkOAuthBodyError(response, 200, 'Token Endpoint');
|
|
1738
|
+
assertReadableResponse(response);
|
|
1739
|
+
const json = await getResponseJsonBody(response);
|
|
1740
|
+
assertString(json.access_token, '"response" body "access_token" property', INVALID_RESPONSE, {
|
|
1741
|
+
body: json
|
|
1742
|
+
});
|
|
1743
|
+
assertString(json.token_type, '"response" body "token_type" property', INVALID_RESPONSE, {
|
|
1744
|
+
body: json
|
|
1745
|
+
});
|
|
1746
|
+
json.token_type = json.token_type.toLowerCase();
|
|
1747
|
+
if (json.expires_in !== undefined) {
|
|
1748
|
+
let expiresIn = typeof json.expires_in !== 'number' ? parseFloat(json.expires_in) : json.expires_in;
|
|
1749
|
+
assertNumber(expiresIn, true, '"response" body "expires_in" property', INVALID_RESPONSE, {
|
|
1750
|
+
body: json
|
|
1751
|
+
});
|
|
1752
|
+
json.expires_in = expiresIn;
|
|
1753
|
+
}
|
|
1754
|
+
if (json.refresh_token !== undefined) {
|
|
1755
|
+
assertString(json.refresh_token, '"response" body "refresh_token" property', INVALID_RESPONSE, {
|
|
1756
|
+
body: json
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
if (json.scope !== undefined && typeof json.scope !== 'string') {
|
|
1760
|
+
throw OPE('"response" body "scope" property must be a string', INVALID_RESPONSE, {
|
|
1761
|
+
body: json
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
if (json.id_token !== undefined) {
|
|
1765
|
+
assertString(json.id_token, '"response" body "id_token" property', INVALID_RESPONSE, {
|
|
1766
|
+
body: json
|
|
1767
|
+
});
|
|
1768
|
+
const requiredClaims = ['aud', 'exp', 'iat', 'iss', 'sub'];
|
|
1769
|
+
if (client.require_auth_time === true) {
|
|
1770
|
+
requiredClaims.push('auth_time');
|
|
1771
|
+
}
|
|
1772
|
+
if (client.default_max_age !== undefined) {
|
|
1773
|
+
assertNumber(client.default_max_age, true, '"client.default_max_age"');
|
|
1774
|
+
requiredClaims.push('auth_time');
|
|
1775
|
+
}
|
|
1776
|
+
if (additionalRequiredIdTokenClaims?.length) {
|
|
1777
|
+
requiredClaims.push(...additionalRequiredIdTokenClaims);
|
|
1778
|
+
}
|
|
1779
|
+
const {
|
|
1780
|
+
claims,
|
|
1781
|
+
jwt
|
|
1782
|
+
} = await validateJwt(json.id_token, checkSigningAlgorithm.bind(undefined, client.id_token_signed_response_alg, as.id_token_signing_alg_values_supported, 'RS256'), getClockSkew(client), getClockTolerance(client), decryptFn).then(validatePresence.bind(undefined, requiredClaims)).then(validateIssuer.bind(undefined, as)).then(validateAudience.bind(undefined, client.client_id));
|
|
1783
|
+
if (Array.isArray(claims.aud) && claims.aud.length !== 1) {
|
|
1784
|
+
if (claims.azp === undefined) {
|
|
1785
|
+
throw OPE('ID Token "aud" (audience) claim includes additional untrusted audiences', JWT_CLAIM_COMPARISON, {
|
|
1786
|
+
claims,
|
|
1787
|
+
claim: 'aud'
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
if (claims.azp !== client.client_id) {
|
|
1791
|
+
throw OPE('unexpected ID Token "azp" (authorized party) claim value', JWT_CLAIM_COMPARISON, {
|
|
1792
|
+
expected: client.client_id,
|
|
1793
|
+
claims,
|
|
1794
|
+
claim: 'azp'
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
if (claims.auth_time !== undefined) {
|
|
1799
|
+
assertNumber(claims.auth_time, true, 'ID Token "auth_time" (authentication time)', INVALID_RESPONSE, {
|
|
1800
|
+
claims
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
jwtRefs.set(response, jwt);
|
|
1804
|
+
idTokenClaims.set(json, claims);
|
|
1805
|
+
}
|
|
1806
|
+
if (recognizedTokenTypes?.[json.token_type] !== undefined) {
|
|
1807
|
+
recognizedTokenTypes[json.token_type](response, json);
|
|
1808
|
+
} else if (json.token_type !== 'dpop' && json.token_type !== 'bearer') {
|
|
1809
|
+
throw new UnsupportedOperationError('unsupported `token_type` value', {
|
|
1810
|
+
cause: {
|
|
1811
|
+
body: json
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
return json;
|
|
1816
|
+
}
|
|
1817
|
+
function checkAuthenticationChallenges(response) {
|
|
1818
|
+
let challenges;
|
|
1819
|
+
if (challenges = parseWwwAuthenticateChallenges(response)) {
|
|
1820
|
+
throw new WWWAuthenticateChallengeError('server responded with a challenge in the WWW-Authenticate HTTP Header', {
|
|
1821
|
+
cause: challenges,
|
|
1822
|
+
response
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
function validateAudience(expected, result) {
|
|
1827
|
+
if (Array.isArray(result.claims.aud)) {
|
|
1828
|
+
if (!result.claims.aud.includes(expected)) {
|
|
1829
|
+
throw OPE('unexpected JWT "aud" (audience) claim value', JWT_CLAIM_COMPARISON, {
|
|
1830
|
+
expected,
|
|
1831
|
+
claims: result.claims,
|
|
1832
|
+
claim: 'aud'
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
} else if (result.claims.aud !== expected) {
|
|
1836
|
+
throw OPE('unexpected JWT "aud" (audience) claim value', JWT_CLAIM_COMPARISON, {
|
|
1837
|
+
expected,
|
|
1838
|
+
claims: result.claims,
|
|
1839
|
+
claim: 'aud'
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
return result;
|
|
1843
|
+
}
|
|
1844
|
+
function validateIssuer(as, result) {
|
|
1845
|
+
const expected = as[_expectedIssuer]?.(result) ?? as.issuer;
|
|
1846
|
+
if (result.claims.iss !== expected) {
|
|
1847
|
+
throw OPE('unexpected JWT "iss" (issuer) claim value', JWT_CLAIM_COMPARISON, {
|
|
1848
|
+
expected,
|
|
1849
|
+
claims: result.claims,
|
|
1850
|
+
claim: 'iss'
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
return result;
|
|
1854
|
+
}
|
|
1855
|
+
const branded = new WeakSet();
|
|
1856
|
+
const nopkce = Symbol();
|
|
1857
|
+
async function authorizationCodeGrantRequest(as, client, clientAuthentication, callbackParameters, redirectUri, codeVerifier, options) {
|
|
1858
|
+
assertAs(as);
|
|
1859
|
+
assertClient(client);
|
|
1860
|
+
if (!branded.has(callbackParameters)) {
|
|
1861
|
+
throw CodedTypeError('"callbackParameters" must be an instance of URLSearchParams obtained from "validateAuthResponse()", or "validateJwtAuthResponse()', ERR_INVALID_ARG_VALUE);
|
|
1862
|
+
}
|
|
1863
|
+
assertString(redirectUri, '"redirectUri"');
|
|
1864
|
+
const code = getURLSearchParameter(callbackParameters, 'code');
|
|
1865
|
+
if (!code) {
|
|
1866
|
+
throw OPE('no authorization code in "callbackParameters"', INVALID_RESPONSE);
|
|
1867
|
+
}
|
|
1868
|
+
const parameters = new URLSearchParams(options?.additionalParameters);
|
|
1869
|
+
parameters.set('redirect_uri', redirectUri);
|
|
1870
|
+
parameters.set('code', code);
|
|
1871
|
+
if (codeVerifier !== nopkce) {
|
|
1872
|
+
assertString(codeVerifier, '"codeVerifier"');
|
|
1873
|
+
parameters.set('code_verifier', codeVerifier);
|
|
1874
|
+
}
|
|
1875
|
+
return tokenEndpointRequest(as, client, clientAuthentication, 'authorization_code', parameters, options);
|
|
1876
|
+
}
|
|
1877
|
+
const jwtClaimNames = {
|
|
1878
|
+
aud: 'audience',
|
|
1879
|
+
c_hash: 'code hash',
|
|
1880
|
+
client_id: 'client id',
|
|
1881
|
+
exp: 'expiration time',
|
|
1882
|
+
iat: 'issued at',
|
|
1883
|
+
iss: 'issuer',
|
|
1884
|
+
jti: 'jwt id',
|
|
1885
|
+
nonce: 'nonce',
|
|
1886
|
+
s_hash: 'state hash',
|
|
1887
|
+
sub: 'subject',
|
|
1888
|
+
ath: 'access token hash',
|
|
1889
|
+
htm: 'http method',
|
|
1890
|
+
htu: 'http uri',
|
|
1891
|
+
cnf: 'confirmation',
|
|
1892
|
+
auth_time: 'authentication time'
|
|
1893
|
+
};
|
|
1894
|
+
function validatePresence(required, result) {
|
|
1895
|
+
for (const claim of required) {
|
|
1896
|
+
if (result.claims[claim] === undefined) {
|
|
1897
|
+
throw OPE(`JWT "${claim}" (${jwtClaimNames[claim]}) claim missing`, INVALID_RESPONSE, {
|
|
1898
|
+
claims: result.claims
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
return result;
|
|
1903
|
+
}
|
|
1904
|
+
const expectNoNonce = Symbol();
|
|
1905
|
+
const skipAuthTimeCheck = Symbol();
|
|
1906
|
+
async function processAuthorizationCodeResponse(as, client, response, options) {
|
|
1907
|
+
if (typeof options?.expectedNonce === 'string' || typeof options?.maxAge === 'number' || options?.requireIdToken) {
|
|
1908
|
+
return processAuthorizationCodeOpenIDResponse(as, client, response, options.expectedNonce, options.maxAge, options[jweDecrypt], options.recognizedTokenTypes);
|
|
1909
|
+
}
|
|
1910
|
+
return processAuthorizationCodeOAuth2Response(as, client, response, options?.[jweDecrypt], options?.recognizedTokenTypes);
|
|
1911
|
+
}
|
|
1912
|
+
async function processAuthorizationCodeOpenIDResponse(as, client, response, expectedNonce, maxAge, decryptFn, recognizedTokenTypes) {
|
|
1913
|
+
const additionalRequiredClaims = [];
|
|
1914
|
+
switch (expectedNonce) {
|
|
1915
|
+
case undefined:
|
|
1916
|
+
expectedNonce = expectNoNonce;
|
|
1917
|
+
break;
|
|
1918
|
+
case expectNoNonce:
|
|
1919
|
+
break;
|
|
1920
|
+
default:
|
|
1921
|
+
assertString(expectedNonce, '"expectedNonce" argument');
|
|
1922
|
+
additionalRequiredClaims.push('nonce');
|
|
1923
|
+
}
|
|
1924
|
+
maxAge ??= client.default_max_age;
|
|
1925
|
+
switch (maxAge) {
|
|
1926
|
+
case undefined:
|
|
1927
|
+
maxAge = skipAuthTimeCheck;
|
|
1928
|
+
break;
|
|
1929
|
+
case skipAuthTimeCheck:
|
|
1930
|
+
break;
|
|
1931
|
+
default:
|
|
1932
|
+
assertNumber(maxAge, true, '"maxAge" argument');
|
|
1933
|
+
additionalRequiredClaims.push('auth_time');
|
|
1934
|
+
}
|
|
1935
|
+
const result = await processGenericAccessTokenResponse(as, client, response, additionalRequiredClaims, decryptFn, recognizedTokenTypes);
|
|
1936
|
+
assertString(result.id_token, '"response" body "id_token" property', INVALID_RESPONSE, {
|
|
1937
|
+
body: result
|
|
1938
|
+
});
|
|
1939
|
+
const claims = getValidatedIdTokenClaims(result);
|
|
1940
|
+
if (maxAge !== skipAuthTimeCheck) {
|
|
1941
|
+
const now = epochTime() + getClockSkew(client);
|
|
1942
|
+
const tolerance = getClockTolerance(client);
|
|
1943
|
+
if (claims.auth_time + maxAge < now - tolerance) {
|
|
1944
|
+
throw OPE('too much time has elapsed since the last End-User authentication', JWT_TIMESTAMP_CHECK, {
|
|
1945
|
+
claims,
|
|
1946
|
+
now,
|
|
1947
|
+
tolerance,
|
|
1948
|
+
claim: 'auth_time'
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
if (expectedNonce === expectNoNonce) {
|
|
1953
|
+
if (claims.nonce !== undefined) {
|
|
1954
|
+
throw OPE('unexpected ID Token "nonce" claim value', JWT_CLAIM_COMPARISON, {
|
|
1955
|
+
expected: undefined,
|
|
1956
|
+
claims,
|
|
1957
|
+
claim: 'nonce'
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
} else if (claims.nonce !== expectedNonce) {
|
|
1961
|
+
throw OPE('unexpected ID Token "nonce" claim value', JWT_CLAIM_COMPARISON, {
|
|
1962
|
+
expected: expectedNonce,
|
|
1963
|
+
claims,
|
|
1964
|
+
claim: 'nonce'
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
return result;
|
|
1968
|
+
}
|
|
1969
|
+
async function processAuthorizationCodeOAuth2Response(as, client, response, decryptFn, recognizedTokenTypes) {
|
|
1970
|
+
const result = await processGenericAccessTokenResponse(as, client, response, undefined, decryptFn, recognizedTokenTypes);
|
|
1971
|
+
const claims = getValidatedIdTokenClaims(result);
|
|
1972
|
+
if (claims) {
|
|
1973
|
+
if (client.default_max_age !== undefined) {
|
|
1974
|
+
assertNumber(client.default_max_age, true, '"client.default_max_age"');
|
|
1975
|
+
const now = epochTime() + getClockSkew(client);
|
|
1976
|
+
const tolerance = getClockTolerance(client);
|
|
1977
|
+
if (claims.auth_time + client.default_max_age < now - tolerance) {
|
|
1978
|
+
throw OPE('too much time has elapsed since the last End-User authentication', JWT_TIMESTAMP_CHECK, {
|
|
1979
|
+
claims,
|
|
1980
|
+
now,
|
|
1981
|
+
tolerance,
|
|
1982
|
+
claim: 'auth_time'
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
if (claims.nonce !== undefined) {
|
|
1987
|
+
throw OPE('unexpected ID Token "nonce" claim value', JWT_CLAIM_COMPARISON, {
|
|
1988
|
+
expected: undefined,
|
|
1989
|
+
claims,
|
|
1990
|
+
claim: 'nonce'
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
return result;
|
|
1995
|
+
}
|
|
1996
|
+
const WWW_AUTHENTICATE_CHALLENGE = 'OAUTH_WWW_AUTHENTICATE_CHALLENGE';
|
|
1997
|
+
const RESPONSE_BODY_ERROR = 'OAUTH_RESPONSE_BODY_ERROR';
|
|
1998
|
+
const UNSUPPORTED_OPERATION = 'OAUTH_UNSUPPORTED_OPERATION';
|
|
1999
|
+
const PARSE_ERROR = 'OAUTH_PARSE_ERROR';
|
|
2000
|
+
const INVALID_RESPONSE = 'OAUTH_INVALID_RESPONSE';
|
|
2001
|
+
const RESPONSE_IS_NOT_JSON = 'OAUTH_RESPONSE_IS_NOT_JSON';
|
|
2002
|
+
const RESPONSE_IS_NOT_CONFORM = 'OAUTH_RESPONSE_IS_NOT_CONFORM';
|
|
2003
|
+
const HTTP_REQUEST_FORBIDDEN = 'OAUTH_HTTP_REQUEST_FORBIDDEN';
|
|
2004
|
+
const REQUEST_PROTOCOL_FORBIDDEN = 'OAUTH_REQUEST_PROTOCOL_FORBIDDEN';
|
|
2005
|
+
const JWT_TIMESTAMP_CHECK = 'OAUTH_JWT_TIMESTAMP_CHECK_FAILED';
|
|
2006
|
+
const JWT_CLAIM_COMPARISON = 'OAUTH_JWT_CLAIM_COMPARISON_FAILED';
|
|
2007
|
+
const MISSING_SERVER_METADATA = 'OAUTH_MISSING_SERVER_METADATA';
|
|
2008
|
+
const INVALID_SERVER_METADATA = 'OAUTH_INVALID_SERVER_METADATA';
|
|
2009
|
+
function assertReadableResponse(response) {
|
|
2010
|
+
if (response.bodyUsed) {
|
|
2011
|
+
throw CodedTypeError('"response" body has been used already', ERR_INVALID_ARG_VALUE);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
async function validateJwt(jws, checkAlg, clockSkew, clockTolerance, decryptJwt) {
|
|
2015
|
+
let {
|
|
2016
|
+
0: protectedHeader,
|
|
2017
|
+
1: payload,
|
|
2018
|
+
length
|
|
2019
|
+
} = jws.split('.');
|
|
2020
|
+
if (length === 5) {
|
|
2021
|
+
if (decryptJwt !== undefined) {
|
|
2022
|
+
jws = await decryptJwt(jws);
|
|
2023
|
+
({
|
|
2024
|
+
0: protectedHeader,
|
|
2025
|
+
1: payload,
|
|
2026
|
+
length
|
|
2027
|
+
} = jws.split('.'));
|
|
2028
|
+
} else {
|
|
2029
|
+
throw new UnsupportedOperationError('JWE decryption is not configured', {
|
|
2030
|
+
cause: jws
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
if (length !== 3) {
|
|
2035
|
+
throw OPE('Invalid JWT', INVALID_RESPONSE, jws);
|
|
2036
|
+
}
|
|
2037
|
+
let header;
|
|
2038
|
+
try {
|
|
2039
|
+
header = JSON.parse(buf(b64u(protectedHeader)));
|
|
2040
|
+
} catch (cause) {
|
|
2041
|
+
throw OPE('failed to parse JWT Header body as base64url encoded JSON', PARSE_ERROR, cause);
|
|
2042
|
+
}
|
|
2043
|
+
if (!isJsonObject(header)) {
|
|
2044
|
+
throw OPE('JWT Header must be a top level object', INVALID_RESPONSE, jws);
|
|
2045
|
+
}
|
|
2046
|
+
checkAlg(header);
|
|
2047
|
+
if (header.crit !== undefined) {
|
|
2048
|
+
throw new UnsupportedOperationError('no JWT "crit" header parameter extensions are supported', {
|
|
2049
|
+
cause: {
|
|
2050
|
+
header
|
|
2051
|
+
}
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
let claims;
|
|
2055
|
+
try {
|
|
2056
|
+
claims = JSON.parse(buf(b64u(payload)));
|
|
2057
|
+
} catch (cause) {
|
|
2058
|
+
throw OPE('failed to parse JWT Payload body as base64url encoded JSON', PARSE_ERROR, cause);
|
|
2059
|
+
}
|
|
2060
|
+
if (!isJsonObject(claims)) {
|
|
2061
|
+
throw OPE('JWT Payload must be a top level object', INVALID_RESPONSE, jws);
|
|
2062
|
+
}
|
|
2063
|
+
const now = epochTime() + clockSkew;
|
|
2064
|
+
if (claims.exp !== undefined) {
|
|
2065
|
+
if (typeof claims.exp !== 'number') {
|
|
2066
|
+
throw OPE('unexpected JWT "exp" (expiration time) claim type', INVALID_RESPONSE, {
|
|
2067
|
+
claims
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
if (claims.exp <= now - clockTolerance) {
|
|
2071
|
+
throw OPE('unexpected JWT "exp" (expiration time) claim value, expiration is past current timestamp', JWT_TIMESTAMP_CHECK, {
|
|
2072
|
+
claims,
|
|
2073
|
+
now,
|
|
2074
|
+
tolerance: clockTolerance,
|
|
2075
|
+
claim: 'exp'
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
if (claims.iat !== undefined) {
|
|
2080
|
+
if (typeof claims.iat !== 'number') {
|
|
2081
|
+
throw OPE('unexpected JWT "iat" (issued at) claim type', INVALID_RESPONSE, {
|
|
2082
|
+
claims
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
if (claims.iss !== undefined) {
|
|
2087
|
+
if (typeof claims.iss !== 'string') {
|
|
2088
|
+
throw OPE('unexpected JWT "iss" (issuer) claim type', INVALID_RESPONSE, {
|
|
2089
|
+
claims
|
|
2090
|
+
});
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
if (claims.nbf !== undefined) {
|
|
2094
|
+
if (typeof claims.nbf !== 'number') {
|
|
2095
|
+
throw OPE('unexpected JWT "nbf" (not before) claim type', INVALID_RESPONSE, {
|
|
2096
|
+
claims
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
if (claims.nbf > now + clockTolerance) {
|
|
2100
|
+
throw OPE('unexpected JWT "nbf" (not before) claim value', JWT_TIMESTAMP_CHECK, {
|
|
2101
|
+
claims,
|
|
2102
|
+
now,
|
|
2103
|
+
tolerance: clockTolerance,
|
|
2104
|
+
claim: 'nbf'
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
if (claims.aud !== undefined) {
|
|
2109
|
+
if (typeof claims.aud !== 'string' && !Array.isArray(claims.aud)) {
|
|
2110
|
+
throw OPE('unexpected JWT "aud" (audience) claim type', INVALID_RESPONSE, {
|
|
2111
|
+
claims
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
return {
|
|
2116
|
+
header,
|
|
2117
|
+
claims,
|
|
2118
|
+
jwt: jws
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
function checkSigningAlgorithm(client, issuer, fallback, header) {
|
|
2122
|
+
if (client !== undefined) {
|
|
2123
|
+
if (typeof client === 'string' ? header.alg !== client : !client.includes(header.alg)) {
|
|
2124
|
+
throw OPE('unexpected JWT "alg" header parameter', INVALID_RESPONSE, {
|
|
2125
|
+
header,
|
|
2126
|
+
expected: client,
|
|
2127
|
+
reason: 'client configuration'
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2132
|
+
if (Array.isArray(issuer)) {
|
|
2133
|
+
if (!issuer.includes(header.alg)) {
|
|
2134
|
+
throw OPE('unexpected JWT "alg" header parameter', INVALID_RESPONSE, {
|
|
2135
|
+
header,
|
|
2136
|
+
expected: issuer,
|
|
2137
|
+
reason: 'authorization server metadata'
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
if (fallback !== undefined) {
|
|
2143
|
+
if (typeof fallback === 'string' ? header.alg !== fallback : typeof fallback === 'function' ? !fallback(header.alg) : !fallback.includes(header.alg)) {
|
|
2144
|
+
throw OPE('unexpected JWT "alg" header parameter', INVALID_RESPONSE, {
|
|
2145
|
+
header,
|
|
2146
|
+
expected: fallback,
|
|
2147
|
+
reason: 'default value'
|
|
2148
|
+
});
|
|
2149
|
+
}
|
|
2150
|
+
return;
|
|
2151
|
+
}
|
|
2152
|
+
throw OPE('missing client or server configuration to verify used JWT "alg" header parameter', undefined, {
|
|
2153
|
+
client,
|
|
2154
|
+
issuer,
|
|
2155
|
+
fallback
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
function getURLSearchParameter(parameters, name) {
|
|
2159
|
+
const {
|
|
2160
|
+
0: value,
|
|
2161
|
+
length
|
|
2162
|
+
} = parameters.getAll(name);
|
|
2163
|
+
if (length > 1) {
|
|
2164
|
+
throw OPE(`"${name}" parameter must be provided only once`, INVALID_RESPONSE);
|
|
2165
|
+
}
|
|
2166
|
+
return value;
|
|
2167
|
+
}
|
|
2168
|
+
async function getResponseJsonBody(response, check = assertApplicationJson) {
|
|
2169
|
+
let json;
|
|
2170
|
+
try {
|
|
2171
|
+
json = await response.json();
|
|
2172
|
+
} catch (cause) {
|
|
2173
|
+
check(response);
|
|
2174
|
+
throw OPE('failed to parse "response" body as JSON', PARSE_ERROR, cause);
|
|
2175
|
+
}
|
|
2176
|
+
if (!isJsonObject(json)) {
|
|
2177
|
+
throw OPE('"response" body must be a top level object', INVALID_RESPONSE, {
|
|
2178
|
+
body: json
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
return json;
|
|
2182
|
+
}
|
|
2183
|
+
const _expectedIssuer = Symbol();
|
|
1361
2184
|
|
|
1362
2185
|
// cspell:ignore pkce
|
|
1363
2186
|
|
|
@@ -1646,19 +2469,18 @@ const getEffectiveRedirectUri = async (platform, uid, redirectUri) => {
|
|
|
1646
2469
|
const oidcClientId = 'lvce-editor-native';
|
|
1647
2470
|
const oidcScope = 'openid offline_access profile email';
|
|
1648
2471
|
|
|
1649
|
-
// cspell:ignore pkce
|
|
1650
|
-
|
|
1651
2472
|
const getBackendLoginRequest = async (backendUrl, platform = 0, uid = 0, redirectUri = '', createPkceValuesFn = createPkceValues, createRandomUuid = () => globalThis.crypto.randomUUID()) => {
|
|
1652
2473
|
const effectiveRedirectUri = await getEffectiveRedirectUri(platform, uid, redirectUri);
|
|
1653
2474
|
const {
|
|
1654
2475
|
codeChallenge,
|
|
1655
2476
|
codeVerifier
|
|
1656
2477
|
} = await createPkceValuesFn();
|
|
2478
|
+
const nonce = createRandomUuid();
|
|
1657
2479
|
const loginUrl = new URL(getBackendAuthUrl(backendUrl, '/oidc/auth'));
|
|
1658
2480
|
loginUrl.searchParams.set('client_id', oidcClientId);
|
|
1659
2481
|
loginUrl.searchParams.set('code_challenge', codeChallenge);
|
|
1660
2482
|
loginUrl.searchParams.set('code_challenge_method', 'S256');
|
|
1661
|
-
loginUrl.searchParams.set('nonce',
|
|
2483
|
+
loginUrl.searchParams.set('nonce', nonce);
|
|
1662
2484
|
loginUrl.searchParams.set('prompt', 'consent');
|
|
1663
2485
|
loginUrl.searchParams.set('response_type', 'code');
|
|
1664
2486
|
loginUrl.searchParams.set('scope', oidcScope);
|
|
@@ -1669,16 +2491,19 @@ const getBackendLoginRequest = async (backendUrl, platform = 0, uid = 0, redirec
|
|
|
1669
2491
|
return {
|
|
1670
2492
|
codeVerifier,
|
|
1671
2493
|
loginUrl: loginUrl.toString(),
|
|
2494
|
+
nonce,
|
|
1672
2495
|
redirectUri: effectiveRedirectUri
|
|
1673
2496
|
};
|
|
1674
2497
|
};
|
|
1675
2498
|
|
|
1676
2499
|
const getLoggedInState = (state, response) => {
|
|
1677
2500
|
const accessToken = typeof response.accessToken === 'string' ? response.accessToken : '';
|
|
2501
|
+
const refreshToken = typeof response.refreshToken === 'string' ? response.refreshToken : '';
|
|
1678
2502
|
return {
|
|
1679
2503
|
...state,
|
|
1680
2504
|
authAccessToken: accessToken,
|
|
1681
2505
|
authErrorMessage: '',
|
|
2506
|
+
authRefreshToken: refreshToken,
|
|
1682
2507
|
userName: typeof response.userName === 'string' ? response.userName : state.userName,
|
|
1683
2508
|
userState: accessToken ? 'loggedIn' : 'loggedOut',
|
|
1684
2509
|
userSubscriptionPlan: typeof response.subscriptionPlan === 'string' ? response.subscriptionPlan : state.userSubscriptionPlan,
|
|
@@ -1693,22 +2518,54 @@ const isLoginResponse = value => {
|
|
|
1693
2518
|
return true;
|
|
1694
2519
|
};
|
|
1695
2520
|
|
|
2521
|
+
const getBackendOidcTokenUrl = backendUrl => {
|
|
2522
|
+
return getBackendAuthUrl(backendUrl, '/oidc/token');
|
|
2523
|
+
};
|
|
2524
|
+
|
|
2525
|
+
const getAuthorizationServer = backendUrl => {
|
|
2526
|
+
return {
|
|
2527
|
+
issuer: getBackendAuthUrl(backendUrl, '/oidc'),
|
|
2528
|
+
jwks_uri: getBackendAuthUrl(backendUrl, '/oidc/jwks'),
|
|
2529
|
+
token_endpoint: getBackendOidcTokenUrl(backendUrl)
|
|
2530
|
+
};
|
|
2531
|
+
};
|
|
2532
|
+
const getClient = () => {
|
|
2533
|
+
return {
|
|
2534
|
+
client_id: oidcClientId
|
|
2535
|
+
};
|
|
2536
|
+
};
|
|
2537
|
+
const exchangeElectronAuthorizationCode = async (backendUrl, code, redirectUri, codeVerifier, nonce, requestAuthorizationCodeGrant = authorizationCodeGrantRequest, processAuthorizationCodeGrantResponse = processAuthorizationCodeResponse) => {
|
|
2538
|
+
const authorizationServer = getAuthorizationServer(backendUrl);
|
|
2539
|
+
const client = getClient();
|
|
2540
|
+
const response = await requestAuthorizationCodeGrant(authorizationServer, client, None(), new URLSearchParams({
|
|
2541
|
+
code
|
|
2542
|
+
}), redirectUri, codeVerifier);
|
|
2543
|
+
const tokenResponse = await processAuthorizationCodeGrantResponse(authorizationServer, client, response, {
|
|
2544
|
+
expectedNonce: nonce
|
|
2545
|
+
});
|
|
2546
|
+
return {
|
|
2547
|
+
accessToken: tokenResponse.access_token,
|
|
2548
|
+
refreshToken: typeof tokenResponse.refresh_token === 'string' ? tokenResponse.refresh_token : ''
|
|
2549
|
+
};
|
|
2550
|
+
};
|
|
2551
|
+
|
|
1696
2552
|
const hasAuthorizationCode = value => {
|
|
1697
2553
|
return typeof value === 'string' && value.length > 0;
|
|
1698
2554
|
};
|
|
1699
2555
|
const getElectronAuthorizationCode = async uid => {
|
|
1700
2556
|
return invoke$2('OAuthServer.getCode', String(uid));
|
|
1701
2557
|
};
|
|
1702
|
-
const waitForElectronBackendLogin = async (uid, codeVerifier, timeoutMs = 30_000, pollIntervalMs = 1000, getAuthorizationCode = getElectronAuthorizationCode) => {
|
|
2558
|
+
const waitForElectronBackendLogin = async (backendUrl, uid, redirectUri, nonce, codeVerifier, timeoutMs = 30_000, pollIntervalMs = 1000, getAuthorizationCode = getElectronAuthorizationCode, exchangeAuthorizationCode = exchangeElectronAuthorizationCode) => {
|
|
1703
2559
|
const deadline = Date.now() + timeoutMs;
|
|
1704
2560
|
while (Date.now() < deadline) {
|
|
1705
2561
|
const authorizationCode = await getAuthorizationCode(uid);
|
|
1706
2562
|
if (hasAuthorizationCode(authorizationCode)) {
|
|
2563
|
+
const tokenResponse = await exchangeAuthorizationCode(backendUrl, authorizationCode, redirectUri, codeVerifier, nonce);
|
|
1707
2564
|
return {
|
|
1708
|
-
|
|
1709
|
-
authCodeVerifier: codeVerifier,
|
|
2565
|
+
authAccessToken: tokenResponse.accessToken,
|
|
1710
2566
|
authErrorMessage: '',
|
|
1711
|
-
|
|
2567
|
+
authRefreshToken: tokenResponse.refreshToken,
|
|
2568
|
+
userState: tokenResponse.accessToken ? 'loggedIn' : 'loggedOut'
|
|
1712
2569
|
};
|
|
1713
2570
|
}
|
|
1714
2571
|
await delay(pollIntervalMs);
|
|
@@ -1752,10 +2609,12 @@ const handleClickLogin = async options => {
|
|
|
1752
2609
|
const uid = 0;
|
|
1753
2610
|
const {
|
|
1754
2611
|
codeVerifier,
|
|
1755
|
-
loginUrl
|
|
2612
|
+
loginUrl,
|
|
2613
|
+
nonce,
|
|
2614
|
+
redirectUri
|
|
1756
2615
|
} = await getBackendLoginRequest(backendUrl, platform, uid);
|
|
1757
2616
|
await invoke$1('Open.openUrl', loginUrl, platform, authUseRedirect);
|
|
1758
|
-
const authState = platform === Electron ? await waitForElectronBackendLogin(uid, codeVerifier) : await waitForBackendLogin(backendUrl);
|
|
2617
|
+
const authState = platform === Electron ? await waitForElectronBackendLogin(backendUrl, uid, redirectUri, nonce, codeVerifier) : await waitForBackendLogin(backendUrl);
|
|
1759
2618
|
return {
|
|
1760
2619
|
...authState
|
|
1761
2620
|
};
|