@telecrypt-io/storage 0.5.26 → 0.5.27

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.
Files changed (2) hide show
  1. package/dist/core/oidc.js +50 -7
  2. package/package.json +1 -1
package/dist/core/oidc.js CHANGED
@@ -35,6 +35,9 @@ const MAX_OIDC_TOKEN_LENGTH = 8192;
35
35
  const MAX_OIDC_SCOPE_LENGTH = 4096;
36
36
  const MAX_OIDC_CONTACTS = 16;
37
37
  const MAX_OIDC_REDIRECT_URIS = 8;
38
+ const LEGACY_MATRIX_SCOPE_PREFIX = "urn:matrix:client:";
39
+ const MSC2967_MATRIX_SCOPE_PREFIX = "urn:matrix:org.matrix.msc2967.client:";
40
+ const MATRIX_SCOPE_PREFIXES = [LEGACY_MATRIX_SCOPE_PREFIX, MSC2967_MATRIX_SCOPE_PREFIX];
38
41
  // MAS advertises a 20-minute device-code lifetime. Keep that provider window
39
42
  // as the hard upper bound while retaining a finite polling budget.
40
43
  const MAX_DEVICE_EXPIRES_IN_SECONDS = 20 * 60;
@@ -791,7 +794,9 @@ export async function waitForDeviceCodeLogin(authMetadata, clientId, session, si
791
794
  }, Math.min(OIDC_REQUEST_TIMEOUT_MS, remaining), signal);
792
795
  if (response.ok && isValidDeviceAccessTokenResponse(body)) {
793
796
  const tokenResponse = validateDeviceAccessTokenResponse(body);
794
- if (!requestedDeviceId || tokenResponse.scope !== generateScope(requestedDeviceId)) {
797
+ if (!requestedDeviceId ||
798
+ typeof tokenResponse.scope !== "string" ||
799
+ !scopeMatchesDevice(tokenResponse.scope, requestedDeviceId)) {
795
800
  throw new OidcValidationError("OIDC device authorization returned an unexpected granted scope");
796
801
  }
797
802
  ensureOidcNotCancelled(signal, "device authorization");
@@ -940,9 +945,8 @@ export async function completeAuthorizationCodeFlow(code, state, signal) {
940
945
  validateBearerTokenResponse(body);
941
946
  validateTokenBounds(body, "authorization code exchange");
942
947
  const normalized = normalizeBearerTokenResponseTokenType(body);
943
- const expectedScope = generateScope(context.deviceId);
944
948
  const grantedScope = normalized.scope;
945
- if (typeof grantedScope !== "string" || grantedScope !== expectedScope) {
949
+ if (typeof grantedScope !== "string" || !scopeMatchesDevice(grantedScope, context.deviceId)) {
946
950
  throw new StorageError("OIDC authorization code exchange returned an unexpected granted scope");
947
951
  }
948
952
  const tokenResponse = {
@@ -975,12 +979,52 @@ export function extractDeviceIdFromScope(scope) {
975
979
  return null;
976
980
  }
977
981
  const tokens = scope.split(/\s+/);
978
- const deviceTokens = tokens.filter((token) => token.startsWith("urn:matrix:client:device:"));
982
+ const deviceTokens = tokens.filter((token) => MATRIX_SCOPE_PREFIXES.some((prefix) => token.startsWith(`${prefix}device:`)));
979
983
  if (deviceTokens.length !== 1)
980
984
  return null;
981
- const deviceId = deviceTokens[0].slice("urn:matrix:client:device:".length);
985
+ const deviceToken = deviceTokens[0];
986
+ const prefix = MATRIX_SCOPE_PREFIXES.find((candidate) => deviceToken.startsWith(`${candidate}device:`));
987
+ if (!prefix)
988
+ return null;
989
+ const deviceId = deviceToken.slice(`${prefix}device:`.length);
982
990
  return DEVICE_ID_PATTERN.test(deviceId) ? deviceId : null;
983
991
  }
992
+ /**
993
+ * Accepts the legacy Matrix OAuth scope and the current MSC2967 namespace
994
+ * returned by MAS. The provider may add the standard `openid` scope, but the
995
+ * API and device grants must remain one exact namespace pair bound to the
996
+ * device that this client requested.
997
+ */
998
+ function scopeMatchesDevice(scope, expectedDeviceId) {
999
+ if (typeof scope !== "string" ||
1000
+ typeof expectedDeviceId !== "string" ||
1001
+ !DEVICE_ID_PATTERN.test(expectedDeviceId) ||
1002
+ scope.length === 0 ||
1003
+ scope.length > MAX_OIDC_SCOPE_LENGTH ||
1004
+ /[\u0000-\u001f\u007f]/.test(scope) ||
1005
+ scope.trim() !== scope ||
1006
+ /\s{2,}/.test(scope)) {
1007
+ return false;
1008
+ }
1009
+ const tokens = scope.split(/\s+/);
1010
+ if (new Set(tokens).size !== tokens.length)
1011
+ return false;
1012
+ return MATRIX_SCOPE_PREFIXES.some((prefix) => {
1013
+ const expected = new Set([
1014
+ `${prefix}api:*`,
1015
+ `${prefix}device:${expectedDeviceId}`,
1016
+ "openid",
1017
+ ]);
1018
+ const required = new Set([
1019
+ `${prefix}api:*`,
1020
+ `${prefix}device:${expectedDeviceId}`,
1021
+ ]);
1022
+ return (tokens.length >= required.size &&
1023
+ tokens.length <= expected.size &&
1024
+ tokens.every((token) => expected.has(token)) &&
1025
+ [...required].every((token) => tokens.includes(token)));
1026
+ });
1027
+ }
984
1028
  function validateMatrixUserId(userId, serverName) {
985
1029
  try {
986
1030
  return validateCanonicalMatrixUserId(userId, serverName);
@@ -1120,8 +1164,7 @@ async function refreshOidcToken(tokenEndpoint, clientId, refreshToken, expectedD
1120
1164
  catch {
1121
1165
  throw new StorageError("OIDC token refresh returned an invalid scope");
1122
1166
  }
1123
- if (extractDeviceIdFromScope(grantedScope) === null ||
1124
- grantedScope !== generateScope(expectedDeviceId)) {
1167
+ if (!scopeMatchesDevice(grantedScope, expectedDeviceId)) {
1125
1168
  throw new StorageError("OIDC token refresh returned an unexpected granted scope");
1126
1169
  }
1127
1170
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telecrypt-io/storage",
3
- "version": "0.5.26",
3
+ "version": "0.5.27",
4
4
  "license": "BUSL-1.1",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.19.0",