@dereekb/firebase-server 13.27.1 → 13.29.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/oidc/index.esm.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { createParamDecorator, Injectable, Inject, Optional, Logger, UnauthorizedException, Get, Param, Req, Res, Post, HttpCode, HttpStatus, Body, Controller, HttpException, All } from '@nestjs/common';
2
2
  import { errors } from 'oidc-provider';
3
- import { SECONDS_IN_MINUTE, SECONDS_IN_DAY, cachedGetter, unixDateTimeSecondsNumberToDate, unixDateTimeSecondsNumberForNow, selectiveFieldEncryptor, filterUndefinedValues, websiteUrlFromPaths, firstValue, hasHttpPrefix } from '@dereekb/util';
3
+ import { SECONDS_IN_MINUTE, SECONDS_IN_DAY, cachedGetter, unixDateTimeSecondsNumberToDate, unixDateTimeSecondsNumberForNow, selectiveFieldEncryptor, filterUndefinedValues, websiteUrlFromPaths, firstValue, hasHttpPrefix, filterMaybeArrayValues } from '@dereekb/util';
4
4
  import { generateKeyPairSync, randomBytes } from 'node:crypto';
5
5
  import { resolveEncryptionKey, encryptValue, decryptValue, createAesStringEncryptionProvider, isValidAES256GCMEncryptionSecret } from '@dereekb/nestjs';
6
- import { where, iterateFirestoreDocumentSnapshotPairs, firestoreModelIdentity, snapshotConverterFunctions, optionalFirestoreDate, firestoreDate, firestoreEnum, firestorePassThroughField, AbstractFirestoreDocument, createOidcClientParamsType, deleteOidcClientParamsType, deleteOidcTokenParamsType, rotateOidcClientSecretParamsType, updateOidcClientParamsType, firestoreModelKey, oidcEntryIdentity, PUBLIC_PKCE_TOKEN_ENDPOINT_AUTH_METHOD, OIDC_ENTRY_CLIENT_TYPE, oidcEntriesByUserCodeQuery, oidcEntriesByUidQuery, oidcEntriesByGrantIdQuery, oidcEntryFirestoreCollection, callModelOidcScopeForCallType, CALL_MODEL_MISSING_OIDC_SCOPE_ERROR_CODE } from '@dereekb/firebase';
6
+ import { where, iterateFirestoreDocumentSnapshotPairs, firestoreModelIdentity, snapshotConverterFunctions, optionalFirestoreDate, firestoreDate, firestoreEnum, firestorePassThroughField, AbstractFirestoreDocument, createOidcClientParamsType, deleteOidcClientParamsType, deleteOidcTokenParamsType, rotateOidcClientSecretParamsType, updateOidcClientParamsType, firestoreModelKey, oidcEntryIdentity, PUBLIC_PKCE_TOKEN_ENDPOINT_AUTH_METHOD, OIDC_ENTRY_CLIENT_TYPE, oidcEntriesByUserCodeQuery, oidcEntriesByUidQuery, oidcEntriesByGrantIdQuery, oidcProviderProfilesForKeys, requiredScopesForOidcProviderProfiles, scopesForOidcProviderProfiles, oidcEntryFirestoreCollection, callModelOidcScopeForCallType, CALL_MODEL_MISSING_OIDC_SCOPE_ERROR_CODE } from '@dereekb/firebase';
7
7
  import { firestoreEncryptedField, firebaseServerActionsContext, FirebaseServerEnvService, FirebaseServerAnalyticsService, FIREBASE_FIRESTORE_CONTEXT_TOKEN, FirebaseServerFirestoreContextModule, forbiddenError } from '@dereekb/firebase-server';
8
8
  import { nanoid } from 'nanoid';
9
9
  import { safeToJsDate } from '@dereekb/date';
@@ -342,6 +342,14 @@ function _define_property$g(obj, key, value) {
342
342
  *
343
343
  * Defaults to `false`.
344
344
  */ _define_property$g(this, "trustProxyInNonProduction", void 0);
345
+ /**
346
+ * Unified CORS configuration for the cross-origin browser OIDC endpoints
347
+ * (discovery GET, token POST). When unset, the Express CORS layer preserves the
348
+ * pre-existing behavior: `Access-Control-Allow-Origin: <appUrl>` on every
349
+ * response, which pre-empts oidc-provider's own CORS handling.
350
+ *
351
+ * @see OidcCorsConfig
352
+ */ _define_property$g(this, "cors", void 0);
345
353
  }
