@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/plugins/auth.js
CHANGED
|
@@ -5,6 +5,7 @@ import { db } from "../db/index.js";
|
|
|
5
5
|
import { apiKeys, users, serviceAccounts } from "../db/schema.js";
|
|
6
6
|
import { verifyAccessToken, hashApiKey } from "../services/tokens.js";
|
|
7
7
|
import { emit } from "../services/events/bus.js";
|
|
8
|
+
import { SERVICE_ACCOUNT_DEFAULT_SCOPES, hasScopes } from "../services/scopes.js";
|
|
8
9
|
const DEFAULT_ACCESS_COOKIE = config.COOKIE_NAME;
|
|
9
10
|
const DEFAULT_REFRESH_COOKIE = `${config.COOKIE_NAME}-refresh`;
|
|
10
11
|
export function accessCookieName(clientId) {
|
|
@@ -77,7 +78,7 @@ async function resolveUserFromApiKey(key, request) {
|
|
|
77
78
|
metadata: { keyId: keyRecord.id, scopes: keyRecord.scopes ?? [] },
|
|
78
79
|
},
|
|
79
80
|
});
|
|
80
|
-
return { user, scopes: keyRecord.scopes ?? [] };
|
|
81
|
+
return { user, scopes: keyRecord.scopes ?? [], keyId: keyRecord.id };
|
|
81
82
|
}
|
|
82
83
|
return undefined;
|
|
83
84
|
}
|
|
@@ -135,13 +136,18 @@ export default fp(async function authPlugin(app) {
|
|
|
135
136
|
if (apiKeyUser) {
|
|
136
137
|
request.user = apiKeyUser.user;
|
|
137
138
|
request.apiKeyScopes = apiKeyUser.scopes;
|
|
139
|
+
request.apiKeyId = apiKeyUser.keyId;
|
|
138
140
|
return;
|
|
139
141
|
}
|
|
140
142
|
const serviceAccountResult = await resolveServiceAccountFromApiKey(token);
|
|
141
143
|
if (serviceAccountResult) {
|
|
142
144
|
const { serviceAccount, keyId, scopes } = serviceAccountResult;
|
|
143
145
|
request.serviceAccount = serviceAccount;
|
|
144
|
-
|
|
146
|
+
// An explicit set from the registry, not a wildcard string. Service
|
|
147
|
+
// accounts are granted scopes explicitly and the grant is stored on the
|
|
148
|
+
// key, so revoking it is a data change rather than a code change.
|
|
149
|
+
request.apiKeyScopes = scopes.length > 0 ? scopes : SERVICE_ACCOUNT_DEFAULT_SCOPES;
|
|
150
|
+
request.apiKeyId = keyId;
|
|
145
151
|
await emit({
|
|
146
152
|
type: "api_key_used",
|
|
147
153
|
payload: {
|
|
@@ -180,12 +186,20 @@ export default fp(async function authPlugin(app) {
|
|
|
180
186
|
});
|
|
181
187
|
app.decorate("requireScopes", function requireScopes(...required) {
|
|
182
188
|
return async function scopeCheck(request, reply) {
|
|
183
|
-
//
|
|
184
|
-
|
|
189
|
+
// A session or bearer token carries the authority of a human and is
|
|
190
|
+
// governed by the permission system instead. Only machine credentials are
|
|
191
|
+
// scope-limited.
|
|
192
|
+
if (!request.apiKeyId)
|
|
185
193
|
return;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
194
|
+
// Fail closed. Previously the guard returned early whenever
|
|
195
|
+
// `apiKeyScopes` was absent, which is indistinguishable from "this is a
|
|
196
|
+
// session" — so a key that resolved without a scope list skipped the check
|
|
197
|
+
// entirely. Keying off `apiKeyId` removes the ambiguity.
|
|
198
|
+
if (!hasScopes(request.apiKeyScopes, required)) {
|
|
199
|
+
await request.audit("unauthorized_access", {
|
|
200
|
+
action: "insufficient_api_key_scope",
|
|
201
|
+
requiredScope: required.join(" "),
|
|
202
|
+
});
|
|
189
203
|
return reply.status(403).send({ error: "Insufficient API key scope", required });
|
|
190
204
|
}
|
|
191
205
|
};
|
package/dist/plugins/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/plugins/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAEhC,OAAO,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,eAAe,EAAa,MAAM,iBAAiB,CAAC;AAC7E,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAEtE,OAAO,EAAE,IAAI,EAAE,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/plugins/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
/**
|
|
3
|
+
* Runs before routing, authentication, and every route handler.
|
|
4
|
+
*
|
|
5
|
+
* Identity-bearing headers are removed from any request that did not arrive
|
|
6
|
+
* from a configured trusted proxy. Stripping — rather than merely ignoring the
|
|
7
|
+
* headers where we happen to check them — means no route, plugin, or future
|
|
8
|
+
* feature can read a spoofed identity by accident.
|
|
9
|
+
*
|
|
10
|
+
* With no trusted proxies configured, this strips unconditionally, which is
|
|
11
|
+
* the safe default.
|
|
12
|
+
*/
|
|
13
|
+
declare const _default: (app: FastifyInstance) => Promise<void>;
|
|
14
|
+
export default _default;
|
|
15
|
+
//# sourceMappingURL=headerSanitization.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headerSanitization.d.ts","sourceRoot":"","sources":["../../src/plugins/headerSanitization.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAQ/C;;;;;;;;;;GAUG;8BAC4D,eAAe"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fp from "fastify-plugin";
|
|
2
|
+
import { describeUntrustedPeer, hasTrustedProxies, peerAddress, stripUntrustedHeaders, } from "../services/trustedProxies.js";
|
|
3
|
+
/**
|
|
4
|
+
* Runs before routing, authentication, and every route handler.
|
|
5
|
+
*
|
|
6
|
+
* Identity-bearing headers are removed from any request that did not arrive
|
|
7
|
+
* from a configured trusted proxy. Stripping — rather than merely ignoring the
|
|
8
|
+
* headers where we happen to check them — means no route, plugin, or future
|
|
9
|
+
* feature can read a spoofed identity by accident.
|
|
10
|
+
*
|
|
11
|
+
* With no trusted proxies configured, this strips unconditionally, which is
|
|
12
|
+
* the safe default.
|
|
13
|
+
*/
|
|
14
|
+
export default fp(async function headerSanitizationPlugin(app) {
|
|
15
|
+
let warned = false;
|
|
16
|
+
app.addHook("onRequest", async (request) => {
|
|
17
|
+
const trusted = stripUntrustedHeaders(request);
|
|
18
|
+
if (trusted)
|
|
19
|
+
return;
|
|
20
|
+
request.log.debug({ peer: peerAddress(request) }, "stripped untrusted client-identity headers");
|
|
21
|
+
// The one stripping outcome that indicates a broken deployment rather than
|
|
22
|
+
// a hostile client: an infrastructure address arriving while no trusted
|
|
23
|
+
// proxy is configured. That state fails silently — every client shares one
|
|
24
|
+
// rate-limit budget — so say so once instead of leaving it to be guessed.
|
|
25
|
+
if (!warned) {
|
|
26
|
+
const reason = describeUntrustedPeer(peerAddress(request));
|
|
27
|
+
if (reason) {
|
|
28
|
+
warned = true;
|
|
29
|
+
request.log.warn({ peer: peerAddress(request) }, `mTLS/proxy trust boundary not configured: ${reason}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
// State the trust posture at boot. A deployment that expected a proxy to be
|
|
34
|
+
// trusted should not have to discover the mistake from a support ticket.
|
|
35
|
+
app.log.info(hasTrustedProxies()
|
|
36
|
+
? "client-identity headers will be honoured from configured trusted proxies"
|
|
37
|
+
: "no trusted proxies configured: client-identity headers will be stripped from every request");
|
|
38
|
+
});
|
|
39
|
+
//# sourceMappingURL=headerSanitization.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headerSanitization.js","sourceRoot":"","sources":["../../src/plugins/headerSanitization.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAEhC,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,WAAW,EACX,qBAAqB,GACtB,MAAM,+BAA+B,CAAC;AAEvC;;;;;;;;;;GAUG;AACH,eAAe,EAAE,CAAC,KAAK,UAAU,wBAAwB,CAAC,GAAoB;IAC5E,IAAI,MAAM,GAAG,KAAK,CAAC;IAEnB,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QACzC,MAAM,OAAO,GAAG,qBAAqB,CAAC,OAAO,CAAC,CAAC;QAC/C,IAAI,OAAO;YAAE,OAAO;QAEpB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,WAAW,CAAC,OAAO,CAAC,EAAE,EAAE,4CAA4C,CAAC,CAAC;QAEhG,2EAA2E;QAC3E,wEAAwE;QACxE,2EAA2E;QAC3E,0EAA0E;QAC1E,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,MAAM,GAAG,qBAAqB,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC;YAC3D,IAAI,MAAM,EAAE,CAAC;gBACX,MAAM,GAAG,IAAI,CAAC;gBACd,OAAO,CAAC,GAAG,CAAC,IAAI,CACd,EAAE,IAAI,EAAE,WAAW,CAAC,OAAO,CAAC,EAAE,EAC9B,6CAA6C,MAAM,EAAE,CACtD,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,4EAA4E;IAC5E,yEAAyE;IACzE,GAAG,CAAC,GAAG,CAAC,IAAI,CACV,iBAAiB,EAAE;QACjB,CAAC,CAAC,0EAA0E;QAC5E,CAAC,CAAC,4FAA4F,CACjG,CAAC;AACJ,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
declare module "fastify" {
|
|
3
|
+
interface FastifyInstance {
|
|
4
|
+
requireHumanPrincipal: () => (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Reject machine credentials on routes that only make sense for a person.
|
|
9
|
+
*
|
|
10
|
+
* A service account authenticates by presenting an API key, and the auth plugin
|
|
11
|
+
* then synthesizes a `request.user` so existing routes keep working. The
|
|
12
|
+
* synthesized principal has an id of `sa:<uuid>`, which matches no row in
|
|
13
|
+
* `users` and no row in `org_memberships`.
|
|
14
|
+
*
|
|
15
|
+
* That makes most routes fail by accident rather than by decision: a write keyed
|
|
16
|
+
* on `request.user.id` updates zero rows, and an org-permission check finds no
|
|
17
|
+
* membership and returns 403. Correct, but incidental — and it relies on every
|
|
18
|
+
* future route happening to key off the user id. A route that acted on
|
|
19
|
+
* `request.state.org`, or on the service account's own `orgId`, would not be
|
|
20
|
+
* stopped by any of that.
|
|
21
|
+
*
|
|
22
|
+
* So the boundary is stated rather than implied. On the routes this guard is
|
|
23
|
+
* applied to, a machine credential is refused with a 403 that says why, instead
|
|
24
|
+
* of being allowed through to fail somewhere less obvious.
|
|
25
|
+
*/
|
|
26
|
+
declare const _default: (app: FastifyInstance) => Promise<void>;
|
|
27
|
+
export default _default;
|
|
28
|
+
//# sourceMappingURL=machinePrincipal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"machinePrincipal.d.ts","sourceRoot":"","sources":["../../src/plugins/machinePrincipal.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAE7E,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,eAAe;QACvB,qBAAqB,EAAE,MAAM,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KAC9F;CACF;AAED;;;;;;;;;;;;;;;;;;GAkBG;8BAC4D,eAAe"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fp from "fastify-plugin";
|
|
2
|
+
/**
|
|
3
|
+
* Reject machine credentials on routes that only make sense for a person.
|
|
4
|
+
*
|
|
5
|
+
* A service account authenticates by presenting an API key, and the auth plugin
|
|
6
|
+
* then synthesizes a `request.user` so existing routes keep working. The
|
|
7
|
+
* synthesized principal has an id of `sa:<uuid>`, which matches no row in
|
|
8
|
+
* `users` and no row in `org_memberships`.
|
|
9
|
+
*
|
|
10
|
+
* That makes most routes fail by accident rather than by decision: a write keyed
|
|
11
|
+
* on `request.user.id` updates zero rows, and an org-permission check finds no
|
|
12
|
+
* membership and returns 403. Correct, but incidental — and it relies on every
|
|
13
|
+
* future route happening to key off the user id. A route that acted on
|
|
14
|
+
* `request.state.org`, or on the service account's own `orgId`, would not be
|
|
15
|
+
* stopped by any of that.
|
|
16
|
+
*
|
|
17
|
+
* So the boundary is stated rather than implied. On the routes this guard is
|
|
18
|
+
* applied to, a machine credential is refused with a 403 that says why, instead
|
|
19
|
+
* of being allowed through to fail somewhere less obvious.
|
|
20
|
+
*/
|
|
21
|
+
export default fp(async function machinePrincipalBoundary(app) {
|
|
22
|
+
/**
|
|
23
|
+
* Refuse the request if a service account authenticated it.
|
|
24
|
+
*
|
|
25
|
+
* Place after `authenticate`, since that is what populates
|
|
26
|
+
* `request.serviceAccount`.
|
|
27
|
+
*/
|
|
28
|
+
app.decorate("requireHumanPrincipal", function requireHumanPrincipal() {
|
|
29
|
+
return async function humanPrincipalCheck(request, reply) {
|
|
30
|
+
if (!request.serviceAccount)
|
|
31
|
+
return;
|
|
32
|
+
await request.audit("unauthorized_access", {
|
|
33
|
+
action: "machine_principal_on_interactive_route",
|
|
34
|
+
serviceAccountId: request.serviceAccount.id,
|
|
35
|
+
method: request.method,
|
|
36
|
+
url: request.url,
|
|
37
|
+
});
|
|
38
|
+
return reply.status(403).send({
|
|
39
|
+
error: "This endpoint is for user sessions only",
|
|
40
|
+
code: "MACHINE_PRINCIPAL_REFUSED",
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
//# sourceMappingURL=machinePrincipal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"machinePrincipal.js","sourceRoot":"","sources":["../../src/plugins/machinePrincipal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAShC;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAe,EAAE,CAAC,KAAK,UAAU,wBAAwB,CAAC,GAAoB;IAC5E;;;;;OAKG;IACH,GAAG,CAAC,QAAQ,CAAC,uBAAuB,EAAE,SAAS,qBAAqB;QAClE,OAAO,KAAK,UAAU,mBAAmB,CAAC,OAAuB,EAAE,KAAmB;YACpF,IAAI,CAAC,OAAO,CAAC,cAAc;gBAAE,OAAO;YACpC,MAAM,OAAO,CAAC,KAAK,CAAC,qBAAqB,EAAE;gBACzC,MAAM,EAAE,wCAAwC;gBAChD,gBAAgB,EAAE,OAAO,CAAC,cAAc,CAAC,EAAE;gBAC3C,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,GAAG,EAAE,OAAO,CAAC,GAAG;aACjB,CAAC,CAAC;YACH,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBAC5B,KAAK,EAAE,yCAAyC;gBAChD,IAAI,EAAE,2BAA2B;aAClC,CAAC,CAAC;QACL,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/dist/plugins/mtls.d.ts
CHANGED
|
@@ -4,11 +4,30 @@ declare module "fastify" {
|
|
|
4
4
|
requireMTLS: (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
5
5
|
}
|
|
6
6
|
}
|
|
7
|
-
export
|
|
7
|
+
export interface ClientCertEvidence {
|
|
8
8
|
fingerprint?: string;
|
|
9
9
|
subject?: string;
|
|
10
|
-
|
|
10
|
+
/** Whether the evidence came from a configured trusted proxy. */
|
|
11
|
+
trusted: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Read certificate evidence. Returns nothing when the peer is not a trusted
|
|
15
|
+
* proxy, even if the headers are present.
|
|
16
|
+
*/
|
|
17
|
+
export declare function extractClientCert(request: FastifyRequest): ClientCertEvidence;
|
|
11
18
|
export declare function requireMTLS(): (request: FastifyRequest, reply: FastifyReply) => Promise<undefined>;
|
|
12
19
|
declare const _default: (app: FastifyInstance) => Promise<void>;
|
|
13
20
|
export default _default;
|
|
21
|
+
/** Exported for tests: find any service account bound to a fingerprint. */
|
|
22
|
+
export declare function findServiceAccountByFingerprint(fingerprint: string): Promise<{
|
|
23
|
+
certFingerprint: string | null;
|
|
24
|
+
createdAt: Date;
|
|
25
|
+
description: string | null;
|
|
26
|
+
id: string;
|
|
27
|
+
isActive: boolean;
|
|
28
|
+
name: string;
|
|
29
|
+
orgId: string;
|
|
30
|
+
revokedAt: Date | null;
|
|
31
|
+
updatedAt: Date;
|
|
32
|
+
} | undefined>;
|
|
14
33
|
//# sourceMappingURL=mtls.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mtls.d.ts","sourceRoot":"","sources":["../../src/plugins/mtls.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"mtls.d.ts","sourceRoot":"","sources":["../../src/plugins/mtls.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AA2C7E,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,eAAe;QACvB,WAAW,EAAE,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KAC9E;CACF;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,cAAc,GAAG,kBAAkB,CAiB7E;AAyCD,wBAAgB,WAAW,cACO,cAAc,SAAS,YAAY,wBAuCpE;8BAEgD,eAAe;;AAIhE,2EAA2E;AAC3E,wBAAsB,+BAA+B,CAAC,WAAW,EAAE,MAAM;;;;;;;;;;eAExE"}
|
package/dist/plugins/mtls.js
CHANGED
|
@@ -1,70 +1,112 @@
|
|
|
1
1
|
import fp from "fastify-plugin";
|
|
2
|
-
import { eq } from "drizzle-orm";
|
|
2
|
+
import { and, eq, isNull, or } from "drizzle-orm";
|
|
3
3
|
import { db } from "../db/index.js";
|
|
4
4
|
import { serviceAccounts } from "../db/schema.js";
|
|
5
|
+
import { canonicalFingerprint, isFromTrustedProxy, isValidFingerprint, peerAddress, } from "../services/trustedProxies.js";
|
|
5
6
|
/**
|
|
6
|
-
* mTLS authentication
|
|
7
|
+
* mTLS service-account authentication.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* certificates to service accounts.
|
|
9
|
+
* TLS is terminated at a reverse proxy that validates the client certificate.
|
|
10
|
+
* Keystone learns the result from headers the proxy sets — but only when the
|
|
11
|
+
* request actually came from that proxy.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* Rules this plugin enforces:
|
|
14
|
+
*
|
|
15
|
+
* 1. **A header never establishes identity by itself.** Identity comes from a
|
|
16
|
+
* client certificate fingerprint that a trusted proxy forwarded, and that
|
|
17
|
+
* fingerprint is cryptographically bound to the service account in the
|
|
18
|
+
* database. The previous `x-service-account-id` shortcut is gone: it let any
|
|
19
|
+
* client name a service account and become it, with no certificate at all.
|
|
20
|
+
* 2. **A request that did not come from a trusted proxy has no certificate
|
|
21
|
+
* identity.** `x-service-account-id` is accepted only as a *hint*, and only
|
|
22
|
+
* when the request also presents a valid fingerprint; the hint is then
|
|
23
|
+
* cross-checked against that fingerprint's binding.
|
|
24
|
+
* 3. **A fingerprint must be a well-formed SHA-256 digest**, so a garbage or
|
|
25
|
+
* oversized header cannot be used as a lookup key.
|
|
26
|
+
*
|
|
27
|
+
* Service accounts without a bound fingerprint cannot authenticate by
|
|
28
|
+
* certificate at all.
|
|
17
29
|
*/
|
|
18
30
|
const MTLS_HEADER = "x-forwarded-client-cert";
|
|
19
31
|
const MTLS_FINGERPRINT_HEADER = "x-client-cert-fingerprint";
|
|
20
32
|
const SERVICE_ACCOUNT_ID_HEADER = "x-service-account-id";
|
|
33
|
+
/** Cap on header length before parsing, so a hostile proxy cannot inflate work. */
|
|
34
|
+
const MAX_CERT_HEADER_LENGTH = 8192;
|
|
35
|
+
/**
|
|
36
|
+
* Read certificate evidence. Returns nothing when the peer is not a trusted
|
|
37
|
+
* proxy, even if the headers are present.
|
|
38
|
+
*/
|
|
21
39
|
export function extractClientCert(request) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
if (!isFromTrustedProxy(request))
|
|
41
|
+
return { trusted: false };
|
|
42
|
+
const rawFingerprint = request.headers[MTLS_FINGERPRINT_HEADER];
|
|
43
|
+
const rawCert = request.headers[MTLS_HEADER];
|
|
44
|
+
const fingerprint = typeof rawFingerprint === "string" && isValidFingerprint(rawFingerprint.trim())
|
|
45
|
+
? canonicalFingerprint(rawFingerprint)
|
|
46
|
+
: undefined;
|
|
47
|
+
const subject = typeof rawCert === "string" && rawCert.length > 0 && rawCert.length <= MAX_CERT_HEADER_LENGTH
|
|
48
|
+
? rawCert
|
|
49
|
+
: undefined;
|
|
50
|
+
return { fingerprint, subject, trusted: true };
|
|
28
51
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (sa?.isActive) {
|
|
38
|
-
return sa;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
const { fingerprint, subject } = extractClientCert(request);
|
|
42
|
-
if (!fingerprint && !subject)
|
|
43
|
-
return undefined;
|
|
44
|
-
// Look up service accounts by name matching the fingerprint or subject.
|
|
45
|
-
// In a production system, you would store the certificate fingerprint in
|
|
46
|
-
// the service_accounts table and query by it directly.
|
|
47
|
-
const [sa] = await db
|
|
52
|
+
/**
|
|
53
|
+
* Resolve a service account from a validated certificate fingerprint.
|
|
54
|
+
*
|
|
55
|
+
* The lookup is by `certFingerprint`, not by `name`: a name is operator-chosen
|
|
56
|
+
* and guessable, whereas the fingerprint is pinned to a certificate.
|
|
57
|
+
*/
|
|
58
|
+
async function resolveByFingerprint(fingerprint) {
|
|
59
|
+
const [record] = await db
|
|
48
60
|
.select()
|
|
49
61
|
.from(serviceAccounts)
|
|
50
|
-
.where(eq(serviceAccounts.
|
|
62
|
+
.where(and(eq(serviceAccounts.certFingerprint, fingerprint), eq(serviceAccounts.isActive, true), isNull(serviceAccounts.revokedAt)))
|
|
51
63
|
.limit(1);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
return record;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Optional narrowing: when the proxy also named the service account, the named
|
|
68
|
+
* account must be the one bound to the presented certificate. A mismatch is a
|
|
69
|
+
* spoofing signal, not a fallback.
|
|
70
|
+
*/
|
|
71
|
+
async function crossCheckHint(hintId, fingerprint) {
|
|
72
|
+
const [hinted] = await db
|
|
73
|
+
.select()
|
|
74
|
+
.from(serviceAccounts)
|
|
75
|
+
.where(and(eq(serviceAccounts.id, hintId), isNull(serviceAccounts.revokedAt)))
|
|
76
|
+
.limit(1);
|
|
77
|
+
if (!hinted)
|
|
78
|
+
return undefined;
|
|
79
|
+
if (hinted.certFingerprint && hinted.certFingerprint !== fingerprint)
|
|
80
|
+
return undefined;
|
|
81
|
+
return resolveByFingerprint(fingerprint);
|
|
56
82
|
}
|
|
57
83
|
export function requireMTLS() {
|
|
58
84
|
return async function (request, reply) {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
85
|
+
const evidence = extractClientCert(request);
|
|
86
|
+
if (!evidence.trusted) {
|
|
87
|
+
// Distinguish "not behind a proxy we trust" from "no certificate" so an
|
|
88
|
+
// operator can tell a misconfiguration from an attack.
|
|
89
|
+
return reply.status(401).send({
|
|
90
|
+
error: "Client certificate required",
|
|
91
|
+
code: isFromTrustedProxy(request) ? "MTLS_CERTIFICATE_MISSING" : "MTLS_UNTRUSTED_PEER",
|
|
92
|
+
});
|
|
63
93
|
}
|
|
64
|
-
|
|
94
|
+
if (!evidence.fingerprint) {
|
|
95
|
+
return reply.status(401).send({
|
|
96
|
+
error: "A valid client certificate fingerprint is required",
|
|
97
|
+
code: "MTLS_FINGERPRINT_MISSING",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const hint = request.headers[SERVICE_ACCOUNT_ID_HEADER];
|
|
101
|
+
const serviceAccount = typeof hint === "string" && hint
|
|
102
|
+
? await crossCheckHint(hint, evidence.fingerprint)
|
|
103
|
+
: await resolveByFingerprint(evidence.fingerprint);
|
|
65
104
|
if (!serviceAccount) {
|
|
66
|
-
request.log.warn({ fingerprint,
|
|
67
|
-
return reply.status(403).send({
|
|
105
|
+
request.log.warn({ fingerprint: evidence.fingerprint, peer: peerAddress(request) }, "mTLS: no active service account is bound to the presented certificate");
|
|
106
|
+
return reply.status(403).send({
|
|
107
|
+
error: "Client certificate not mapped to a service account",
|
|
108
|
+
code: "MTLS_CERTIFICATE_UNMAPPED",
|
|
109
|
+
});
|
|
68
110
|
}
|
|
69
111
|
request.serviceAccount = serviceAccount;
|
|
70
112
|
request.log.debug({ serviceAccountId: serviceAccount.id }, "mTLS: service account authenticated");
|
|
@@ -73,4 +115,9 @@ export function requireMTLS() {
|
|
|
73
115
|
export default fp(async function mtlsPlugin(app) {
|
|
74
116
|
app.decorate("requireMTLS", requireMTLS());
|
|
75
117
|
});
|
|
118
|
+
/** Exported for tests: find any service account bound to a fingerprint. */
|
|
119
|
+
export async function findServiceAccountByFingerprint(fingerprint) {
|
|
120
|
+
return resolveByFingerprint(canonicalFingerprint(fingerprint));
|
|
121
|
+
}
|
|
122
|
+
void or;
|
|
76
123
|
//# sourceMappingURL=mtls.js.map
|
package/dist/plugins/mtls.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mtls.js","sourceRoot":"","sources":["../../src/plugins/mtls.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAEhC,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"mtls.js","sourceRoot":"","sources":["../../src/plugins/mtls.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
|
2
|
+
import { clientAddress } from "../services/trustedProxies.js";
|
|
2
3
|
interface RateLimitPluginOptions {
|
|
3
4
|
keyPrefix: string;
|
|
4
5
|
maxAttempts: number;
|
|
@@ -17,11 +18,11 @@ export interface GlobalRateLimitOptions {
|
|
|
17
18
|
}
|
|
18
19
|
declare function isAllowed(key: string, maxAttempts: number, windowSeconds: number): Promise<boolean>;
|
|
19
20
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* Re-exported so a route that must run before the normal limiter (because it
|
|
22
|
+
* emits audit events on failure) can still budget itself rather than being
|
|
23
|
+
* unbounded.
|
|
23
24
|
*/
|
|
24
|
-
export
|
|
25
|
+
export { clientAddress };
|
|
25
26
|
/** Raw sliding-window check, for the same pre-limiter case. */
|
|
26
27
|
export { isAllowed };
|
|
27
28
|
export declare function rateLimit(options: RateLimitPluginOptions): (request: FastifyRequest, reply: FastifyReply) => Promise<undefined>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rateLimit.d.ts","sourceRoot":"","sources":["../../src/plugins/rateLimit.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"rateLimit.d.ts","sourceRoot":"","sources":["../../src/plugins/rateLimit.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE7E,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAE9D,UAAU,sBAAsB;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,MAAM,CAAC;CAC/C;AAED,MAAM,WAAW,sBAAsB;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA4BD,iBAAe,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAyBlG;AAeD;;;;GAIG;AACH,OAAO,EAAE,aAAa,EAAE,CAAC;AAEzB,+DAA+D;AAC/D,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,wBAAgB,SAAS,CAAC,OAAO,EAAE,sBAAsB,aACb,cAAc,SAAS,YAAY,wBAW9E;AAED,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,aAKzB,cAAc,SAAS,YAAY,wBAW7E;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,aACxC,cAAc,SAAS,YAAY,wBAY9E;8BAEqD,eAAe"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fp from "fastify-plugin";
|
|
3
3
|
import { redis, isRedisReady } from "../services/redis.js";
|
|
4
|
+
import { clientAddress } from "../services/trustedProxies.js";
|
|
4
5
|
/**
|
|
5
6
|
* Atomic sliding-window rate limit script.
|
|
6
7
|
*
|
|
@@ -46,18 +47,20 @@ async function isAllowed(key, maxAttempts, windowSeconds) {
|
|
|
46
47
|
function cryptoRandom() {
|
|
47
48
|
return crypto.randomBytes(8).toString("hex");
|
|
48
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Rate-limit key source. Uses the peer address unless the request came from a
|
|
52
|
+
* configured trusted proxy, so a client cannot rotate `x-forwarded-for` to get a
|
|
53
|
+
* fresh budget.
|
|
54
|
+
*/
|
|
49
55
|
function clientIdentifier(request) {
|
|
50
|
-
return (request
|
|
51
|
-
request.ip);
|
|
56
|
+
return clientAddress(request);
|
|
52
57
|
}
|
|
53
58
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
59
|
+
* Re-exported so a route that must run before the normal limiter (because it
|
|
60
|
+
* emits audit events on failure) can still budget itself rather than being
|
|
61
|
+
* unbounded.
|
|
57
62
|
*/
|
|
58
|
-
export
|
|
59
|
-
return clientIdentifier(request);
|
|
60
|
-
}
|
|
63
|
+
export { clientAddress };
|
|
61
64
|
/** Raw sliding-window check, for the same pre-limiter case. */
|
|
62
65
|
export { isAllowed };
|
|
63
66
|
export function rateLimit(options) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rateLimit.js","sourceRoot":"","sources":["../../src/plugins/rateLimit.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAEhC,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"rateLimit.js","sourceRoot":"","sources":["../../src/plugins/rateLimit.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,7 +3,7 @@ import type { ApplicationRepository, CreateApplicationInput, UpdateApplicationIn
|
|
|
3
3
|
export declare class DrizzleApplicationRepository implements ApplicationRepository {
|
|
4
4
|
private generateClientId;
|
|
5
5
|
create(input: CreateApplicationInput): Promise<Application & {
|
|
6
|
-
clientSecret: string;
|
|
6
|
+
clientSecret: string | null;
|
|
7
7
|
}>;
|
|
8
8
|
findByClientId(clientId: string): Promise<Application | undefined>;
|
|
9
9
|
listByOrgId(orgId: string): Promise<Application[]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"application.d.ts","sourceRoot":"","sources":["../../src/repositories/application.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgB,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEjE,OAAO,KAAK,EACV,qBAAqB,EACrB,sBAAsB,EACtB,sBAAsB,EACvB,MAAM,YAAY,CAAC;AAEpB,qBAAa,4BAA6B,YAAW,qBAAqB;IACxE,OAAO,CAAC,gBAAgB;IAIlB,MAAM,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,WAAW,GAAG;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"application.d.ts","sourceRoot":"","sources":["../../src/repositories/application.ts"],"names":[],"mappings":"AAGA,OAAO,EAAgB,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEjE,OAAO,KAAK,EACV,qBAAqB,EACrB,sBAAsB,EACtB,sBAAsB,EACvB,MAAM,YAAY,CAAC;AAEpB,qBAAa,4BAA6B,YAAW,qBAAqB;IACxE,OAAO,CAAC,gBAAgB;IAIlB,MAAM,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,WAAW,GAAG;QAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAoBlG;IAEK,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAOvE;IAEK,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAEvD;IAEK,MAAM,CACV,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,sBAAsB,GAC5B,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAOlC;IAEK,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAU3F;CACF"}
|
|
@@ -8,13 +8,19 @@ export class DrizzleApplicationRepository {
|
|
|
8
8
|
return `app_${crypto.randomBytes(16).toString("base64url")}`;
|
|
9
9
|
}
|
|
10
10
|
async create(input) {
|
|
11
|
-
|
|
11
|
+
// A public client is issued no secret at all, rather than a secret it is
|
|
12
|
+
// expected to ignore. Storing one would leave a credential in the database
|
|
13
|
+
// that appears to protect the client and does not.
|
|
14
|
+
const clientType = input.clientType ?? "confidential";
|
|
15
|
+
const clientSecret = clientType === "public" ? null : input.clientSecret || generateClientSecret();
|
|
12
16
|
const [app] = await db
|
|
13
17
|
.insert(applications)
|
|
14
18
|
.values({
|
|
15
19
|
orgId: input.orgId,
|
|
16
20
|
clientId: input.clientId || this.generateClientId(),
|
|
17
|
-
clientSecretHash: hashClientSecret(clientSecret),
|
|
21
|
+
clientSecretHash: clientSecret ? hashClientSecret(clientSecret) : null,
|
|
22
|
+
clientType,
|
|
23
|
+
allowedScopes: input.allowedScopes ?? [],
|
|
18
24
|
name: input.name,
|
|
19
25
|
redirectUris: input.redirectUris ?? [],
|
|
20
26
|
allowedOrigins: input.allowedOrigins ?? [],
|