@frontmcp/auth 1.10.16 → 1.11.0-rc.2
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/browser/authorities/authorities.context.mjs +115 -0
- package/browser/authorities/authorities.engine.mjs +272 -0
- package/browser/authorities/authorities.errors.mjs +59 -0
- package/browser/authorities/authorities.evaluator.mjs +225 -0
- package/browser/authorities/authorities.metadata-augment.mjs +18 -0
- package/browser/authorities/authorities.registry.mjs +112 -0
- package/browser/authorities/authorities.schema.mjs +92 -0
- package/browser/authorities/authorities.scope-mapping.mjs +30 -0
- package/browser/authorities/authorities.validation.mjs +208 -0
- package/browser/authorities/index.mjs +43 -0
- package/browser/authorization/authorization.class.mjs +252 -0
- package/browser/authorization/authorization.types.mjs +64 -0
- package/browser/authorization/dcr-client.registry.mjs +222 -0
- package/browser/authorization/index.mjs +41 -0
- package/browser/authorization/orchestrated.accessor.mjs +86 -0
- package/browser/authorization/orchestrated.authorization.mjs +289 -0
- package/browser/authorization/public.authorization.mjs +124 -0
- package/browser/authorization/transparent.authorization.mjs +130 -0
- package/browser/cimd/cimd-redis.cache.mjs +231 -0
- package/browser/cimd/cimd.cache.mjs +215 -0
- package/browser/cimd/cimd.errors.mjs +142 -0
- package/browser/cimd/cimd.logger.mjs +15 -0
- package/browser/cimd/cimd.service.mjs +376 -0
- package/browser/cimd/cimd.types.mjs +228 -0
- package/browser/cimd/cimd.validator.mjs +217 -0
- package/browser/cimd/index.mjs +63 -0
- package/browser/common/auth-logger.interface.mjs +14 -0
- package/browser/common/index.mjs +6 -0
- package/browser/common/jwt.types.mjs +38 -0
- package/browser/common/session-user.types.mjs +0 -0
- package/browser/common/session.types.mjs +58 -0
- package/browser/common/zod-utils.mjs +0 -0
- package/browser/consent/consent.types.mjs +94 -0
- package/browser/consent/index.mjs +17 -0
- package/browser/context/frontmcp-auth-context.factory.mjs +29 -0
- package/browser/context/frontmcp-auth-context.impl.mjs +112 -0
- package/browser/context/index.mjs +7 -0
- package/browser/detection/auth-provider-detection.mjs +176 -0
- package/browser/detection/index.mjs +19 -0
- package/browser/errors/auth-internal.error.mjs +42 -0
- package/browser/errors/auth-internal.errors.mjs +150 -0
- package/browser/errors/index.mjs +53 -0
- package/browser/fetch/fetch-credential-middleware.mjs +91 -0
- package/browser/fetch/index.mjs +19 -0
- package/browser/index.mjs +573 -0
- package/browser/jwks/index.mjs +9 -0
- package/browser/jwks/jose.loader.mjs +28 -0
- package/browser/jwks/jwks.service.mjs +521 -0
- package/browser/jwks/jwks.utils.mjs +33 -0
- package/browser/options/app-auth.schema.mjs +18 -0
- package/browser/options/index.mjs +89 -0
- package/browser/options/orchestrated.schema.mjs +128 -0
- package/browser/options/public.schema.mjs +39 -0
- package/browser/options/schema.mjs +16 -0
- package/browser/options/shared.schemas.mjs +506 -0
- package/browser/options/static.schema.mjs +41 -0
- package/browser/options/transparent.schema.mjs +43 -0
- package/browser/options/utils.mjs +47 -0
- package/browser/people/index.mjs +15 -0
- package/browser/people/loopback-token.auth.mjs +115 -0
- package/browser/people/people-auth.options.mjs +37 -0
- package/browser/session/authorization-vault.mjs +279 -0
- package/browser/session/authorization.store.mjs +336 -0
- package/browser/session/consent.store.mjs +34 -0
- package/browser/session/credential-resume-link.mjs +43 -0
- package/browser/session/credentials-accessor.impl.mjs +54 -0
- package/browser/session/credentials-accessor.mjs +5 -0
- package/browser/session/encrypted-authorization-vault.mjs +405 -0
- package/browser/session/federated-auth.session.mjs +176 -0
- package/browser/session/incremental-auth-ticket.mjs +42 -0
- package/browser/session/index.mjs +266 -0
- package/browser/session/orchestrated-token.crypto.mjs +47 -0
- package/browser/session/orchestrated-token.store.mjs +286 -0
- package/browser/session/pending-login.mjs +37 -0
- package/browser/session/redis-session.store.mjs +255 -0
- package/browser/session/secure-store-accessor.impl.mjs +78 -0
- package/browser/session/secure-store-accessor.mjs +5 -0
- package/browser/session/secure-store-backends.mjs +111 -0
- package/browser/session/secure-store.factory.mjs +77 -0
- package/browser/session/session-credential-vault.factory.mjs +18 -0
- package/browser/session/session-credential-vault.mjs +243 -0
- package/browser/session/session-crypto.mjs +39 -0
- package/browser/session/session-rate-limiter.mjs +126 -0
- package/browser/session/session.transport.mjs +16 -0
- package/browser/session/signin-binding.mjs +30 -0
- package/browser/session/storage/in-memory-authorization-vault.mjs +28 -0
- package/browser/session/storage/index.mjs +14 -0
- package/browser/session/storage/storage-authorization-vault.mjs +301 -0
- package/browser/session/storage/storage-token-store.mjs +83 -0
- package/browser/session/storage-authorization.store.mjs +134 -0
- package/browser/session/storage-consent.store.mjs +33 -0
- package/browser/session/storage-federated-auth.session.mjs +55 -0
- package/browser/session/storage-orchestrated-token.store.mjs +197 -0
- package/browser/session/token-storage.factory.mjs +58 -0
- package/browser/session/token.refresh.mjs +36 -0
- package/browser/session/token.vault.mjs +61 -0
- package/browser/session/transport-session.types.mjs +108 -0
- package/browser/session/utils/auth-token.utils.mjs +58 -0
- package/browser/session/utils/index.mjs +23 -0
- package/browser/session/utils/session-crypto.utils.mjs +49 -0
- package/browser/session/utils/tiny-ttl-cache.mjs +46 -0
- package/browser/session/vault-encryption.mjs +187 -0
- package/browser/session/vercel-kv-session.store.mjs +230 -0
- package/browser/shared-server/index.mjs +17 -0
- package/browser/shared-server/shared-server-identity.mjs +55 -0
- package/browser/ui/base-layout.mjs +184 -0
- package/browser/ui/index.mjs +51 -0
- package/browser/ui/local-login.helper.mjs +46 -0
- package/browser/ui/templates.mjs +482 -0
- package/browser/utils/audience.validator.mjs +110 -0
- package/browser/utils/authorization-id.utils.mjs +10 -0
- package/browser/utils/index.mjs +45 -0
- package/browser/utils/redirect-uri.utils.mjs +22 -0
- package/browser/utils/scope-grant.utils.mjs +36 -0
- package/browser/utils/token-scopes.utils.mjs +12 -0
- package/browser/utils/www-authenticate.utils.mjs +164 -0
- package/browser/vault/auth-providers.accessor.impl.mjs +236 -0
- package/browser/vault/auth-providers.accessor.mjs +5 -0
- package/browser/vault/auth-providers.registry.mjs +140 -0
- package/browser/vault/auth-providers.types.mjs +47 -0
- package/browser/vault/auth-providers.vault.mjs +206 -0
- package/browser/vault/credential-cache.mjs +187 -0
- package/browser/vault/credential-helpers.mjs +15 -0
- package/browser/vault/credential-loaders/eager-loader.mjs +91 -0
- package/browser/vault/credential-loaders/lazy-loader.mjs +120 -0
- package/browser/vault/index.mjs +35 -0
- package/cimd/cimd.types.d.ts +1 -1
- package/cimd/cimd.types.d.ts.map +1 -1
- package/common/jwt.types.d.ts +1 -1
- package/common/jwt.types.d.ts.map +1 -1
- package/common/session.types.d.ts +9 -1
- package/common/session.types.d.ts.map +1 -1
- package/consent/consent.types.d.ts +1 -1
- package/consent/consent.types.d.ts.map +1 -1
- package/detection/auth-provider-detection.d.ts +1 -1
- package/detection/auth-provider-detection.d.ts.map +1 -1
- package/errors/auth-internal.errors.d.ts +6 -0
- package/errors/auth-internal.errors.d.ts.map +1 -1
- package/errors/index.d.ts +1 -1
- package/errors/index.d.ts.map +1 -1
- package/esm/index.mjs +2 -2
- package/esm/package.json +21 -5
- package/index.d.ts +5 -1
- package/index.d.ts.map +1 -1
- package/index.js +2 -2
- package/options/app-auth.schema.d.ts +19 -1
- package/options/app-auth.schema.d.ts.map +1 -1
- package/options/orchestrated.schema.d.ts +37 -1
- package/options/orchestrated.schema.d.ts.map +1 -1
- package/options/public.schema.d.ts +1 -1
- package/options/public.schema.d.ts.map +1 -1
- package/options/schema.d.ts +19 -1
- package/options/schema.d.ts.map +1 -1
- package/options/shared.schemas.d.ts +10 -14
- package/options/shared.schemas.d.ts.map +1 -1
- package/options/static.schema.d.ts +1 -1
- package/options/static.schema.d.ts.map +1 -1
- package/options/transparent.schema.d.ts +1 -1
- package/options/transparent.schema.d.ts.map +1 -1
- package/package.json +21 -5
- package/people/index.d.ts +3 -0
- package/people/index.d.ts.map +1 -0
- package/people/loopback-token.auth.d.ts +75 -0
- package/people/loopback-token.auth.d.ts.map +1 -0
- package/people/people-auth.options.d.ts +36 -0
- package/people/people-auth.options.d.ts.map +1 -0
- package/session/authorization-vault.d.ts +1 -1
- package/session/authorization-vault.d.ts.map +1 -1
- package/session/encrypted-authorization-vault.d.ts +1 -1
- package/session/encrypted-authorization-vault.d.ts.map +1 -1
- package/session/secure-store.factory.d.ts.map +1 -1
- package/shared-server/index.d.ts +3 -0
- package/shared-server/index.d.ts.map +1 -0
- package/shared-server/shared-server-identity.d.ts +26 -0
- package/shared-server/shared-server-identity.d.ts.map +1 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// libs/auth/src/session/redis-session.store.ts
|
|
2
|
+
import { randomUUID, RedisStorageAdapter } from "@frontmcp/utils";
|
|
3
|
+
import {
|
|
4
|
+
storedSessionSchema
|
|
5
|
+
} from "./transport-session.types.mjs";
|
|
6
|
+
import { signSession, verifyOrParseSession } from "./session-crypto.mjs";
|
|
7
|
+
import { SessionRateLimiter } from "./session-rate-limiter.mjs";
|
|
8
|
+
import { SessionIdEmptyError } from "../errors/auth-internal.errors.mjs";
|
|
9
|
+
var RedisSessionStore = class {
|
|
10
|
+
storage;
|
|
11
|
+
keyPrefix;
|
|
12
|
+
defaultTtlMs;
|
|
13
|
+
logger;
|
|
14
|
+
externalInstance = false;
|
|
15
|
+
// Security features
|
|
16
|
+
security;
|
|
17
|
+
rateLimiter;
|
|
18
|
+
constructor(config, logger) {
|
|
19
|
+
this.defaultTtlMs = ("defaultTtlMs" in config ? config.defaultTtlMs : void 0) ?? 36e5;
|
|
20
|
+
this.logger = logger;
|
|
21
|
+
this.keyPrefix = ("keyPrefix" in config ? config.keyPrefix : void 0) ?? "mcp:session:";
|
|
22
|
+
this.security = ("security" in config ? config.security : void 0) ?? {};
|
|
23
|
+
if (this.security.enableRateLimiting) {
|
|
24
|
+
this.rateLimiter = new SessionRateLimiter({
|
|
25
|
+
windowMs: this.security.rateLimiting?.windowMs,
|
|
26
|
+
maxRequests: this.security.rateLimiting?.maxRequests
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if ("redis" in config && config.redis) {
|
|
30
|
+
this.storage = new RedisStorageAdapter({
|
|
31
|
+
client: config.redis,
|
|
32
|
+
keyPrefix: this.keyPrefix
|
|
33
|
+
});
|
|
34
|
+
this.externalInstance = true;
|
|
35
|
+
} else {
|
|
36
|
+
const redisConfig = config;
|
|
37
|
+
this.storage = new RedisStorageAdapter({
|
|
38
|
+
config: {
|
|
39
|
+
host: redisConfig.host,
|
|
40
|
+
port: redisConfig.port ?? 6379,
|
|
41
|
+
password: redisConfig.password,
|
|
42
|
+
db: redisConfig.db ?? 0,
|
|
43
|
+
tls: redisConfig.tls
|
|
44
|
+
},
|
|
45
|
+
keyPrefix: this.keyPrefix
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get the full key for a session ID (without prefix, adapter handles it)
|
|
51
|
+
* @throws Error if sessionId is empty
|
|
52
|
+
*/
|
|
53
|
+
validateSessionId(sessionId) {
|
|
54
|
+
if (!sessionId || sessionId.trim() === "") {
|
|
55
|
+
throw new SessionIdEmptyError("RedisSessionStore");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Ensure the storage adapter is connected
|
|
60
|
+
*/
|
|
61
|
+
async ensureConnected() {
|
|
62
|
+
await this.storage.connect();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get a stored session by ID
|
|
66
|
+
*
|
|
67
|
+
* Note: Uses atomic GETEX to extend TTL while reading, preventing race conditions
|
|
68
|
+
* where concurrent readers might resurrect expired sessions.
|
|
69
|
+
*
|
|
70
|
+
* @param sessionId - The session ID to look up
|
|
71
|
+
* @param options - Optional parameters for rate limiting
|
|
72
|
+
* @param options.clientIdentifier - Client identifier (e.g., IP address) for rate limiting.
|
|
73
|
+
* When provided, rate limiting is applied per-client to prevent session enumeration.
|
|
74
|
+
* If not provided, falls back to sessionId which provides DoS protection per-session.
|
|
75
|
+
*/
|
|
76
|
+
async get(sessionId, options) {
|
|
77
|
+
this.validateSessionId(sessionId);
|
|
78
|
+
if (this.rateLimiter) {
|
|
79
|
+
const rateLimitKey = options?.clientIdentifier || sessionId;
|
|
80
|
+
const rateLimitResult = this.rateLimiter.check(rateLimitKey);
|
|
81
|
+
if (!rateLimitResult.allowed) {
|
|
82
|
+
this.logger?.warn("[RedisSessionStore] Rate limit exceeded for session lookup", {
|
|
83
|
+
sessionId: sessionId.slice(0, 20),
|
|
84
|
+
clientIdentifier: options?.clientIdentifier ? options.clientIdentifier.slice(0, 20) : void 0,
|
|
85
|
+
retryAfterMs: rateLimitResult.retryAfterMs
|
|
86
|
+
});
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
await this.ensureConnected();
|
|
91
|
+
let raw;
|
|
92
|
+
const redis = this.storage.getClient();
|
|
93
|
+
const ttlSeconds = Math.ceil(this.defaultTtlMs / 1e3);
|
|
94
|
+
if (redis) {
|
|
95
|
+
try {
|
|
96
|
+
raw = await redis.getex(this.keyPrefix + sessionId, "EX", ttlSeconds);
|
|
97
|
+
} catch {
|
|
98
|
+
raw = await this.storage.get(sessionId);
|
|
99
|
+
if (raw) {
|
|
100
|
+
await this.storage.expire(sessionId, ttlSeconds);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
raw = await this.storage.get(sessionId);
|
|
105
|
+
if (raw) {
|
|
106
|
+
await this.storage.expire(sessionId, ttlSeconds);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!raw) return null;
|
|
110
|
+
try {
|
|
111
|
+
let parsed;
|
|
112
|
+
if (this.security.enableSigning) {
|
|
113
|
+
parsed = verifyOrParseSession(raw, { secret: this.security.signingSecret });
|
|
114
|
+
if (!parsed) {
|
|
115
|
+
this.logger?.warn("[RedisSessionStore] Session signature verification failed", {
|
|
116
|
+
sessionId: sessionId.slice(0, 20)
|
|
117
|
+
});
|
|
118
|
+
this.delete(sessionId).catch(() => void 0);
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
parsed = JSON.parse(raw);
|
|
123
|
+
}
|
|
124
|
+
const result = storedSessionSchema.safeParse(parsed);
|
|
125
|
+
if (!result.success) {
|
|
126
|
+
this.logger?.warn("[RedisSessionStore] Invalid session format", {
|
|
127
|
+
sessionId: sessionId.slice(0, 20),
|
|
128
|
+
errors: result.error.issues.slice(0, 3).map((i) => ({ path: i.path, message: i.message }))
|
|
129
|
+
});
|
|
130
|
+
this.delete(sessionId).catch(() => void 0);
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const session = result.data;
|
|
134
|
+
if (session.maxLifetimeAt && session.maxLifetimeAt < Date.now()) {
|
|
135
|
+
this.logger?.info("[RedisSessionStore] Session exceeded max lifetime", {
|
|
136
|
+
sessionId: sessionId.slice(0, 20),
|
|
137
|
+
maxLifetimeAt: session.maxLifetimeAt
|
|
138
|
+
});
|
|
139
|
+
await this.delete(sessionId);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
if (session.session.expiresAt && session.session.expiresAt < Date.now()) {
|
|
143
|
+
await this.delete(sessionId);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
if (session.session.expiresAt) {
|
|
147
|
+
const ttlMs = Math.min(this.defaultTtlMs, session.session.expiresAt - Date.now());
|
|
148
|
+
if (ttlMs > 0 && ttlMs < this.defaultTtlMs) {
|
|
149
|
+
const boundedTtlSeconds = Math.ceil(ttlMs / 1e3);
|
|
150
|
+
this.storage.expire(sessionId, boundedTtlSeconds).catch((err) => {
|
|
151
|
+
this.logger?.warn("[RedisSessionStore] TTL extension failed", {
|
|
152
|
+
sessionId: sessionId.slice(0, 20),
|
|
153
|
+
error: err.message
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const updatedSession = {
|
|
159
|
+
...session,
|
|
160
|
+
lastAccessedAt: Date.now()
|
|
161
|
+
};
|
|
162
|
+
return updatedSession;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
this.logger?.warn("[RedisSessionStore] Failed to parse session", {
|
|
165
|
+
sessionId: sessionId.slice(0, 20),
|
|
166
|
+
error: error.message
|
|
167
|
+
});
|
|
168
|
+
this.delete(sessionId).catch(() => void 0);
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Store a session with optional TTL
|
|
174
|
+
*/
|
|
175
|
+
async set(sessionId, session, ttlMs) {
|
|
176
|
+
this.validateSessionId(sessionId);
|
|
177
|
+
await this.ensureConnected();
|
|
178
|
+
let value;
|
|
179
|
+
if (this.security.enableSigning) {
|
|
180
|
+
value = signSession(session, { secret: this.security.signingSecret });
|
|
181
|
+
} else {
|
|
182
|
+
value = JSON.stringify(session);
|
|
183
|
+
}
|
|
184
|
+
if (ttlMs && ttlMs > 0) {
|
|
185
|
+
const ttlSeconds = Math.ceil(ttlMs / 1e3);
|
|
186
|
+
await this.storage.set(sessionId, value, { ttlSeconds });
|
|
187
|
+
} else if (session.session.expiresAt) {
|
|
188
|
+
const ttl = session.session.expiresAt - Date.now();
|
|
189
|
+
if (ttl > 0) {
|
|
190
|
+
const ttlSeconds = Math.ceil(ttl / 1e3);
|
|
191
|
+
await this.storage.set(sessionId, value, { ttlSeconds });
|
|
192
|
+
} else {
|
|
193
|
+
await this.storage.set(sessionId, value);
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
await this.storage.set(sessionId, value);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Delete a session
|
|
201
|
+
*/
|
|
202
|
+
async delete(sessionId) {
|
|
203
|
+
this.validateSessionId(sessionId);
|
|
204
|
+
await this.ensureConnected();
|
|
205
|
+
await this.storage.delete(sessionId);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Check if a session exists
|
|
209
|
+
*/
|
|
210
|
+
async exists(sessionId) {
|
|
211
|
+
this.validateSessionId(sessionId);
|
|
212
|
+
await this.ensureConnected();
|
|
213
|
+
return this.storage.exists(sessionId);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Allocate a new session ID
|
|
217
|
+
*/
|
|
218
|
+
allocId() {
|
|
219
|
+
return randomUUID();
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Disconnect from Redis (only if we created the connection)
|
|
223
|
+
*/
|
|
224
|
+
async disconnect() {
|
|
225
|
+
if (!this.externalInstance) {
|
|
226
|
+
await this.storage.disconnect();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Get the underlying Redis client (for advanced use cases)
|
|
231
|
+
*/
|
|
232
|
+
getRedisClient() {
|
|
233
|
+
return this.storage.getClient();
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Test Redis connection by sending a PING command.
|
|
237
|
+
* Useful for validating connection on startup.
|
|
238
|
+
*
|
|
239
|
+
* @returns true if connection is healthy, false otherwise
|
|
240
|
+
*/
|
|
241
|
+
async ping() {
|
|
242
|
+
try {
|
|
243
|
+
await this.ensureConnected();
|
|
244
|
+
return this.storage.ping();
|
|
245
|
+
} catch (error) {
|
|
246
|
+
this.logger?.error("[RedisSessionStore] Connection failed", {
|
|
247
|
+
error: error.message
|
|
248
|
+
});
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
export {
|
|
254
|
+
RedisSessionStore
|
|
255
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// libs/auth/src/session/secure-store-accessor.impl.ts
|
|
2
|
+
import { sha256Hex } from "@frontmcp/utils";
|
|
3
|
+
import { noopAuthLogger } from "../common/auth-logger.interface.mjs";
|
|
4
|
+
var GLOBAL_NAMESPACE = "global";
|
|
5
|
+
var SecureStoreAccessorImpl = class {
|
|
6
|
+
backend;
|
|
7
|
+
scope;
|
|
8
|
+
resolveSub;
|
|
9
|
+
resolveSessionId;
|
|
10
|
+
ttlMs;
|
|
11
|
+
logger;
|
|
12
|
+
constructor(deps) {
|
|
13
|
+
this.backend = deps.backend;
|
|
14
|
+
this.scope = deps.scope ?? "user";
|
|
15
|
+
this.resolveSub = deps.resolveSub;
|
|
16
|
+
this.resolveSessionId = deps.resolveSessionId;
|
|
17
|
+
this.ttlMs = deps.ttlMs;
|
|
18
|
+
this.logger = deps.logger ?? noopAuthLogger;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the namespace for the current request from the configured scope.
|
|
22
|
+
* Returns undefined when the scope's identity is unavailable (anonymous user
|
|
23
|
+
* under `user` scope, or no session under `session` scope), in which case
|
|
24
|
+
* reads return empty and writes are skipped.
|
|
25
|
+
*/
|
|
26
|
+
resolveNamespace() {
|
|
27
|
+
switch (this.scope) {
|
|
28
|
+
case "global":
|
|
29
|
+
return GLOBAL_NAMESPACE;
|
|
30
|
+
case "session": {
|
|
31
|
+
const sid = this.resolveSessionId();
|
|
32
|
+
return sid ? `s:${sha256Hex(sid).slice(0, 32)}` : void 0;
|
|
33
|
+
}
|
|
34
|
+
case "user":
|
|
35
|
+
default: {
|
|
36
|
+
const sub = this.resolveSub();
|
|
37
|
+
return sub ? `u:${sha256Hex(sub).slice(0, 32)}` : void 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async get(key) {
|
|
42
|
+
const namespace = this.resolveNamespace();
|
|
43
|
+
if (!namespace) return void 0;
|
|
44
|
+
const raw = await this.backend.get(namespace, key);
|
|
45
|
+
if (raw === null) return void 0;
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(raw);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
this.logger.warn(`SecureStore: failed to parse secret "${key}": ${errMsg(err)}`);
|
|
50
|
+
return void 0;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async set(key, value, options) {
|
|
54
|
+
const namespace = this.resolveNamespace();
|
|
55
|
+
if (!namespace) {
|
|
56
|
+
this.logger.debug(`SecureStore: no ${this.scope} scope for request; skipping set("${key}")`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const ttlMs = options?.ttlMs ?? this.ttlMs;
|
|
60
|
+
await this.backend.set(namespace, key, JSON.stringify(value), ttlMs);
|
|
61
|
+
}
|
|
62
|
+
async delete(key) {
|
|
63
|
+
const namespace = this.resolveNamespace();
|
|
64
|
+
if (!namespace) return false;
|
|
65
|
+
return this.backend.delete(namespace, key);
|
|
66
|
+
}
|
|
67
|
+
async list() {
|
|
68
|
+
const namespace = this.resolveNamespace();
|
|
69
|
+
if (!namespace) return [];
|
|
70
|
+
return this.backend.list(namespace);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
function errMsg(err) {
|
|
74
|
+
return err instanceof Error ? err.message : String(err);
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
SecureStoreAccessorImpl
|
|
78
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// libs/auth/src/session/secure-store-backends.ts
|
|
2
|
+
import { noopAuthLogger } from "../common/auth-logger.interface.mjs";
|
|
3
|
+
import { VaultEncryption } from "./vault-encryption.mjs";
|
|
4
|
+
var DEFAULT_KEY_PREFIX = "mcp:secret:";
|
|
5
|
+
var EncryptedStorageSecureStoreBackend = class {
|
|
6
|
+
storage;
|
|
7
|
+
encryption;
|
|
8
|
+
keyPrefix;
|
|
9
|
+
logger;
|
|
10
|
+
/** Cache of derived AES keys, keyed by namespace. */
|
|
11
|
+
keyCache = /* @__PURE__ */ new Map();
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.storage = options.storage;
|
|
14
|
+
this.keyPrefix = options.keyPrefix ?? DEFAULT_KEY_PREFIX;
|
|
15
|
+
this.logger = options.logger ?? noopAuthLogger;
|
|
16
|
+
this.encryption = new VaultEncryption({
|
|
17
|
+
pepper: options.pepper,
|
|
18
|
+
hkdfInfo: "frontmcp-secure-store-v1"
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
// ============================================
|
|
22
|
+
// Storage-key helpers
|
|
23
|
+
// ============================================
|
|
24
|
+
/** `secret:keys:<namespace>` \u2192 JSON array of key names. */
|
|
25
|
+
keysKey(namespace) {
|
|
26
|
+
return `${this.keyPrefix}keys:${namespace}`;
|
|
27
|
+
}
|
|
28
|
+
/** `secret:data:<namespace>:<key>` \u2192 encrypted record. */
|
|
29
|
+
dataKey(namespace, key) {
|
|
30
|
+
return `${this.keyPrefix}data:${namespace}:${key}`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Derive (and cache) the AES-256 key for a namespace. The namespace acts as
|
|
34
|
+
* the HKDF salt material, so values are cryptographically isolated per scope.
|
|
35
|
+
*/
|
|
36
|
+
deriveKey(namespace) {
|
|
37
|
+
const cached = this.keyCache.get(namespace);
|
|
38
|
+
if (cached) return cached;
|
|
39
|
+
const promise = this.encryption.deriveKey({ jti: namespace, vaultKey: namespace, sub: namespace, iat: 0 });
|
|
40
|
+
this.keyCache.set(namespace, promise);
|
|
41
|
+
return promise;
|
|
42
|
+
}
|
|
43
|
+
setOpts(ttlMs) {
|
|
44
|
+
return ttlMs && ttlMs > 0 ? { ttlSeconds: Math.ceil(ttlMs / 1e3) } : void 0;
|
|
45
|
+
}
|
|
46
|
+
// ============================================
|
|
47
|
+
// SecureStoreBackend
|
|
48
|
+
// ============================================
|
|
49
|
+
async get(namespace, key) {
|
|
50
|
+
const raw = await this.storage.get(this.dataKey(namespace, key));
|
|
51
|
+
if (!raw) return null;
|
|
52
|
+
try {
|
|
53
|
+
const record = JSON.parse(raw);
|
|
54
|
+
const aesKey = await this.deriveKey(namespace);
|
|
55
|
+
return await this.encryption.decrypt(record.enc, aesKey);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
this.logger.warn(`SecureStore: failed to read secret "${key}": ${errMsg(err)}`);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async set(namespace, key, value, ttlMs) {
|
|
62
|
+
const aesKey = await this.deriveKey(namespace);
|
|
63
|
+
const enc = await this.encryption.encrypt(value, aesKey);
|
|
64
|
+
const record = { enc };
|
|
65
|
+
const opts = this.setOpts(ttlMs);
|
|
66
|
+
await this.storage.set(this.dataKey(namespace, key), JSON.stringify(record), opts);
|
|
67
|
+
await this.addKeyToSet(namespace, key, opts);
|
|
68
|
+
}
|
|
69
|
+
async delete(namespace, key) {
|
|
70
|
+
const existed = await this.storage.delete(this.dataKey(namespace, key));
|
|
71
|
+
const keys = await this.readKeySet(namespace);
|
|
72
|
+
const next = keys.filter((k) => k !== key);
|
|
73
|
+
if (next.length !== keys.length) {
|
|
74
|
+
await this.writeKeySet(namespace, next);
|
|
75
|
+
}
|
|
76
|
+
return existed;
|
|
77
|
+
}
|
|
78
|
+
async list(namespace) {
|
|
79
|
+
return this.readKeySet(namespace);
|
|
80
|
+
}
|
|
81
|
+
// ============================================
|
|
82
|
+
// Key-set helpers (the namespace's key SET)
|
|
83
|
+
// ============================================
|
|
84
|
+
async readKeySet(namespace) {
|
|
85
|
+
const raw = await this.storage.get(this.keysKey(namespace));
|
|
86
|
+
if (!raw) return [];
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(raw);
|
|
89
|
+
return Array.isArray(parsed) ? parsed.filter((k) => typeof k === "string") : [];
|
|
90
|
+
} catch {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async writeKeySet(namespace, keys, opts) {
|
|
95
|
+
const unique = [...new Set(keys)];
|
|
96
|
+
await this.storage.set(this.keysKey(namespace), JSON.stringify(unique), opts);
|
|
97
|
+
}
|
|
98
|
+
async addKeyToSet(namespace, key, opts) {
|
|
99
|
+
const keys = await this.readKeySet(namespace);
|
|
100
|
+
if (!keys.includes(key)) {
|
|
101
|
+
keys.push(key);
|
|
102
|
+
await this.writeKeySet(namespace, keys, opts);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
function errMsg(err) {
|
|
107
|
+
return err instanceof Error ? err.message : String(err);
|
|
108
|
+
}
|
|
109
|
+
export {
|
|
110
|
+
EncryptedStorageSecureStoreBackend
|
|
111
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// libs/auth/src/session/secure-store.factory.ts
|
|
2
|
+
import { MemoryStorageAdapter } from "@frontmcp/utils";
|
|
3
|
+
import { SecureStoreConfigError } from "../errors/auth-internal.errors.mjs";
|
|
4
|
+
import { secureStoreConfigSchema } from "../options/shared.schemas.mjs";
|
|
5
|
+
import { EncryptedStorageSecureStoreBackend } from "./secure-store-backends.mjs";
|
|
6
|
+
import { createTokenStorageAdapter } from "./token-storage.factory.mjs";
|
|
7
|
+
function isCustomBackendConfig(config) {
|
|
8
|
+
return typeof config === "object" && config !== null && "backend" in config && !!config.backend;
|
|
9
|
+
}
|
|
10
|
+
function isSqliteConfig(config) {
|
|
11
|
+
return typeof config === "object" && config !== null && "sqlite" in config && !!config.sqlite;
|
|
12
|
+
}
|
|
13
|
+
function isRedisConfig(config) {
|
|
14
|
+
return typeof config === "object" && config !== null && "redis" in config && !!config.redis;
|
|
15
|
+
}
|
|
16
|
+
function readScope(config) {
|
|
17
|
+
if (typeof config === "object" && config !== null && "scope" in config && config.scope) {
|
|
18
|
+
return config.scope;
|
|
19
|
+
}
|
|
20
|
+
return "user";
|
|
21
|
+
}
|
|
22
|
+
function readTtlMs(config) {
|
|
23
|
+
if (typeof config === "object" && config !== null && "ttlMs" in config) {
|
|
24
|
+
return config.ttlMs;
|
|
25
|
+
}
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
function readPepper(config) {
|
|
29
|
+
if (typeof config === "object" && config !== null && "encryption" in config && config.encryption) {
|
|
30
|
+
return config.encryption.pepper;
|
|
31
|
+
}
|
|
32
|
+
return void 0;
|
|
33
|
+
}
|
|
34
|
+
function toTokenStorageConfig(config) {
|
|
35
|
+
if (isSqliteConfig(config)) {
|
|
36
|
+
return { sqlite: config.sqlite };
|
|
37
|
+
}
|
|
38
|
+
if (isRedisConfig(config)) {
|
|
39
|
+
return { redis: config.redis };
|
|
40
|
+
}
|
|
41
|
+
return "memory";
|
|
42
|
+
}
|
|
43
|
+
function refuseUnusableConfig(config) {
|
|
44
|
+
if (config === void 0) return;
|
|
45
|
+
const issue = secureStoreConfigSchema.safeParse(config).error?.issues[0];
|
|
46
|
+
if (!issue) return;
|
|
47
|
+
const where = issue.path.length > 0 ? `secureStore.${issue.path.map(String).join(".")}: ` : "";
|
|
48
|
+
throw new SecureStoreConfigError(`${where}${issue.message}`);
|
|
49
|
+
}
|
|
50
|
+
async function createSecureStore(options) {
|
|
51
|
+
const { config, pepper, storage, logger } = options;
|
|
52
|
+
refuseUnusableConfig(config);
|
|
53
|
+
const scope = readScope(config);
|
|
54
|
+
const ttlMs = readTtlMs(config);
|
|
55
|
+
if (isCustomBackendConfig(config)) {
|
|
56
|
+
return { backend: config.backend, scope, ttlMs, kind: "custom" };
|
|
57
|
+
}
|
|
58
|
+
const kind = isSqliteConfig(config) ? "sqlite" : isRedisConfig(config) ? "redis" : "memory";
|
|
59
|
+
let adapter;
|
|
60
|
+
if (storage) {
|
|
61
|
+
adapter = storage;
|
|
62
|
+
} else if (kind === "memory") {
|
|
63
|
+
adapter = new MemoryStorageAdapter();
|
|
64
|
+
await adapter.connect();
|
|
65
|
+
} else {
|
|
66
|
+
adapter = await createTokenStorageAdapter(toTokenStorageConfig(config));
|
|
67
|
+
}
|
|
68
|
+
const backend = new EncryptedStorageSecureStoreBackend({
|
|
69
|
+
storage: adapter,
|
|
70
|
+
pepper: readPepper(config) ?? pepper,
|
|
71
|
+
logger
|
|
72
|
+
});
|
|
73
|
+
return { backend, scope, ttlMs, kind, ...storage ? {} : { close: () => adapter.disconnect() } };
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
createSecureStore
|
|
77
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// libs/auth/src/session/session-credential-vault.factory.ts
|
|
2
|
+
import { SessionCredentialVault } from "./session-credential-vault.mjs";
|
|
3
|
+
import { createTokenStorageAdapter } from "./token-storage.factory.mjs";
|
|
4
|
+
async function createSessionCredentialVault(options) {
|
|
5
|
+
const storage = options.storage ?? await createTokenStorageAdapter(options.tokenStorage);
|
|
6
|
+
const vault = new SessionCredentialVault({
|
|
7
|
+
storage,
|
|
8
|
+
pepper: options.pepper,
|
|
9
|
+
keyPrefix: options.keyPrefix,
|
|
10
|
+
ttlMs: options.ttlMs,
|
|
11
|
+
logger: options.logger
|
|
12
|
+
});
|
|
13
|
+
const type = options.tokenStorage === void 0 || options.tokenStorage === "memory" ? "memory" : "sqlite" in options.tokenStorage ? "sqlite" : "redis";
|
|
14
|
+
return { vault, type };
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
createSessionCredentialVault
|
|
18
|
+
};
|