346
354
  _create_class$e(OidcModuleConfig, null, [
347
355
  {
@@ -2421,6 +2429,9 @@ function _ts_generator$7(thisArg, body) {
2421
2429
  if (params.dbx_max_session_ttl != null) {
2422
2430
  properties.dbx_max_session_ttl = params.dbx_max_session_ttl;
2423
2431
  }
2432
+ if (params.dbx_provider_profiles != null) {
2433
+ properties.dbx_provider_profiles = params.dbx_provider_profiles;
2434
+ }
2424
2435
  // Merge any pre-validated metadata (e.g., inline jwks for private_key_jwt in tests)
2425
2436
  if (validatedMetadata === null || validatedMetadata === void 0 ? void 0 : validatedMetadata.jwks) {
2426
2437
  properties.jwks = validatedMetadata.jwks;
@@ -2478,7 +2489,7 @@ function _ts_generator$7(thisArg, body) {
2478
2489
  * @throws {Error} When the client is not found.
2479
2490
  */ function updateClient(clientId, params) {
2480
2491
  return _async_to_generator$7(function() {
2481
- var provider, ProviderClient, existing, updatedMetadata, _params_logo_uri, _params_client_uri, _params_dbx_max_session_ttl, client;
2492
+ var provider, ProviderClient, existing, updatedMetadata, _params_logo_uri, _params_client_uri, _params_dbx_max_session_ttl, _params_dbx_provider_profiles, client;
2482
2493
  return _ts_generator$7(this, function(_state) {
2483
2494
  switch(_state.label){
2484
2495
  case 0:
@@ -2510,6 +2521,12 @@ function _ts_generator$7(thisArg, body) {
2510
2521
  if (params.dbx_max_session_ttl !== undefined) {
2511
2522
  updatedMetadata.dbx_max_session_ttl = (_params_dbx_max_session_ttl = params.dbx_max_session_ttl) !== null && _params_dbx_max_session_ttl !== void 0 ? _params_dbx_max_session_ttl : undefined;
2512
2523
  }
2524
+ // Only overwrite the profile assignment when the field is explicitly provided. An omitted value
2525
+ // (including one stripped for a non-admin caller) preserves the existing assignment; `null`/`[]`
2526
+ // clears it. This is what makes the non-admin strip a no-op rather than a wipe.
2527
+ if (params.dbx_provider_profiles !== undefined) {
2528
+ updatedMetadata.dbx_provider_profiles = (_params_dbx_provider_profiles = params.dbx_provider_profiles) !== null && _params_dbx_provider_profiles !== void 0 ? _params_dbx_provider_profiles : undefined;
2529
+ }
2513
2530
  // Mirrors oidc-provider's lib/helpers/add_client.js: re-validates and persists.
2514
2531
  return [
2515
2532
  4,
@@ -3223,13 +3240,13 @@ var GRANTABLE_MODELS = new Set(GRANTABLE_MODEL_NAMES);
3223
3240
  return FirestoreAdapter;
3224
3241
  }
3225
3242
 
3226
- function _array_like_to_array$5(arr, len) {
3243
+ function _array_like_to_array$6(arr, len) {
3227
3244
  if (len == null || len > arr.length) len = arr.length;
3228
3245
  for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i];
3229
3246
  return arr2;
3230
3247
  }
3231
- function _array_without_holes$5(arr) {
3232
- if (Array.isArray(arr)) return _array_like_to_array$5(arr);
3248
+ function _array_without_holes$6(arr) {
3249
+ if (Array.isArray(arr)) return _array_like_to_array$6(arr);
3233
3250
  }
3234
3251
  function _class_call_check$a(instance, Constructor) {
3235
3252
  if (!(instance instanceof Constructor)) {
@@ -3262,22 +3279,22 @@ function _define_property$a(obj, key, value) {
3262
3279
  }
3263
3280
  return obj;
3264
3281
  }
3265
- function _iterable_to_array$5(iter) {
3282
+ function _iterable_to_array$6(iter) {
3266
3283
  if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter);
3267
3284
  }
3268
- function _non_iterable_spread$5() {
3285
+ function _non_iterable_spread$6() {
3269
3286
  throw new TypeError("Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
3270
3287
  }
3271
- function _to_consumable_array$5(arr) {
3272
- return _array_without_holes$5(arr) || _iterable_to_array$5(arr) || _unsupported_iterable_to_array$5(arr) || _non_iterable_spread$5();
3288
+ function _to_consumable_array$6(arr) {
3289
+ return _array_without_holes$6(arr) || _iterable_to_array$6(arr) || _unsupported_iterable_to_array$6(arr) || _non_iterable_spread$6();
3273
3290
  }
3274
- function _unsupported_iterable_to_array$5(o, minLen) {
3291
+ function _unsupported_iterable_to_array$6(o, minLen) {
3275
3292
  if (!o) return;
3276
- if (typeof o === "string") return _array_like_to_array$5(o, minLen);
3293
+ if (typeof o === "string") return _array_like_to_array$6(o, minLen);
3277
3294
  var n = Object.prototype.toString.call(o).slice(8, -1);
3278
3295
  if (n === "Object" && o.constructor) n = o.constructor.name;
3279
3296
  if (n === "Map" || n === "Set") return Array.from(n);
3280
- if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array$5(o, minLen);
3297
+ if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array$6(o, minLen);
3281
3298
  }
3282
3299
  // MARK: Encrypted Fields
3283
3300
  /**
@@ -3305,7 +3322,7 @@ function _unsupported_iterable_to_array$5(o, minLen) {
3305
3322
  this.provider = createAesStringEncryptionProvider(config.jwksKeyConverterConfig.encryptionSecret);
3306
3323
  this.adapterPayloadEncryptor = selectiveFieldEncryptor({
3307
3324
  provider: this.provider,
3308
- fields: _to_consumable_array$5(OIDC_ENCRYPTED_PAYLOAD_FIELDS)
3325
+ fields: _to_consumable_array$6(OIDC_ENCRYPTED_PAYLOAD_FIELDS)
3309
3326
  });
3310
3327
  }
3311
3328
  _create_class$8(OidcEncryptionService, [
@@ -3341,13 +3358,13 @@ OidcEncryptionService = __decorate([
3341
3358
  __param(0, Inject(OidcModuleConfig))
3342
3359
  ], OidcEncryptionService);
3343
3360
 
3344
- function _array_like_to_array$4(arr, len) {
3361
+ function _array_like_to_array$5(arr, len) {
3345
3362
  if (len == null || len > arr.length) len = arr.length;
3346
3363
  for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i];
3347
3364
  return arr2;
3348
3365
  }
3349
- function _array_without_holes$4(arr) {
3350
- if (Array.isArray(arr)) return _array_like_to_array$4(arr);
3366
+ function _array_without_holes$5(arr) {
3367
+ if (Array.isArray(arr)) return _array_like_to_array$5(arr);
3351
3368
  }
3352
3369
  function _class_call_check$9(instance, Constructor) {
3353
3370
  if (!(instance instanceof Constructor)) {
@@ -3380,22 +3397,22 @@ function _define_property$9(obj, key, value) {
3380
3397
  }
3381
3398
  return obj;
3382
3399
  }
3383
- function _iterable_to_array$4(iter) {
3400
+ function _iterable_to_array$5(iter) {
3384
3401
  if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter);
3385
3402
  }
3386
- function _non_iterable_spread$4() {
3403
+ function _non_iterable_spread$5() {
3387
3404
  throw new TypeError("Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
3388
3405
  }
3389
- function _to_consumable_array$4(arr) {
3390
- return _array_without_holes$4(arr) || _iterable_to_array$4(arr) || _unsupported_iterable_to_array$4(arr) || _non_iterable_spread$4();
3406
+ function _to_consumable_array$5(arr) {
3407
+ return _array_without_holes$5(arr) || _iterable_to_array$5(arr) || _unsupported_iterable_to_array$5(arr) || _non_iterable_spread$5();
3391
3408
  }
3392
- function _unsupported_iterable_to_array$4(o, minLen) {
3409
+ function _unsupported_iterable_to_array$5(o, minLen) {
3393
3410
  if (!o) return;
3394
- if (typeof o === "string") return _array_like_to_array$4(o, minLen);
3411
+ if (typeof o === "string") return _array_like_to_array$5(o, minLen);
3395
3412
  var n = Object.prototype.toString.call(o).slice(8, -1);
3396
3413
  if (n === "Object" && o.constructor) n = o.constructor.name;
3397
3414
  if (n === "Map" || n === "Set") return Array.from(n);
3398
- if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array$4(o, minLen);
3415
+ if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array$5(o, minLen);
3399
3416
  }
3400
3417
  // MARK: Routes
3401
3418
  /**
@@ -3472,7 +3489,7 @@ var DEFAULT_OIDC_CODE_CHALLENGE_METHODS = [
3472
3489
  this.providerConfig = accountService.providerConfig;
3473
3490
  this.scopesSupported = Object.keys(this.providerConfig.claims);
3474
3491
  this.claimsSupported = Array.from(new Set(Object.values(this.providerConfig.claims).flat()));
3475
- this.tokenEndpointAuthMethodsSupported = (_this_config_tokenEndpointAuthMethods = this.config.tokenEndpointAuthMethods) !== null && _this_config_tokenEndpointAuthMethods !== void 0 ? _this_config_tokenEndpointAuthMethods : _to_consumable_array$4(DEFAULT_OIDC_TOKEN_ENDPOINT_AUTH_METHODS);
3492
+ this.tokenEndpointAuthMethodsSupported = (_this_config_tokenEndpointAuthMethods = this.config.tokenEndpointAuthMethods) !== null && _this_config_tokenEndpointAuthMethods !== void 0 ? _this_config_tokenEndpointAuthMethods : _to_consumable_array$5(DEFAULT_OIDC_TOKEN_ENDPOINT_AUTH_METHODS);
3476
3493
  var appUrl = envService.appUrl;
3477
3494
  this.appLoginUrl = websiteUrlFromPaths(appUrl, [
3478
3495
  this.config.appOAuthInteractionPath,
@@ -3505,16 +3522,16 @@ var DEFAULT_OIDC_CODE_CHALLENGE_METHODS = [
3505
3522
  jwks_uri: jwksUri !== null && jwksUri !== void 0 ? jwksUri : "".concat(issuer).concat(routes.jwks),
3506
3523
  registration_endpoint: this.oidcRegistrationRouteEnabled ? "".concat(issuer).concat(routes.registration) : undefined,
3507
3524
  scopes_supported: this.scopesSupported,
3508
- response_types_supported: _to_consumable_array$4(providerConfig.responseTypes),
3525
+ response_types_supported: _to_consumable_array$5(providerConfig.responseTypes),
3509
3526
  response_modes_supported: [
3510
3527
  'query'
3511
3528
  ],
3512
- grant_types_supported: _to_consumable_array$4(providerConfig.grantTypes),
3513
- subject_types_supported: _to_consumable_array$4(DEFAULT_OIDC_SUBJECT_TYPES),
3514
- id_token_signing_alg_values_supported: _to_consumable_array$4(DEFAULT_OIDC_ID_TOKEN_SIGNING_ALG_VALUES),
3515
- token_endpoint_auth_methods_supported: _to_consumable_array$4(this.tokenEndpointAuthMethodsSupported),
3529
+ grant_types_supported: _to_consumable_array$5(providerConfig.grantTypes),
3530
+ subject_types_supported: _to_consumable_array$5(DEFAULT_OIDC_SUBJECT_TYPES),
3531
+ id_token_signing_alg_values_supported: _to_consumable_array$5(DEFAULT_OIDC_ID_TOKEN_SIGNING_ALG_VALUES),
3532
+ token_endpoint_auth_methods_supported: _to_consumable_array$5(this.tokenEndpointAuthMethodsSupported),
3516
3533
  claims_supported: this.claimsSupported,
3517
- code_challenge_methods_supported: _to_consumable_array$4(DEFAULT_OIDC_CODE_CHALLENGE_METHODS)
3534
+ code_challenge_methods_supported: _to_consumable_array$5(DEFAULT_OIDC_CODE_CHALLENGE_METHODS)
3518
3535
  };
3519
3536
  }
3520
3537
  }
@@ -3528,13 +3545,32 @@ OidcProviderConfigService = __decorate([
3528
3545
  __param(2, Inject(FirebaseServerEnvService))
3529
3546
  ], OidcProviderConfigService);
3530
3547
 
3531
- function _array_like_to_array$3(arr, len) {
3548
+ /**
3549
+ * Custom oidc-provider client metadata field holding the {@link OidcProviderProfile} keys assigned
3550
+ * to a client. Registered via `extraClientMetadata` and persisted on the client's `OidcEntry` payload.
3551
+ */ var DBX_FIREBASE_SERVER_OIDC_PROVIDER_PROFILES_CLIENT_METADATA = 'dbx_provider_profiles';
3552
+ /**
3553
+ * Resolves the unlocked + required scope sets for a client from the provider-profile registry and
3554
+ * the profile keys assigned to that client.
3555
+ *
3556
+ * @param providerProfiles - The provider-profile registry (from {@link OidcProviderConfig.providerProfiles}).
3557
+ * @param clientProfileKeys - The profile keys assigned to the client (its `dbx_provider_profiles`).
3558
+ * @returns The unlocked and required scope sets for the client.
3559
+ */ function oidcClientProviderProfileScopes(providerProfiles, clientProfileKeys) {
3560
+ var clientProfiles = oidcProviderProfilesForKeys(providerProfiles !== null && providerProfiles !== void 0 ? providerProfiles : [], clientProfileKeys);
3561
+ return {
3562
+ unlocked: scopesForOidcProviderProfiles(clientProfiles),
3563
+ required: requiredScopesForOidcProviderProfiles(clientProfiles)
3564
+ };
3565
+ }
3566
+
3567
+ function _array_like_to_array$4(arr, len) {
3532
3568
  if (len == null || len > arr.length) len = arr.length;
3533
3569
  for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i];
3534
3570
  return arr2;
3535
3571
  }
3536
- function _array_without_holes$3(arr) {
3537
- if (Array.isArray(arr)) return _array_like_to_array$3(arr);
3572
+ function _array_without_holes$4(arr) {
3573
+ if (Array.isArray(arr)) return _array_like_to_array$4(arr);
3538
3574
  }
3539
3575
  function asyncGeneratorStep$5(gen, resolve, reject, _next, _throw, key, arg) {
3540
3576
  try {
@@ -3596,10 +3632,10 @@ function _define_property$8(obj, key, value) {
3596
3632
  }
3597
3633
  return obj;
3598
3634
  }
3599
- function _iterable_to_array$3(iter) {
3635
+ function _iterable_to_array$4(iter) {
3600
3636
  if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter);
3601
3637
  }
3602
- function _non_iterable_spread$3() {
3638
+ function _non_iterable_spread$4() {
3603
3639
  throw new TypeError("Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
3604
3640
  }
3605
3641
  function _object_spread$3(target) {
@@ -3672,16 +3708,16 @@ function _object_without_properties_loose(source, excluded) {
3672
3708
  }
3673
3709
  return target;
3674
3710
  }
3675
- function _to_consumable_array$3(arr) {
3676
- return _array_without_holes$3(arr) || _iterable_to_array$3(arr) || _unsupported_iterable_to_array$3(arr) || _non_iterable_spread$3();
3711
+ function _to_consumable_array$4(arr) {
3712
+ return _array_without_holes$4(arr) || _iterable_to_array$4(arr) || _unsupported_iterable_to_array$4(arr) || _non_iterable_spread$4();
3677
3713
  }
3678
- function _unsupported_iterable_to_array$3(o, minLen) {
3714
+ function _unsupported_iterable_to_array$4(o, minLen) {
3679
3715
  if (!o) return;
3680
- if (typeof o === "string") return _array_like_to_array$3(o, minLen);
3716
+ if (typeof o === "string") return _array_like_to_array$4(o, minLen);
3681
3717
  var n = Object.prototype.toString.call(o).slice(8, -1);
3682
3718
  if (n === "Object" && o.constructor) n = o.constructor.name;
3683
3719
  if (n === "Map" || n === "Set") return Array.from(n);
3684
- if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array$3(o, minLen);
3720
+ if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array$4(o, minLen);
3685
3721
  }
3686
3722
  function _ts_generator$5(thisArg, body) {
3687
3723
  var f, y, t, _ = {
@@ -4032,7 +4068,8 @@ function _ts_generator$5(thisArg, body) {
4032
4068
  logo_uri: existing.logo_uri,
4033
4069
  client_uri: existing.client_uri,
4034
4070
  created_at: existing.created_at,
4035
- dbx_max_session_ttl: existing.dbx_max_session_ttl
4071
+ dbx_max_session_ttl: existing.dbx_max_session_ttl,
4072
+ dbx_provider_profiles: existing.dbx_provider_profiles
4036
4073
  };
4037
4074
  }
4038
4075
  return [
@@ -4058,6 +4095,7 @@ function _ts_generator$5(thisArg, body) {
4058
4095
  value: function buildProviderConfiguration(cookieKeys) {
4059
4096
  var _this = this;
4060
4097
  var _config_maxRequestedLoginDuration, _config_minRequestedLoginDuration;
4098
+ var _config_cors;
4061
4099
  var config = this.config;
4062
4100
  var providerConfig = this.providerConfigService.providerConfig;
4063
4101
  var serverMaxSeconds = (_config_maxRequestedLoginDuration = config.maxRequestedLoginDuration) !== null && _config_maxRequestedLoginDuration !== void 0 ? _config_maxRequestedLoginDuration : DEFAULT_MAX_REQUESTED_LOGIN_DURATION_SECONDS;
@@ -4075,7 +4113,7 @@ function _ts_generator$5(thisArg, body) {
4075
4113
  return _object_spread_props(_object_spread$3({
4076
4114
  routes: _object_spread$3({}, this.providerConfigService.routes),
4077
4115
  claims: _object_spread$3({}, providerConfig.claims),
4078
- responseTypes: _to_consumable_array$3(providerConfig.responseTypes),
4116
+ responseTypes: _to_consumable_array$4(providerConfig.responseTypes),
4079
4117
  pkce: {
4080
4118
  required: function required() {
4081
4119
  return true;
@@ -4138,7 +4176,8 @@ function _ts_generator$5(thisArg, body) {
4138
4176
  ],
4139
4177
  extraClientMetadata: {
4140
4178
  properties: [
4141
- DBX_FIREBASE_SERVER_OIDC_MAX_SESSION_TTL_CLIENT_METADATA
4179
+ DBX_FIREBASE_SERVER_OIDC_MAX_SESSION_TTL_CLIENT_METADATA,
4180
+ DBX_FIREBASE_SERVER_OIDC_PROVIDER_PROFILES_CLIENT_METADATA
4142
4181
  ],
4143
4182
  validator: function validator(_ctx, key, value) {
4144
4183
  if (key === DBX_FIREBASE_SERVER_OIDC_MAX_SESSION_TTL_CLIENT_METADATA && value !== undefined && value !== null) {
@@ -4148,6 +4187,22 @@ function _ts_generator$5(thisArg, body) {
4148
4187
  if (value > serverMaxSeconds) {
4149
4188
  throw new errors.InvalidClientMetadata("".concat(DBX_FIREBASE_SERVER_OIDC_MAX_SESSION_TTL_CLIENT_METADATA, " cannot exceed the server max of ").concat(serverMaxSeconds, " seconds."));
4150
4189
  }
4190
+ } else if (key === DBX_FIREBASE_SERVER_OIDC_PROVIDER_PROFILES_CLIENT_METADATA && value !== undefined && value !== null) {
4191
+ var _providerConfig_providerProfiles;
4192
+ if (!Array.isArray(value) || value.some(function(entry) {
4193
+ return typeof entry !== 'string';
4194
+ })) {
4195
+ throw new errors.InvalidClientMetadata("".concat(DBX_FIREBASE_SERVER_OIDC_PROVIDER_PROFILES_CLIENT_METADATA, " must be an array of provider profile keys."));
4196
+ }
4197
+ var knownProfileKeys = new Set(((_providerConfig_providerProfiles = providerConfig.providerProfiles) !== null && _providerConfig_providerProfiles !== void 0 ? _providerConfig_providerProfiles : []).map(function(profile) {
4198
+ return profile.key;
4199
+ }));
4200
+ var unknownKey = value.find(function(entry) {
4201
+ return !knownProfileKeys.has(entry);
4202
+ });
4203
+ if (unknownKey !== undefined) {
4204
+ throw new errors.InvalidClientMetadata("".concat(DBX_FIREBASE_SERVER_OIDC_PROVIDER_PROFILES_CLIENT_METADATA, " contains an unknown provider profile key: ").concat(unknownKey, "."));
4205
+ }
4151
4206
  }
4152
4207
  }
4153
4208
  },
@@ -4175,7 +4230,7 @@ function _ts_generator$5(thisArg, body) {
4175
4230
  interactions: {
4176
4231
  url: function url(_ctx, interaction) {
4177
4232
  return _async_to_generator$5(function() {
4178
- var baseUrl, client_id, paramsToEncode, client, scopes, interactionLoginDetails, paramsString;
4233
+ var baseUrl, client_id, paramsToEncode, client, _client_dbx_provider_profiles, scopes, requestedScopeSet, required, requiredScopes, interactionLoginDetails, paramsString;
4179
4234
  return _ts_generator$5(this, function(_state) {
4180
4235
  switch(_state.label){
4181
4236
  case 0:
@@ -4203,13 +4258,23 @@ function _ts_generator$5(thisArg, body) {
4203
4258
  client = _state.sent();
4204
4259
  if (client) {
4205
4260
  scopes = interaction.params.scope;
4206
- interactionLoginDetails = {
4261
+ // Surface the scopes the client's provider profiles force-require so the consent UI can
4262
+ // render them as required. Intersect with the actually-requested scopes so only visible
4263
+ // scope rows are flagged.
4264
+ requestedScopeSet = new Set(scopes.split(' ').filter(Boolean));
4265
+ required = oidcClientProviderProfileScopes(providerConfig.providerProfiles, (_client_dbx_provider_profiles = client.dbx_provider_profiles) !== null && _client_dbx_provider_profiles !== void 0 ? _client_dbx_provider_profiles : undefined).required;
4266
+ requiredScopes = Array.from(required).filter(function(scope) {
4267
+ return requestedScopeSet.has(scope);
4268
+ });
4269
+ interactionLoginDetails = _object_spread$3({
4207
4270
  client_id: client_id,
4208
4271
  client_name: client.client_name,
4209
4272
  client_uri: client.client_uri,
4210
4273
  logo_uri: client.logo_uri,
4211
4274
  scopes: scopes
4212
- };
4275
+ }, requiredScopes.length > 0 ? {
4276
+ requiredScopes: requiredScopes.join(' ')
4277
+ } : {});
4213
4278
  paramsToEncode = _object_spread$3({}, paramsToEncode, interactionLoginDetails);
4214
4279
  }
4215
4280
  _state.label = 3;
@@ -4231,6 +4296,19 @@ function _ts_generator$5(thisArg, body) {
4231
4296
  }
4232
4297
  }, config.renderError ? {
4233
4298
  renderError: config.renderError
4299
+ } : {}, ((_config_cors = config.cors) === null || _config_cors === void 0 ? void 0 : _config_cors.clientBased) ? {
4300
+ clientBasedCORS: function clientBasedCORS(_ctx, origin, client) {
4301
+ var _client_redirectUris;
4302
+ return ((_client_redirectUris = client.redirectUris) !== null && _client_redirectUris !== void 0 ? _client_redirectUris : []).some(function(uri) {
4303
+ var matches = false;
4304
+ try {
4305
+ matches = new URL(uri).origin === origin;
4306
+ } catch (unused) {
4307
+ // malformed persisted redirect_uri — treat as non-matching
4308
+ }
4309
+ return matches;
4310
+ });
4311
+ }
4234
4312
  } : {}), {
4235
4313
  // Setting `rotateRefreshToken` disables oidc-provider's built-in default, so we both honor the
4236
4314
  // dbx-components `nonRotatingScopes` extension (service tokens never rotate, keeping a stable
@@ -4860,9 +4938,69 @@ OidcAuthBearerTokenMiddleware = __decorate([
4860
4938
  __param(1, Inject(OidcAuthMiddlewareConfig))
4861
4939
  ], OidcAuthBearerTokenMiddleware);
4862
4940
 
4941
+ function _array_like_to_array$3(arr, len) {
4942
+ if (len == null || len > arr.length) len = arr.length;
4943
+ for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i];
4944
+ return arr2;
4945
+ }
4946
+ function _array_without_holes$3(arr) {
4947
+ if (Array.isArray(arr)) return _array_like_to_array$3(arr);
4948
+ }
4949
+ function _iterable_to_array$3(iter) {
4950
+ if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter);
4951
+ }
4952
+ function _non_iterable_spread$3() {
4953
+ throw new TypeError("Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
4954
+ }
4955
+ function _to_consumable_array$3(arr) {
4956
+ return _array_without_holes$3(arr) || _iterable_to_array$3(arr) || _unsupported_iterable_to_array$3(arr) || _non_iterable_spread$3();
4957
+ }
4958
+ function _unsupported_iterable_to_array$3(o, minLen) {
4959
+ if (!o) return;
4960
+ if (typeof o === "string") return _array_like_to_array$3(o, minLen);
4961
+ var n = Object.prototype.toString.call(o).slice(8, -1);
4962
+ if (n === "Object" && o.constructor) n = o.constructor.name;
4963
+ if (n === "Map" || n === "Set") return Array.from(n);
4964
+ if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array$3(o, minLen);
4965
+ }
4966
+ /**
4967
+ * Builds the `cors`-package origin delegate implementing the composed OIDC CORS policy.
4968
+ *
4969
+ * - An allowlisted origin (`appUrl` or any {@link OidcCorsConfig.allowOrigins} entry) is
4970
+ * reflected exactly, which pre-empts oidc-provider's own CORS handling.
4971
+ * - Otherwise, when {@link OidcCorsConfig.clientBased} is enabled, the header is OMITTED so
4972
+ * oidc-provider runs its `clientBasedCORS` check on client routes (`/token`) and its builtin
4973
+ * reflect on client-less routes (discovery/JWKS).
4974
+ * - Otherwise (client-based off), `appUrl` is returned unconditionally so oidc-provider stays
4975
+ * pre-empted and there is never a reflect-any.
4976
+ *
4977
+ * Exported for unit testing without a running Nest application.
4978
+ *
4979
+ * @param appUrl - The authoritative frontend origin (always allowed).
4980
+ * @param cors - The unified CORS configuration.
4981
+ * @returns A `cors`-package origin delegate.
4982
+ */ function oidcCorsOriginDelegate(appUrl, cors) {
4983
+ var _cors_allowOrigins;
4984
+ var clientBased = cors.clientBased === true;
4985
+ var allowlist = new Set([
4986
+ appUrl
4987
+ ].concat(_to_consumable_array$3((_cors_allowOrigins = cors.allowOrigins) !== null && _cors_allowOrigins !== void 0 ? _cors_allowOrigins : [])));
4988
+ return function(requestOrigin, callback) {
4989
+ var resolved;
4990
+ if (requestOrigin && allowlist.has(requestOrigin)) {
4991
+ resolved = requestOrigin; // reflect the exact allowlisted origin; pre-empts oidc-provider
4992
+ } else if (clientBased) {
4993
+ resolved = false; // OMIT ACAO → oidc-provider runs clientBasedCORS (token) / builtin reflect (discovery)
4994
+ } else {
4995
+ resolved = appUrl; // legacy fallback: ACAO=appUrl always; no reflect-any
4996
+ }
4997
+ callback(null, resolved);
4998
+ };
4999
+ }
4863
5000
  // MARK: Express-Level Helper
4864
5001
  /**
4865
- * Enables CORS on the NestJS app for OIDC cross-origin requests originating from the frontend.
5002
+ * Enables CORS on the NestJS app for OIDC cross-origin requests originating from the frontend
5003
+ * and any additional relying-party origins.
4866
5004
  *
4867
5005
  * When the OIDC issuer lives on a different origin than the frontend app (e.g. the frontend is
4868
5006
  * `https://app.example.com` and the OIDC server is at `https://api.example.com`), the frontend's
@@ -4870,16 +5008,22 @@ OidcAuthBearerTokenMiddleware = __decorate([
4870
5008
  * and `/interaction/{uid}/consent`. Because the POST body is `application/json`, the browser
4871
5009
  * sends a CORS preflight. Without an `Access-Control-Allow-Origin` reply the preflight fails,
4872
5010
  * the POST never reaches the server, and the OAuth flow stalls before `finishInteractionByUid`
4873
- * can run.
5011
+ * can run. Browser-based OIDC relying parties (public PKCE clients) likewise need CORS on the
5012
+ * discovery GET and the token POST.
5013
+ *
5014
+ * `appUrl` (from `FirebaseServerEnvService.appUrl`) is always allowed. Additional behavior is
5015
+ * driven by the optional {@link OidcModuleConfig.cors} configuration:
5016
+ * - {@link OidcCorsConfig.allowOrigins} adds an explicit allowlist of relying-party origins.
5017
+ * - {@link OidcCorsConfig.clientBased} defers non-allowlisted origins to oidc-provider's
5018
+ * `clientBasedCORS`, which trusts each requesting client's registered `redirect_uris`.
4874
5019
  *
4875
- * Origin is sourced from `FirebaseServerEnvService.appUrl`, which is the authoritative frontend
4876
- * origin per environment. Single-origin deployments (e.g. local dev where the frontend and the
4877
- * OIDC server share `http://localhost:9010`) still benefit: their cross-origin requests are
4878
- * actually same-origin and CORS is a no-op.
5020
+ * When `cors` is unset, this preserves the legacy single-origin behavior exactly:
5021
+ * `Access-Control-Allow-Origin: <appUrl>` on every response, which pre-empts oidc-provider's
5022
+ * own CORS handling.
4879
5023
  *
4880
5024
  * Credentials are intentionally NOT enabled (`Access-Control-Allow-Credentials` stays off). The
4881
- * `/interaction/{uid}/*` POSTs carry the Firebase ID token in the JSON body, not in cookies, so
4882
- * we keep the CORS surface minimal.
5025
+ * `/interaction/{uid}/*` POSTs and the token exchange carry their bearer material in the request
5026
+ * body, not in cookies, so we keep the CORS surface minimal.
4883
5027
  *
4884
5028
  * @param nestApp - The NestJS application to configure.
4885
5029
  *
@@ -4895,18 +5039,40 @@ OidcAuthBearerTokenMiddleware = __decorate([
4895
5039
  * ```
4896
5040
  */ function applyOidcCorsMiddleware(nestApp) {
4897
5041
  var envService = nestApp.get(FirebaseServerEnvService);
4898
- var origin = envService.appUrl;
4899
- if (!origin) {
5042
+ var appUrl = envService.appUrl;
5043
+ if (!appUrl) {
4900
5044
  return;
4901
5045
  }
4902
- nestApp.enableCors({
4903
- origin: origin,
4904
- methods: [
4905
- 'GET',
4906
- 'POST',
4907
- 'OPTIONS'
4908
- ]
4909
- });
5046
+ // OidcModuleConfig may be absent (module not registered / out of the resolution scope).
5047
+ var cors;
5048
+ try {
5049
+ var _nestApp_get;
5050
+ cors = (_nestApp_get = nestApp.get(OidcModuleConfig, {
5051
+ strict: false
5052
+ })) === null || _nestApp_get === void 0 ? void 0 : _nestApp_get.cors;
5053
+ } catch (unused) {
5054
+ cors = undefined;
5055
+ }
5056
+ if (cors) {
5057
+ nestApp.enableCors({
5058
+ origin: oidcCorsOriginDelegate(appUrl, cors),
5059
+ methods: [
5060
+ 'GET',
5061
+ 'POST',
5062
+ 'OPTIONS'
5063
+ ]
5064
+ });
5065
+ } else {
5066
+ // No cors config → byte-identical to the legacy behavior: ACAO=appUrl unconditionally.
5067
+ nestApp.enableCors({
5068
+ origin: appUrl,
5069
+ methods: [
5070
+ 'GET',
5071
+ 'POST',
5072
+ 'OPTIONS'
5073
+ ]
5074
+ });
5075
+ }
4910
5076
  }
4911
5077
 
4912
5078
  /**
@@ -6047,7 +6213,7 @@ var OidcInteractionController_1;
6047
6213
  * @throws {HttpException} HTTP 400 when the consent interaction cannot be completed.
6048
6214
  */ function postConsent(uid, body, res) {
6049
6215
  return _async_to_generator$2(function() {
6050
- var startedAt, clientId, accountId, _ref, _prompt_details_missingOIDCScope, _this_accountService_providerConfig_adminOnlyScopes, _ref1, _ref2, _prompt_details_missingOIDCClaims, _prompt_details_missingResourceScopes, _this_accountService_delegate_isAdminUser, _this_accountService_delegate, _ref3, redirectTo, interaction, prompt, params, session, missingOIDCScope, existingGrant, _tmp, encounteredOIDCScopes, requestedOIDCScopeSet, adminOnlyScopes, requestsServiceToken, isAdmin, _tmp1, _ref4, redirectTo1, requestedRawTtl, clientPayload, clientMaxSessionTtl, expiresInSeconds, grant, _tmp2, _resolveEffectiveSubset, granted, rejected, _iteratorNormalCompletion, _didIteratorError, _iteratorError, _iterator, _step, value, encounteredOIDCClaims, missingOIDCClaims, _resolveEffectiveSubset1, granted1, rejected1, missingResourceScopes, _iteratorNormalCompletion1, _didIteratorError1, _iteratorError1, _iterator1, _step1, _step_value, indicator, scopes, _body_grantedResourceScopes, requestedSubset, encounteredResourceScopes, _resolveEffectiveSubset2, granted2, rejected2, _iteratorNormalCompletion2, _didIteratorError2, _iteratorError2, _iterator2, _step2, value1, grantId, _ref5, redirectTo2, err;
6216
+ var startedAt, clientId, accountId, _ref, _prompt_details_missingOIDCScope, _this_accountService_providerConfig_adminOnlyScopes, _ref1, _ref2, _ref3, _prompt_details_missingOIDCClaims, _prompt_details_missingResourceScopes, _this_accountService_delegate_isAdminUser, _this_accountService_delegate, _ref4, redirectTo, interaction, prompt, params, session, missingOIDCScope, existingGrant, _tmp, encounteredOIDCScopes, clientPayload, requestedOIDCScopeSet, adminOnlyScopes, requestsServiceToken, isAdmin, _tmp1, _ref5, redirectTo1, providerProfiles, _oidcClientProviderProfileScopes, clientUnlockedScopes, clientRequiredScopes, profileGatedScopes, disallowedGatedScopes, _ref6, redirectTo2, missingRequiredScopes, _ref7, redirectTo3, requestedRawTtl, clientMaxSessionTtl, expiresInSeconds, grant, _tmp2, _resolveEffectiveSubset, granted, rejected, _iteratorNormalCompletion, _didIteratorError, _iteratorError, _iterator, _step, value, encounteredOIDCClaims, missingOIDCClaims, _resolveEffectiveSubset1, granted1, rejected1, missingResourceScopes, _iteratorNormalCompletion1, _didIteratorError1, _iteratorError1, _iterator1, _step1, _step_value, indicator, scopes, _body_grantedResourceScopes, requestedSubset, encounteredResourceScopes, _resolveEffectiveSubset2, granted2, rejected2, _iteratorNormalCompletion2, _didIteratorError2, _iteratorError2, _iterator2, _step2, value1, grantId, _ref8, redirectTo4, err;
6051
6217
  return _ts_generator$2(this, function(_state) {
6052
6218
  switch(_state.label){
6053
6219
  case 0:
@@ -6062,9 +6228,9 @@ var OidcInteractionController_1;
6062
6228
  case 2:
6063
6229
  _state.trys.push([
6064
6230
  2,
6065
- 20,
6231
+ 24,
6066
6232
  ,
6067
- 21
6233
+ 25
6068
6234
  ]);
6069
6235
  if (!!body.approved) return [
6070
6236
  3,
@@ -6080,7 +6246,7 @@ var OidcInteractionController_1;
6080
6246
  })
6081
6247
  ];
6082
6248
  case 3:
6083
- _ref3 = _state.sent(), redirectTo = _ref3.returnTo;
6249
+ _ref4 = _state.sent(), redirectTo = _ref4.returnTo;
6084
6250
  res.json({
6085
6251
  redirectTo: redirectTo
6086
6252
  });
@@ -6123,6 +6289,12 @@ var OidcInteractionController_1;
6123
6289
  // and re-submits them all. We accept already-encountered values as silent no-ops so a re-consent on a
6124
6290
  // client the user has previously authorized doesn't fail validation.
6125
6291
  encounteredOIDCScopes = existingGrant ? existingGrant.getOIDCScopeEncountered().split(' ').filter(Boolean) : [];
6292
+ return [
6293
+ 4,
6294
+ this.oidcService.findClientPayload(clientId)
6295
+ ];
6296
+ case 9:
6297
+ clientPayload = _state.sent();
6126
6298
  // Admin-only scope gate. The requested OIDC scope set is `missingOIDCScope ∪ encounteredOIDCScopes`
6127
6299
  // (a fresh consent has everything in `missing`; a re-consent may carry already-granted scopes in
6128
6300
  // `encountered`). If that set intersects the provider's `adminOnlyScopes` and the resolving user is
@@ -6134,26 +6306,26 @@ var OidcInteractionController_1;
6134
6306
  });
6135
6307
  if (!accountId) return [
6136
6308
  3,
6137
- 10
6309
+ 11
6138
6310
  ];
6139
6311
  return [
6140
6312
  4,
6141
6313
  (_ref1 = (_this_accountService_delegate_isAdminUser = (_this_accountService_delegate = this.accountService.delegate).isAdminUser) === null || _this_accountService_delegate_isAdminUser === void 0 ? void 0 : _this_accountService_delegate_isAdminUser.call(_this_accountService_delegate, this.accountService.userContext(accountId).authUserContext)) !== null && _ref1 !== void 0 ? _ref1 : false
6142
6314
  ];
6143
- case 9:
6315
+ case 10:
6144
6316
  _tmp1 = _state.sent();
6145
6317
  return [
6146
6318
  3,
6147
- 11
6319
+ 12
6148
6320
  ];
6149
- case 10:
6150
- _tmp1 = false;
6151
- _state.label = 11;
6152
6321
  case 11:
6322
+ _tmp1 = false;
6323
+ _state.label = 12;
6324
+ case 12:
6153
6325
  isAdmin = _tmp1;
6154
6326
  if (!(requestsServiceToken && !isAdmin)) return [
6155
6327
  3,
6156
- 13
6328
+ 14
6157
6329
  ];
6158
6330
  return [
6159
6331
  4,
@@ -6164,8 +6336,8 @@ var OidcInteractionController_1;
6164
6336
  mergeWithLastSubmission: true
6165
6337
  })
6166
6338
  ];
6167
- case 12:
6168
- _ref4 = _state.sent(), redirectTo1 = _ref4.returnTo;
6339
+ case 13:
6340
+ _ref5 = _state.sent(), redirectTo1 = _ref5.returnTo;
6169
6341
  emitOidcAnalyticsEvent(this._analytics, {
6170
6342
  type: 'consent',
6171
6343
  isSuccessful: false,
@@ -6182,20 +6354,89 @@ var OidcInteractionController_1;
6182
6354
  return [
6183
6355
  2
6184
6356
  ];
6185
- case 13:
6357
+ case 14:
6358
+ // Provider-profile scope gate. Any scope referenced by a provider profile is "gated" — available
6359
+ // only to clients whose assigned profiles unlock it. Independent of the admin-only gate above; a
6360
+ // scope may be subject to both.
6361
+ providerProfiles = this.accountService.providerConfig.providerProfiles;
6362
+ _oidcClientProviderProfileScopes = oidcClientProviderProfileScopes(providerProfiles, (_ref2 = clientPayload === null || clientPayload === void 0 ? void 0 : clientPayload.dbx_provider_profiles) !== null && _ref2 !== void 0 ? _ref2 : undefined), clientUnlockedScopes = _oidcClientProviderProfileScopes.unlocked, clientRequiredScopes = _oidcClientProviderProfileScopes.required;
6363
+ profileGatedScopes = scopesForOidcProviderProfiles(providerProfiles !== null && providerProfiles !== void 0 ? providerProfiles : []);
6364
+ // Unlock gate: reject any requested gated scope this client's profiles do not unlock.
6365
+ disallowedGatedScopes = Array.from(requestedOIDCScopeSet).filter(function(scope) {
6366
+ return profileGatedScopes.has(scope) && !clientUnlockedScopes.has(scope);
6367
+ });
6368
+ if (!(disallowedGatedScopes.length > 0)) return [
6369
+ 3,
6370
+ 16
6371
+ ];
6372
+ return [
6373
+ 4,
6374
+ this.oidcInteractionService.finishInteractionByUid(uid, {
6375
+ error: 'access_denied',
6376
+ error_description: "The following scope(s) are not available to this client: ".concat(disallowedGatedScopes.join(', '), ".")
6377
+ }, {
6378
+ mergeWithLastSubmission: true
6379
+ })
6380
+ ];
6381
+ case 15:
6382
+ _ref6 = _state.sent(), redirectTo2 = _ref6.returnTo;
6383
+ emitOidcAnalyticsEvent(this._analytics, {
6384
+ type: 'consent',
6385
+ isSuccessful: false,
6386
+ uid: accountId,
6387
+ clientId: clientId,
6388
+ reason: 'scope_not_unlocked_for_client',
6389
+ durationMs: Date.now() - startedAt
6390
+ }, this._logger);
6391
+ res.json({
6392
+ redirectTo: redirectTo2
6393
+ });
6394
+ return [
6395
+ 2
6396
+ ];
6397
+ case 16:
6398
+ // Required gate: a scope a client's profiles force-require (`require: 'required'`) must be present
6399
+ // in the request. Reject when the client did not request a required scope.
6400
+ missingRequiredScopes = Array.from(clientRequiredScopes).filter(function(scope) {
6401
+ return !requestedOIDCScopeSet.has(scope);
6402
+ });
6403
+ if (!(missingRequiredScopes.length > 0)) return [
6404
+ 3,
6405
+ 18
6406
+ ];
6407
+ return [
6408
+ 4,
6409
+ this.oidcInteractionService.finishInteractionByUid(uid, {
6410
+ error: 'access_denied',
6411
+ error_description: "This client must request the required scope(s): ".concat(missingRequiredScopes.join(', '), ".")
6412
+ }, {
6413
+ mergeWithLastSubmission: true
6414
+ })
6415
+ ];
6416
+ case 17:
6417
+ _ref7 = _state.sent(), redirectTo3 = _ref7.returnTo;
6418
+ emitOidcAnalyticsEvent(this._analytics, {
6419
+ type: 'consent',
6420
+ isSuccessful: false,
6421
+ uid: accountId,
6422
+ clientId: clientId,
6423
+ reason: 'required_scope_missing',
6424
+ durationMs: Date.now() - startedAt
6425
+ }, this._logger);
6426
+ res.json({
6427
+ redirectTo: redirectTo3
6428
+ });
6429
+ return [
6430
+ 2
6431
+ ];
6432
+ case 18:
6186
6433
  // Resolve the requested login duration up-front. The configured Grant TTL function (in
6187
6434
  // OidcService.buildProviderConfiguration) only fires when oidc-provider's koa middleware
6188
6435
  // drives `grant.save()`, so its `ctx.oidc.params` lookup of `dbx_session_ttl` returns
6189
6436
  // undefined when the consent submit runs in this controller. We pre-set `expiresIn` on
6190
6437
  // newly-created grants so they persist with the correct (tiered) TTL.
6191
6438
  requestedRawTtl = params[DBX_FIREBASE_SERVER_OIDC_SESSION_TTL_PARAM];
6192
- return [
6193
- 4,
6194
- this.oidcService.findClientPayload(clientId)
6195
- ];
6196
- case 14:
6197
- clientPayload = _state.sent();
6198
- clientMaxSessionTtl = (_ref2 = clientPayload === null || clientPayload === void 0 ? void 0 : clientPayload.dbx_max_session_ttl) !== null && _ref2 !== void 0 ? _ref2 : undefined;
6439
+ clientMaxSessionTtl = (_ref3 = clientPayload === null || clientPayload === void 0 ? void 0 : clientPayload.dbx_max_session_ttl) !== null && _ref3 !== void 0 ? _ref3 : undefined;
6199
6440
  expiresInSeconds = this.oidcService.resolveLoginDurationForGrant(requestedRawTtl, {
6200
6441
  dbx_max_session_ttl: clientMaxSessionTtl
6201
6442
  }, {
@@ -6204,28 +6445,30 @@ var OidcInteractionController_1;
6204
6445
  });
6205
6446
  if (!(existingGrant !== null && existingGrant !== void 0)) return [
6206
6447
  3,
6207
- 15
6448
+ 19
6208
6449
  ];
6209
6450
  _tmp2 = existingGrant;
6210
6451
  return [
6211
6452
  3,
6212
- 17
6453
+ 21
6213
6454
  ];
6214
- case 15:
6455
+ case 19:
6215
6456
  return [
6216
6457
  4,
6217
6458
  this.oidcInteractionService.findOrCreateGrant(interaction.grantId, accountId, clientId, expiresInSeconds)
6218
6459
  ];
6219
- case 16:
6460
+ case 20:
6220
6461
  _tmp2 = _state.sent();
6221
- _state.label = 17;
6222
- case 17:
6462
+ _state.label = 21;
6463
+ case 21:
6223
6464
  grant = _tmp2;
6224
6465
  if (missingOIDCScope.length > 0) {
6466
+ // Force-grant the client's required profile scopes (in addition to `openid`) so they cannot be
6467
+ // dropped at consent — the required gate above already guarantees they were requested.
6225
6468
  _resolveEffectiveSubset = resolveEffectiveSubset({
6226
6469
  missing: missingOIDCScope,
6227
6470
  requestedSubset: body.grantedOIDCScopes,
6228
- alwaysGranted: ALWAYS_GRANTED_OIDC_SCOPES,
6471
+ alwaysGranted: _to_consumable_array$1(ALWAYS_GRANTED_OIDC_SCOPES).concat(_to_consumable_array$1(clientRequiredScopes)),
6229
6472
  alreadyEncountered: encounteredOIDCScopes
6230
6473
  }), granted = _resolveEffectiveSubset.granted, rejected = _resolveEffectiveSubset.rejected;
6231
6474
  if (granted.length > 0) {
@@ -6322,7 +6565,7 @@ var OidcInteractionController_1;
6322
6565
  4,
6323
6566
  grant.save()
6324
6567
  ];
6325
- case 18:
6568
+ case 22:
6326
6569
  grantId = _state.sent();
6327
6570
  return [
6328
6571
  4,
@@ -6334,8 +6577,8 @@ var OidcInteractionController_1;
6334
6577
  mergeWithLastSubmission: true
6335
6578
  })
6336
6579
  ];
6337
- case 19:
6338
- _ref5 = _state.sent(), redirectTo2 = _ref5.returnTo;
6580
+ case 23:
6581
+ _ref8 = _state.sent(), redirectTo4 = _ref8.returnTo;
6339
6582
  emitOidcAnalyticsEvent(this._analytics, {
6340
6583
  type: 'consent',
6341
6584
  isSuccessful: true,
@@ -6347,13 +6590,13 @@ var OidcInteractionController_1;
6347
6590
  durationMs: Date.now() - startedAt
6348
6591
  }, this._logger);
6349
6592
  res.json({
6350
- redirectTo: redirectTo2
6593
+ redirectTo: redirectTo4
6351
6594
  });
6352
6595
  return [
6353
6596
  3,
6354
- 21
6597
+ 25
6355
6598
  ];
6356
- case 20:
6599
+ case 24:
6357
6600
  err = _state.sent();
6358
6601
  emitOidcAnalyticsEvent(this._analytics, {
6359
6602
  type: 'consent',
@@ -6368,7 +6611,7 @@ var OidcInteractionController_1;
6368
6611
  throw err;
6369
6612
  }
6370
6613
  throw new HttpException('Consent interaction failed', HttpStatus.BAD_REQUEST);
6371
- case 21:
6614
+ case 25:
6372
6615
  return [
6373
6616
  2
6374
6617
  ];
@@ -7538,31 +7781,43 @@ function _unsupported_iterable_to_array(o, minLen) {
7538
7781
 
7539
7782
  /**
7540
7783
  * Builds a {@link AssertModelCrudRequestFunction} that rejects callModel requests
7541
- * lacking the OIDC scope mapped from the call type.
7784
+ * lacking the required OIDC scope(s) for the call.
7785
+ *
7786
+ * Enforcement is additive: an OIDC caller must hold BOTH the per-verb scope mapped
7787
+ * from the call type (`model.<call>`) AND any per-function scope a handler declared
7788
+ * via `withApiDetails({ requiredScope })` — surfaced on `context.requiredScope`.
7542
7789
  *
7543
7790
  * Bypasses the check (no-op) when the request is not OIDC-authenticated — i.e.
7544
7791
  * when {@link getOidcScopesFromRequest} returns `undefined` because there is no
7545
7792
  * `auth.token.scope` claim on the request. Regular Firebase ID-token callers are
7546
7793
  * unaffected and continue to be gated by auth roles.
7547
7794
  *
7548
- * Bypasses the check for custom (non-CRUD) call types so app-specific verbs
7549
- * remain unrestricted unless an app wires its own assertion.
7795
+ * Bypasses the check for custom (non-CRUD) call types that declare no per-function
7796
+ * scope, so app-specific verbs remain unrestricted unless an app wires its own
7797
+ * assertion or declares a `requiredScope`.
7550
7798
  *
7551
7799
  * Wire as the `preAssert` of {@link onCallModel}.
7552
7800
  *
7553
7801
  * @returns The pre-assertion function ready to plug into `OnCallModelConfig.preAssert`.
7554
7802
  */ function oidcCallModelScopePreAssert() {
7555
7803
  var fn = function fn(context) {
7556
- var requiredScope = callModelOidcScopeForCallType(context.call);
7557
- var scopes = requiredScope == null ? undefined : getOidcScopesFromRequest(context.request);
7558
- var isMissingScope = requiredScope != null && scopes != null && !scopes.has(requiredScope);
7559
- if (isMissingScope) {
7804
+ var requiredScopes = filterMaybeArrayValues([
7805
+ callModelOidcScopeForCallType(context.call),
7806
+ context.requiredScope
7807
+ ]);
7808
+ // No requirement (custom verb + no per-function scope) short-circuits without reading scopes.
7809
+ // A non-OIDC caller yields `undefined` scopes below and bypasses just like the per-verb check.
7810
+ var scopes = requiredScopes.length === 0 ? undefined : getOidcScopesFromRequest(context.request);
7811
+ var missingScopes = scopes == null ? [] : requiredScopes.filter(function(scope) {
7812
+ return !scopes.has(scope);
7813
+ });
7814
+ if (missingScopes.length > 0) {
7560
7815
  throw forbiddenError({
7561
7816
  status: 403,
7562
7817
  code: CALL_MODEL_MISSING_OIDC_SCOPE_ERROR_CODE,
7563
- message: "Missing required OIDC scope for callModel: ".concat(requiredScope),
7818
+ message: "Missing required OIDC scope for callModel: ".concat(missingScopes.join(', ')),
7564
7819
  data: {
7565
- requiredScope: requiredScope,
7820
+ requiredScopes: missingScopes,
7566
7821
  call: context.call
7567
7822
  }
7568
7823
  });
@@ -7571,4 +7826,4 @@ function _unsupported_iterable_to_array(o, minLen) {
7571
7826
  return fn;
7572
7827
  }
7573
7828
 
7574
- export { DEFAULT_APP_OAUTH_CONSENT_PATH_PART, DEFAULT_APP_OAUTH_INTERACTION_PATH, DEFAULT_APP_OAUTH_LOGIN_PATH_PART, DEFAULT_MAX_ADMIN_LOGIN_DURATION_SECONDS, DEFAULT_MAX_NONADMIN_LOGIN_DURATION_SECONDS, DEFAULT_MAX_REQUESTED_LOGIN_DURATION_SECONDS, DEFAULT_MAX_SERVICE_TOKEN_LOGIN_DURATION_SECONDS, DEFAULT_MIN_REQUESTED_LOGIN_DURATION_SECONDS, DEFAULT_OIDC_ANALYTICS_EVENT_NAMES, DEFAULT_OIDC_ANALYTICS_EVENT_NAME_PREFIX, DEFAULT_OIDC_CODE_CHALLENGE_METHODS, DEFAULT_OIDC_ID_TOKEN_SIGNING_ALG_VALUES, DEFAULT_OIDC_ISSUER_PATH, DEFAULT_OIDC_ROUTES, DEFAULT_OIDC_SUBJECT_TYPES, DEFAULT_OIDC_TOKEN_ENDPOINT_AUTH_METHODS, DEFAULT_OIDC_TOKEN_LIFETIMES, DEFAULT_ROTATED_KEY_MAX_AGE, FIREBASE_SERVER_OIDC_ANALYTICS_CONFIG, FIREBASE_SERVER_OIDC_ROUTES_FOR_GLOBAL_ROUTE_EXCLUDE, FirebaseServerOidcAnalyticsService, GRANTABLE_MODEL_NAMES, JwksFirestoreCollections, JwksKeyDocument, JwksService, JwksServiceConfig, JwksServiceStorageConfig, OIDC_ANALYTICS_SERVICE, OIDC_ENCRYPTED_PAYLOAD_FIELDS, OIDC_JSON_RENDER_ERROR_FUNCTION, OIDC_JWKS_ENCRYPTION_SECRET_ENV_KEY, OidcAccountService, OidcAccountServiceDelegate, OidcAccountServiceUserContext, OidcAnalyticsService, OidcAuth, OidcAuthBearerTokenMiddleware, OidcAuthMiddlewareConfig, OidcClientService, OidcEncryptionService, OidcInteractionController, OidcInteractionService, OidcModelServerActions, OidcModuleConfig, OidcProviderConfigService, OidcProviderController, OidcServerFirestoreCollections, OidcService, OidcWellKnownController, activeJwksKeysQuery, allOidcScopesStringForProviderConfig, appOidcAnalyticsModuleMetadata, appOidcModelModuleMetadata, applyOidcAuthMiddleware, applyOidcCorsMiddleware, buildBearerChallenge, buildFirebaseServerMcpResourceServer, createAdapterFactory, createOidcClientFactory, deleteOidcClientFactory, deleteOidcTokenFactory, deriveResourceMetadataUrlFromEnv, emitOidcAnalyticsEvent, getOidcScopesFromRequest, jwksKeyCollectionReference, jwksKeyConverter, jwksKeyFirestoreCollection, jwksKeyIdentity, jwksKeysWithStatusQuery, nonRetiredJwksKeysQuery, noopOidcAnalyticsService, oidcCallModelScopePreAssert, oidcFirestoreCollectionsFactory, oidcModelServerActions, oidcModelServerActionsFactory, oidcModuleConfigFactory, oidcModuleMetadata, resolveEffectiveSubset, rotateOidcClientSecretFactory, rotatedJwksKeysQuery, updateOidcClientFactory };
7829
+ export { DBX_FIREBASE_SERVER_OIDC_PROVIDER_PROFILES_CLIENT_METADATA, DEFAULT_APP_OAUTH_CONSENT_PATH_PART, DEFAULT_APP_OAUTH_INTERACTION_PATH, DEFAULT_APP_OAUTH_LOGIN_PATH_PART, DEFAULT_MAX_ADMIN_LOGIN_DURATION_SECONDS, DEFAULT_MAX_NONADMIN_LOGIN_DURATION_SECONDS, DEFAULT_MAX_REQUESTED_LOGIN_DURATION_SECONDS, DEFAULT_MAX_SERVICE_TOKEN_LOGIN_DURATION_SECONDS, DEFAULT_MIN_REQUESTED_LOGIN_DURATION_SECONDS, DEFAULT_OIDC_ANALYTICS_EVENT_NAMES, DEFAULT_OIDC_ANALYTICS_EVENT_NAME_PREFIX, DEFAULT_OIDC_CODE_CHALLENGE_METHODS, DEFAULT_OIDC_ID_TOKEN_SIGNING_ALG_VALUES, DEFAULT_OIDC_ISSUER_PATH, DEFAULT_OIDC_ROUTES, DEFAULT_OIDC_SUBJECT_TYPES, DEFAULT_OIDC_TOKEN_ENDPOINT_AUTH_METHODS, DEFAULT_OIDC_TOKEN_LIFETIMES, DEFAULT_ROTATED_KEY_MAX_AGE, FIREBASE_SERVER_OIDC_ANALYTICS_CONFIG, FIREBASE_SERVER_OIDC_ROUTES_FOR_GLOBAL_ROUTE_EXCLUDE, FirebaseServerOidcAnalyticsService, GRANTABLE_MODEL_NAMES, JwksFirestoreCollections, JwksKeyDocument, JwksService, JwksServiceConfig, JwksServiceStorageConfig, OIDC_ANALYTICS_SERVICE, OIDC_ENCRYPTED_PAYLOAD_FIELDS, OIDC_JSON_RENDER_ERROR_FUNCTION, OIDC_JWKS_ENCRYPTION_SECRET_ENV_KEY, OidcAccountService, OidcAccountServiceDelegate, OidcAccountServiceUserContext, OidcAnalyticsService, OidcAuth, OidcAuthBearerTokenMiddleware, OidcAuthMiddlewareConfig, OidcClientService, OidcEncryptionService, OidcInteractionController, OidcInteractionService, OidcModelServerActions, OidcModuleConfig, OidcProviderConfigService, OidcProviderController, OidcServerFirestoreCollections, OidcService, OidcWellKnownController, activeJwksKeysQuery, allOidcScopesStringForProviderConfig, appOidcAnalyticsModuleMetadata, appOidcModelModuleMetadata, applyOidcAuthMiddleware, applyOidcCorsMiddleware, buildBearerChallenge, buildFirebaseServerMcpResourceServer, createAdapterFactory, createOidcClientFactory, deleteOidcClientFactory, deleteOidcTokenFactory, deriveResourceMetadataUrlFromEnv, emitOidcAnalyticsEvent, getOidcScopesFromRequest, jwksKeyCollectionReference, jwksKeyConverter, jwksKeyFirestoreCollection, jwksKeyIdentity, jwksKeysWithStatusQuery, nonRetiredJwksKeysQuery, noopOidcAnalyticsService, oidcCallModelScopePreAssert, oidcClientProviderProfileScopes, oidcCorsOriginDelegate, oidcFirestoreCollectionsFactory, oidcModelServerActions, oidcModelServerActionsFactory, oidcModuleConfigFactory, oidcModuleMetadata, resolveEffectiveSubset, rotateOidcClientSecretFactory, rotatedJwksKeysQuery, updateOidcClientFactory };