@dereekb/firebase-server 14.2.0 → 14.4.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/package.json CHANGED
@@ -1,22 +1,24 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server/oidc",
3
- "version": "14.2.0",
3
+ "version": "14.4.0",
4
4
  "sideEffects": false,
5
5
  "type": "module",
6
6
  "peerDependencies": {
7
- "@dereekb/analytics": "14.2.0",
8
- "@dereekb/date": "14.2.0",
9
- "@dereekb/firebase": "14.2.0",
10
- "@dereekb/firebase-server": "14.2.0",
11
- "@dereekb/model": "14.2.0",
12
- "@dereekb/nestjs": "14.2.0",
13
- "@dereekb/rxjs": "14.2.0",
14
- "@dereekb/util": "14.2.0",
15
- "@dereekb/zoho": "14.2.0",
7
+ "@dereekb/analytics": "14.4.0",
8
+ "@dereekb/date": "14.4.0",
9
+ "@dereekb/firebase": "14.4.0",
10
+ "@dereekb/firebase-server": "14.4.0",
11
+ "@dereekb/model": "14.4.0",
12
+ "@dereekb/nestjs": "14.4.0",
13
+ "@dereekb/oauth-resource": "14.4.0",
14
+ "@dereekb/rxjs": "14.4.0",
15
+ "@dereekb/util": "14.4.0",
16
+ "@dereekb/zoho": "14.4.0",
16
17
  "@nestjs/common": "^12.0.1",
17
18
  "@nestjs/config": "^12.0.0",
18
19
  "express": "^5.2.1",
19
20
  "firebase-admin": "^13.10.0",
21
+ "jose": "^6.2.12",
20
22
  "nanoid": "^6.0.1",
21
23
  "oidc-provider": "^9.8.2"
22
24
  },
@@ -86,7 +86,9 @@ export interface ResolveEffectiveSubsetInput {
86
86
  */
87
87
  readonly alwaysGranted?: readonly string[];
88
88
  /**
89
- * Entries the existing Grant has previously granted or rejected. Tolerated as no-ops on re-consent.
89
+ * Entries the existing Grant has already decided and this consent leaves as-is. Tolerated as no-ops on
90
+ * re-consent. A previously-rejected entry the request names again belongs in `missing` instead — see
91
+ * `reconsiderRejectedValues` — or it can never be granted on that Grant.
90
92
  */
91
93
  readonly alreadyEncountered?: readonly string[];
92
94
  }
@@ -3,5 +3,6 @@ export * from './model';
3
3
  export * from './oidc.config';
4
4
  export * from './controller';
5
5
  export * from './oidc.module';
6
+ export * from './oidc.resource-server';
6
7
  export * from './profile';
7
8
  export * from './service';
