@frontmcp/auth 1.10.15 → 1.11.0-rc.1
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,197 @@
|
|
|
1
|
+
// libs/auth/src/session/storage-orchestrated-token.store.ts
|
|
2
|
+
import { EncryptionKeyNotConfiguredError } from "../errors/auth-internal.errors.mjs";
|
|
3
|
+
import {
|
|
4
|
+
decryptRecord,
|
|
5
|
+
deriveKeyForRecord,
|
|
6
|
+
encryptRecord,
|
|
7
|
+
hasLiveAccessToken,
|
|
8
|
+
recordDropAt
|
|
9
|
+
} from "./orchestrated-token.crypto.mjs";
|
|
10
|
+
var StorageOrchestratedTokenStore = class {
|
|
11
|
+
storage;
|
|
12
|
+
encryptionKey;
|
|
13
|
+
defaultTtlMs;
|
|
14
|
+
refreshSkewMs;
|
|
15
|
+
namespace;
|
|
16
|
+
storageIsNamespaced;
|
|
17
|
+
/** HKDF-derived subkey cache, keyed by composite key. */
|
|
18
|
+
derivedKeys = /* @__PURE__ */ new Map();
|
|
19
|
+
constructor(storage, options = {}) {
|
|
20
|
+
this.encryptionKey = options.encryptionKey;
|
|
21
|
+
this.defaultTtlMs = options.defaultTtlMs;
|
|
22
|
+
this.refreshSkewMs = options.refreshSkewMs ?? 0;
|
|
23
|
+
this.namespace = options.namespace ?? "otok";
|
|
24
|
+
this.storageIsNamespaced = this.isNamespacedStorage(storage);
|
|
25
|
+
this.storage = this.storageIsNamespaced ? storage.namespace(this.namespace) : storage;
|
|
26
|
+
}
|
|
27
|
+
// ============================================
|
|
28
|
+
// TokenStore interface
|
|
29
|
+
// ============================================
|
|
30
|
+
async getAccessToken(authorizationId, providerId) {
|
|
31
|
+
const record = await this.getRecord(authorizationId, providerId);
|
|
32
|
+
return record && hasLiveAccessToken(record, this.refreshSkewMs) ? record.accessToken : null;
|
|
33
|
+
}
|
|
34
|
+
async getRefreshToken(authorizationId, providerId) {
|
|
35
|
+
const record = await this.getRecord(authorizationId, providerId);
|
|
36
|
+
return record?.refreshToken ?? null;
|
|
37
|
+
}
|
|
38
|
+
async storeTokens(authorizationId, providerId, tokens) {
|
|
39
|
+
const compositeKey = this.compositeKey(authorizationId, providerId);
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
const record = {
|
|
42
|
+
accessToken: tokens.accessToken,
|
|
43
|
+
refreshToken: tokens.refreshToken,
|
|
44
|
+
expiresAt: tokens.expiresAt ?? (this.defaultTtlMs ? now + this.defaultTtlMs : void 0),
|
|
45
|
+
createdAt: now,
|
|
46
|
+
updatedAt: now
|
|
47
|
+
};
|
|
48
|
+
const value = this.encryptionKey ? encryptRecord(this.keyFor(compositeKey), record) : JSON.stringify(record);
|
|
49
|
+
await this.storage.set(this.storageKey(compositeKey), value, this.ttlOptions(recordDropAt(record)));
|
|
50
|
+
}
|
|
51
|
+
async deleteTokens(authorizationId, providerId) {
|
|
52
|
+
const compositeKey = this.compositeKey(authorizationId, providerId);
|
|
53
|
+
await this.storage.delete(this.storageKey(compositeKey));
|
|
54
|
+
this.derivedKeys.delete(compositeKey);
|
|
55
|
+
}
|
|
56
|
+
async hasTokens(authorizationId, providerId) {
|
|
57
|
+
const record = await this.getRecord(authorizationId, providerId);
|
|
58
|
+
return record !== null;
|
|
59
|
+
}
|
|
60
|
+
async getProviderIds(authorizationId) {
|
|
61
|
+
const prefix = `${authorizationId}:`;
|
|
62
|
+
const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
|
|
63
|
+
const providerIds = [];
|
|
64
|
+
for (const compositeKey of compositeKeys) {
|
|
65
|
+
if (!compositeKey.startsWith(prefix)) continue;
|
|
66
|
+
const providerId = compositeKey.slice(prefix.length);
|
|
67
|
+
const record = await this.getRecord(authorizationId, providerId);
|
|
68
|
+
if (record) {
|
|
69
|
+
providerIds.push(providerId);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return providerIds;
|
|
73
|
+
}
|
|
74
|
+
async migrateTokens(fromAuthId, toAuthId) {
|
|
75
|
+
for (const oldComposite of await this.copyRecords(fromAuthId, toAuthId)) {
|
|
76
|
+
await this.storage.delete(this.storageKey(oldComposite));
|
|
77
|
+
this.derivedKeys.delete(oldComposite);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async copyTokens(fromAuthId, toAuthId) {
|
|
81
|
+
await this.copyRecords(fromAuthId, toAuthId);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Delete all tokens for an authorization id.
|
|
85
|
+
*/
|
|
86
|
+
async deleteAllForAuthorization(authorizationId) {
|
|
87
|
+
const prefix = `${authorizationId}:`;
|
|
88
|
+
const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
|
|
89
|
+
for (const compositeKey of compositeKeys) {
|
|
90
|
+
if (!compositeKey.startsWith(prefix)) continue;
|
|
91
|
+
await this.storage.delete(this.storageKey(compositeKey));
|
|
92
|
+
this.derivedKeys.delete(compositeKey);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// ============================================
|
|
96
|
+
// Internals
|
|
97
|
+
// ============================================
|
|
98
|
+
/** Copy every record of `fromAuthId` to `toAuthId`, skipping a corrupted one; returns the composite keys copied. */
|
|
99
|
+
async copyRecords(fromAuthId, toAuthId) {
|
|
100
|
+
const prefix = `${fromAuthId}:`;
|
|
101
|
+
const compositeKeys = await this.listCompositeKeys(`${prefix}*`);
|
|
102
|
+
const copiedKeys = [];
|
|
103
|
+
for (const oldComposite of compositeKeys) {
|
|
104
|
+
if (!oldComposite.startsWith(prefix)) continue;
|
|
105
|
+
const newComposite = this.compositeKey(toAuthId, oldComposite.slice(prefix.length));
|
|
106
|
+
const stored = await this.storage.get(this.storageKey(oldComposite));
|
|
107
|
+
if (stored === null) continue;
|
|
108
|
+
if (this.encryptionKey) {
|
|
109
|
+
let record;
|
|
110
|
+
try {
|
|
111
|
+
record = decryptRecord(this.keyFor(oldComposite), stored);
|
|
112
|
+
} catch {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const value = encryptRecord(this.keyFor(newComposite), record);
|
|
116
|
+
await this.storage.set(this.storageKey(newComposite), value, this.ttlOptions(recordDropAt(record)));
|
|
117
|
+
} else {
|
|
118
|
+
let dropAt;
|
|
119
|
+
try {
|
|
120
|
+
dropAt = recordDropAt(JSON.parse(stored));
|
|
121
|
+
} catch {
|
|
122
|
+
dropAt = void 0;
|
|
123
|
+
}
|
|
124
|
+
await this.storage.set(this.storageKey(newComposite), stored, this.ttlOptions(dropAt));
|
|
125
|
+
}
|
|
126
|
+
copiedKeys.push(oldComposite);
|
|
127
|
+
}
|
|
128
|
+
return copiedKeys;
|
|
129
|
+
}
|
|
130
|
+
async getRecord(authorizationId, providerId) {
|
|
131
|
+
const compositeKey = this.compositeKey(authorizationId, providerId);
|
|
132
|
+
const stored = await this.storage.get(this.storageKey(compositeKey));
|
|
133
|
+
if (stored === null) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
let record;
|
|
137
|
+
if (this.encryptionKey) {
|
|
138
|
+
try {
|
|
139
|
+
record = decryptRecord(this.keyFor(compositeKey), stored);
|
|
140
|
+
} catch {
|
|
141
|
+
await this.storage.delete(this.storageKey(compositeKey));
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
try {
|
|
146
|
+
record = JSON.parse(stored);
|
|
147
|
+
} catch {
|
|
148
|
+
await this.storage.delete(this.storageKey(compositeKey));
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const dropAt = recordDropAt(record);
|
|
153
|
+
if (dropAt && dropAt < Date.now()) {
|
|
154
|
+
await this.storage.delete(this.storageKey(compositeKey));
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return record;
|
|
158
|
+
}
|
|
159
|
+
keyFor(compositeKey) {
|
|
160
|
+
if (!this.encryptionKey) {
|
|
161
|
+
throw new EncryptionKeyNotConfiguredError();
|
|
162
|
+
}
|
|
163
|
+
return deriveKeyForRecord(this.encryptionKey, compositeKey, this.derivedKeys);
|
|
164
|
+
}
|
|
165
|
+
compositeKey(authorizationId, providerId) {
|
|
166
|
+
return `${authorizationId}:${providerId}`;
|
|
167
|
+
}
|
|
168
|
+
storageKey(compositeKey) {
|
|
169
|
+
return this.storageIsNamespaced ? compositeKey : `${this.namespace}:${compositeKey}`;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* List composite keys (authorizationId:providerId) matching a glob over the
|
|
173
|
+
* composite-key space, stripping any non-namespaced prefix we added.
|
|
174
|
+
*/
|
|
175
|
+
async listCompositeKeys(compositeGlob) {
|
|
176
|
+
const pattern = this.storageIsNamespaced ? compositeGlob : `${this.namespace}:${compositeGlob}`;
|
|
177
|
+
const raw = await this.storage.keys(pattern);
|
|
178
|
+
if (this.storageIsNamespaced) {
|
|
179
|
+
return raw;
|
|
180
|
+
}
|
|
181
|
+
const stripPrefix = `${this.namespace}:`;
|
|
182
|
+
return raw.map((k) => k.startsWith(stripPrefix) ? k.slice(stripPrefix.length) : k);
|
|
183
|
+
}
|
|
184
|
+
ttlOptions(expiresAtMs) {
|
|
185
|
+
if (!expiresAtMs) {
|
|
186
|
+
return void 0;
|
|
187
|
+
}
|
|
188
|
+
const ttl = Math.ceil((expiresAtMs - Date.now()) / 1e3);
|
|
189
|
+
return { ttlSeconds: ttl > 0 ? ttl : 1 };
|
|
190
|
+
}
|
|
191
|
+
isNamespacedStorage(storage) {
|
|
192
|
+
return "namespace" in storage && typeof storage.namespace === "function";
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
export {
|
|
196
|
+
StorageOrchestratedTokenStore
|
|
197
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
|
|
2
|
+
const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
|
|
3
|
+
// __frontmcpCreateRequire
|
|
4
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
5
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
6
|
+
}) : x)(function(x) {
|
|
7
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
8
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// libs/auth/src/session/token-storage.factory.ts
|
|
12
|
+
import { MemoryStorageAdapter, RedisStorageAdapter } from "@frontmcp/utils";
|
|
13
|
+
function isRedisTokenStorage(config) {
|
|
14
|
+
return typeof config === "object" && config !== null && "redis" in config && !!config.redis;
|
|
15
|
+
}
|
|
16
|
+
function isSqliteTokenStorage(config) {
|
|
17
|
+
return typeof config === "object" && config !== null && "sqlite" in config && !!config.sqlite;
|
|
18
|
+
}
|
|
19
|
+
function isPersistentTokenStorage(config) {
|
|
20
|
+
return config !== void 0 && config !== "memory";
|
|
21
|
+
}
|
|
22
|
+
async function createTokenStorageAdapter(config) {
|
|
23
|
+
if (config === void 0 || config === "memory") {
|
|
24
|
+
const adapter = new MemoryStorageAdapter();
|
|
25
|
+
await adapter.connect();
|
|
26
|
+
return adapter;
|
|
27
|
+
}
|
|
28
|
+
if (isSqliteTokenStorage(config)) {
|
|
29
|
+
const { SqliteStorageAdapter } = __require("@frontmcp/storage-sqlite");
|
|
30
|
+
const adapter = new SqliteStorageAdapter({
|
|
31
|
+
path: config.sqlite.path,
|
|
32
|
+
encryption: config.sqlite.encryption,
|
|
33
|
+
ttlCleanupIntervalMs: config.sqlite.ttlCleanupIntervalMs ?? 6e4,
|
|
34
|
+
walMode: config.sqlite.walMode ?? true,
|
|
35
|
+
busyTimeoutMs: config.sqlite.busyTimeoutMs
|
|
36
|
+
});
|
|
37
|
+
await adapter.connect();
|
|
38
|
+
return adapter;
|
|
39
|
+
}
|
|
40
|
+
if (isRedisTokenStorage(config)) {
|
|
41
|
+
const { host, port, password, db, tls, keyPrefix } = config.redis;
|
|
42
|
+
const adapter = new RedisStorageAdapter({
|
|
43
|
+
config: { host, port, password, db, tls },
|
|
44
|
+
keyPrefix: keyPrefix ?? ""
|
|
45
|
+
});
|
|
46
|
+
await adapter.connect();
|
|
47
|
+
return adapter;
|
|
48
|
+
}
|
|
49
|
+
const fallback = new MemoryStorageAdapter();
|
|
50
|
+
await fallback.connect();
|
|
51
|
+
return fallback;
|
|
52
|
+
}
|
|
53
|
+
export {
|
|
54
|
+
createTokenStorageAdapter,
|
|
55
|
+
isPersistentTokenStorage,
|
|
56
|
+
isRedisTokenStorage,
|
|
57
|
+
isSqliteTokenStorage
|
|
58
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// libs/auth/src/session/token.refresh.ts
|
|
2
|
+
import { base64urlDecode } from "@frontmcp/utils";
|
|
3
|
+
function toEpochSeconds(exp) {
|
|
4
|
+
if (exp == null) return void 0;
|
|
5
|
+
if (exp instanceof Date) return Math.floor(exp.getTime() / 1e3);
|
|
6
|
+
return exp > 1e12 ? Math.floor(exp / 1e3) : Math.floor(exp);
|
|
7
|
+
}
|
|
8
|
+
function isSoonExpiring(exp, skewSec = 60) {
|
|
9
|
+
const expSec = toEpochSeconds(exp);
|
|
10
|
+
if (expSec == null) return false;
|
|
11
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
12
|
+
return expSec <= now + Math.max(0, skewSec);
|
|
13
|
+
}
|
|
14
|
+
function isSoonExpiringProvider(sessionLike, providerId, skewSec = 60) {
|
|
15
|
+
const snap = sessionLike.authorizedProviders[providerId];
|
|
16
|
+
if (!snap) return false;
|
|
17
|
+
return isSoonExpiring(snap.exp, skewSec);
|
|
18
|
+
}
|
|
19
|
+
function tryJwtExp(token) {
|
|
20
|
+
if (!token) return void 0;
|
|
21
|
+
const parts = token.split(".");
|
|
22
|
+
if (parts.length < 2) return void 0;
|
|
23
|
+
try {
|
|
24
|
+
const json = JSON.parse(new TextDecoder().decode(base64urlDecode(parts[1])));
|
|
25
|
+
const e = json?.exp;
|
|
26
|
+
return typeof e === "number" ? toEpochSeconds(e) : void 0;
|
|
27
|
+
} catch {
|
|
28
|
+
return void 0;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
isSoonExpiring,
|
|
33
|
+
isSoonExpiringProvider,
|
|
34
|
+
toEpochSeconds,
|
|
35
|
+
tryJwtExp
|
|
36
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// libs/auth/src/session/token.vault.ts
|
|
2
|
+
import { encryptAesGcm, decryptAesGcm, randomBytes, base64urlEncode, base64urlDecode } from "@frontmcp/utils";
|
|
3
|
+
var TokenVault = class {
|
|
4
|
+
/** Active key used for new encryptions */
|
|
5
|
+
active;
|
|
6
|
+
/** All known keys by kid for decryption (includes active) */
|
|
7
|
+
keys = /* @__PURE__ */ new Map();
|
|
8
|
+
constructor(keys) {
|
|
9
|
+
if (!Array.isArray(keys) || keys.length === 0) {
|
|
10
|
+
throw new Error("TokenVault requires at least one key");
|
|
11
|
+
}
|
|
12
|
+
for (const k of keys) {
|
|
13
|
+
if (!(k.key instanceof Uint8Array) || k.key.length !== 32) {
|
|
14
|
+
throw new Error(`TokenVault key "${k.kid}" must be a 32-byte Uint8Array for AES-256-GCM`);
|
|
15
|
+
}
|
|
16
|
+
if (this.keys.has(k.kid)) {
|
|
17
|
+
throw new Error(`TokenVault duplicate kid: "${k.kid}"`);
|
|
18
|
+
}
|
|
19
|
+
this.keys.set(k.kid, k.key);
|
|
20
|
+
}
|
|
21
|
+
this.active = keys[0];
|
|
22
|
+
}
|
|
23
|
+
rotateTo(k) {
|
|
24
|
+
if (!(k.key instanceof Uint8Array) || k.key.length !== 32) {
|
|
25
|
+
throw new Error(`TokenVault key "${k.kid}" must be a 32-byte Uint8Array for AES-256-GCM`);
|
|
26
|
+
}
|
|
27
|
+
this.active = k;
|
|
28
|
+
this.keys.set(k.kid, k.key);
|
|
29
|
+
}
|
|
30
|
+
async encrypt(plaintext, opts) {
|
|
31
|
+
const iv = randomBytes(12);
|
|
32
|
+
const { ciphertext, tag } = await encryptAesGcm(this.active.key, new TextEncoder().encode(plaintext), iv);
|
|
33
|
+
return {
|
|
34
|
+
alg: "A256GCM",
|
|
35
|
+
kid: this.active.kid,
|
|
36
|
+
iv: base64urlEncode(iv),
|
|
37
|
+
tag: base64urlEncode(tag),
|
|
38
|
+
data: base64urlEncode(ciphertext),
|
|
39
|
+
exp: opts?.exp,
|
|
40
|
+
meta: opts?.meta
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async decrypt(blob) {
|
|
44
|
+
if (blob.exp !== void 0) {
|
|
45
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
46
|
+
if (nowSeconds > blob.exp) {
|
|
47
|
+
throw new Error(`vault_expired:${blob.kid}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const key = this.keys.get(blob.kid);
|
|
51
|
+
if (!key) throw new Error(`vault_unknown_kid:${blob.kid}`);
|
|
52
|
+
const iv = base64urlDecode(blob.iv);
|
|
53
|
+
const tag = base64urlDecode(blob.tag);
|
|
54
|
+
const data = base64urlDecode(blob.data);
|
|
55
|
+
const plaintext = await decryptAesGcm(key, data, iv, tag);
|
|
56
|
+
return new TextDecoder().decode(plaintext);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
export {
|
|
60
|
+
TokenVault
|
|
61
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// libs/auth/src/session/transport-session.types.ts
|
|
2
|
+
import { internalZ as z } from "@frontmcp/lazy-zod";
|
|
3
|
+
var transportProtocolSchema = z.enum([
|
|
4
|
+
"legacy-sse",
|
|
5
|
+
"sse",
|
|
6
|
+
"streamable-http",
|
|
7
|
+
"stateful-http",
|
|
8
|
+
"stateless-http"
|
|
9
|
+
]);
|
|
10
|
+
var sseTransportStateSchema = z.object({
|
|
11
|
+
type: z.literal("sse"),
|
|
12
|
+
lastEventId: z.string().optional(),
|
|
13
|
+
lastPing: z.number().optional(),
|
|
14
|
+
connectionState: z.enum(["connecting", "open", "closed"]).optional()
|
|
15
|
+
});
|
|
16
|
+
var streamableHttpTransportStateSchema = z.object({
|
|
17
|
+
type: z.literal("streamable-http"),
|
|
18
|
+
requestSeq: z.number(),
|
|
19
|
+
activeStreamId: z.string().optional(),
|
|
20
|
+
pendingRequests: z.array(z.string()).optional()
|
|
21
|
+
});
|
|
22
|
+
var statefulHttpTransportStateSchema = z.object({
|
|
23
|
+
type: z.literal("stateful-http"),
|
|
24
|
+
requestSeq: z.number(),
|
|
25
|
+
pendingResponses: z.array(z.string()).optional(),
|
|
26
|
+
lastActivity: z.number().optional()
|
|
27
|
+
});
|
|
28
|
+
var statelessHttpTransportStateSchema = z.object({
|
|
29
|
+
type: z.literal("stateless-http"),
|
|
30
|
+
requestCount: z.number(),
|
|
31
|
+
windowStart: z.number().optional()
|
|
32
|
+
});
|
|
33
|
+
var legacySseTransportStateSchema = z.object({
|
|
34
|
+
type: z.literal("legacy-sse"),
|
|
35
|
+
messagePath: z.string(),
|
|
36
|
+
lastEventId: z.string().optional(),
|
|
37
|
+
connectionState: z.enum(["connecting", "open", "closed"]).optional()
|
|
38
|
+
});
|
|
39
|
+
var transportStateSchema = z.discriminatedUnion("type", [
|
|
40
|
+
sseTransportStateSchema,
|
|
41
|
+
streamableHttpTransportStateSchema,
|
|
42
|
+
statefulHttpTransportStateSchema,
|
|
43
|
+
statelessHttpTransportStateSchema,
|
|
44
|
+
legacySseTransportStateSchema
|
|
45
|
+
]);
|
|
46
|
+
var transportSessionSchema = z.object({
|
|
47
|
+
id: z.string(),
|
|
48
|
+
authorizationId: z.string(),
|
|
49
|
+
protocol: transportProtocolSchema,
|
|
50
|
+
createdAt: z.number(),
|
|
51
|
+
expiresAt: z.number().optional(),
|
|
52
|
+
nodeId: z.string(),
|
|
53
|
+
clientFingerprint: z.string().optional(),
|
|
54
|
+
transportState: transportStateSchema.optional()
|
|
55
|
+
});
|
|
56
|
+
var sessionJwtPayloadSchema = z.object({
|
|
57
|
+
sid: z.string(),
|
|
58
|
+
aid: z.string(),
|
|
59
|
+
proto: transportProtocolSchema,
|
|
60
|
+
nid: z.string(),
|
|
61
|
+
iat: z.number(),
|
|
62
|
+
exp: z.number().optional()
|
|
63
|
+
});
|
|
64
|
+
var encryptedBlobSchema = z.object({
|
|
65
|
+
alg: z.literal("A256GCM"),
|
|
66
|
+
kid: z.string().optional(),
|
|
67
|
+
iv: z.string(),
|
|
68
|
+
tag: z.string(),
|
|
69
|
+
data: z.string(),
|
|
70
|
+
exp: z.number().optional(),
|
|
71
|
+
meta: z.record(z.string(), z.unknown()).optional()
|
|
72
|
+
});
|
|
73
|
+
var storedSessionSchema = z.object({
|
|
74
|
+
session: transportSessionSchema,
|
|
75
|
+
authorizationId: z.string(),
|
|
76
|
+
tokens: z.record(z.string(), encryptedBlobSchema).optional(),
|
|
77
|
+
createdAt: z.number(),
|
|
78
|
+
lastAccessedAt: z.number(),
|
|
79
|
+
initialized: z.boolean().optional(),
|
|
80
|
+
maxLifetimeAt: z.number().optional(),
|
|
81
|
+
clientCapabilities: z.record(z.string(), z.unknown()).optional(),
|
|
82
|
+
reassignedAt: z.number().optional(),
|
|
83
|
+
reassignedFrom: z.string().optional()
|
|
84
|
+
});
|
|
85
|
+
var redisConfigSchema = z.object({
|
|
86
|
+
host: z.string().min(1),
|
|
87
|
+
port: z.number().int().positive().optional().default(6379),
|
|
88
|
+
password: z.string().optional(),
|
|
89
|
+
db: z.number().int().nonnegative().optional().default(0),
|
|
90
|
+
tls: z.boolean().optional().default(false),
|
|
91
|
+
keyPrefix: z.string().optional().default("mcp:session:"),
|
|
92
|
+
defaultTtlMs: z.number().int().positive().optional().default(36e5)
|
|
93
|
+
// 1 hour default
|
|
94
|
+
});
|
|
95
|
+
export {
|
|
96
|
+
encryptedBlobSchema,
|
|
97
|
+
legacySseTransportStateSchema,
|
|
98
|
+
redisConfigSchema,
|
|
99
|
+
sessionJwtPayloadSchema,
|
|
100
|
+
sseTransportStateSchema,
|
|
101
|
+
statefulHttpTransportStateSchema,
|
|
102
|
+
statelessHttpTransportStateSchema,
|
|
103
|
+
storedSessionSchema,
|
|
104
|
+
streamableHttpTransportStateSchema,
|
|
105
|
+
transportProtocolSchema,
|
|
106
|
+
transportSessionSchema,
|
|
107
|
+
transportStateSchema
|
|
108
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// libs/auth/src/session/utils/auth-token.utils.ts
|
|
2
|
+
import { sha256Base64url } from "@frontmcp/utils";
|
|
3
|
+
function isJwt(token) {
|
|
4
|
+
if (!token) return false;
|
|
5
|
+
return token.split(".").length === 3;
|
|
6
|
+
}
|
|
7
|
+
function getTokenSignatureFingerprint(token) {
|
|
8
|
+
if (isJwt(token)) {
|
|
9
|
+
const sig = token.split(".")[2];
|
|
10
|
+
if (sig) return sig;
|
|
11
|
+
}
|
|
12
|
+
return sha256Base64url(token);
|
|
13
|
+
}
|
|
14
|
+
function extractClaimValue(claims, key, validator) {
|
|
15
|
+
const value = claims[key];
|
|
16
|
+
return validator(value) ? value : void 0;
|
|
17
|
+
}
|
|
18
|
+
var isString = (value) => typeof value === "string";
|
|
19
|
+
var isNumber = (value) => typeof value === "number";
|
|
20
|
+
var isStringOrStringArray = (value) => typeof value === "string" || Array.isArray(value);
|
|
21
|
+
function deriveTypedUser(claims) {
|
|
22
|
+
return {
|
|
23
|
+
...claims,
|
|
24
|
+
iss: extractClaimValue(claims, "iss", isString) ?? "",
|
|
25
|
+
sid: extractClaimValue(claims, "sid", isString),
|
|
26
|
+
// RFC 9068 \u00a72.2: a client-credentials token names the client as its subject; some IdPs only put it in client_id / azp
|
|
27
|
+
sub: extractClaimValue(claims, "sub", isString) ?? extractClaimValue(claims, "client_id", isString) ?? extractClaimValue(claims, "azp", isString) ?? "",
|
|
28
|
+
exp: extractClaimValue(claims, "exp", isNumber),
|
|
29
|
+
iat: extractClaimValue(claims, "iat", isNumber),
|
|
30
|
+
aud: extractClaimValue(claims, "aud", isStringOrStringArray),
|
|
31
|
+
email: extractClaimValue(claims, "email", isString),
|
|
32
|
+
preferred_username: extractClaimValue(claims, "preferred_username", isString),
|
|
33
|
+
username: extractClaimValue(claims, "username", isString),
|
|
34
|
+
name: extractClaimValue(claims, "name", isString),
|
|
35
|
+
picture: extractClaimValue(claims, "picture", isString)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function anonymousCallerClaims(options, anonymousId) {
|
|
39
|
+
const { issuer, scopes = ["anonymous"], subject } = options;
|
|
40
|
+
return {
|
|
41
|
+
sub: subject ?? `anon:${anonymousId}`,
|
|
42
|
+
iss: issuer,
|
|
43
|
+
name: subject ? "Static token" : "Anonymous",
|
|
44
|
+
scope: scopes.join(" ")
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function extractBearerToken(header) {
|
|
48
|
+
if (!header) return void 0;
|
|
49
|
+
const m = header.match(/^\s*Bearer\s+(\S+)\s*$/i);
|
|
50
|
+
return m ? m[1] : void 0;
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
anonymousCallerClaims,
|
|
54
|
+
deriveTypedUser,
|
|
55
|
+
extractBearerToken,
|
|
56
|
+
getTokenSignatureFingerprint,
|
|
57
|
+
isJwt
|
|
58
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// libs/auth/src/session/utils/index.ts
|
|
2
|
+
import { TinyTtlCache } from "./tiny-ttl-cache.mjs";
|
|
3
|
+
import {
|
|
4
|
+
isJwt,
|
|
5
|
+
getTokenSignatureFingerprint,
|
|
6
|
+
deriveTypedUser,
|
|
7
|
+
anonymousCallerClaims,
|
|
8
|
+
extractBearerToken
|
|
9
|
+
} from "./auth-token.utils.mjs";
|
|
10
|
+
import { getKey, encryptJson, decryptSessionJson, safeDecrypt, resetCachedKey } from "./session-crypto.utils.mjs";
|
|
11
|
+
export {
|
|
12
|
+
TinyTtlCache,
|
|
13
|
+
anonymousCallerClaims,
|
|
14
|
+
decryptSessionJson,
|
|
15
|
+
deriveTypedUser,
|
|
16
|
+
encryptJson,
|
|
17
|
+
extractBearerToken,
|
|
18
|
+
getKey,
|
|
19
|
+
getTokenSignatureFingerprint,
|
|
20
|
+
isJwt,
|
|
21
|
+
resetCachedKey,
|
|
22
|
+
safeDecrypt
|
|
23
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// libs/auth/src/session/utils/session-crypto.utils.ts
|
|
2
|
+
import { decryptValue, encryptValue, getEnv, getMachineId, isProduction, sha256 } from "@frontmcp/utils";
|
|
3
|
+
import { SessionSecretRequiredError } from "../../errors/auth-internal.errors.mjs";
|
|
4
|
+
var cachedKey = null;
|
|
5
|
+
function getKey() {
|
|
6
|
+
if (cachedKey) return cachedKey;
|
|
7
|
+
const secret = getEnv("MCP_SESSION_SECRET");
|
|
8
|
+
if (!secret) {
|
|
9
|
+
if (isProduction()) {
|
|
10
|
+
throw new SessionSecretRequiredError("session ID encryption");
|
|
11
|
+
}
|
|
12
|
+
console.warn(
|
|
13
|
+
"[SessionIdUtils] Using machine ID as session encryption secret - NOT SECURE FOR PRODUCTION. Set MCP_SESSION_SECRET environment variable for secure session encryption."
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
const base = secret || getMachineId();
|
|
17
|
+
cachedKey = sha256(new TextEncoder().encode(base));
|
|
18
|
+
return cachedKey;
|
|
19
|
+
}
|
|
20
|
+
function encryptJson(obj) {
|
|
21
|
+
const key = getKey();
|
|
22
|
+
const encrypted = encryptValue(obj, key);
|
|
23
|
+
return `${encrypted.iv}.${encrypted.tag}.${encrypted.data}`;
|
|
24
|
+
}
|
|
25
|
+
function decryptSessionJson(sessionId) {
|
|
26
|
+
const parts = sessionId.split(".");
|
|
27
|
+
if (parts.length !== 3) return null;
|
|
28
|
+
const [ivB64, tagB64, ctB64] = parts;
|
|
29
|
+
if (!ivB64 || !tagB64 || !ctB64) return null;
|
|
30
|
+
const key = getKey();
|
|
31
|
+
return decryptValue({ alg: "A256GCM", iv: ivB64, tag: tagB64, data: ctB64 }, key);
|
|
32
|
+
}
|
|
33
|
+
function safeDecrypt(sessionId) {
|
|
34
|
+
try {
|
|
35
|
+
return decryptSessionJson(sessionId);
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function resetCachedKey() {
|
|
41
|
+
cachedKey = null;
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
decryptSessionJson,
|
|
45
|
+
encryptJson,
|
|
46
|
+
getKey,
|
|
47
|
+
resetCachedKey,
|
|
48
|
+
safeDecrypt
|
|
49
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// libs/auth/src/session/utils/tiny-ttl-cache.ts
|
|
2
|
+
var TinyTtlCache = class {
|
|
3
|
+
constructor(ttlMs) {
|
|
4
|
+
this.ttlMs = ttlMs;
|
|
5
|
+
}
|
|
6
|
+
ttlMs;
|
|
7
|
+
map = /* @__PURE__ */ new Map();
|
|
8
|
+
get(k) {
|
|
9
|
+
const hit = this.map.get(k);
|
|
10
|
+
if (!hit) return void 0;
|
|
11
|
+
if (hit.exp < Date.now()) {
|
|
12
|
+
this.map.delete(k);
|
|
13
|
+
return void 0;
|
|
14
|
+
}
|
|
15
|
+
return hit.v;
|
|
16
|
+
}
|
|
17
|
+
set(k, v) {
|
|
18
|
+
this.map.set(k, { v, exp: Date.now() + this.ttlMs });
|
|
19
|
+
}
|
|
20
|
+
delete(k) {
|
|
21
|
+
return this.map.delete(k);
|
|
22
|
+
}
|
|
23
|
+
clear() {
|
|
24
|
+
this.map.clear();
|
|
25
|
+
}
|
|
26
|
+
size() {
|
|
27
|
+
return this.map.size;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Remove all expired entries
|
|
31
|
+
*/
|
|
32
|
+
cleanup() {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
let removed = 0;
|
|
35
|
+
for (const [k, { exp }] of this.map) {
|
|
36
|
+
if (exp < now) {
|
|
37
|
+
this.map.delete(k);
|
|
38
|
+
removed++;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return removed;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
export {
|
|
45
|
+
TinyTtlCache
|
|
46
|
+
};
|