@dereekb/firebase-server 13.28.0 → 13.30.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
@@ -3,8 +3,8 @@ import { errors } from 'oidc-provider';
3
3
  import { SECONDS_IN_MINUTE, SECONDS_IN_DAY, cachedGetter, unixDateTimeSecondsNumberToDate, unixDateTimeSecondsNumberForNow, selectiveFieldEncryptor, filterUndefinedValues, websiteUrlFromPaths, firstValue, hasHttpPrefix } 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';
7
- import { firestoreEncryptedField, firebaseServerActionsContext, FirebaseServerEnvService, FirebaseServerAnalyticsService, FIREBASE_FIRESTORE_CONTEXT_TOKEN, FirebaseServerFirestoreContextModule, forbiddenError } from '@dereekb/firebase-server';
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, oidcScopesFromScopeClaim, oidcEntryFirestoreCollection } from '@dereekb/firebase';
7
+ import { firestoreEncryptedField, firebaseServerActionsContext, FirebaseServerEnvService, FirebaseServerAnalyticsService, FIREBASE_FIRESTORE_CONTEXT_TOKEN, FirebaseServerFirestoreContextModule } from '@dereekb/firebase-server';
8
8
  import { nanoid } from 'nanoid';
9
9
  import { safeToJsDate } from '@dereekb/date';
10
10
  import { makeUrlSearchParamsString } from '@dereekb/util/fetch';