@@ -52,6 +52,9 @@ export declare abstract class OidcAuthMiddlewareConfig {
52
52
  * included as the RFC 9728 `resource_metadata` hint so clients can locate the
53
53
  * discovery doc directly instead of relying on origin-rooted path-walkback.
54
54
  *
55
+ * Delegates to `@dereekb/oauth-resource`'s builder so the challenge an off-box resource server
56
+ * emits and the one this API emits are produced by the same code.
57
+ *
55
58
  * @param error - The RFC 6750 `error` token (e.g. `invalid_token`, `invalid_request`).
56
59
  * @param resourceMetadataUrl - Optional protected-resource metadata URL.
57
60
  * @returns Header value, e.g. `Bearer resource_metadata="…", error="invalid_token"`.
@@ -1,6 +1,6 @@
1
1
  import { type ModuleMetadata } from '@nestjs/common';
2
2
  import { ConfigService } from '@nestjs/config';
3
- import { OidcModuleConfig, type OidcProviderConfig, type OidcResourceServerInfo } from './oidc.config';
3
+ import { OidcModuleConfig } from './oidc.config';
4
4
  import { type FirestoreContext } from '@dereekb/firebase';
5
5
  import { FirebaseServerEnvService } from '@dereekb/firebase-server';
6
6
  import { OidcServerFirestoreCollections } from './model/model';
@@ -88,43 +88,6 @@ export declare function oidcModuleConfigFactory(configService: ConfigService, en
88
88
  * @returns The discovery URL, or `undefined` if `appMcpUrl` is not set.
89
89
  */
90
90
  export declare function deriveResourceMetadataUrlFromEnv(envService: FirebaseServerEnvService): string | undefined;
91
- /**
92
- * Input for {@link buildFirebaseServerMcpResourceServer}.
93
- */
94
- export interface BuildFirebaseServerMcpResourceServerInput {
95
- /**
96
- * The Firebase server environment service. The MCP URL is read from `envService.appMcpUrl`.
97
- */
98
- readonly envService: FirebaseServerEnvService;
99
- /**
100
- * The OIDC provider config whose scopes back the resource server's `scope` value.
101
- */
102
- readonly providerConfig: OidcProviderConfig;
103
- }
104
- /**
105
- * Builds a single-entry {@link OidcResourceServerInfo} map for the MCP endpoint
106
- * declared on `envService.appMcpUrl`, with `scope` set to every scope declared
107
- * on `providerConfig.claims` (so any scope the provider issues is valid on the
108
- * resource server) and `audience` set to the MCP URL.
109
- *
110
- * Returns `undefined` when no `appMcpUrl` is configured.
111
- *
112
- * Used internally by {@link oidcModuleMetadata} when {@link OidcModuleConfig.configureMcpResourceServer}
113
- * is enabled, and exported for apps that need to combine the MCP resource server
114
- * with additional app-specific entries:
115
- *
116
- * @example
117
- * ```ts
118
- * resourceServers: {
119
- * ...(buildFirebaseServerMcpResourceServer({ envService, providerConfig: APP_PROVIDER_CONFIG }) ?? {}),
120
- * 'https://api.example.com/extras': {
121
- * scope: 'openid profile',
122
- * audience: 'https://api.example.com/extras'
123
- * }
124
- * }
125
- * ```
126
- */
127
- export declare function buildFirebaseServerMcpResourceServer(input: BuildFirebaseServerMcpResourceServerInput): Record<string, OidcResourceServerInfo> | undefined;
128
91
  /**
129
92
  * Factory that creates {@link OidcServerFirestoreCollections} using the provided Firestore context
130
93
  * and JWKS encryption config from {@link OidcModuleConfig}.
@@ -0,0 +1,170 @@
1
+ import { type BuildIssuerProfilesConfig, type BuildIssuerProfilesOidcIssuerInput } from '@dereekb/oauth-resource';
2
+ import { type FirebaseServerEnvService } from '@dereekb/firebase-server';
3
+ import { type Maybe, type Seconds, type WebsiteUrlWithPrefix } from '@dereekb/util';
4
+ import { type OidcModuleConfig, type OidcProviderConfig, type OidcResourceServerInfo } from './oidc.config';
5
+ export interface BuildOidcResourceServerConfig {
6
+ /**
7
+ * The RFC 8707 resource indicator — the value a client sends as `resource=` on `/authorize` and
8
+ * `/token`, and the key this entry is registered under. Usually the resource's full URL, e.g.
9
+ * `https://db.example.com/mcp`.
10
+ */
11
+ readonly url: WebsiteUrlWithPrefix;
12
+ /**
13
+ * Space-delimited scopes valid on this resource server. Issued access tokens are filtered to the
14
+ * intersection of the client-requested scopes and this allow-list, so keep it to what the
15
+ * resource actually understands rather than every scope the provider issues.
16
+ */
17
+ readonly scope: string;
18
+ /**
19
+ * `aud` claim placed on tokens bound to this resource. Defaults to {@link url}.
20
+ *
21
+ * An OFF-BOX resource server usually wants its ORIGIN here (`https://db.example.com`) rather
22
+ * than the `/mcp` resource-indicator URL, so one audience covers every route it serves.
23
+ */
24
+ readonly audience?: Maybe<string>;
25
+ /**
26
+ * Access-token format. Defaults to oidc-provider's `'opaque'`.
27
+ *
28
+ * **Set `'jwt'` for any resource server that is not this API process.** An opaque access token is
29
+ * a database key: it can only be validated by the issuer's own adapter store, so a remote service
30
+ * fundamentally cannot verify it. A `'jwt'` token is an RS256 JWT signed with the provider's
31
+ * active JWKS key (no extra provider config needed — `getResourceServerConfig` falls back to
32
+ * `clientDefaults.id_token_signed_response_alg`, i.e. `RS256`), which any service can verify via
33
+ * `@dereekb/oauth-resource` against the published `/.well-known/jwks.json`.
34
+ *
35
+ * The trade-off: a JWT access token is NOT persisted by oidc-provider, so it has no adapter
36
+ * record and **cannot be revoked before `exp`**. Revoking the Grant stops refresh and future
37
+ * issuance but leaves outstanding JWTs valid — keep {@link accessTokenTTL} short.
38
+ */
39
+ readonly accessTokenFormat?: Maybe<'opaque' | 'jwt'>;
40
+ /**
41
+ * Access-token TTL for this resource, in seconds. Falls back to the provider's `ttl.AccessToken`.
42
+ */
43
+ readonly accessTokenTTL?: Maybe<Seconds>;
44
+ }
45
+ /**
46
+ * Builds a single-entry {@link OidcResourceServerInfo} map for an RFC 8707 resource indicator.
47
+ *
48
+ * This is the supported way to declare a resource server the provider issues tokens for —
49
+ * including one that runs OFF-BOX (a Docker'd sidecar, a worker, a standalone MCP host), which is
50
+ * what {@link BuildOidcResourceServerConfig.accessTokenFormat} `'jwt'` exists for.
51
+ *
52
+ * @param config - The resource indicator, its scopes, and the token shape to issue for it.
53
+ * @returns A single-entry map keyed by the resource indicator.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * const DB_ORIGIN = 'https://db.example.com';
58
+ *
59
+ * oidcModuleMetadata({
60
+ * dependencyModule: MyOidcDependencyModule,
61
+ * config: {
62
+ * resourceServers: buildOidcResourceServer({
63
+ * url: `${DB_ORIGIN}/mcp`,
64
+ * scope: 'openid profile email offline_access',
65
+ * audience: DB_ORIGIN,
66
+ * accessTokenFormat: 'jwt',
67
+ * accessTokenTTL: 3600
68
+ * })
69
+ * }
70
+ * })
71
+ * ```
72
+ */
73
+ export declare function buildOidcResourceServer(config: BuildOidcResourceServerConfig): Record<string, OidcResourceServerInfo>;
74
+ /**
75
+ * Input for {@link buildFirebaseServerMcpResourceServer}.
76
+ */
77
+ export interface BuildFirebaseServerMcpResourceServerInput {
78
+ /**
79
+ * The Firebase server environment service. The MCP URL is read from `envService.appMcpUrl`.
80
+ */
81
+ readonly envService: FirebaseServerEnvService;
82
+ /**
83
+ * The OIDC provider config whose scopes back the resource server's `scope` value.
84
+ */
85
+ readonly providerConfig: OidcProviderConfig;
86
+ /**
87
+ * Access-token format. Defaults to oidc-provider's `'opaque'`.
88
+ *
89
+ * See {@link BuildOidcResourceServerConfig.accessTokenFormat}.
90
+ */
91
+ readonly accessTokenFormat?: Maybe<'opaque' | 'jwt'>;
92
+ /**
93
+ * Access-token TTL for the MCP resource, in seconds.
94
+ */
95
+ readonly accessTokenTTL?: Maybe<Seconds>;
96
+ }
97
+ /**
98
+ * Builds a single-entry {@link OidcResourceServerInfo} map for the MCP endpoint
99
+ * declared on `envService.appMcpUrl`, with `scope` set to every scope declared
100
+ * on `providerConfig.claims` (so any scope the provider issues is valid on the
101
+ * resource server) and `audience` set to the MCP URL.
102
+ *
103
+ * Returns `undefined` when no `appMcpUrl` is configured.
104
+ *
105
+ * Used internally by `oidcModuleMetadata` when `OidcModuleConfig.configureMcpResourceServer`
106
+ * is enabled.
107
+ *
108
+ * @param input - The environment service and provider config backing the entry.
109
+ * @returns The single-entry map, or `undefined` when no MCP URL is configured.
110
+ *
111
+ * @deprecated Prefer {@link buildOidcResourceServer}, which takes the URL, scope, audience, and
112
+ * token format explicitly instead of deriving a fixed shape from the environment.
113
+ */
114
+ export declare function buildFirebaseServerMcpResourceServer(input: BuildFirebaseServerMcpResourceServerInput): Record<string, OidcResourceServerInfo> | undefined;
115
+ export interface FirebaseServerIssuerProfilesConfig {
116
+ /**
117
+ * The provider's own module config. Its `issuer` becomes the trusted OIDC issuer, and every
118
+ * registered `resourceServers` entry contributes its key and `audience` to the accepted
119
+ * audience list.
120
+ */
121
+ readonly oidcModuleConfig: Pick<OidcModuleConfig, 'issuer' | 'resourceServers'>;
122
+ /**
123
+ * Firebase project ids whose ID tokens the resource server also trusts.
124
+ */
125
+ readonly firebaseProjectIds?: Maybe<readonly string[]>;
126
+ /**
127
+ * Extra audiences to accept beyond the ones derived from `resourceServers` — typically the
128
+ * resource server's own origin when it is not itself a registered resource indicator.
129
+ */
130
+ readonly audiences?: Maybe<readonly string[]>;
131
+ /**
132
+ * Overrides how the provider's verification keys are resolved, bypassing discovery. The API
133
+ * verifying its OWN tokens in-process passes a local key set here rather than making an HTTP
134
+ * call back to itself.
135
+ */
136
+ readonly getKey?: Maybe<Required<Exclude<BuildIssuerProfilesOidcIssuerInput, string>>['getKey']>;
137
+ /**
138
+ * `fetch` used for OIDC discovery; injected in specs.
139
+ */
140
+ readonly fetch?: Maybe<typeof fetch>;
141
+ }
142
+ /**
143
+ * Emits the `@dereekb/oauth-resource` {@link BuildIssuerProfilesConfig} for THIS provider, so an
144
+ * API and the satellite services it issues tokens for cannot drift on issuer / audience strings —
145
+ * both sides read the same `OidcModuleConfig`.
146
+ *
147
+ * The type dependency points `firebase-server → oauth-resource` and never the reverse: the light
148
+ * verify package must stay installable in a plain Express service with no Firebase, Nest, or
149
+ * oidc-provider in its install graph.
150
+ *
151
+ * @param config - The provider's module config plus any extra trusted projects / audiences.
152
+ * @returns The issuer-profile config to hand to `buildIssuerProfiles()`.
153
+ *
154
+ * @example
155
+ * ```ts
156
+ * // inside the API, to hand its satellites a config they cannot get wrong
157
+ * const profilesConfig = firebaseServerIssuerProfiles({ oidcModuleConfig, firebaseProjectIds: [projectId] });
158
+ * const profiles = buildIssuerProfiles(profilesConfig);
159
+ * ```
160
+ */
161
+ export declare function firebaseServerIssuerProfiles(config: FirebaseServerIssuerProfilesConfig): BuildIssuerProfilesConfig;
162
+ /**
163
+ * Reads every audience a registered resource-server map can put on an access token: each entry's
164
+ * key (the resource indicator, which oidc-provider uses as the audience by default) plus its
165
+ * explicit `audience` when one is configured.
166
+ *
167
+ * @param resourceServers - The provider's registered resource servers, if any.
168
+ * @returns The unique audience values.
169
+ */
170
+ export declare function oidcResourceServerAudiences(resourceServers: Maybe<Record<string, OidcResourceServerInfo>>): string[];
@@ -9,4 +9,6 @@ export * from './oidc.jwks.service';
9
9
  export * from './oidc.config.service';
10
10
  export * from './oidc.client.service';
11
11
  export * from './oidc.interaction.service';
12
+ export * from './oidc.grant';
12
13
  export * from './oidc.interaction-policy';
14
+ export * from './oidc.jwt-signing.service';
@@ -0,0 +1,103 @@
1
+ import { type Grant } from 'oidc-provider';
2
+ /**
3
+ * Removes previously-rejected OIDC scopes from a Grant's rejected set.
4
+ *
5
+ * oidc-provider exposes `rejectOIDCScope()` but no inverse, and a rejected scope is subtracted from
6
+ * `getOIDCScope()` on every read — so `addOIDCScope()` alone cannot put a once-rejected scope back in
7
+ * force. `rejected` is plain persisted data (`Pick<Grant, 'openid' | 'resources'>`), which is what
8
+ * makes editing it directly the supported shape of an un-reject.
9
+ *
10
+ * @param grant - The grant to edit in place.
11
+ * @param scopes - The scopes to un-reject. Scopes not currently rejected are ignored.
12
+ */
13
+ export declare function unrejectOIDCScopes(grant: Grant, scopes: Iterable<string>): void;
14
+ /**
15
+ * Removes previously-rejected OIDC claims from a Grant's rejected set.
16
+ *
17
+ * @param grant - The grant to edit in place.
18
+ * @param claims - The claims to un-reject. Claims not currently rejected are ignored.
19
+ * @see unrejectOIDCScopes
20
+ */
21
+ export declare function unrejectOIDCClaims(grant: Grant, claims: Iterable<string>): void;
22
+ /**
23
+ * Removes previously-rejected resource scopes for one resource indicator from a Grant's rejected set.
24
+ *
25
+ * @param grant - The grant to edit in place.
26
+ * @param resource - The resource indicator.
27
+ * @param scopes - The scopes to un-reject. Scopes not currently rejected for the resource are ignored.
28
+ * @see unrejectOIDCScopes
29
+ */
30
+ export declare function unrejectResourceScopes(grant: Grant, resource: string, scopes: Iterable<string>): void;
31
+ /**
32
+ * Input for {@link reconsiderRejectedValues}.
33
+ */
34
+ export interface ReconsiderRejectedValuesInput {
35
+ /**
36
+ * Values oidc-provider reports as still undecided for this consent (e.g. `prompt.details.missingOIDCScope`).
37
+ */
38
+ readonly missing: readonly string[];
39
+ /**
40
+ * Values the existing Grant has already decided — granted or rejected.
41
+ */
42
+ readonly encountered: readonly string[];
43
+ /**
44
+ * The subset of `encountered` the existing Grant has REJECTED.
45
+ */
46
+ readonly rejected: readonly string[];
47
+ /**
48
+ * Values the current authorization request asks for. Only a rejected value the request names again is
49
+ * reconsidered; a rejection from an earlier, wider request stays in force.
50
+ */
51
+ readonly requested: ReadonlySet<string>;
52
+ }
53
+ /**
54
+ * Output of {@link reconsiderRejectedValues}.
55
+ */
56
+ export interface ReconsiderRejectedValuesResult {
57
+ /**
58
+ * `missing` plus the reconsidered values, so a consent decides them again.
59
+ */
60
+ readonly missing: string[];
61
+ /**
62
+ * `encountered` minus the reconsidered values, so they are no longer treated as settled no-ops.
63
+ */
64
+ readonly encountered: string[];
65
+ /**
66
+ * The values moved from `encountered` back into `missing`: rejected on the Grant and named by the
67
+ * current request. A consent that grants one of these must un-reject it on the Grant as well.
68
+ */
69
+ readonly reconsidered: string[];
70
+ }
71
+ /**
72
+ * Moves previously-rejected values the current request asks for again from the "encountered" set back into
73
+ * the "missing" set, so a re-consent decides them afresh.
74
+ *
75
+ * oidc-provider counts a rejected value as encountered: it never reappears in the prompt's `missing*`
76
+ * details, so without this a consent that names it again is a silent no-op and the value can never be
77
+ * granted on that Grant — even when the user explicitly ticks it. Values the Grant has GRANTED are left in
78
+ * `encountered`: they are already in force and need no re-application.
79
+ *
80
+ * @param input - The missing, encountered, rejected, and requested sets.
81
+ * @returns The adjusted missing/encountered sets plus the reconsidered values.
82
+ *
83
+ * @__NO_SIDE_EFFECTS__
84
+ */
85
+ export declare function reconsiderRejectedValues(input: ReconsiderRejectedValuesInput): ReconsiderRejectedValuesResult;
86
+ /**
87
+ * Names every claim an authorization request's `claims` parameter asks for, across `id_token` and `userinfo`.
88
+ *
89
+ * @param claimsParam - The raw `claims` request parameter (a JSON string), or undefined.
90
+ * @returns The requested claim names; empty when the parameter is absent or malformed.
91
+ *
92
+ * @__NO_SIDE_EFFECTS__
93
+ */
94
+ export declare function requestedOIDCClaimNames(claimsParam: unknown): Set<string>;
95
+ /**
96
+ * Normalizes an authorization request's `resource` parameter to its list of resource indicators.
97
+ *
98
+ * @param resourceParam - The raw `resource` request parameter (a string, an array of strings, or undefined).
99
+ * @returns The resource indicators; empty when the parameter is absent.
100
+ *
101
+ * @__NO_SIDE_EFFECTS__
102
+ */
103
+ export declare function requestedResourceIndicators(resourceParam: unknown): string[];
@@ -0,0 +1,77 @@
1
+ import { type Maybe, type Milliseconds, type Seconds } from '@dereekb/util';
2
+ import { OidcModuleConfig } from '../oidc.config';
3
+ import { JwksService } from './oidc.jwks.service';
4
+ /**
5
+ * Default lifetime of a minted JWT, in seconds.
6
+ */
7
+ export declare const DEFAULT_OIDC_SIGNED_JWT_EXPIRES_IN_SECONDS: Seconds;
8
+ /**
9
+ * How long the active signing key is cached before it is re-read from the JWKS store.
10
+ *
11
+ * A rotation is picked up within this window; rotated keys stay in the public JWKS for 30 days, so
12
+ * tokens signed with the previous key keep verifying in the meantime.
13
+ */
14
+ export declare const DEFAULT_OIDC_JWT_SIGNING_KEY_CACHE_MS: Milliseconds;
15
+ /**
16
+ * Header `typ` for a minted JWT: an RFC 9068 JWT access token, matching the format oidc-provider
17
+ * uses for the `accessTokenFormat: 'jwt'` resource-server tokens.
18
+ */
19
+ export declare const DEFAULT_OIDC_SIGNED_JWT_TYP = "at+jwt";
20
+ export interface OidcSignJwtInput {
21
+ /**
22
+ * `aud` claim(s) — the resource the token is for, e.g. an off-box service's origin.
23
+ */
24
+ readonly audience: string | readonly string[];
25
+ /**
26
+ * `sub` claim — the first-party identity of the caller, e.g. `demo-api`.
27
+ */
28
+ readonly subject: string;
29
+ /**
30
+ * Lifetime in seconds. Defaults to {@link DEFAULT_OIDC_SIGNED_JWT_EXPIRES_IN_SECONDS}.
31
+ */
32
+ readonly expiresIn?: Maybe<Seconds>;
33
+ /**
34
+ * Extra claims merged onto the payload, e.g. `{ client_id, scope }`.
35
+ */
36
+ readonly claims?: Maybe<Record<string, unknown>>;
37
+ /**
38
+ * Header `typ`. Defaults to {@link DEFAULT_OIDC_SIGNED_JWT_TYP}.
39
+ */
40
+ readonly typ?: Maybe<string>;
41
+ }
42
+ export interface OidcSignedJwt {
43
+ readonly token: string;
44
+ readonly expiresAt: Date;
45
+ /**
46
+ * `kid` of the JWKS key that signed the token.
47
+ */
48
+ readonly kid: string;
49
+ }
50
+ /**
51
+ * Signs first-party JWTs with the OIDC provider's active JWKS signing key.
52
+ *
53
+ * The result is an RS256 token with `iss` = this provider's issuer, so any resource server that
54
+ * already trusts the provider's JWKS for OAuth access tokens verifies it with no new secret or
55
+ * trust anchor: "the API mints a token for the satellite service" is literally "sign with the
56
+ * active key, `aud` = the satellite's origin". The counterpart on the resource server is
57
+ * `verifyBearerJwt` from `@dereekb/oauth-resource`, whose `oidc` issuer profile discovers this
58
+ * provider's `jwks_uri` from its own discovery document.
59
+ *
60
+ * Registered and exported by `oidcModuleMetadata`, so a downstream app injects it with no extra
61
+ * wiring. Pair it with `createCachedTokenProvider` from `@dereekb/util/oidc` to hold a minted
62
+ * token until it nears expiry instead of re-signing per call.
63
+ */
64
+ export declare class OidcJwtSigningService {
65
+ private readonly _jwks;
66
+ private readonly _config;
67
+ private _cached;
68
+ constructor(_jwks: JwksService, _config: OidcModuleConfig);
69
+ /**
70
+ * Mints a signed JWT.
71
+ *
72
+ * @param input - Audience, subject, lifetime, and extra claims.
73
+ * @returns The compact token, its expiry, and the signing key id.
74
+ */
75
+ signJwt(input: OidcSignJwtInput): Promise<OidcSignedJwt>;
76
+ private _loadSigningKey;
77
+ }
@@ -0,0 +1,37 @@
1
+ import { type IssuerProfiles } from '@dereekb/oauth-resource';
2
+ import { type JWTVerifyGetKey } from 'jose';
3
+ import { type OidcModuleConfig } from '../oidc.config';
4
+ import { type JwksService } from './oidc.jwks.service';
5
+ /**
6
+ * Builds a jose {@link JWTVerifyGetKey} backed by the provider's OWN published JWKS, read directly
7
+ * from {@link JwksService} rather than over HTTP.
8
+ *
9
+ * An in-process verifier must not `createRemoteJWKSet` against its own `/.well-known/jwks.json`:
10
+ * that would make every bearer-authenticated request depend on the process being able to reach
11
+ * itself over the network (which it may not be, behind a load balancer, in a Functions emulator,
12
+ * or under supertest).
13
+ *
14
+ * The key set is cached and re-read only when a token presents a `kid` the cache does not hold —
15
+ * so a freshly rotated key is picked up on its first use instead of after a fixed TTL.
16
+ *
17
+ * @param jwksService - The provider's JWKS service.
18
+ * @returns The key resolver.
19
+ */
20
+ export declare function jwksServiceVerifyGetKey(jwksService: JwksService): JWTVerifyGetKey;
21
+ /**
22
+ * Builds the {@link IssuerProfiles} the provider uses to verify its OWN JWT-format access tokens
23
+ * in-process.
24
+ *
25
+ * A JWT access token is issued for a specific `resource=`, so it carries THAT resource server's
26
+ * audience — not the API's own identity. The accepted audiences are therefore every registered
27
+ * `resourceServers` key and audience, plus the issuer itself. The trust decision that implies is
28
+ * deliberate and narrow: a resource server the user consented to may present the token it was
29
+ * given back to the API that issued it. Scope enforcement is unchanged and still does the real
30
+ * authorization work (e.g. the admin-only `session.firestore` consent gate and the endpoint's own
31
+ * admin predicate).
32
+ *
33
+ * @param config - The provider's module config.
34
+ * @param jwksService - The provider's JWKS service, used for local key resolution.
35
+ * @returns The issuer profiles, holding exactly this provider.
36
+ */
37
+ export declare function oidcProviderIssuerProfiles(config: OidcModuleConfig, jwksService: JwksService): IssuerProfiles;
@@ -23,6 +23,11 @@ export interface ResolveLoginDurationTier {
23
23
  */
24
24
  readonly hasServiceScope: boolean;
25
25
  }
26
+ /**
27
+ * Claims a JWT access token carries that the {@link OidcAuthData} shape sets explicitly, and that
28
+ * are therefore NOT app account claims.
29
+ */
30
+ export declare const OIDC_JWT_ACCESS_TOKEN_RESERVED_CLAIMS: readonly string[];
26
31
  /**
27
32
  * Core OIDC service that wraps the oidc-provider instance and exposes
28
33
  * typed methods for interaction handling, provider initialization, and JWKS management.
@@ -36,6 +41,7 @@ export declare class OidcService {
36
41
  private readonly encryptionService;
37
42
  private readonly _logger;
38
43
  private readonly _getProvider;
44
+ private readonly _getIssuerProfiles;
39
45
  constructor(config: OidcModuleConfig, providerConfigService: OidcProviderConfigService, jwksService: JwksService, accountService: OidcAccountService, collections: OidcServerFirestoreCollections, encryptionService: OidcEncryptionService);
40
46
  /**
41
47
  * Returns the oidc-provider instance, initializing it on first access.
@@ -67,15 +73,40 @@ export declare class OidcService {
67
73
  dbx_max_session_ttl?: number;
68
74
  } | undefined, tier: ResolveLoginDurationTier): number;
69
75
  /**
70
- * Verifies an opaque access token and returns the {@link OidcAuthData}.
76
+ * Verifies an access token and returns the {@link OidcAuthData}.
77
+ *
78
+ * Two formats are accepted, because oidc-provider issues two:
79
+ *
80
+ * - **opaque** (the default) — a database key. Looked up through the provider's `AccessToken`
81
+ * model, which reads the Firestore adapter record written at issuance.
82
+ * - **JWT** (a resource server registered with `accessTokenFormat: 'jwt'`) — verified by
83
+ * signature against this provider's own JWKS via `@dereekb/oauth-resource`. A JWT access token
84
+ * is deliberately NOT persisted by oidc-provider (`lib/models/formats/jwt.js` returns no
85
+ * payload, so `base_model.js` skips the `adapter.upsert`), which means `AccessToken.find()`
86
+ * always misses for one and the store lookup alone would 401 every token the provider just
87
+ * issued.
71
88
  *
72
- * Uses the provider's `AccessToken` model to look up the token and extract
73
- * the account ID, scope, and client ID.
89
+ * The same absence of an adapter record is why a JWT access token **cannot be revoked before
90
+ * `exp`**: {@link revokeGrant} deletes the Grant and every persisted token referencing it, which
91
+ * stops refresh and future issuance, but an outstanding JWT stays verifiable until it expires.
92
+ * Keep `accessTokenTTL` short on any resource server that opts into the JWT format.
74
93
  *
75
- * @param rawToken - The opaque access token string.
94
+ * @param rawToken - The access token string, opaque or JWT.
76
95
  * @returns The auth context, or `undefined` if the token is invalid or expired.
77
96
  */
78
97
  verifyAccessToken(rawToken: string): Promise<OidcAuthData | undefined>;
98
+ /**
99
+ * Verifies a JWT-format access token issued by THIS provider for one of its registered resource
100
+ * servers, and builds the same {@link OidcAuthData} shape the opaque path produces.
101
+ *
102
+ * `sub` is the `accountId` oidc-provider stamped at issuance, which IS the Firebase uid (the
103
+ * opaque path reads the identical value off `accessToken.accountId`), so no mapping table is
104
+ * involved.
105
+ *
106
+ * @param rawToken - The raw token, which may not be a JWT at all.
107
+ * @returns The auth context, or `undefined` when the token is not a verifiable JWT from this provider.
108
+ */
109
+ private _verifyJwtAccessToken;
79
110
  /**
80
111
  * Revokes a Grant entry and every grantable token entry that references it.
81
112
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dereekb/firebase-server",
3
- "version": "14.2.0",
3
+ "version": "14.4.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "exports": {
@@ -57,18 +57,19 @@
57
57
  }
58
58
  },
59
59
  "peerDependencies": {
60
- "@cantoo/pdf-lib": "^2.6.5",
61
- "@dereekb/analytics": "14.2.0",
62
- "@dereekb/calcom": "14.2.0",
63
- "@dereekb/date": "14.2.0",
64
- "@dereekb/dbx-core": "14.2.0",
65
- "@dereekb/discord": "14.2.0",
66
- "@dereekb/firebase": "14.2.0",
67
- "@dereekb/model": "14.2.0",
68
- "@dereekb/nestjs": "14.2.0",
69
- "@dereekb/rxjs": "14.2.0",
70
- "@dereekb/util": "14.2.0",
71
- "@dereekb/zoho": "14.2.0",
60
+ "@cantoo/pdf-lib": ">=2.6.5 <2.11.0",
61
+ "@dereekb/analytics": "14.4.0",
62
+ "@dereekb/calcom": "14.4.0",
63
+ "@dereekb/date": "14.4.0",
64
+ "@dereekb/dbx-core": "14.4.0",
65
+ "@dereekb/discord": "14.4.0",
66
+ "@dereekb/firebase": "14.4.0",
67
+ "@dereekb/model": "14.4.0",
68
+ "@dereekb/nestjs": "14.4.0",
69
+ "@dereekb/oauth-resource": "14.4.0",
70
+ "@dereekb/rxjs": "14.4.0",
71
+ "@dereekb/util": "14.4.0",
72
+ "@dereekb/zoho": "14.4.0",
72
73
  "@google-cloud/firestore": "^7.11.6",
73
74
  "@google-cloud/storage": "^7.22.0",
74
75
  "@modelcontextprotocol/node": "2.0.0",
@@ -85,6 +86,7 @@
85
86
  "firebase-admin": "^13.10.0",
86
87
  "firebase-functions": "^7.3.2",
87
88
  "firebase-functions-test": "3.5.0",
89
+ "jose": "^6.2.12",
88
90
  "jsonwebtoken": "^9.0.3",
89
91
  "make-error": "^1.3.6",
90
92
  "nanoid": "^6.0.1",