@dereekb/firebase-server 14.3.0 → 14.5.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/calcom/package.json +11 -10
- package/discord/package.json +14 -10
- package/index.esm.js +1295 -317
- package/mailgun/package.json +9 -9
- package/mcp/index.esm.js +483 -83
- package/mcp/package.json +12 -11
- package/mcp/src/lib/mcp.config.d.ts +73 -3
- package/mcp/src/lib/service/index.d.ts +1 -0
- package/mcp/src/lib/service/mcp.server.factory.d.ts +3 -2
- package/mcp/src/lib/service/mcp.tool-generator.d.ts +9 -0
- package/mcp/src/lib/service/tools/mcp.tool.cli-token.d.ts +62 -0
- package/model/package.json +13 -10
- package/oidc/index.esm.js +4552 -1902
- package/oidc/package.json +12 -10
- package/oidc/src/lib/controller/index.d.ts +3 -0
- package/oidc/src/lib/controller/oidc.cli-token.config.d.ts +323 -0
- package/oidc/src/lib/controller/oidc.cli-token.controller.d.ts +45 -0
- package/oidc/src/lib/controller/oidc.cli-token.service.d.ts +138 -0
- package/oidc/src/lib/controller/oidc.interaction.controller.d.ts +38 -0
- package/oidc/src/lib/index.d.ts +1 -0
- package/oidc/src/lib/middleware/oauth-auth.module.d.ts +11 -0
- package/oidc/src/lib/oidc.config.d.ts +9 -0
- package/oidc/src/lib/oidc.module.d.ts +2 -39
- package/oidc/src/lib/oidc.resource-server.d.ts +170 -0
- package/oidc/src/lib/service/index.d.ts +2 -0
- package/oidc/src/lib/service/oidc.config.service.d.ts +8 -3
- package/oidc/src/lib/service/oidc.download-signer.d.ts +17 -0
- package/oidc/src/lib/service/oidc.jwt-signing.service.d.ts +112 -0
- package/oidc/src/lib/service/oidc.jwt-verify.d.ts +37 -0
- package/oidc/src/lib/service/oidc.service.d.ts +35 -4
- package/package.json +14 -12
- package/src/lib/nest/controller/download/download.api.config.d.ts +203 -0
- package/src/lib/nest/controller/download/download.api.controller.d.ts +25 -0
- package/src/lib/nest/controller/download/download.api.module.d.ts +47 -0
- package/src/lib/nest/controller/download/download.api.service.d.ts +111 -0
- package/src/lib/nest/controller/download/index.d.ts +4 -0
- package/src/lib/nest/controller/index.d.ts +2 -0
- package/src/lib/nest/controller/request.ip.d.ts +60 -0
- package/test/index.esm.js +169 -43
- package/test/package.json +12 -11
- package/test/src/lib/oidc/oidc.test.flow.d.ts +170 -11
- package/twilio/package.json +8 -8
- package/zoho/package.json +14 -10
package/oidc/package.json
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dereekb/firebase-server/oidc",
|
|
3
|
-
"version": "14.
|
|
3
|
+
"version": "14.5.0",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"peerDependencies": {
|
|
7
|
-
"@dereekb/analytics": "14.
|
|
8
|
-
"@dereekb/date": "14.
|
|
9
|
-
"@dereekb/firebase": "14.
|
|
10
|
-
"@dereekb/firebase-server": "14.
|
|
11
|
-
"@dereekb/model": "14.
|
|
12
|
-
"@dereekb/nestjs": "14.
|
|
13
|
-
"@dereekb/
|
|
14
|
-
"@dereekb/
|
|
15
|
-
"@dereekb/
|
|
7
|
+
"@dereekb/analytics": "14.5.0",
|
|
8
|
+
"@dereekb/date": "14.5.0",
|
|
9
|
+
"@dereekb/firebase": "14.5.0",
|
|
10
|
+
"@dereekb/firebase-server": "14.5.0",
|
|
11
|
+
"@dereekb/model": "14.5.0",
|
|
12
|
+
"@dereekb/nestjs": "14.5.0",
|
|
13
|
+
"@dereekb/oauth-resource": "14.5.0",
|
|
14
|
+
"@dereekb/rxjs": "14.5.0",
|
|
15
|
+
"@dereekb/util": "14.5.0",
|
|
16
|
+
"@dereekb/zoho": "14.5.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
|
},
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export * from './oidc.cli-token.config';
|
|
2
|
+
export * from './oidc.cli-token.service';
|
|
3
|
+
export * from './oidc.cli-token.controller';
|
|
1
4
|
export * from './oidc.interaction.controller';
|
|
2
5
|
export * from './oidc.provider.controller';
|
|
3
6
|
export * from './oidc.wellknown.controller';
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { type ISO8601DateString, type Maybe, type PromiseOrValue, type Seconds, type WebsiteUrl } from '@dereekb/util';
|
|
2
|
+
import { type FirebaseAuthUserId, type OidcScope, type OidcScopeTerm } from '@dereekb/firebase';
|
|
3
|
+
import { type FirebaseServerAuthData } from '@dereekb/firebase-server';
|
|
4
|
+
/**
|
|
5
|
+
* Path (relative to the OIDC issuer) of the CLI-token mint endpoint.
|
|
6
|
+
*
|
|
7
|
+
* Under the `oidc` controller prefix the route is `POST /oidc/cli-token`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const CLI_TOKEN_MINT_PATH_PART = "cli-token";
|
|
10
|
+
/**
|
|
11
|
+
* Path (relative to the OIDC issuer) of the one-time claim redemption endpoint.
|
|
12
|
+
*
|
|
13
|
+
* Under the `oidc` controller prefix the route is `POST /oidc/cli-token/claim`.
|
|
14
|
+
*/
|
|
15
|
+
export declare const CLI_TOKEN_CLAIM_PATH_PART = "cli-token/claim";
|
|
16
|
+
/**
|
|
17
|
+
* Full path of the mint endpoint, as an app must list it in the OIDC module's `protectedPaths`.
|
|
18
|
+
*
|
|
19
|
+
* The mint reads `req.auth`, which only the bearer-token middleware populates. Note this is the
|
|
20
|
+
* MINT path specifically — {@link FIREBASE_SERVER_CLI_TOKEN_CLAIM_PATH} must NOT be protected, since
|
|
21
|
+
* the claim code IS the credential there and a bearer middleware would 401 the machine redeeming it.
|
|
22
|
+
*
|
|
23
|
+
* The OIDC routes are excluded from the global `/api` prefix (see
|
|
24
|
+
* `FIREBASE_SERVER_OIDC_ROUTES_FOR_GLOBAL_ROUTE_EXCLUDE`), so the path carries no `/api` prefix.
|
|
25
|
+
*/
|
|
26
|
+
export declare const FIREBASE_SERVER_CLI_TOKEN_API_PROTECTED_PATH = "/oidc/cli-token";
|
|
27
|
+
/**
|
|
28
|
+
* Full path of the unauthenticated claim endpoint.
|
|
29
|
+
*
|
|
30
|
+
* Deliberately a SUFFIX of {@link FIREBASE_SERVER_CLI_TOKEN_API_PROTECTED_PATH}'s prefix, so an app
|
|
31
|
+
* that protects `/oidc/cli-token` by prefix would also protect this one. The middleware config
|
|
32
|
+
* therefore has to exclude it — see `OidcAuthMiddlewareConfig.unprotectedPaths`.
|
|
33
|
+
*/
|
|
34
|
+
export declare const FIREBASE_SERVER_CLI_TOKEN_CLAIM_PATH = "/oidc/cli-token/claim";
|
|
35
|
+
/**
|
|
36
|
+
* Hard ceiling on a minted CLI credential's lifetime, in seconds (one hour).
|
|
37
|
+
*
|
|
38
|
+
* NEVER configurable upward: the credential is minted non-interactively by whatever already holds
|
|
39
|
+
* the session, and the child grant does not cascade-revoke with its parent (see
|
|
40
|
+
* {@link OidcCliTokenService}), so the short ceiling IS the mitigation.
|
|
41
|
+
*/
|
|
42
|
+
export declare const MAX_CLI_TOKEN_TTL_SECONDS: Seconds;
|
|
43
|
+
/**
|
|
44
|
+
* Default lifetime requested for a minted CLI credential when the caller asks for none.
|
|
45
|
+
*/
|
|
46
|
+
export declare const DEFAULT_CLI_TOKEN_TTL_SECONDS: Seconds;
|
|
47
|
+
/**
|
|
48
|
+
* Window a one-time claim code may be redeemed within, in seconds.
|
|
49
|
+
*
|
|
50
|
+
* Short by design: the code only has to survive the hop from the minting agent to the machine
|
|
51
|
+
* running the CLI, which is an immediate copy-paste or an automated follow-up command — not
|
|
52
|
+
* something a human sits on. The credential it unwraps lives its own (also short) life from the
|
|
53
|
+
* mint, so this window is purely the exposure of the code in transit and is kept near its floor.
|
|
54
|
+
*/
|
|
55
|
+
export declare const CLI_TOKEN_CLAIM_TTL_SECONDS: Seconds;
|
|
56
|
+
/**
|
|
57
|
+
* Number of random bytes behind a claim code. 32 bytes base64url-encodes to 43 characters.
|
|
58
|
+
*
|
|
59
|
+
* Applies to the opaque-random code form. A JWT-shaped code (see {@link CliTokenMintResult.claimCode})
|
|
60
|
+
* would size itself from its payload instead, and should spend at least this much entropy on a `jti`.
|
|
61
|
+
*/
|
|
62
|
+
export declare const CLI_TOKEN_CLAIM_CODE_BYTES = 32;
|
|
63
|
+
/**
|
|
64
|
+
* Scopes a minted CLI credential NEVER inherits, no matter what the minting session holds.
|
|
65
|
+
*
|
|
66
|
+
* - `token.cli` — no chaining: a handoff credential must not be able to mint another one.
|
|
67
|
+
* - `token.service` — no escalation: that scope is the 365-day, non-rotating tier, and a one-hour
|
|
68
|
+
* credential must not be able to trade itself up into one.
|
|
69
|
+
*
|
|
70
|
+
* Everything else the caller holds (including `session.firestore`) inherits normally — the child is
|
|
71
|
+
* exactly as privileged as its parent, minus these two.
|
|
72
|
+
*/
|
|
73
|
+
export declare const CLI_TOKEN_DENIED_INHERITED_OIDC_SCOPES: readonly OidcScope[];
|
|
74
|
+
/**
|
|
75
|
+
* Scope always unioned into a minted credential's grant, so the refresh-token grant is coherent.
|
|
76
|
+
*/
|
|
77
|
+
export declare const CLI_TOKEN_REQUIRED_OIDC_SCOPE: OidcScope;
|
|
78
|
+
/**
|
|
79
|
+
* The default {@link CliTokenApiModuleConfig.requiredScope}, re-exported for apps that want to widen
|
|
80
|
+
* it into an OR-group rather than replace it.
|
|
81
|
+
*/
|
|
82
|
+
export declare const DEFAULT_CLI_TOKEN_REQUIRED_OIDC_SCOPE: OidcScopeTerm;
|
|
83
|
+
/**
|
|
84
|
+
* Signature for the predicate that authorizes a caller to mint a CLI credential.
|
|
85
|
+
*
|
|
86
|
+
* Receives the calling request's auth data (`undefined` for an unauthenticated request) and returns
|
|
87
|
+
* true when that caller may mint. Typically an admin check, e.g.
|
|
88
|
+
* `(auth) => authRoleClaimsService.toRoles(auth?.token ?? {}).has('admin')`.
|
|
89
|
+
*
|
|
90
|
+
* This is the LOAD-BEARING gate — the `token.cli` scope check beside it is defence in depth only,
|
|
91
|
+
* because a non-OIDC caller carries no `scope` claim and every enforcement site treats that as
|
|
92
|
+
* "skip". When no predicate is provided the endpoint fails closed for EVERY caller.
|
|
93
|
+
*/
|
|
94
|
+
export type CliTokenAdminPredicate = (auth: Maybe<FirebaseServerAuthData>) => PromiseOrValue<boolean>;
|
|
95
|
+
/**
|
|
96
|
+
* NestJS injection token for the {@link CliTokenAdminPredicate} provider.
|
|
97
|
+
*/
|
|
98
|
+
export declare const CLI_TOKEN_ADMIN_PREDICATE = "CLI_TOKEN_ADMIN_PREDICATE";
|
|
99
|
+
/**
|
|
100
|
+
* Resolver form of {@link CliTokenApiModuleConfig.cliClientId}.
|
|
101
|
+
*
|
|
102
|
+
* Invoked once per mint rather than read at boot, so an app may discover — or lazily provision — its
|
|
103
|
+
* CLI client after the DI graph was built. Returning a nullish value leaves the endpoint disabled for
|
|
104
|
+
* that call, exactly as an empty literal does.
|
|
105
|
+
*
|
|
106
|
+
* @returns The CLI client's `client_id`, or nullish when none is available.
|
|
107
|
+
*/
|
|
108
|
+
export type CliTokenClientIdResolver = () => Promise<Maybe<string>> | Maybe<string>;
|
|
109
|
+
/**
|
|
110
|
+
* Resolves the configured CLI `client_id`, invoking the resolver form when one was supplied.
|
|
111
|
+
*
|
|
112
|
+
* Takes the configured VALUE rather than the whole config so it can be exercised — and reused —
|
|
113
|
+
* without standing up a module config. An empty result is normalized to `undefined`, so a blank
|
|
114
|
+
* literal disables the endpoint exactly as an absent one does.
|
|
115
|
+
*
|
|
116
|
+
* Memoization is deliberately NOT done here: the resolver is invoked once per mint so an app may
|
|
117
|
+
* provision its CLI client lazily, which means an app that must see a stable id has to memoize its
|
|
118
|
+
* own resolver (e.g. with `cachedGetter`).
|
|
119
|
+
*
|
|
120
|
+
* @param cliClientId - The configured value, in either the literal or the resolver form.
|
|
121
|
+
* @returns The resolved `client_id`, or `undefined` when the mint is not configured.
|
|
122
|
+
*/
|
|
123
|
+
export declare function resolveCliTokenClientId(cliClientId: Maybe<string | CliTokenClientIdResolver>): Promise<Maybe<string>>;
|
|
124
|
+
/**
|
|
125
|
+
* Configuration for the CLI-token mint endpoint, supplied by the app via its dependency module.
|
|
126
|
+
*
|
|
127
|
+
* Without a config the endpoint is DISABLED: there is no safe default for `cliClientId`, and minting
|
|
128
|
+
* against the wrong client would hand the CLI a credential its own `client_id` cannot refresh.
|
|
129
|
+
*/
|
|
130
|
+
export declare abstract class CliTokenApiModuleConfig {
|
|
131
|
+
/**
|
|
132
|
+
* The registered OAuth `client_id` of the app's CLI client — the same client the CLI's
|
|
133
|
+
* `auth setup` was configured with.
|
|
134
|
+
*
|
|
135
|
+
* REQUIRED. When absent (or unresolvable at the provider) the endpoint is disabled.
|
|
136
|
+
*
|
|
137
|
+
* May be a {@link CliTokenClientIdResolver} instead of a literal id, for an app that discovers or
|
|
138
|
+
* provisions its CLI client at runtime rather than reading a configured value. The resolver is
|
|
139
|
+
* invoked per mint, so it can return a value that did not exist when the DI graph was built.
|
|
140
|
+
*/
|
|
141
|
+
readonly cliClientId: string | CliTokenClientIdResolver;
|
|
142
|
+
/**
|
|
143
|
+
* OIDC scope term an OIDC caller must hold to mint. Defaults to
|
|
144
|
+
* {@link DEFAULT_CLI_TOKEN_REQUIRED_OIDC_SCOPE}. Pass `null` to disable scope enforcement entirely
|
|
145
|
+
* (the admin predicate remains the real gate either way).
|
|
146
|
+
*/
|
|
147
|
+
readonly requiredScope?: Maybe<Maybe<OidcScopeTerm>>;
|
|
148
|
+
/**
|
|
149
|
+
* Lifetime requested for a minted credential when the caller names none. Clamped to
|
|
150
|
+
* {@link MAX_CLI_TOKEN_TTL_SECONDS}. Defaults to {@link DEFAULT_CLI_TOKEN_TTL_SECONDS}.
|
|
151
|
+
*/
|
|
152
|
+
readonly defaultTtlSeconds?: Seconds;
|
|
153
|
+
/**
|
|
154
|
+
* Whether a claim code may only be redeemed from the same IP that minted it. Defaults to FALSE.
|
|
155
|
+
*
|
|
156
|
+
* Defence in depth on top of the code's own controls (unguessable, single-use, short-lived): a code
|
|
157
|
+
* that leaks out of a transcript is useless from anywhere but the minting host.
|
|
158
|
+
*
|
|
159
|
+
* **Off by default because it is incompatible with the feature's primary use case.** A cloud-hosted
|
|
160
|
+
* agent provisioning a *different* machine mints and redeems from different addresses by
|
|
161
|
+
* construction, and enforcing a match would make that handoff impossible. Enable it only where the
|
|
162
|
+
* minting session and the redeeming machine are genuinely the same host.
|
|
163
|
+
*
|
|
164
|
+
* Two further limits worth knowing before relying on it. Behind NAT every host on the network shares
|
|
165
|
+
* one egress address, so the check passes for any of them — it is weakest exactly where several
|
|
166
|
+
* machines could race for the code. And the claim route is unauthenticated and proxied, so the
|
|
167
|
+
* observed address is only as trustworthy as the proxy that set `X-Forwarded-For`; where nothing
|
|
168
|
+
* rewrites that header it is caller-supplied and the binding is decorative.
|
|
169
|
+
*/
|
|
170
|
+
readonly bindClaimToMintIp?: boolean;
|
|
171
|
+
/**
|
|
172
|
+
* The app's API base URL, echoed into the handoff bundle so a machine with no prior `auth setup`
|
|
173
|
+
* can bootstrap an env from the bundle alone.
|
|
174
|
+
*/
|
|
175
|
+
readonly apiBaseUrl?: WebsiteUrl;
|
|
176
|
+
/**
|
|
177
|
+
* The CLI env name this deployment should be stored under, echoed into the handoff bundle.
|
|
178
|
+
*
|
|
179
|
+
* The SERVER is the only party that knows which deployment it is. Without this the redeeming CLI
|
|
180
|
+
* can only fall back to whatever env is already active locally, which has two bad outcomes: on a
|
|
181
|
+
* bare machine there is no active env at all and the redeem fails, and on a configured machine a
|
|
182
|
+
* code minted here can silently repoint an env named for a DIFFERENT deployment at this one.
|
|
183
|
+
*
|
|
184
|
+
* Name it after the deployment (`local`, `staging`, `prod`), not after the app.
|
|
185
|
+
*/
|
|
186
|
+
readonly envName?: string;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Result of a successful mint. Carries ONLY the one-time claim code — never the refresh token, which
|
|
190
|
+
* would otherwise land in an MCP transcript and a shell history.
|
|
191
|
+
*/
|
|
192
|
+
export interface CliTokenMintResult {
|
|
193
|
+
/**
|
|
194
|
+
* The one-time code to redeem at `POST /oidc/cli-token/claim`.
|
|
195
|
+
*
|
|
196
|
+
* OPAQUE to its holder: the CLI passes it back verbatim and reads nothing out of it. Today it is
|
|
197
|
+
* {@link CLI_TOKEN_CLAIM_CODE_BYTES} random bytes, base64url-encoded — a pointer at the stored
|
|
198
|
+
* claim, carrying no data of its own.
|
|
199
|
+
*
|
|
200
|
+
* It may instead become an encoded (or signed) JWT if the code ever has to carry information the
|
|
201
|
+
* redeeming machine needs BEFORE it can redeem — an issuer or API base URL it would otherwise have
|
|
202
|
+
* no way to learn. Nothing downstream assumes the random form: `cliTokenClaimDocumentId` normalizes
|
|
203
|
+
* the value into a document id, and base64url (which a compact JWS also uses) already satisfies it.
|
|
204
|
+
*
|
|
205
|
+
* Two properties must survive such a change. The code has to stay unguessable, so a JWT form must
|
|
206
|
+
* still carry its own random `jti` rather than being derivable from the mint's inputs; and a failed
|
|
207
|
+
* redemption must stay undifferentiated, so a self-describing code must not let a caller distinguish
|
|
208
|
+
* "expired" from "unknown" locally and turn the unauthenticated claim route into an oracle.
|
|
209
|
+
*/
|
|
210
|
+
readonly claimCode: string;
|
|
211
|
+
/**
|
|
212
|
+
* When the CLAIM CODE stops being redeemable.
|
|
213
|
+
*/
|
|
214
|
+
readonly claimExpiresAt: ISO8601DateString;
|
|
215
|
+
/**
|
|
216
|
+
* When the minted CREDENTIAL itself expires — always within {@link MAX_CLI_TOKEN_TTL_SECONDS}.
|
|
217
|
+
*/
|
|
218
|
+
readonly expiresAt: ISO8601DateString;
|
|
219
|
+
/**
|
|
220
|
+
* The space-delimited scopes the minted credential carries.
|
|
221
|
+
*/
|
|
222
|
+
readonly scope: string;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* The credential bundle handed back by `POST /oidc/cli-token/claim`. Everything a bare machine needs
|
|
226
|
+
* to create a CLI env and be logged in.
|
|
227
|
+
*/
|
|
228
|
+
export interface CliTokenHandoffBundle {
|
|
229
|
+
/**
|
|
230
|
+
* The uid the credential was minted for — the minting caller's own uid.
|
|
231
|
+
*/
|
|
232
|
+
readonly uid: FirebaseAuthUserId;
|
|
233
|
+
/**
|
|
234
|
+
* The OIDC issuer the credential is valid at.
|
|
235
|
+
*/
|
|
236
|
+
readonly issuer: WebsiteUrl;
|
|
237
|
+
/**
|
|
238
|
+
* The app's API base URL, when the app configured one. Lets a machine with no prior `auth setup`
|
|
239
|
+
* create the env from this bundle alone.
|
|
240
|
+
*/
|
|
241
|
+
readonly apiBaseUrl?: WebsiteUrl;
|
|
242
|
+
/**
|
|
243
|
+
* The CLI env name this deployment should be stored under, when the app configured one via
|
|
244
|
+
* {@link CliTokenApiModuleConfig.envName}.
|
|
245
|
+
*
|
|
246
|
+
* Advisory: an explicit `--env` on the redeeming side still wins. It exists so the rendered
|
|
247
|
+
* one-line handoff command works on a machine with no config, and so a redeem cannot silently
|
|
248
|
+
* repoint an env named for another deployment.
|
|
249
|
+
*/
|
|
250
|
+
readonly envName?: string;
|
|
251
|
+
/**
|
|
252
|
+
* The CLI OAuth client the refresh token is bound to. The CLI must present THIS `client_id` when
|
|
253
|
+
* refreshing — a refresh token is client-bound.
|
|
254
|
+
*/
|
|
255
|
+
readonly clientId: string;
|
|
256
|
+
/**
|
|
257
|
+
* The refresh token itself. The only place it is ever returned.
|
|
258
|
+
*/
|
|
259
|
+
readonly refreshToken: string;
|
|
260
|
+
/**
|
|
261
|
+
* The space-delimited scopes the credential carries.
|
|
262
|
+
*/
|
|
263
|
+
readonly scope: string;
|
|
264
|
+
/**
|
|
265
|
+
* When the credential expires.
|
|
266
|
+
*/
|
|
267
|
+
readonly expiresAt: ISO8601DateString;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Inputs to {@link resolveCliTokenScopes}.
|
|
271
|
+
*/
|
|
272
|
+
export interface ResolveCliTokenScopesInput {
|
|
273
|
+
/**
|
|
274
|
+
* The scopes the MINTING caller holds. The minted credential can never exceed this set.
|
|
275
|
+
*/
|
|
276
|
+
readonly callerScopes: Maybe<ReadonlySet<OidcScope>>;
|
|
277
|
+
/**
|
|
278
|
+
* An optional subset the caller asked for. May only NARROW `callerScopes` — any scope named here
|
|
279
|
+
* that the caller does not hold is dropped.
|
|
280
|
+
*/
|
|
281
|
+
readonly requestedScopes?: Maybe<readonly OidcScope[]>;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Resolves the scope set a minted CLI credential carries.
|
|
285
|
+
*
|
|
286
|
+
* `granted = callerScopes ∩ (requestedSubset ?? callerScopes) − {token.cli, token.service} ∪ {offline_access}`
|
|
287
|
+
*
|
|
288
|
+
* The intersection is what makes the mint non-amplifying: the caller may narrow, never widen. The
|
|
289
|
+
* deny-list is the only asymmetry, and it exists so a one-hour credential cannot manufacture a
|
|
290
|
+
* longer-lived or further-minting one.
|
|
291
|
+
*
|
|
292
|
+
* @param input - The caller's scopes and the optional requested subset.
|
|
293
|
+
* @returns The space-delimited scope string to grant.
|
|
294
|
+
* @__NO_SIDE_EFFECTS__
|
|
295
|
+
*/
|
|
296
|
+
export declare function resolveCliTokenScopes(input: ResolveCliTokenScopesInput): string;
|
|
297
|
+
/**
|
|
298
|
+
* Inputs to {@link resolveCliTokenTtlSeconds}.
|
|
299
|
+
*/
|
|
300
|
+
export interface ResolveCliTokenTtlSecondsInput {
|
|
301
|
+
/**
|
|
302
|
+
* The TTL the caller asked for, if any.
|
|
303
|
+
*/
|
|
304
|
+
readonly requestedTtlSeconds?: Maybe<Seconds>;
|
|
305
|
+
/**
|
|
306
|
+
* The app-configured default, used when the caller asks for none.
|
|
307
|
+
*/
|
|
308
|
+
readonly defaultTtlSeconds?: Maybe<Seconds>;
|
|
309
|
+
/**
|
|
310
|
+
* Seconds of life remaining on the MINTING caller's own grant, when known. A child must never
|
|
311
|
+
* outlive its parent.
|
|
312
|
+
*/
|
|
313
|
+
readonly parentRemainingSeconds?: Maybe<Seconds>;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Resolves a minted credential's lifetime as
|
|
317
|
+
* `min(requested ?? default, MAX_CLI_TOKEN_TTL_SECONDS, parentRemaining)`, floored at one second.
|
|
318
|
+
*
|
|
319
|
+
* @param input - The requested/default TTL and the parent grant's remaining life.
|
|
320
|
+
* @returns The TTL to mint with, in seconds.
|
|
321
|
+
* @__NO_SIDE_EFFECTS__
|
|
322
|
+
*/
|
|
323
|
+
export declare function resolveCliTokenTtlSeconds(input: ResolveCliTokenTtlSecondsInput): Seconds;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type Request } from 'express';
|
|
2
|
+
import { type CliTokenHandoffBundle, type CliTokenMintResult } from './oidc.cli-token.config';
|
|
3
|
+
import { type ClaimCliTokenParams, type MintCliTokenParams, OidcCliTokenService } from './oidc.cli-token.service';
|
|
4
|
+
/**
|
|
5
|
+
* REST controller for the CLI credential handoff.
|
|
6
|
+
*
|
|
7
|
+
* Mounted at `oidc`, giving `POST /oidc/cli-token` and `POST /oidc/cli-token/claim`. It lives in
|
|
8
|
+
* `@dereekb/firebase-server/oidc` rather than beside the session API because minting needs
|
|
9
|
+
* `OidcService.getProvider()`, which `@dereekb/firebase-server` core does not have.
|
|
10
|
+
*
|
|
11
|
+
* **Registration order matters.** `OidcProviderController` also mounts at `oidc` and ends in an
|
|
12
|
+
* `@All('{*path}')` catch-all that proxies to the oidc-provider callback. Express matches routes in
|
|
13
|
+
* registration order, so this controller MUST be listed BEFORE `OidcProviderController` in
|
|
14
|
+
* `oidcModuleMetadata`'s `controllers` array — otherwise both routes are swallowed by the catch-all
|
|
15
|
+
* and answered by the provider as unknown endpoints.
|
|
16
|
+
*
|
|
17
|
+
* Auth on the mint route comes from the global OIDC bearer middleware, so
|
|
18
|
+
* `FIREBASE_SERVER_CLI_TOKEN_API_PROTECTED_PATH` must be listed in the OIDC module's
|
|
19
|
+
* `protectedPaths`. The claim route is unauthenticated BY DESIGN — the one-time code is the
|
|
20
|
+
* credential there — and because protection matches by prefix, the app must also list
|
|
21
|
+
* `FIREBASE_SERVER_CLI_TOKEN_CLAIM_PATH` in `unprotectedPaths`.
|
|
22
|
+
*/
|
|
23
|
+
export declare class OidcCliTokenController {
|
|
24
|
+
private readonly cliTokenService;
|
|
25
|
+
constructor(cliTokenService: OidcCliTokenService);
|
|
26
|
+
/**
|
|
27
|
+
* Mints a short-lived CLI credential for the calling user and returns the one-time claim code.
|
|
28
|
+
*
|
|
29
|
+
* @param req - The Express request carrying auth credentials on `req.auth`.
|
|
30
|
+
* @param body - The optional requested scope subset + TTL.
|
|
31
|
+
* @returns The {@link CliTokenMintResult}. Never the refresh token itself.
|
|
32
|
+
*/
|
|
33
|
+
mintCliToken(req: Request, body: MintCliTokenParams): Promise<CliTokenMintResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Redeems a one-time claim code for the credential bundle it wraps.
|
|
36
|
+
*
|
|
37
|
+
* Unauthenticated by design: the machine redeeming the code has no credential yet — obtaining one
|
|
38
|
+
* is the entire point. Every failure returns the same generic error so the route is not an oracle.
|
|
39
|
+
*
|
|
40
|
+
* @param req - The Express request, read only for the caller's address.
|
|
41
|
+
* @param body - The claim code.
|
|
42
|
+
* @returns The {@link CliTokenHandoffBundle}.
|
|
43
|
+
*/
|
|
44
|
+
claimCliToken(req: Request, body: ClaimCliTokenParams): Promise<CliTokenHandoffBundle>;
|
|
45
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { type Maybe, type Seconds } from '@dereekb/util';
|
|
2
|
+
import { type OidcScope } from '@dereekb/firebase';
|
|
3
|
+
import { type FirebaseServerAuthData } from '@dereekb/firebase-server';
|
|
4
|
+
import { OidcModuleConfig } from '../oidc.config';
|
|
5
|
+
import { OidcService } from '../service/oidc.service';
|
|
6
|
+
import { OidcEncryptionService } from '../service/oidc.encryption.service';
|
|
7
|
+
import { OidcServerFirestoreCollections } from '../model/model';
|
|
8
|
+
import { type CliTokenAdminPredicate, CliTokenApiModuleConfig, type CliTokenHandoffBundle, type CliTokenMintResult } from './oidc.cli-token.config';
|
|
9
|
+
/**
|
|
10
|
+
* Error code thrown when the caller is not authorized to mint a CLI credential.
|
|
11
|
+
*/
|
|
12
|
+
export declare const CLI_TOKEN_FORBIDDEN_ERROR_CODE = "CLI_TOKEN_FORBIDDEN_ERROR";
|
|
13
|
+
/**
|
|
14
|
+
* Error code thrown when the app did not configure a usable CLI client, so minting is disabled.
|
|
15
|
+
*/
|
|
16
|
+
export declare const CLI_TOKEN_DISABLED_ERROR_CODE = "CLI_TOKEN_DISABLED_ERROR";
|
|
17
|
+
/**
|
|
18
|
+
* Error code returned for EVERY failed claim redemption — not found, expired, and already-consumed
|
|
19
|
+
* alike. Deliberately undifferentiated so the unauthenticated claim endpoint is not an oracle a
|
|
20
|
+
* caller can probe for valid codes.
|
|
21
|
+
*/
|
|
22
|
+
export declare const CLI_TOKEN_CLAIM_INVALID_ERROR_CODE = "CLI_TOKEN_CLAIM_INVALID_ERROR";
|
|
23
|
+
/**
|
|
24
|
+
* Body of `POST /oidc/cli-token`.
|
|
25
|
+
*/
|
|
26
|
+
export interface MintCliTokenParams {
|
|
27
|
+
/**
|
|
28
|
+
* An optional subset of the caller's own scopes to grant. May only NARROW — a scope the caller
|
|
29
|
+
* does not hold is ignored.
|
|
30
|
+
*/
|
|
31
|
+
readonly scopes?: Maybe<readonly OidcScope[]>;
|
|
32
|
+
/**
|
|
33
|
+
* An optional requested lifetime in seconds. Clamped to one hour and to the remaining life of the
|
|
34
|
+
* caller's own grant.
|
|
35
|
+
*/
|
|
36
|
+
readonly ttlSeconds?: Maybe<Seconds>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Body of `POST /oidc/cli-token/claim`.
|
|
40
|
+
*/
|
|
41
|
+
export interface ClaimCliTokenParams {
|
|
42
|
+
readonly code: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Per-request context for a mint or claim, carrying what the transport layer observed.
|
|
46
|
+
*/
|
|
47
|
+
export interface CliTokenRequestContext {
|
|
48
|
+
/**
|
|
49
|
+
* The calling client's address, as resolved by the controller.
|
|
50
|
+
*/
|
|
51
|
+
readonly requestIp?: Maybe<string>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Mints a short-lived CLI login credential for the caller, and redeems the one-time claim code that
|
|
55
|
+
* carries it.
|
|
56
|
+
*
|
|
57
|
+
* ## Security
|
|
58
|
+
*
|
|
59
|
+
* Gates apply in this order, mirroring `FirestoreSessionApiService`:
|
|
60
|
+
*
|
|
61
|
+
* 1. `auth?.uid` present, else unauthenticated.
|
|
62
|
+
* 2. The app-supplied {@link CliTokenAdminPredicate} — the load-bearing check. **Fails closed** when
|
|
63
|
+
* the app provides no predicate.
|
|
64
|
+
* 3. The `token.cli` OIDC scope — defence in depth only. It cannot stand alone: a non-OIDC caller
|
|
65
|
+
* carries no `scope` claim and every enforcement site treats that as "skip".
|
|
66
|
+
* 4. A configured, provider-resolvable CLI `client_id`, else the endpoint is disabled.
|
|
67
|
+
*
|
|
68
|
+
* The minted credential is never MORE privileged than the session that minted it: its scopes are the
|
|
69
|
+
* caller's own, minus `token.cli` / `token.service` (see {@link resolveCliTokenScopes}), and its TTL
|
|
70
|
+
* is capped at one hour AND at the remaining life of the caller's own grant.
|
|
71
|
+
*
|
|
72
|
+
* ## Accepted limitation — no revocation cascade
|
|
73
|
+
*
|
|
74
|
+
* A refresh token is CLIENT-BOUND: oidc-provider's `validateGrant` requires
|
|
75
|
+
* `grant.clientId === client.clientId`, so the parent grant (owned by the MCP client) cannot be
|
|
76
|
+
* reused for the CLI client and a **separate** Grant must be created. Revoking the parent session
|
|
77
|
+
* therefore does NOT cascade to the minted credential. The ≤1h cap is the mitigation; `auth logout
|
|
78
|
+
* --revoke` and the `deleteOidcToken` callModel action are the manual kill switches.
|
|
79
|
+
*
|
|
80
|
+
* Rotation is deliberately left ON (the scope is NOT added to `nonRotatingScopes`): the CLI client is
|
|
81
|
+
* a public PKCE client, so oidc-provider rotates the refresh token on every exchange and its reuse
|
|
82
|
+
* detection kills the whole grant if a stolen copy is replayed. That is why the credential must land
|
|
83
|
+
* in the CLI's PERSISTED token cache rather than the non-persisting `fromEnv` path.
|
|
84
|
+
*/
|
|
85
|
+
export declare class OidcCliTokenService {
|
|
86
|
+
private readonly oidcService;
|
|
87
|
+
private readonly oidcModuleConfig;
|
|
88
|
+
private readonly encryptionService;
|
|
89
|
+
private readonly collections;
|
|
90
|
+
private readonly config?;
|
|
91
|
+
private readonly adminPredicate?;
|
|
92
|
+
private readonly _logger;
|
|
93
|
+
constructor(oidcService: OidcService, oidcModuleConfig: OidcModuleConfig, encryptionService: OidcEncryptionService, collections: OidcServerFirestoreCollections, config?: CliTokenApiModuleConfig | undefined, adminPredicate?: CliTokenAdminPredicate | undefined);
|
|
94
|
+
/**
|
|
95
|
+
* Mints a CLI credential for the calling user and returns the one-time claim code that unwraps it.
|
|
96
|
+
*
|
|
97
|
+
* @param auth - The authenticated request's auth data (`req.auth`).
|
|
98
|
+
* @param params - The optional requested scope subset + TTL.
|
|
99
|
+
* @returns The claim code and the minted credential's metadata. The refresh token itself is NOT returned.
|
|
100
|
+
* @throws {HttpsError} `401` with no uid, `403` when a gate rejects the caller, `400` when minting is disabled.
|
|
101
|
+
*/
|
|
102
|
+
mintCliToken(auth: Maybe<FirebaseServerAuthData>, params: MintCliTokenParams, context?: CliTokenRequestContext): Promise<CliTokenMintResult>;
|
|
103
|
+
/**
|
|
104
|
+
* Redeems a one-time claim code, consuming it inside a Firestore transaction so a double-redeem
|
|
105
|
+
* loses the race rather than handing out the same credential twice.
|
|
106
|
+
*
|
|
107
|
+
* @param params - The claim code to redeem.
|
|
108
|
+
* @returns The handoff bundle: everything a bare machine needs to be logged in.
|
|
109
|
+
* @throws {HttpsError} A `404` with the SAME generic error for a missing, expired, or already-consumed code.
|
|
110
|
+
*/
|
|
111
|
+
claimCliToken(params: ClaimCliTokenParams, context?: CliTokenRequestContext): Promise<CliTokenHandoffBundle>;
|
|
112
|
+
private _storeClaim;
|
|
113
|
+
private _claimInvalidError;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Generates a high-entropy, URL-safe one-time claim code.
|
|
117
|
+
*
|
|
118
|
+
* @returns A base64url-encoded random string of {@link CLI_TOKEN_CLAIM_CODE_BYTES} bytes.
|
|
119
|
+
*/
|
|
120
|
+
export declare function generateCliTokenClaimCode(): string;
|
|
121
|
+
/**
|
|
122
|
+
* Prefix of the `oidcEntry` document id a pending CLI-token claim is stored under.
|
|
123
|
+
*
|
|
124
|
+
* Namespaced so a claim can never collide with an oidc-provider adapter entry, which keys documents
|
|
125
|
+
* by the provider's own token/grant ids.
|
|
126
|
+
*/
|
|
127
|
+
export declare const CLI_TOKEN_CLAIM_DOCUMENT_ID_PREFIX = "cli-token-claim-";
|
|
128
|
+
/**
|
|
129
|
+
* Builds the `oidcEntry` document id for a claim code.
|
|
130
|
+
*
|
|
131
|
+
* The code IS the lookup key — there is no query — so a redeem is a single point read and a wrong
|
|
132
|
+
* code is indistinguishable from an expired one.
|
|
133
|
+
*
|
|
134
|
+
* @param code - The one-time claim code.
|
|
135
|
+
* @returns The namespaced document id.
|
|
136
|
+
* @__NO_SIDE_EFFECTS__
|
|
137
|
+
*/
|
|
138
|
+
export declare function cliTokenClaimDocumentId(code: string): string;
|
|
@@ -69,6 +69,36 @@ export declare class OidcInteractionController {
|
|
|
69
69
|
*/
|
|
70
70
|
private _verifyIdToken;
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Inputs to {@link withheldGatedScopesForClient}.
|
|
74
|
+
*/
|
|
75
|
+
export interface WithheldGatedScopesForClientInput {
|
|
76
|
+
/**
|
|
77
|
+
* Every scope the authorization request named.
|
|
78
|
+
*/
|
|
79
|
+
readonly requestedScopes: ReadonlySet<string>;
|
|
80
|
+
/**
|
|
81
|
+
* Every scope referenced by ANY provider profile — the "gated" set.
|
|
82
|
+
*/
|
|
83
|
+
readonly profileGatedScopes: ReadonlySet<string>;
|
|
84
|
+
/**
|
|
85
|
+
* The scopes THIS client's resolved profiles unlock.
|
|
86
|
+
*/
|
|
87
|
+
readonly clientUnlockedScopes: ReadonlySet<string>;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The requested scopes that are provider-profile gated and NOT unlocked by this client's profiles —
|
|
91
|
+
* the set a consent must withhold from the user and drop from the grant.
|
|
92
|
+
*
|
|
93
|
+
* A gated scope is not necessarily unavailable: one unlocked by a DEFAULT profile is reachable by
|
|
94
|
+
* every client, which `clientUnlockedScopes` already accounts for (it is resolved through
|
|
95
|
+
* `oidcClientProviderProfileScopes`, whose fallback is the registry's default profiles).
|
|
96
|
+
*
|
|
97
|
+
* @param input - The requested, gated, and client-unlocked scope sets.
|
|
98
|
+
* @returns The requested scopes to withhold, in request order.
|
|
99
|
+
* @__NO_SIDE_EFFECTS__
|
|
100
|
+
*/
|
|
101
|
+
export declare function withheldGatedScopesForClient(input: WithheldGatedScopesForClientInput): string[];
|
|
72
102
|
/**
|
|
73
103
|
* Inputs for {@link resolveEffectiveSubset}.
|
|
74
104
|
*/
|
|
@@ -91,6 +121,12 @@ export interface ResolveEffectiveSubsetInput {
|
|
|
91
121
|
* `reconsiderRejectedValues` — or it can never be granted on that Grant.
|
|
92
122
|
*/
|
|
93
123
|
readonly alreadyEncountered?: readonly string[];
|
|
124
|
+
/**
|
|
125
|
+
* Entries to withhold from the grant no matter what else applies. Subtracted LAST, so a withheld
|
|
126
|
+
* entry lands in `rejected` even when the caller explicitly named it or an `alwaysGranted` rule
|
|
127
|
+
* would otherwise have added it. Withholding can only ever subtract, never widen a grant.
|
|
128
|
+
*/
|
|
129
|
+
readonly withheld?: readonly string[];
|
|
94
130
|
}
|
|
95
131
|
/**
|
|
96
132
|
* Resolves the effective set to grant — and the complementary set to
|
|
@@ -107,6 +143,8 @@ export interface ResolveEffectiveSubsetInput {
|
|
|
107
143
|
* entry so oidc-provider does not re-prompt for them. Values in `alreadyEncountered` pass validation but
|
|
108
144
|
* are not re-applied to the grant (no-op).
|
|
109
145
|
* - Always-granted entries are union'd into `granted` (clamped to `missing`).
|
|
146
|
+
* - Withheld entries are subtracted last, so they end up in `rejected` even when the caller named
|
|
147
|
+
* them or an always-granted rule would otherwise have added them.
|
|
110
148
|
*
|
|
111
149
|
* @param input - Missing entries plus optional subset, always-granted, and already-encountered lists.
|
|
112
150
|
* @returns `granted` to add to the grant and `rejected` to record on the grant.
|
package/oidc/src/lib/index.d.ts
CHANGED
|
@@ -33,6 +33,14 @@ export declare abstract class OidcAuthMiddlewareConfig {
|
|
|
33
33
|
* since those are protected by AppCheck.
|
|
34
34
|
*/
|
|
35
35
|
readonly protectedPaths: SlashPath[];
|
|
36
|
+
/**
|
|
37
|
+
* Path prefixes excluded from verification, checked BEFORE {@link protectedPaths}.
|
|
38
|
+
*
|
|
39
|
+
* Needed because protection matches by prefix: a deliberately-public sub-route of a protected
|
|
40
|
+
* prefix (e.g. `/oidc/cli-token/claim` under `/oidc/cli-token`) would otherwise be 401'd for
|
|
41
|
+
* exactly the callers it exists for.
|
|
42
|
+
*/
|
|
43
|
+
readonly unprotectedPaths?: SlashPath[];
|
|
36
44
|
/**
|
|
37
45
|
* Absolute URL of the OAuth 2.0 Protected Resource Metadata document
|
|
38
46
|
* (RFC 9728). When set, included as the `resource_metadata` parameter
|
|
@@ -52,6 +60,9 @@ export declare abstract class OidcAuthMiddlewareConfig {
|
|
|
52
60
|
* included as the RFC 9728 `resource_metadata` hint so clients can locate the
|
|
53
61
|
* discovery doc directly instead of relying on origin-rooted path-walkback.
|
|
54
62
|
*
|
|
63
|
+
* Delegates to `@dereekb/oauth-resource`'s builder so the challenge an off-box resource server
|
|
64
|
+
* emits and the one this API emits are produced by the same code.
|
|
65
|
+
*
|
|
55
66
|
* @param error - The RFC 6750 `error` token (e.g. `invalid_token`, `invalid_request`).
|
|
56
67
|
* @param resourceMetadataUrl - Optional protected-resource metadata URL.
|
|
57
68
|
* @returns Header value, e.g. `Bearer resource_metadata="…", error="invalid_token"`.
|
|
@@ -367,6 +367,15 @@ export declare abstract class OidcModuleConfig {
|
|
|
367
367
|
* since those are typically protected by AppCheck.
|
|
368
368
|
*/
|
|
369
369
|
readonly protectedPaths?: SlashPath[];
|
|
370
|
+
/**
|
|
371
|
+
* Path prefixes EXCLUDED from bearer verification, checked before {@link protectedPaths}.
|
|
372
|
+
*
|
|
373
|
+
* Protection matches by prefix, so a public sub-route of a protected prefix is otherwise
|
|
374
|
+
* unreachable: `POST /oidc/cli-token/claim` is deliberately unauthenticated (the one-time claim
|
|
375
|
+
* code IS the credential there), yet it sits under the protected `/oidc/cli-token` prefix. Listing
|
|
376
|
+
* it here is what keeps it reachable.
|
|
377
|
+
*/
|
|
378
|
+
readonly unprotectedPaths?: SlashPath[];
|
|
370
379
|
/**
|
|
371
380
|
* Map of recognized OAuth resource indicator URLs (RFC 8707) to their
|
|
372
381
|
* {@link OidcResourceServerInfo}. When non-empty, oidc-provider's
|