@@ -2429,6 +2429,9 @@ function _ts_generator$7(thisArg, body) {
2429
2429
  if (params.dbx_max_session_ttl != null) {
2430
2430
  properties.dbx_max_session_ttl = params.dbx_max_session_ttl;
2431
2431
  }
2432
+ if (params.dbx_provider_profiles != null) {
2433
+ properties.dbx_provider_profiles = params.dbx_provider_profiles;
2434
+ }
2432
2435
  // Merge any pre-validated metadata (e.g., inline jwks for private_key_jwt in tests)
2433
2436
  if (validatedMetadata === null || validatedMetadata === void 0 ? void 0 : validatedMetadata.jwks) {
2434
2437
  properties.jwks = validatedMetadata.jwks;
@@ -2486,7 +2489,7 @@ function _ts_generator$7(thisArg, body) {
2486
2489
  * @throws {Error} When the client is not found.
2487
2490
  */ function updateClient(clientId, params) {
2488
2491
  return _async_to_generator$7(function() {
2489
- 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;
2490
2493
  return _ts_generator$7(this, function(_state) {
2491
2494
  switch(_state.label){
2492
2495
  case 0:
@@ -2518,6 +2521,12 @@ function _ts_generator$7(thisArg, body) {
2518
2521
  if (params.dbx_max_session_ttl !== undefined) {
2519
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;
2520
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
+ }
2521
2530
  // Mirrors oidc-provider's lib/helpers/add_client.js: re-validates and persists.
2522
2531
  return [
2523
2532
  4,
@@ -3536,6 +3545,25 @@ OidcProviderConfigService = __decorate([
3536
3545
  __param(2, Inject(FirebaseServerEnvService))
3537
3546
  ], OidcProviderConfigService);
3538
3547
 
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
+
3539
3567
  function _array_like_to_array$4(arr, len) {
3540
3568
  if (len == null || len > arr.length) len = arr.length;
3541
3569
  for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i];
@@ -4040,7 +4068,8 @@ function _ts_generator$5(thisArg, body) {
4040
4068
  logo_uri: existing.logo_uri,
4041
4069
  client_uri: existing.client_uri,
4042
4070
  created_at: existing.created_at,
4043
- 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
4044
4073
  };
4045
4074
  }
4046
4075
  return [
@@ -4147,7 +4176,8 @@ function _ts_generator$5(thisArg, body) {
4147
4176
  ],
4148
4177
  extraClientMetadata: {
4149
4178
  properties: [
4150
- 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
4151
4181
  ],
4152
4182
  validator: function validator(_ctx, key, value) {
4153
4183
  if (key === DBX_FIREBASE_SERVER_OIDC_MAX_SESSION_TTL_CLIENT_METADATA && value !== undefined && value !== null) {
@@ -4157,6 +4187,22 @@ function _ts_generator$5(thisArg, body) {
4157
4187
  if (value > serverMaxSeconds) {
4158
4188
  throw new errors.InvalidClientMetadata("".concat(DBX_FIREBASE_SERVER_OIDC_MAX_SESSION_TTL_CLIENT_METADATA, " cannot exceed the server max of ").concat(serverMaxSeconds, " seconds."));
4159
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
+ }
4160
4206
  }
4161
4207
  }
4162
4208
  },
@@ -4184,7 +4230,7 @@ function _ts_generator$5(thisArg, body) {
4184
4230
  interactions: {
4185
4231
  url: function url(_ctx, interaction) {
4186
4232
  return _async_to_generator$5(function() {
4187
- 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;
4188
4234
  return _ts_generator$5(this, function(_state) {
4189
4235
  switch(_state.label){
4190
4236
  case 0:
@@ -4212,13 +4258,23 @@ function _ts_generator$5(thisArg, body) {
4212
4258
  client = _state.sent();
4213
4259
  if (client) {
4214
4260
  scopes = interaction.params.scope;
4215
- 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({
4216
4270
  client_id: client_id,
4217
4271
  client_name: client.client_name,
4218
4272
  client_uri: client.client_uri,
4219
4273
  logo_uri: client.logo_uri,
4220
4274
  scopes: scopes
4221
- };
4275
+ }, requiredScopes.length > 0 ? {
4276
+ requiredScopes: requiredScopes.join(' ')
4277
+ } : {});
4222
4278
  paramsToEncode = _object_spread$3({}, paramsToEncode, interactionLoginDetails);
4223
4279
  }
4224
4280
  _state.label = 3;
@@ -5337,7 +5393,7 @@ function _unsupported_iterable_to_array$2(o, minLen) {
5337
5393
  * `request.auth.token.scope` for OIDC callers and is `undefined` for non-OIDC
5338
5394
  * (regular Firebase ID-token) callers.
5339
5395
  *
5340
- * Returning `undefined` for non-OIDC callers lets callers (e.g. `oidcCallModelScopePreAssert`)
5396
+ * Returning `undefined` for non-OIDC callers lets consumers (e.g. the MCP tool-visibility filter)
5341
5397
  * skip scope enforcement instead of falsely treating the missing claim as "no scopes granted".
5342
5398
  *
5343
5399
  * The parameter is intentionally typed as `unknown` because `DecodedIdToken` (Firebase
@@ -5349,10 +5405,7 @@ function _unsupported_iterable_to_array$2(o, minLen) {
5349
5405
  */ function getOidcScopesFromRequest(request) {
5350
5406
  var _request_auth_token, _request_auth;
5351
5407
  var scope = request === null || request === void 0 ? void 0 : (_request_auth = request.auth) === null || _request_auth === void 0 ? void 0 : (_request_auth_token = _request_auth.token) === null || _request_auth_token === void 0 ? void 0 : _request_auth_token.scope;
5352
- var result = typeof scope === 'string' ? new Set(scope.split(' ').filter(function(value) {
5353
- return value.length > 0;
5354
- })) : undefined;
5355
- return result;
5408
+ return oidcScopesFromScopeClaim(scope);
5356
5409
  }
5357
5410
 
5358
5411
  function asyncGeneratorStep$3(gen, resolve, reject, _next, _throw, key, arg) {
@@ -6157,7 +6210,7 @@ var OidcInteractionController_1;
6157
6210
  * @throws {HttpException} HTTP 400 when the consent interaction cannot be completed.
6158
6211
  */ function postConsent(uid, body, res) {
6159
6212
  return _async_to_generator$2(function() {
6160
- 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;
6213
+ 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;
6161
6214
  return _ts_generator$2(this, function(_state) {
6162
6215
  switch(_state.label){
6163
6216
  case 0:
@@ -6172,9 +6225,9 @@ var OidcInteractionController_1;
6172
6225
  case 2:
6173
6226
  _state.trys.push([
6174
6227
  2,
6175
- 20,
6228
+ 24,
6176
6229
  ,
6177
- 21
6230
+ 25
6178
6231
  ]);
6179
6232
  if (!!body.approved) return [
6180
6233
  3,
@@ -6190,7 +6243,7 @@ var OidcInteractionController_1;
6190
6243
  })
6191
6244
  ];
6192
6245
  case 3:
6193
- _ref3 = _state.sent(), redirectTo = _ref3.returnTo;
6246
+ _ref4 = _state.sent(), redirectTo = _ref4.returnTo;
6194
6247
  res.json({
6195
6248
  redirectTo: redirectTo
6196
6249
  });
@@ -6233,6 +6286,12 @@ var OidcInteractionController_1;
6233
6286
  // and re-submits them all. We accept already-encountered values as silent no-ops so a re-consent on a
6234
6287
  // client the user has previously authorized doesn't fail validation.
6235
6288
  encounteredOIDCScopes = existingGrant ? existingGrant.getOIDCScopeEncountered().split(' ').filter(Boolean) : [];
6289
+ return [
6290
+ 4,
6291
+ this.oidcService.findClientPayload(clientId)
6292
+ ];
6293
+ case 9:
6294
+ clientPayload = _state.sent();
6236
6295
  // Admin-only scope gate. The requested OIDC scope set is `missingOIDCScope ∪ encounteredOIDCScopes`
6237
6296
  // (a fresh consent has everything in `missing`; a re-consent may carry already-granted scopes in
6238
6297
  // `encountered`). If that set intersects the provider's `adminOnlyScopes` and the resolving user is
@@ -6244,26 +6303,26 @@ var OidcInteractionController_1;
6244
6303
  });
6245
6304
  if (!accountId) return [
6246
6305
  3,
6247
- 10
6306
+ 11
6248
6307
  ];
6249
6308
  return [
6250
6309
  4,
6251
6310
  (_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
6252
6311
  ];
6253
- case 9:
6312
+ case 10:
6254
6313
  _tmp1 = _state.sent();
6255
6314
  return [
6256
6315
  3,
6257
- 11
6316
+ 12
6258
6317
  ];
6259
- case 10:
6260
- _tmp1 = false;
6261
- _state.label = 11;
6262
6318
  case 11:
6319
+ _tmp1 = false;
6320
+ _state.label = 12;
6321
+ case 12:
6263
6322
  isAdmin = _tmp1;
6264
6323
  if (!(requestsServiceToken && !isAdmin)) return [
6265
6324
  3,
6266
- 13
6325
+ 14
6267
6326
  ];
6268
6327
  return [
6269
6328
  4,
@@ -6274,8 +6333,8 @@ var OidcInteractionController_1;
6274
6333
  mergeWithLastSubmission: true
6275
6334
  })
6276
6335
  ];
6277
- case 12:
6278
- _ref4 = _state.sent(), redirectTo1 = _ref4.returnTo;
6336
+ case 13:
6337
+ _ref5 = _state.sent(), redirectTo1 = _ref5.returnTo;
6279
6338
  emitOidcAnalyticsEvent(this._analytics, {
6280
6339
  type: 'consent',
6281
6340
  isSuccessful: false,
@@ -6292,20 +6351,89 @@ var OidcInteractionController_1;
6292
6351
  return [
6293
6352
  2
6294
6353
  ];
6295
- case 13:
6354
+ case 14:
6355
+ // Provider-profile scope gate. Any scope referenced by a provider profile is "gated" — available
6356
+ // only to clients whose assigned profiles unlock it. Independent of the admin-only gate above; a
6357
+ // scope may be subject to both.
6358
+ providerProfiles = this.accountService.providerConfig.providerProfiles;
6359
+ _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;
6360
+ profileGatedScopes = scopesForOidcProviderProfiles(providerProfiles !== null && providerProfiles !== void 0 ? providerProfiles : []);
6361
+ // Unlock gate: reject any requested gated scope this client's profiles do not unlock.
6362
+ disallowedGatedScopes = Array.from(requestedOIDCScopeSet).filter(function(scope) {
6363
+ return profileGatedScopes.has(scope) && !clientUnlockedScopes.has(scope);
6364
+ });
6365
+ if (!(disallowedGatedScopes.length > 0)) return [
6366
+ 3,
6367
+ 16
6368
+ ];
6369
+ return [
6370
+ 4,
6371
+ this.oidcInteractionService.finishInteractionByUid(uid, {
6372
+ error: 'access_denied',
6373
+ error_description: "The following scope(s) are not available to this client: ".concat(disallowedGatedScopes.join(', '), ".")
6374
+ }, {
6375
+ mergeWithLastSubmission: true
6376
+ })
6377
+ ];
6378
+ case 15:
6379
+ _ref6 = _state.sent(), redirectTo2 = _ref6.returnTo;
6380
+ emitOidcAnalyticsEvent(this._analytics, {
6381
+ type: 'consent',
6382
+ isSuccessful: false,
6383
+ uid: accountId,
6384
+ clientId: clientId,
6385
+ reason: 'scope_not_unlocked_for_client',
6386
+ durationMs: Date.now() - startedAt
6387
+ }, this._logger);
6388
+ res.json({
6389
+ redirectTo: redirectTo2
6390
+ });
6391
+ return [
6392
+ 2
6393
+ ];
6394
+ case 16:
6395
+ // Required gate: a scope a client's profiles force-require (`require: 'required'`) must be present
6396
+ // in the request. Reject when the client did not request a required scope.
6397
+ missingRequiredScopes = Array.from(clientRequiredScopes).filter(function(scope) {
6398
+ return !requestedOIDCScopeSet.has(scope);
6399
+ });
6400
+ if (!(missingRequiredScopes.length > 0)) return [
6401
+ 3,
6402
+ 18
6403
+ ];
6404
+ return [
6405
+ 4,
6406
+ this.oidcInteractionService.finishInteractionByUid(uid, {
6407
+ error: 'access_denied',
6408
+ error_description: "This client must request the required scope(s): ".concat(missingRequiredScopes.join(', '), ".")
6409
+ }, {
6410
+ mergeWithLastSubmission: true
6411
+ })
6412
+ ];
6413
+ case 17:
6414
+ _ref7 = _state.sent(), redirectTo3 = _ref7.returnTo;
6415
+ emitOidcAnalyticsEvent(this._analytics, {
6416
+ type: 'consent',
6417
+ isSuccessful: false,
6418
+ uid: accountId,
6419
+ clientId: clientId,
6420
+ reason: 'required_scope_missing',
6421
+ durationMs: Date.now() - startedAt
6422
+ }, this._logger);
6423
+ res.json({
6424
+ redirectTo: redirectTo3
6425
+ });
6426
+ return [
6427
+ 2
6428
+ ];
6429
+ case 18:
6296
6430
  // Resolve the requested login duration up-front. The configured Grant TTL function (in
6297
6431
  // OidcService.buildProviderConfiguration) only fires when oidc-provider's koa middleware
6298
6432
  // drives `grant.save()`, so its `ctx.oidc.params` lookup of `dbx_session_ttl` returns
6299
6433
  // undefined when the consent submit runs in this controller. We pre-set `expiresIn` on
6300
6434
  // newly-created grants so they persist with the correct (tiered) TTL.
6301
6435
  requestedRawTtl = params[DBX_FIREBASE_SERVER_OIDC_SESSION_TTL_PARAM];
6302
- return [
6303
- 4,
6304
- this.oidcService.findClientPayload(clientId)
6305
- ];
6306
- case 14:
6307
- clientPayload = _state.sent();
6308
- clientMaxSessionTtl = (_ref2 = clientPayload === null || clientPayload === void 0 ? void 0 : clientPayload.dbx_max_session_ttl) !== null && _ref2 !== void 0 ? _ref2 : undefined;
6436
+ clientMaxSessionTtl = (_ref3 = clientPayload === null || clientPayload === void 0 ? void 0 : clientPayload.dbx_max_session_ttl) !== null && _ref3 !== void 0 ? _ref3 : undefined;
6309
6437
  expiresInSeconds = this.oidcService.resolveLoginDurationForGrant(requestedRawTtl, {
6310
6438
  dbx_max_session_ttl: clientMaxSessionTtl
6311
6439
  }, {
@@ -6314,28 +6442,30 @@ var OidcInteractionController_1;
6314
6442
  });
6315
6443
  if (!(existingGrant !== null && existingGrant !== void 0)) return [
6316
6444
  3,
6317
- 15
6445
+ 19
6318
6446
  ];
6319
6447
  _tmp2 = existingGrant;
6320
6448
  return [
6321
6449
  3,
6322
- 17
6450
+ 21
6323
6451
  ];
6324
- case 15:
6452
+ case 19:
6325
6453
  return [
6326
6454
  4,
6327
6455
  this.oidcInteractionService.findOrCreateGrant(interaction.grantId, accountId, clientId, expiresInSeconds)
6328
6456
  ];
6329
- case 16:
6457
+ case 20:
6330
6458
  _tmp2 = _state.sent();
6331
- _state.label = 17;
6332
- case 17:
6459
+ _state.label = 21;
6460
+ case 21:
6333
6461
  grant = _tmp2;
6334
6462
  if (missingOIDCScope.length > 0) {
6463
+ // Force-grant the client's required profile scopes (in addition to `openid`) so they cannot be
6464
+ // dropped at consent — the required gate above already guarantees they were requested.
6335
6465
  _resolveEffectiveSubset = resolveEffectiveSubset({
6336
6466
  missing: missingOIDCScope,
6337
6467
  requestedSubset: body.grantedOIDCScopes,
6338
- alwaysGranted: ALWAYS_GRANTED_OIDC_SCOPES,
6468
+ alwaysGranted: _to_consumable_array$1(ALWAYS_GRANTED_OIDC_SCOPES).concat(_to_consumable_array$1(clientRequiredScopes)),
6339
6469
  alreadyEncountered: encounteredOIDCScopes
6340
6470
  }), granted = _resolveEffectiveSubset.granted, rejected = _resolveEffectiveSubset.rejected;
6341
6471
  if (granted.length > 0) {
@@ -6432,7 +6562,7 @@ var OidcInteractionController_1;
6432
6562
  4,
6433
6563
  grant.save()
6434
6564
  ];
6435
- case 18:
6565
+ case 22:
6436
6566
  grantId = _state.sent();
6437
6567
  return [
6438
6568
  4,
@@ -6444,8 +6574,8 @@ var OidcInteractionController_1;
6444
6574
  mergeWithLastSubmission: true
6445
6575
  })
6446
6576
  ];
6447
- case 19:
6448
- _ref5 = _state.sent(), redirectTo2 = _ref5.returnTo;
6577
+ case 23:
6578
+ _ref8 = _state.sent(), redirectTo4 = _ref8.returnTo;
6449
6579
  emitOidcAnalyticsEvent(this._analytics, {
6450
6580
  type: 'consent',
6451
6581
  isSuccessful: true,
@@ -6457,13 +6587,13 @@ var OidcInteractionController_1;
6457
6587
  durationMs: Date.now() - startedAt
6458
6588
  }, this._logger);
6459
6589
  res.json({
6460
- redirectTo: redirectTo2
6590
+ redirectTo: redirectTo4
6461
6591
  });
6462
6592
  return [
6463
6593
  3,
6464
- 21
6594
+ 25
6465
6595
  ];
6466
- case 20:
6596
+ case 24:
6467
6597
  err = _state.sent();
6468
6598
  emitOidcAnalyticsEvent(this._analytics, {
6469
6599
  type: 'consent',
@@ -6478,7 +6608,7 @@ var OidcInteractionController_1;
6478
6608
  throw err;
6479
6609
  }
6480
6610
  throw new HttpException('Consent interaction failed', HttpStatus.BAD_REQUEST);
6481
- case 21:
6611
+ case 25:
6482
6612
  return [
6483
6613
  2
6484
6614
  ];
@@ -7646,39 +7776,4 @@ function _unsupported_iterable_to_array(o, minLen) {
7646
7776
  };
7647
7777
  }
7648
7778
 
7649
- /**
7650
- * Builds a {@link AssertModelCrudRequestFunction} that rejects callModel requests
7651
- * lacking the OIDC scope mapped from the call type.
7652
- *
7653
- * Bypasses the check (no-op) when the request is not OIDC-authenticated — i.e.
7654
- * when {@link getOidcScopesFromRequest} returns `undefined` because there is no
7655
- * `auth.token.scope` claim on the request. Regular Firebase ID-token callers are
7656
- * unaffected and continue to be gated by auth roles.
7657
- *
7658
- * Bypasses the check for custom (non-CRUD) call types so app-specific verbs
7659
- * remain unrestricted unless an app wires its own assertion.
7660
- *
7661
- * Wire as the `preAssert` of {@link onCallModel}.
7662
- *
7663
- * @returns The pre-assertion function ready to plug into `OnCallModelConfig.preAssert`.
7664
- */ function oidcCallModelScopePreAssert() {
7665
- var fn = function fn(context) {
7666
- var requiredScope = callModelOidcScopeForCallType(context.call);
7667
- var scopes = requiredScope == null ? undefined : getOidcScopesFromRequest(context.request);
7668
- var isMissingScope = requiredScope != null && scopes != null && !scopes.has(requiredScope);
7669
- if (isMissingScope) {
7670
- throw forbiddenError({
7671
- status: 403,
7672
- code: CALL_MODEL_MISSING_OIDC_SCOPE_ERROR_CODE,
7673
- message: "Missing required OIDC scope for callModel: ".concat(requiredScope),
7674
- data: {
7675
- requiredScope: requiredScope,
7676
- call: context.call
7677
- }
7678
- });
7679
- }
7680
- };
7681
- return fn;
7682
- }
7683
-
7684
- 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, oidcCorsOriginDelegate, oidcFirestoreCollectionsFactory, oidcModelServerActions, oidcModelServerActionsFactory, oidcModuleConfigFactory, oidcModuleMetadata, resolveEffectiveSubset, rotateOidcClientSecretFactory, rotatedJwksKeysQuery, updateOidcClientFactory };
7779
+ 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, oidcClientProviderProfileScopes, oidcCorsOriginDelegate, oidcFirestoreCollectionsFactory, oidcModelServerActions, oidcModelServerActionsFactory, oidcModuleConfigFactory, oidcModuleMetadata, resolveEffectiveSubset, rotateOidcClientSecretFactory, rotatedJwksKeysQuery, updateOidcClientFactory };
package/oidc/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/oidc",
3
- "version": "13.28.0",
3
+ "version": "13.30.0",
4
4
  "peerDependencies": {
5
- "@dereekb/analytics": "13.28.0",
6
- "@dereekb/date": "13.28.0",
7
- "@dereekb/firebase": "13.28.0",
8
- "@dereekb/firebase-server": "13.28.0",
9
- "@dereekb/model": "13.28.0",
10
- "@dereekb/nestjs": "13.28.0",
11
- "@dereekb/rxjs": "13.28.0",
12
- "@dereekb/util": "13.28.0",
13
- "@dereekb/zoho": "13.28.0",
5
+ "@dereekb/analytics": "13.30.0",
6
+ "@dereekb/date": "13.30.0",
7
+ "@dereekb/firebase": "13.30.0",
8
+ "@dereekb/firebase-server": "13.30.0",
9
+ "@dereekb/model": "13.30.0",
10
+ "@dereekb/nestjs": "13.30.0",
11
+ "@dereekb/rxjs": "13.30.0",
12
+ "@dereekb/util": "13.30.0",
13
+ "@dereekb/zoho": "13.30.0",
14
14
  "@nestjs/common": "^11.1.19",
15
15
  "@nestjs/config": "^4.0.4",
16
16
  "express": "^5.2.1",
@@ -3,5 +3,5 @@ export * from './model';
3
3
  export * from './oidc.config';
4
4
  export * from './controller';
5
5
  export * from './oidc.module';
6
- export * from './scope';
6
+ export * from './profile';
7
7
  export * from './service';
@@ -1,6 +1,6 @@
1
1
  import type { Configuration } from 'oidc-provider';
2
2
  import { type WebsitePath, type SlashPath, type Seconds } from '@dereekb/util';
3
- import { type OidcScope, type OidcTokenEndpointAuthMethod } from '@dereekb/firebase';
3
+ import { type OidcProviderProfile, type OidcScope, type OidcTokenEndpointAuthMethod } from '@dereekb/firebase';
4
4
  import { type JwksServiceConfig } from './service/oidc.jwks.service';
5
5
  import { type JwksKeyConverterConfig } from './model';
6
6
  /**
@@ -104,6 +104,19 @@ export interface OidcProviderConfig<S extends OidcScope = OidcScope> {
104
104
  * All other grants keep oidc-provider's default rotation behavior.
105
105
  */
106
106
  readonly nonRotatingScopes?: readonly string[];
107
+ /**
108
+ * Named provider profiles that unlock (and optionally force-require) scopes for the OIDC clients
109
+ * they are assigned to. Any scope referenced by a profile is treated as restricted: a client may
110
+ * only obtain it when one of its assigned profiles (persisted as `dbx_provider_profiles` client
111
+ * metadata) unlocks it. A profile scope marked `require: 'required'` is force-required — surfaced
112
+ * as a required scope at consent and hard-rejected if not granted.
113
+ *
114
+ * Keeps the generic package app-agnostic: apps declare their profiles statically and list them
115
+ * here, rather than the package hard-coding any profile or scope name. This gate is independent of
116
+ * {@link adminOnlyScopes} (which gates by the resolving user's admin status); a scope may be
117
+ * subject to both.
118
+ */
119
+ readonly providerProfiles?: readonly OidcProviderProfile<S>[];
107
120
  }
108
121
  /**
109
122
  * Returns the space-delimited list of every scope declared on `providerConfig.claims`.
@@ -0,0 +1,29 @@
1
+ import { type OidcProviderProfile, type OidcScope, type OidcProviderProfileKey } from '@dereekb/firebase';
2
+ /**
3
+ * Custom oidc-provider client metadata field holding the {@link OidcProviderProfile} keys assigned
4
+ * to a client. Registered via `extraClientMetadata` and persisted on the client's `OidcEntry` payload.
5
+ */
6
+ export declare const DBX_FIREBASE_SERVER_OIDC_PROVIDER_PROFILES_CLIENT_METADATA = "dbx_provider_profiles";
7
+ /**
8
+ * The scopes a client with the given assigned profile keys is allowed/required to obtain, resolved
9
+ * against the provider-profile registry.
10
+ */
11
+ export interface OidcClientProviderProfileScopes<S extends OidcScope = OidcScope> {
12
+ /**
13
+ * Scopes unlocked for the client (the union of its assigned profiles' scopes).
14
+ */
15
+ readonly unlocked: Set<S>;
16
+ /**
17
+ * Scopes the client's profiles force-require (`require: 'required'`).
18
+ */
19
+ readonly required: Set<S>;
20
+ }
21
+ /**
22
+ * Resolves the unlocked + required scope sets for a client from the provider-profile registry and
23
+ * the profile keys assigned to that client.
24
+ *
25
+ * @param providerProfiles - The provider-profile registry (from {@link OidcProviderConfig.providerProfiles}).
26
+ * @param clientProfileKeys - The profile keys assigned to the client (its `dbx_provider_profiles`).
27
+ * @returns The unlocked and required scope sets for the client.
28
+ */
29
+ export declare function oidcClientProviderProfileScopes<S extends OidcScope = OidcScope>(providerProfiles: readonly OidcProviderProfile<S>[] | undefined, clientProfileKeys: readonly OidcProviderProfileKey[] | undefined): OidcClientProviderProfileScopes<S>;
@@ -33,7 +33,7 @@ export type OidcAuthenticatedRequest = FirebaseServerAuthenticatedRequest<OidcAu
33
33
  * `request.auth.token.scope` for OIDC callers and is `undefined` for non-OIDC
34
34
  * (regular Firebase ID-token) callers.
35
35
  *
36
- * Returning `undefined` for non-OIDC callers lets callers (e.g. `oidcCallModelScopePreAssert`)
36
+ * Returning `undefined` for non-OIDC callers lets consumers (e.g. the MCP tool-visibility filter)
37
37
  * skip scope enforcement instead of falsely treating the missing claim as "no scopes granted".
38
38
  *
39
39
  * The parameter is intentionally typed as `unknown` because `DecodedIdToken` (Firebase
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server",
3
- "version": "13.28.0",
3
+ "version": "13.30.0",
4
4
  "sideEffects": false,
5
5
  "exports": {
6
6
  "./test": {
@@ -57,15 +57,15 @@
57
57
  "types": "./src/index.d.ts",
58
58
  "peerDependencies": {
59
59
  "@cantoo/pdf-lib": "^2.6.5",
60
- "@dereekb/analytics": "13.28.0",
61
- "@dereekb/date": "13.28.0",
62
- "@dereekb/dbx-core": "13.28.0",
63
- "@dereekb/firebase": "13.28.0",
64
- "@dereekb/model": "13.28.0",
65
- "@dereekb/nestjs": "13.28.0",
66
- "@dereekb/rxjs": "13.28.0",
67
- "@dereekb/util": "13.28.0",
68
- "@dereekb/zoho": "13.28.0",
60
+ "@dereekb/analytics": "13.30.0",
61
+ "@dereekb/date": "13.30.0",
62
+ "@dereekb/dbx-core": "13.30.0",
63
+ "@dereekb/firebase": "13.30.0",
64
+ "@dereekb/model": "13.30.0",
65
+ "@dereekb/nestjs": "13.30.0",
66
+ "@dereekb/rxjs": "13.30.0",
67
+ "@dereekb/util": "13.30.0",
68
+ "@dereekb/zoho": "13.30.0",
69
69
  "@google-cloud/firestore": "^7.11.6",
70
70
  "@google-cloud/storage": "^7.19.0",
71
71
  "@modelcontextprotocol/sdk": "1.29.0",
@@ -1,3 +1,4 @@
1
+ export * from './model.api.scope';
1
2
  export * from './model.api.get.service';
2
3
  export * from './model.api.dispatch';
3
4
  export * from './model.api.controller';