@hilbras/keystone 1.9.0 → 2.6.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/CHANGELOG.md +459 -0
- package/README.md +163 -32
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/db/migrations/0015_chief_vargas.sql +7 -0
- package/dist/db/migrations/0016_gorgeous_charles_xavier.sql +5 -0
- package/dist/db/migrations/0017_flashy_nekra.sql +1 -0
- package/dist/db/migrations/meta/0015_snapshot.json +4340 -0
- package/dist/db/migrations/meta/0016_snapshot.json +4363 -0
- package/dist/db/migrations/meta/0017_snapshot.json +4370 -0
- package/dist/db/migrations/meta/_journal.json +21 -0
- package/dist/db/schema.d.ts +138 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +39 -1
- package/dist/db/schema.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/auth.d.ts +2 -0
- package/dist/plugins/auth.d.ts.map +1 -1
- package/dist/plugins/auth.js +21 -7
- package/dist/plugins/auth.js.map +1 -1
- package/dist/plugins/headerSanitization.d.ts +15 -0
- package/dist/plugins/headerSanitization.d.ts.map +1 -0
- package/dist/plugins/headerSanitization.js +39 -0
- package/dist/plugins/headerSanitization.js.map +1 -0
- package/dist/plugins/machinePrincipal.d.ts +28 -0
- package/dist/plugins/machinePrincipal.d.ts.map +1 -0
- package/dist/plugins/machinePrincipal.js +45 -0
- package/dist/plugins/machinePrincipal.js.map +1 -0
- package/dist/plugins/mtls.d.ts +21 -2
- package/dist/plugins/mtls.d.ts.map +1 -1
- package/dist/plugins/mtls.js +94 -47
- package/dist/plugins/mtls.js.map +1 -1
- package/dist/plugins/rateLimit.d.ts +5 -4
- package/dist/plugins/rateLimit.d.ts.map +1 -1
- package/dist/plugins/rateLimit.js +11 -8
- package/dist/plugins/rateLimit.js.map +1 -1
- package/dist/repositories/application.d.ts +1 -1
- package/dist/repositories/application.d.ts.map +1 -1
- package/dist/repositories/application.js +8 -2
- package/dist/repositories/application.js.map +1 -1
- package/dist/repositories/session.d.ts +1 -0
- package/dist/repositories/session.d.ts.map +1 -1
- package/dist/repositories/types.d.ts +5 -1
- package/dist/repositories/types.d.ts.map +1 -1
- package/dist/routes/admin/organizations.d.ts.map +1 -1
- package/dist/routes/admin/organizations.js +31 -2
- package/dist/routes/admin/organizations.js.map +1 -1
- package/dist/routes/apiKeys.d.ts.map +1 -1
- package/dist/routes/apiKeys.js +23 -5
- package/dist/routes/apiKeys.js.map +1 -1
- package/dist/routes/federation.d.ts.map +1 -1
- package/dist/routes/federation.js +8 -2
- package/dist/routes/federation.js.map +1 -1
- package/dist/routes/oauth.d.ts.map +1 -1
- package/dist/routes/oauth.js +38 -2
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/oauth2.d.ts.map +1 -1
- package/dist/routes/oauth2.js +92 -13
- package/dist/routes/oauth2.js.map +1 -1
- package/dist/routes/profile.js +2 -2
- package/dist/routes/profile.js.map +1 -1
- package/dist/routes/saml.d.ts +24 -0
- package/dist/routes/saml.d.ts.map +1 -1
- package/dist/routes/saml.js +32 -3
- package/dist/routes/saml.js.map +1 -1
- package/dist/routes/serviceAccounts.d.ts.map +1 -1
- package/dist/routes/serviceAccounts.js +78 -2
- package/dist/routes/serviceAccounts.js.map +1 -1
- package/dist/routes/sessions.js +3 -3
- package/dist/routes/sessions.js.map +1 -1
- package/dist/routes/smsOtp.d.ts.map +1 -1
- package/dist/routes/smsOtp.js +6 -0
- package/dist/routes/smsOtp.js.map +1 -1
- package/dist/routes/totp.d.ts.map +1 -1
- package/dist/routes/totp.js +24 -10
- package/dist/routes/totp.js.map +1 -1
- package/dist/routes/webauthn.d.ts.map +1 -1
- package/dist/routes/webauthn.js +8 -2
- package/dist/routes/webauthn.js.map +1 -1
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/types.d.ts +4 -1
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/services/application/organization.d.ts +5 -1
- package/dist/services/application/organization.d.ts.map +1 -1
- package/dist/services/application/organization.js.map +1 -1
- package/dist/services/connectors/google.d.ts +18 -1
- package/dist/services/connectors/google.d.ts.map +1 -1
- package/dist/services/connectors/google.js +25 -3
- package/dist/services/connectors/google.js.map +1 -1
- package/dist/services/connectors/oidc.d.ts +14 -2
- package/dist/services/connectors/oidc.d.ts.map +1 -1
- package/dist/services/connectors/oidc.js +29 -3
- package/dist/services/connectors/oidc.js.map +1 -1
- package/dist/services/connectors/types.d.ts +13 -1
- package/dist/services/connectors/types.d.ts.map +1 -1
- package/dist/services/domain/authentication.d.ts.map +1 -1
- package/dist/services/domain/authentication.js +36 -19
- package/dist/services/domain/authentication.js.map +1 -1
- package/dist/services/domain/organization.d.ts +5 -1
- package/dist/services/domain/organization.d.ts.map +1 -1
- package/dist/services/domain/organization.js.map +1 -1
- package/dist/services/events/types.d.ts +1 -1
- package/dist/services/events/types.d.ts.map +1 -1
- package/dist/services/events/validate.d.ts.map +1 -1
- package/dist/services/events/validate.js +6 -0
- package/dist/services/events/validate.js.map +1 -1
- package/dist/services/magicLinks.d.ts.map +1 -1
- package/dist/services/magicLinks.js +13 -9
- package/dist/services/magicLinks.js.map +1 -1
- package/dist/services/oauth2.d.ts +72 -2
- package/dist/services/oauth2.d.ts.map +1 -1
- package/dist/services/oauth2.js +106 -6
- package/dist/services/oauth2.js.map +1 -1
- package/dist/services/redirectUri.d.ts +56 -0
- package/dist/services/redirectUri.d.ts.map +1 -0
- package/dist/services/redirectUri.js +129 -0
- package/dist/services/redirectUri.js.map +1 -0
- package/dist/services/scopes.d.ts +113 -0
- package/dist/services/scopes.d.ts.map +1 -0
- package/dist/services/scopes.js +138 -0
- package/dist/services/scopes.js.map +1 -0
- package/dist/services/serviceAccounts.d.ts +20 -0
- package/dist/services/serviceAccounts.d.ts.map +1 -1
- package/dist/services/serviceAccounts.js +47 -1
- package/dist/services/serviceAccounts.js.map +1 -1
- package/dist/services/sessionRevocation.d.ts +76 -0
- package/dist/services/sessionRevocation.d.ts.map +1 -0
- package/dist/services/sessionRevocation.js +86 -0
- package/dist/services/sessionRevocation.js.map +1 -0
- package/dist/services/singleUse.d.ts +68 -0
- package/dist/services/singleUse.d.ts.map +1 -0
- package/dist/services/singleUse.js +54 -0
- package/dist/services/singleUse.js.map +1 -0
- package/dist/services/smsOtp.d.ts.map +1 -1
- package/dist/services/smsOtp.js +12 -8
- package/dist/services/smsOtp.js.map +1 -1
- package/dist/services/tokens.d.ts +2 -0
- package/dist/services/tokens.d.ts.map +1 -1
- package/dist/services/tokens.js +5 -0
- package/dist/services/tokens.js.map +1 -1
- package/dist/services/trustedProxies.d.ts +90 -0
- package/dist/services/trustedProxies.d.ts.map +1 -0
- package/dist/services/trustedProxies.js +283 -0
- package/dist/services/trustedProxies.js.map +1 -0
- package/dist/setup-server.js +2 -1
- package/dist/setup-server.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +4 -5
package/dist/services/oauth2.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import { eq, and, gt, isNull
|
|
2
|
+
import { eq, and, gt, isNull } from "drizzle-orm";
|
|
3
3
|
import { db } from "../db/index.js";
|
|
4
4
|
import { oauth2AuthorizationCodes, oauth2Consents, applications, orgMemberships, } from "../db/schema.js";
|
|
5
5
|
import { config } from "../config.js";
|
|
@@ -38,8 +38,11 @@ export async function consumeAuthorizationCode(code, appId, redirectUri) {
|
|
|
38
38
|
gt(oauth2AuthorizationCodes.expiresAt, now),
|
|
39
39
|
isNull(oauth2AuthorizationCodes.usedAt),
|
|
40
40
|
];
|
|
41
|
+
// Exact match, no exceptions. `redirect_uri` is required at /authorize, so
|
|
42
|
+
// there is always a stored value to compare against; tolerating a missing one
|
|
43
|
+
// here would let a stolen code be redeemed against any redirect URI.
|
|
41
44
|
if (redirectUri) {
|
|
42
|
-
conditions.push(
|
|
45
|
+
conditions.push(eq(oauth2AuthorizationCodes.redirectUri, redirectUri));
|
|
43
46
|
}
|
|
44
47
|
const [record] = await db
|
|
45
48
|
.update(oauth2AuthorizationCodes)
|
|
@@ -48,13 +51,92 @@ export async function consumeAuthorizationCode(code, appId, redirectUri) {
|
|
|
48
51
|
.returning();
|
|
49
52
|
return record;
|
|
50
53
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Verify a PKCE code verifier against a stored challenge.
|
|
56
|
+
*
|
|
57
|
+
* `requireChallenge` exists because the previous shape of this function was the
|
|
58
|
+
* whole PKCE story at the token endpoint, and it was not a check at all:
|
|
59
|
+
*
|
|
60
|
+
* ```ts
|
|
61
|
+
* if (!challenge) return true; // no challenge was registered -> accept
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* A client that omitted `code_challenge` at `/authorize` and then sent any
|
|
65
|
+
* string as `code_verifier` at `/token` passed. The endpoint required a
|
|
66
|
+
* verifier to be *present*, so the requirement looked enforced while proving
|
|
67
|
+
* nothing. For a public client — an SPA or a mobile app that cannot hold a
|
|
68
|
+
* secret — PKCE is the only thing standing between an intercepted authorization
|
|
69
|
+
* code and a stolen account, so "optional" meant "unprotected".
|
|
70
|
+
*
|
|
71
|
+
* The two failure modes are now separated deliberately:
|
|
72
|
+
* - no challenge and `requireChallenge` false: a confidential client that chose
|
|
73
|
+
* not to use PKCE. Permitted, because it authenticates with a secret.
|
|
74
|
+
* - a challenge but a mismatched or malformed verifier: always a failure.
|
|
75
|
+
*/
|
|
76
|
+
export function verifyPKCE(challenge, method, verifier, options = {}) {
|
|
77
|
+
if (!challenge) {
|
|
78
|
+
// No challenge to verify against. Only acceptable when PKCE was not
|
|
79
|
+
// required for this client.
|
|
80
|
+
return !options.requireChallenge;
|
|
81
|
+
}
|
|
82
|
+
if (typeof verifier !== "string" || verifier === "")
|
|
83
|
+
return false;
|
|
54
84
|
if (method?.toLowerCase() !== "s256")
|
|
55
85
|
return false;
|
|
56
86
|
const hash = crypto.createHash("sha256").update(verifier).digest("base64url");
|
|
57
|
-
return hash
|
|
87
|
+
return timingSafeEqual(hash, challenge);
|
|
88
|
+
}
|
|
89
|
+
/** Constant-time comparison, so a verifier cannot be recovered byte by byte. */
|
|
90
|
+
function timingSafeEqual(a, b) {
|
|
91
|
+
const bufA = Buffer.from(a, "utf8");
|
|
92
|
+
const bufB = Buffer.from(b, "utf8");
|
|
93
|
+
if (bufA.length !== bufB.length)
|
|
94
|
+
return false;
|
|
95
|
+
return crypto.timingSafeEqual(bufA, bufB);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A client that cannot keep a secret must use PKCE.
|
|
99
|
+
*
|
|
100
|
+
* Today every application has a non-null `client_secret_hash`, so this reduces to
|
|
101
|
+
* "the client has no usable secret" — which is the honest test regardless of how
|
|
102
|
+
* the column is populated, and stays correct if secretless public clients are
|
|
103
|
+
* introduced later.
|
|
104
|
+
*/
|
|
105
|
+
export function requiresPkce(clientSecretHash) {
|
|
106
|
+
return !clientSecretHash || clientSecretHash === "";
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Reduce requested scopes to the ones actually granted.
|
|
110
|
+
*
|
|
111
|
+
* The effective set is the intersection of what the client is registered to ask
|
|
112
|
+
* for, what it asked for, and what the user consented to. Previously the
|
|
113
|
+
* client's `scope` parameter was stored verbatim and the only filter applied was
|
|
114
|
+
* consent, so a client could put any string it liked into the issued token.
|
|
115
|
+
*
|
|
116
|
+
* Rejecting rather than silently dropping is deliberate: a client asking for
|
|
117
|
+
* something it was never registered for is a misconfiguration or an attempt to
|
|
118
|
+
* escalate, and quietly handing back a lesser scope set hides both. The error
|
|
119
|
+
* names the offending scope so the client can be fixed.
|
|
120
|
+
*
|
|
121
|
+
* An empty `allowedScopes` means "no restriction registered", which preserves
|
|
122
|
+
* every existing application. Once an application has an allowlist, it is
|
|
123
|
+
* enforced.
|
|
124
|
+
*/
|
|
125
|
+
export function resolveEffectiveScopes(input) {
|
|
126
|
+
const requested = [...new Set(input.requested.filter(Boolean))];
|
|
127
|
+
const allowed = new Set(input.allowed);
|
|
128
|
+
const consented = input.consented === null ? null : new Set(input.consented);
|
|
129
|
+
const effective = [];
|
|
130
|
+
for (const scope of requested) {
|
|
131
|
+
if (allowed.size > 0 && !allowed.has(scope)) {
|
|
132
|
+
return { ok: false, error: "invalid_scope", scope };
|
|
133
|
+
}
|
|
134
|
+
if (consented && !consented.has(scope)) {
|
|
135
|
+
return { ok: false, error: "invalid_scope", scope };
|
|
136
|
+
}
|
|
137
|
+
effective.push(scope);
|
|
138
|
+
}
|
|
139
|
+
return { ok: true, scopes: effective };
|
|
58
140
|
}
|
|
59
141
|
export async function hasConsent(userId, appId, requestedScopes) {
|
|
60
142
|
if (requestedScopes.length === 0)
|
|
@@ -69,6 +151,21 @@ export async function hasConsent(userId, appId, requestedScopes) {
|
|
|
69
151
|
const granted = new Set(consent.scopes);
|
|
70
152
|
return requestedScopes.every((scope) => granted.has(scope));
|
|
71
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* The user's current consent record, or undefined if they have never consented
|
|
156
|
+
* or have revoked it.
|
|
157
|
+
*
|
|
158
|
+
* Kept separate from `hasConsent` because the scope intersection needs the
|
|
159
|
+
* granted set itself, not a yes/no answer to it.
|
|
160
|
+
*/
|
|
161
|
+
export async function findConsent(userId, appId) {
|
|
162
|
+
const [consent] = await db
|
|
163
|
+
.select()
|
|
164
|
+
.from(oauth2Consents)
|
|
165
|
+
.where(and(eq(oauth2Consents.appId, appId), eq(oauth2Consents.userId, userId), isNull(oauth2Consents.revokedAt)))
|
|
166
|
+
.limit(1);
|
|
167
|
+
return consent;
|
|
168
|
+
}
|
|
72
169
|
export async function grantConsent(userId, appId, scopes) {
|
|
73
170
|
const now = new Date();
|
|
74
171
|
const [record] = await db
|
|
@@ -114,6 +211,9 @@ export async function createTokenResponse(user, app, scopes, opts = {}) {
|
|
|
114
211
|
appId: app.id,
|
|
115
212
|
orgId: app.orgId,
|
|
116
213
|
clientId: app.clientId,
|
|
214
|
+
// The granted set is persisted on the refresh token so it survives rotation
|
|
215
|
+
// instead of being dropped at the first refresh.
|
|
216
|
+
...(scopes.length > 0 ? { scopes } : {}),
|
|
117
217
|
...(opts.mfaFactor ? { mfaFactor: opts.mfaFactor } : {}),
|
|
118
218
|
};
|
|
119
219
|
const tokenSet = await createTokenSet(user, opts.ip, opts.userAgent, tokenOpts, opts.deviceFingerprint);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth2.js","sourceRoot":"","sources":["../../src/services/oauth2.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"oauth2.js","sourceRoot":"","sources":["../../src/services/oauth2.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redirect URI validation.
|
|
3
|
+
*
|
|
4
|
+
* Redirect URIs are the one place an authorization server sends a user's browser
|
|
5
|
+
* somewhere on its own authority. Anything accepted here becomes a `Location`
|
|
6
|
+
* header the identity provider itself emits, so the accepted set has to be a
|
|
7
|
+
* set a browser cannot be talked out of.
|
|
8
|
+
*
|
|
9
|
+
* Two distinct jobs, kept separate on purpose:
|
|
10
|
+
*
|
|
11
|
+
* - {@link validateRedirectUri} decides whether a URI may be **registered**.
|
|
12
|
+
* - {@link isRedirectUriRegistered} decides whether a URI may be **used**.
|
|
13
|
+
*
|
|
14
|
+
* Matching is exact string comparison. That is the strictest form available and
|
|
15
|
+
* the one RFC 9700 requires: no prefix matching, no normalization, no wildcards.
|
|
16
|
+
* Normalizing before comparing is what turns a strict check into a loose one,
|
|
17
|
+
* because the two sides then stop being compared as the bytes that will
|
|
18
|
+
* actually be sent.
|
|
19
|
+
*/
|
|
20
|
+
export interface RedirectUriRejection {
|
|
21
|
+
ok: false;
|
|
22
|
+
reason: string;
|
|
23
|
+
}
|
|
24
|
+
export type RedirectUriCheck = {
|
|
25
|
+
ok: true;
|
|
26
|
+
} | RedirectUriRejection;
|
|
27
|
+
/**
|
|
28
|
+
* Whether a redirect URI may be registered.
|
|
29
|
+
*
|
|
30
|
+
* Rejects, in order of how badly they fail:
|
|
31
|
+
* - `javascript:`, `data:`, and friends — script execution on our own origin
|
|
32
|
+
* - wildcards — accepted by the URL parser but can never match exactly, so they
|
|
33
|
+
* mislead the operator into thinking a redirect is configured when it is not
|
|
34
|
+
* - fragments — never transmitted to the server, so an exact comparison against
|
|
35
|
+
* them can never succeed
|
|
36
|
+
* - embedded credentials — `https://user:pass@host` is a phishing primitive
|
|
37
|
+
* - plaintext HTTP to anything that is not loopback
|
|
38
|
+
*/
|
|
39
|
+
export declare function validateRedirectUri(uri: string): RedirectUriCheck;
|
|
40
|
+
/**
|
|
41
|
+
* Validate a whole registration set, returning every problem rather than the
|
|
42
|
+
* first, so an operator fixing a configuration sees all of it at once.
|
|
43
|
+
*/
|
|
44
|
+
export declare function validateRedirectUris(uris: string[]): RedirectUriRejection[];
|
|
45
|
+
/**
|
|
46
|
+
* Whether a requested redirect URI is registered.
|
|
47
|
+
*
|
|
48
|
+
* Exact string comparison, deliberately. The requested value is compared as the
|
|
49
|
+
* exact bytes that will be sent in the `Location` header, with no parsing,
|
|
50
|
+
* case-folding, trailing-slash adjustment, or default-port removal. Any of those
|
|
51
|
+
* would widen the check: `https://example.com` and `https://example.com/` are
|
|
52
|
+
* different origins paths-wise, and a comparison that treats them as equal is a
|
|
53
|
+
* comparison that can be made to match something it should not.
|
|
54
|
+
*/
|
|
55
|
+
export declare function isRedirectUriRegistered(registered: string[], requested: string | undefined): boolean;
|
|
56
|
+
//# sourceMappingURL=redirectUri.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redirectUri.d.ts","sourceRoot":"","sources":["../../src/services/redirectUri.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,KAAK,CAAC;IACV,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,gBAAgB,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG,oBAAoB,CAAC;AAcnE;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,CA6DjE;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,oBAAoB,EAAE,CAiB3E;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAGpG"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redirect URI validation.
|
|
3
|
+
*
|
|
4
|
+
* Redirect URIs are the one place an authorization server sends a user's browser
|
|
5
|
+
* somewhere on its own authority. Anything accepted here becomes a `Location`
|
|
6
|
+
* header the identity provider itself emits, so the accepted set has to be a
|
|
7
|
+
* set a browser cannot be talked out of.
|
|
8
|
+
*
|
|
9
|
+
* Two distinct jobs, kept separate on purpose:
|
|
10
|
+
*
|
|
11
|
+
* - {@link validateRedirectUri} decides whether a URI may be **registered**.
|
|
12
|
+
* - {@link isRedirectUriRegistered} decides whether a URI may be **used**.
|
|
13
|
+
*
|
|
14
|
+
* Matching is exact string comparison. That is the strictest form available and
|
|
15
|
+
* the one RFC 9700 requires: no prefix matching, no normalization, no wildcards.
|
|
16
|
+
* Normalizing before comparing is what turns a strict check into a loose one,
|
|
17
|
+
* because the two sides then stop being compared as the bytes that will
|
|
18
|
+
* actually be sent.
|
|
19
|
+
*/
|
|
20
|
+
/** Schemes that would execute script or inline a document if navigated to. */
|
|
21
|
+
const SCRIPT_SCHEMES = new Set(["javascript", "data", "vbscript", "blob", "file"]);
|
|
22
|
+
/**
|
|
23
|
+
* Hostnames that may legitimately be reached over plaintext HTTP, because they
|
|
24
|
+
* never leave the developer's machine.
|
|
25
|
+
*/
|
|
26
|
+
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]", "::1", "0.0.0.0"]);
|
|
27
|
+
/** `scheme:` per RFC 3986, which custom app schemes (`myapp://`) must match. */
|
|
28
|
+
const SCHEME = /^([a-z][a-z0-9+.-]*):/i;
|
|
29
|
+
/**
|
|
30
|
+
* Whether a redirect URI may be registered.
|
|
31
|
+
*
|
|
32
|
+
* Rejects, in order of how badly they fail:
|
|
33
|
+
* - `javascript:`, `data:`, and friends — script execution on our own origin
|
|
34
|
+
* - wildcards — accepted by the URL parser but can never match exactly, so they
|
|
35
|
+
* mislead the operator into thinking a redirect is configured when it is not
|
|
36
|
+
* - fragments — never transmitted to the server, so an exact comparison against
|
|
37
|
+
* them can never succeed
|
|
38
|
+
* - embedded credentials — `https://user:pass@host` is a phishing primitive
|
|
39
|
+
* - plaintext HTTP to anything that is not loopback
|
|
40
|
+
*/
|
|
41
|
+
export function validateRedirectUri(uri) {
|
|
42
|
+
if (typeof uri !== "string" || uri.trim() === "") {
|
|
43
|
+
return { ok: false, reason: "must be a non-empty string" };
|
|
44
|
+
}
|
|
45
|
+
const value = uri.trim();
|
|
46
|
+
// A wildcard is a valid URL character sequence to the parser, so it has to be
|
|
47
|
+
// rejected explicitly rather than caught by scheme or host rules.
|
|
48
|
+
if (value.includes("*")) {
|
|
49
|
+
return { ok: false, reason: "must not contain a wildcard; register each redirect URI exactly" };
|
|
50
|
+
}
|
|
51
|
+
let url;
|
|
52
|
+
try {
|
|
53
|
+
url = new URL(value);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return { ok: false, reason: "must be a valid absolute URL" };
|
|
57
|
+
}
|
|
58
|
+
const scheme = url.protocol.replace(/:$/, "").toLowerCase();
|
|
59
|
+
if (SCRIPT_SCHEMES.has(scheme)) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
reason: `scheme "${scheme}" would execute script or inline a document and is never a valid redirect target`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (url.hash) {
|
|
66
|
+
return { ok: false, reason: "must not contain a fragment; fragments are not sent to the server" };
|
|
67
|
+
}
|
|
68
|
+
if (url.username || url.password) {
|
|
69
|
+
return { ok: false, reason: "must not embed credentials" };
|
|
70
|
+
}
|
|
71
|
+
if (scheme === "https") {
|
|
72
|
+
return { ok: true };
|
|
73
|
+
}
|
|
74
|
+
if (scheme === "http") {
|
|
75
|
+
const host = url.hostname.toLowerCase();
|
|
76
|
+
if (LOOPBACK_HOSTS.has(host))
|
|
77
|
+
return { ok: true };
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
reason: "must use https; plaintext http is accepted only for loopback addresses during development",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// A custom scheme is how native and mobile clients receive the redirect, so it
|
|
84
|
+
// is allowed — provided it is not one of the script-bearing schemes handled
|
|
85
|
+
// above. It must be a reverse-DNS style identifier, not a bare word.
|
|
86
|
+
if (SCHEME.test(value) && url.protocol !== ":") {
|
|
87
|
+
if (!/^[a-z][a-z0-9+.-]*:$/i.test(url.protocol)) {
|
|
88
|
+
return { ok: false, reason: "scheme is malformed" };
|
|
89
|
+
}
|
|
90
|
+
return { ok: true };
|
|
91
|
+
}
|
|
92
|
+
return { ok: false, reason: `scheme "${scheme}" is not supported` };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Validate a whole registration set, returning every problem rather than the
|
|
96
|
+
* first, so an operator fixing a configuration sees all of it at once.
|
|
97
|
+
*/
|
|
98
|
+
export function validateRedirectUris(uris) {
|
|
99
|
+
const problems = [];
|
|
100
|
+
for (const uri of uris) {
|
|
101
|
+
const result = validateRedirectUri(uri);
|
|
102
|
+
if (!result.ok)
|
|
103
|
+
problems.push({ ok: false, reason: `${uri}: ${result.reason}` });
|
|
104
|
+
}
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
for (const uri of uris) {
|
|
107
|
+
if (seen.has(uri)) {
|
|
108
|
+
problems.push({ ok: false, reason: `${uri}: registered more than once` });
|
|
109
|
+
}
|
|
110
|
+
seen.add(uri);
|
|
111
|
+
}
|
|
112
|
+
return problems;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Whether a requested redirect URI is registered.
|
|
116
|
+
*
|
|
117
|
+
* Exact string comparison, deliberately. The requested value is compared as the
|
|
118
|
+
* exact bytes that will be sent in the `Location` header, with no parsing,
|
|
119
|
+
* case-folding, trailing-slash adjustment, or default-port removal. Any of those
|
|
120
|
+
* would widen the check: `https://example.com` and `https://example.com/` are
|
|
121
|
+
* different origins paths-wise, and a comparison that treats them as equal is a
|
|
122
|
+
* comparison that can be made to match something it should not.
|
|
123
|
+
*/
|
|
124
|
+
export function isRedirectUriRegistered(registered, requested) {
|
|
125
|
+
if (typeof requested !== "string" || requested === "")
|
|
126
|
+
return false;
|
|
127
|
+
return registered.includes(requested);
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=redirectUri.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redirectUri.js","sourceRoot":"","sources":["../../src/services/redirectUri.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key scope registry.
|
|
3
|
+
*
|
|
4
|
+
* API key scopes used to be decorative. `requireScopes` existed and was
|
|
5
|
+
* decorated onto the Fastify instance, but no route referenced it, so a key
|
|
6
|
+
* issued with the default `["api:read"]` carried exactly the same authority as
|
|
7
|
+
* the user who created it. Scopes were also free-form: a caller could request
|
|
8
|
+
* any string at key creation, including the literal `"service_account"` that
|
|
9
|
+
* `requireScopes` treated as a wildcard.
|
|
10
|
+
*
|
|
11
|
+
* A registry fixes both halves. A scope that is not defined here cannot be
|
|
12
|
+
* granted, so there is no string an attacker can invent; and a route that needs a
|
|
13
|
+
* scope names it from this list, so enforcement cannot silently drift out of
|
|
14
|
+
* date the way an unreferenced decorator did.
|
|
15
|
+
*/
|
|
16
|
+
/** Every scope Keystone recognises. Order is the order shown to operators. */
|
|
17
|
+
export declare const API_KEY_SCOPES: {
|
|
18
|
+
/** Read the caller's own profile, sessions, and keys. */
|
|
19
|
+
readonly "profile:read": "Read your own profile and identity";
|
|
20
|
+
readonly "profile:write": "Update your own profile";
|
|
21
|
+
/** Read sessions belonging to the caller. */
|
|
22
|
+
readonly "sessions:read": "List your own sessions";
|
|
23
|
+
readonly "sessions:revoke": "Revoke your own sessions";
|
|
24
|
+
/** Manage the caller's own API keys. */
|
|
25
|
+
readonly "api_keys:read": "List your own API keys";
|
|
26
|
+
readonly "api_keys:revoke": "Revoke your own API keys";
|
|
27
|
+
/** Read organization members and applications. */
|
|
28
|
+
readonly "organizations:read": "Read organization membership and applications";
|
|
29
|
+
/** Manage organization members, applications, and roles. */
|
|
30
|
+
readonly "organizations:write": "Manage organization membership and applications";
|
|
31
|
+
/** Read and manage webhooks. */
|
|
32
|
+
readonly "webhooks:manage": "Create, update, and delete webhooks";
|
|
33
|
+
/** Read and manage workflows. */
|
|
34
|
+
readonly "workflows:read": "Read workflows and their runs";
|
|
35
|
+
readonly "workflows:write": "Create and update workflows";
|
|
36
|
+
/** Second-factor management. Never appropriate for a machine credential. */
|
|
37
|
+
readonly "mfa:manage": "Enroll, verify, and disable TOTP and backup codes";
|
|
38
|
+
};
|
|
39
|
+
export type ApiKeyScope = keyof typeof API_KEY_SCOPES;
|
|
40
|
+
export declare function isApiKeyScope(value: string): value is ApiKeyScope;
|
|
41
|
+
export declare function knownScopes(): ApiKeyScope[];
|
|
42
|
+
/** Human-readable description of a scope, for API responses and dashboards. */
|
|
43
|
+
export declare function describeScope(scope: string): string | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Scopes granted to a service account by default.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately excludes anything interactive: a machine credential has no user
|
|
48
|
+
* present to complete a second factor, no real profile, and no sessions worth
|
|
49
|
+
* listing. A service account that needs more is granted it explicitly.
|
|
50
|
+
*/
|
|
51
|
+
export declare const SERVICE_ACCOUNT_DEFAULT_SCOPES: ApiKeyScope[];
|
|
52
|
+
/**
|
|
53
|
+
* Scopes a new personal API key receives when the caller names none.
|
|
54
|
+
*
|
|
55
|
+
* Read-only and confined to the caller's own resources. The previous default,
|
|
56
|
+
* `["api:read"]`, was not in the registry at all — which is a fair indicator that
|
|
57
|
+
* nothing was checking it.
|
|
58
|
+
*/
|
|
59
|
+
export declare const PERSONAL_KEY_DEFAULT_SCOPES: ApiKeyScope[];
|
|
60
|
+
/**
|
|
61
|
+
* Scopes that may never be granted to a machine credential.
|
|
62
|
+
*
|
|
63
|
+
* `mfa:manage` because a second factor is enrolled by a person holding an
|
|
64
|
+
* authenticator; `MfaRequiredError` is raised when a token is minted for a user
|
|
65
|
+
* with TOTP enabled and no recorded factor, and a service account has no
|
|
66
|
+
* authenticator to record.
|
|
67
|
+
*
|
|
68
|
+
* `profile:*` because a service account's "profile" is a synthesized object with
|
|
69
|
+
* an id of `sa:<uuid>` that matches no user row. Reading it returns nothing real,
|
|
70
|
+
* and writing it would update zero rows -- so the scope is meaningless for a
|
|
71
|
+
* machine and misleading in an audit log that shows what a key was used for.
|
|
72
|
+
*/
|
|
73
|
+
export declare const HUMAN_ONLY_SCOPES: readonly ApiKeyScope[];
|
|
74
|
+
export interface ScopeValidation {
|
|
75
|
+
ok: boolean;
|
|
76
|
+
/** Unknown scopes, which must be refused rather than stored. */
|
|
77
|
+
unknown: string[];
|
|
78
|
+
/** Scopes refused because a service account may not hold them. */
|
|
79
|
+
forbiddenForServiceAccount: string[];
|
|
80
|
+
/** The accepted set, de-duplicated and in registry order. */
|
|
81
|
+
scopes: ApiKeyScope[];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Validate a requested scope set.
|
|
85
|
+
*
|
|
86
|
+
* Unknown scopes are refused rather than dropped. Silently discarding a scope a
|
|
87
|
+
* caller asked for hides both a typo and an attempt to smuggle an unrecognised
|
|
88
|
+
* privilege past a review, and it leaves the caller believing they hold
|
|
89
|
+
* something they do not.
|
|
90
|
+
*/
|
|
91
|
+
export declare function validateScopes(requested: string[] | undefined, options?: {
|
|
92
|
+
principal: "user" | "service_account";
|
|
93
|
+
fallback?: ApiKeyScope[];
|
|
94
|
+
}): ScopeValidation;
|
|
95
|
+
/**
|
|
96
|
+
* Narrow a key's stored scopes to what the caller is actually entitled to.
|
|
97
|
+
*
|
|
98
|
+
* A key never gains authority from its own record. The effective set is what the
|
|
99
|
+
* key holds *intersected with* what its principal could do directly, so revoking
|
|
100
|
+
* a role or removing a membership takes effect on the key too rather than
|
|
101
|
+
* leaving a stale grant behind.
|
|
102
|
+
*/
|
|
103
|
+
export declare function intersectScopes(granted: string[] | null | undefined, principalAllows: string[]): ApiKeyScope[];
|
|
104
|
+
/**
|
|
105
|
+
* Grant check used by `requireScopes`.
|
|
106
|
+
*
|
|
107
|
+
* Explicit allow-list membership only. The previous implementation also accepted
|
|
108
|
+
* a key whose scopes merely *contained* the string `"service_account"`, which is
|
|
109
|
+
* a bypass by construction: that string was client-suppliable at key creation, so
|
|
110
|
+
* any key created with `scopes: ["service_account"]` satisfied every check.
|
|
111
|
+
*/
|
|
112
|
+
export declare function hasScopes(granted: string[] | null | undefined, required: readonly string[]): boolean;
|
|
113
|
+
//# sourceMappingURL=scopes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scopes.d.ts","sourceRoot":"","sources":["../../src/services/scopes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,8EAA8E;AAC9E,eAAO,MAAM,cAAc;IACzB,yDAAyD;aACzD,cAAc,EAAE,oCAAoC;aACpD,eAAe,EAAE,yBAAyB;IAC1C,6CAA6C;aAC7C,eAAe,EAAE,wBAAwB;aACzC,iBAAiB,EAAE,0BAA0B;IAC7C,wCAAwC;aACxC,eAAe,EAAE,wBAAwB;aACzC,iBAAiB,EAAE,0BAA0B;IAC7C,kDAAkD;aAClD,oBAAoB,EAAE,+CAA+C;IACrE,4DAA4D;aAC5D,qBAAqB,EAAE,iDAAiD;IACxE,gCAAgC;aAChC,iBAAiB,EAAE,qCAAqC;IACxD,iCAAiC;aACjC,gBAAgB,EAAE,+BAA+B;aACjD,iBAAiB,EAAE,6BAA6B;IAChD,4EAA4E;aAC5E,YAAY,EAAE,mDAAmD;CACzD,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,cAAc,CAAC;AAItD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,WAAW,CAEjE;AAED,wBAAgB,WAAW,IAAI,WAAW,EAAE,CAE3C;AAED,+EAA+E;AAC/E,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE/D;AAED;;;;;;GAMG;AACH,eAAO,MAAM,8BAA8B,EAAE,WAAW,EAA2B,CAAC;AAEpF;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,EAAE,WAAW,EAAuD,CAAC;AAE7G;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iBAAiB,EAAE,SAAS,WAAW,EAInD,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,gEAAgE;IAChE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,kEAAkE;IAClE,0BAA0B,EAAE,MAAM,EAAE,CAAC;IACrC,6DAA6D;IAC7D,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EAAE,GAAG,SAAS,EAC/B,OAAO,GAAE;IAAE,SAAS,EAAE,MAAM,GAAG,iBAAiB,CAAC;IAAC,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAA;CAA0B,GACnG,eAAe,CAwBjB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,GAAG,SAAS,EAAE,eAAe,EAAE,MAAM,EAAE,GAAG,WAAW,EAAE,CAI9G;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,GAAG,SAAS,EAAE,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAIpG"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API key scope registry.
|
|
3
|
+
*
|
|
4
|
+
* API key scopes used to be decorative. `requireScopes` existed and was
|
|
5
|
+
* decorated onto the Fastify instance, but no route referenced it, so a key
|
|
6
|
+
* issued with the default `["api:read"]` carried exactly the same authority as
|
|
7
|
+
* the user who created it. Scopes were also free-form: a caller could request
|
|
8
|
+
* any string at key creation, including the literal `"service_account"` that
|
|
9
|
+
* `requireScopes` treated as a wildcard.
|
|
10
|
+
*
|
|
11
|
+
* A registry fixes both halves. A scope that is not defined here cannot be
|
|
12
|
+
* granted, so there is no string an attacker can invent; and a route that needs a
|
|
13
|
+
* scope names it from this list, so enforcement cannot silently drift out of
|
|
14
|
+
* date the way an unreferenced decorator did.
|
|
15
|
+
*/
|
|
16
|
+
/** Every scope Keystone recognises. Order is the order shown to operators. */
|
|
17
|
+
export const API_KEY_SCOPES = {
|
|
18
|
+
/** Read the caller's own profile, sessions, and keys. */
|
|
19
|
+
"profile:read": "Read your own profile and identity",
|
|
20
|
+
"profile:write": "Update your own profile",
|
|
21
|
+
/** Read sessions belonging to the caller. */
|
|
22
|
+
"sessions:read": "List your own sessions",
|
|
23
|
+
"sessions:revoke": "Revoke your own sessions",
|
|
24
|
+
/** Manage the caller's own API keys. */
|
|
25
|
+
"api_keys:read": "List your own API keys",
|
|
26
|
+
"api_keys:revoke": "Revoke your own API keys",
|
|
27
|
+
/** Read organization members and applications. */
|
|
28
|
+
"organizations:read": "Read organization membership and applications",
|
|
29
|
+
/** Manage organization members, applications, and roles. */
|
|
30
|
+
"organizations:write": "Manage organization membership and applications",
|
|
31
|
+
/** Read and manage webhooks. */
|
|
32
|
+
"webhooks:manage": "Create, update, and delete webhooks",
|
|
33
|
+
/** Read and manage workflows. */
|
|
34
|
+
"workflows:read": "Read workflows and their runs",
|
|
35
|
+
"workflows:write": "Create and update workflows",
|
|
36
|
+
/** Second-factor management. Never appropriate for a machine credential. */
|
|
37
|
+
"mfa:manage": "Enroll, verify, and disable TOTP and backup codes",
|
|
38
|
+
};
|
|
39
|
+
const ALL_SCOPES = Object.keys(API_KEY_SCOPES);
|
|
40
|
+
export function isApiKeyScope(value) {
|
|
41
|
+
return Object.prototype.hasOwnProperty.call(API_KEY_SCOPES, value);
|
|
42
|
+
}
|
|
43
|
+
export function knownScopes() {
|
|
44
|
+
return [...ALL_SCOPES];
|
|
45
|
+
}
|
|
46
|
+
/** Human-readable description of a scope, for API responses and dashboards. */
|
|
47
|
+
export function describeScope(scope) {
|
|
48
|
+
return isApiKeyScope(scope) ? API_KEY_SCOPES[scope] : undefined;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Scopes granted to a service account by default.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately excludes anything interactive: a machine credential has no user
|
|
54
|
+
* present to complete a second factor, no real profile, and no sessions worth
|
|
55
|
+
* listing. A service account that needs more is granted it explicitly.
|
|
56
|
+
*/
|
|
57
|
+
export const SERVICE_ACCOUNT_DEFAULT_SCOPES = ["organizations:read"];
|
|
58
|
+
/**
|
|
59
|
+
* Scopes a new personal API key receives when the caller names none.
|
|
60
|
+
*
|
|
61
|
+
* Read-only and confined to the caller's own resources. The previous default,
|
|
62
|
+
* `["api:read"]`, was not in the registry at all — which is a fair indicator that
|
|
63
|
+
* nothing was checking it.
|
|
64
|
+
*/
|
|
65
|
+
export const PERSONAL_KEY_DEFAULT_SCOPES = ["profile:read", "sessions:read", "api_keys:read"];
|
|
66
|
+
/**
|
|
67
|
+
* Scopes that may never be granted to a machine credential.
|
|
68
|
+
*
|
|
69
|
+
* `mfa:manage` because a second factor is enrolled by a person holding an
|
|
70
|
+
* authenticator; `MfaRequiredError` is raised when a token is minted for a user
|
|
71
|
+
* with TOTP enabled and no recorded factor, and a service account has no
|
|
72
|
+
* authenticator to record.
|
|
73
|
+
*
|
|
74
|
+
* `profile:*` because a service account's "profile" is a synthesized object with
|
|
75
|
+
* an id of `sa:<uuid>` that matches no user row. Reading it returns nothing real,
|
|
76
|
+
* and writing it would update zero rows -- so the scope is meaningless for a
|
|
77
|
+
* machine and misleading in an audit log that shows what a key was used for.
|
|
78
|
+
*/
|
|
79
|
+
export const HUMAN_ONLY_SCOPES = [
|
|
80
|
+
"mfa:manage",
|
|
81
|
+
"profile:read",
|
|
82
|
+
"profile:write",
|
|
83
|
+
];
|
|
84
|
+
/**
|
|
85
|
+
* Validate a requested scope set.
|
|
86
|
+
*
|
|
87
|
+
* Unknown scopes are refused rather than dropped. Silently discarding a scope a
|
|
88
|
+
* caller asked for hides both a typo and an attempt to smuggle an unrecognised
|
|
89
|
+
* privilege past a review, and it leaves the caller believing they hold
|
|
90
|
+
* something they do not.
|
|
91
|
+
*/
|
|
92
|
+
export function validateScopes(requested, options = { principal: "user" }) {
|
|
93
|
+
const wanted = requested && requested.length > 0
|
|
94
|
+
? requested
|
|
95
|
+
: (options.fallback ??
|
|
96
|
+
(options.principal === "service_account" ? SERVICE_ACCOUNT_DEFAULT_SCOPES : PERSONAL_KEY_DEFAULT_SCOPES));
|
|
97
|
+
const unknown = wanted.filter((scope) => !isApiKeyScope(scope));
|
|
98
|
+
const known = wanted.filter(isApiKeyScope);
|
|
99
|
+
const forbidden = options.principal === "service_account"
|
|
100
|
+
? known.filter((scope) => HUMAN_ONLY_SCOPES.includes(scope))
|
|
101
|
+
: [];
|
|
102
|
+
const allowed = known.filter((scope) => !forbidden.includes(scope));
|
|
103
|
+
return {
|
|
104
|
+
ok: unknown.length === 0 && forbidden.length === 0,
|
|
105
|
+
unknown,
|
|
106
|
+
forbiddenForServiceAccount: forbidden,
|
|
107
|
+
// Registry order, so two equivalent requests produce identical rows.
|
|
108
|
+
scopes: ALL_SCOPES.filter((scope) => allowed.includes(scope)),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Narrow a key's stored scopes to what the caller is actually entitled to.
|
|
113
|
+
*
|
|
114
|
+
* A key never gains authority from its own record. The effective set is what the
|
|
115
|
+
* key holds *intersected with* what its principal could do directly, so revoking
|
|
116
|
+
* a role or removing a membership takes effect on the key too rather than
|
|
117
|
+
* leaving a stale grant behind.
|
|
118
|
+
*/
|
|
119
|
+
export function intersectScopes(granted, principalAllows) {
|
|
120
|
+
const held = new Set((granted ?? []).filter(isApiKeyScope));
|
|
121
|
+
const allowed = new Set(principalAllows.filter(isApiKeyScope));
|
|
122
|
+
return ALL_SCOPES.filter((scope) => held.has(scope) && allowed.has(scope));
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Grant check used by `requireScopes`.
|
|
126
|
+
*
|
|
127
|
+
* Explicit allow-list membership only. The previous implementation also accepted
|
|
128
|
+
* a key whose scopes merely *contained* the string `"service_account"`, which is
|
|
129
|
+
* a bypass by construction: that string was client-suppliable at key creation, so
|
|
130
|
+
* any key created with `scopes: ["service_account"]` satisfied every check.
|
|
131
|
+
*/
|
|
132
|
+
export function hasScopes(granted, required) {
|
|
133
|
+
if (required.length === 0)
|
|
134
|
+
return true;
|
|
135
|
+
const held = new Set(granted ?? []);
|
|
136
|
+
return required.every((scope) => held.has(scope));
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=scopes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scopes.js","sourceRoot":"","sources":["../../src/services/scopes.ts"],"names":[],"mappings":"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"}
|