@frontmcp/auth 1.10.16 → 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,405 @@
|
|
|
1
|
+
// libs/auth/src/session/encrypted-authorization-vault.ts
|
|
2
|
+
import { internalZ as z } from "@frontmcp/lazy-zod";
|
|
3
|
+
import { AsyncLocalStorage, randomUUID } from "@frontmcp/utils";
|
|
4
|
+
import { EncryptionContextNotSetError, VaultLoadError, VaultNotFoundError } from "../errors/auth-internal.errors.mjs";
|
|
5
|
+
import { encryptedDataSchema, VaultEncryption } from "./vault-encryption.mjs";
|
|
6
|
+
var redisVaultEntrySchema = z.object({
|
|
7
|
+
/** Vault ID */
|
|
8
|
+
id: z.string(),
|
|
9
|
+
/** User sub (for lookup) */
|
|
10
|
+
userSub: z.string(),
|
|
11
|
+
/** User email (optional, for display) */
|
|
12
|
+
userEmail: z.string().optional(),
|
|
13
|
+
/** User name (optional, for display) */
|
|
14
|
+
userName: z.string().optional(),
|
|
15
|
+
/** Client ID */
|
|
16
|
+
clientId: z.string(),
|
|
17
|
+
/** Creation timestamp */
|
|
18
|
+
createdAt: z.number(),
|
|
19
|
+
/** Last access timestamp */
|
|
20
|
+
lastAccessAt: z.number(),
|
|
21
|
+
/** Authorized app IDs (unencrypted for quick auth checks) */
|
|
22
|
+
authorizedAppIds: z.array(z.string()),
|
|
23
|
+
/** Skipped app IDs (unencrypted for quick checks) */
|
|
24
|
+
skippedAppIds: z.array(z.string()),
|
|
25
|
+
/** Pending auth request IDs (unencrypted for lookup) */
|
|
26
|
+
pendingAuthIds: z.array(z.string()),
|
|
27
|
+
/** Encrypted sensitive data blob */
|
|
28
|
+
encrypted: encryptedDataSchema
|
|
29
|
+
});
|
|
30
|
+
var encryptionContextStorage = new AsyncLocalStorage();
|
|
31
|
+
var EncryptedRedisVault = class {
|
|
32
|
+
constructor(redis, encryption, namespace = "vault:") {
|
|
33
|
+
this.redis = redis;
|
|
34
|
+
this.encryption = encryption;
|
|
35
|
+
this.namespace = namespace;
|
|
36
|
+
}
|
|
37
|
+
redis;
|
|
38
|
+
encryption;
|
|
39
|
+
namespace;
|
|
40
|
+
/**
|
|
41
|
+
* Run a callback with encryption context set for the current async scope.
|
|
42
|
+
* This is the recommended way to set encryption context as it is safe for
|
|
43
|
+
* concurrent requests (each request gets its own isolated context).
|
|
44
|
+
*
|
|
45
|
+
* @param context - Encryption context with key and vaultId
|
|
46
|
+
* @param fn - Async function to run with the context
|
|
47
|
+
* @returns The result of the callback
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* const result = await vault.runWithContext({ key, vaultId }, async () => {
|
|
52
|
+
* await vault.get(id);
|
|
53
|
+
* await vault.update(id, data);
|
|
54
|
+
* return 'done';
|
|
55
|
+
* });
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
runWithContext(context, fn) {
|
|
59
|
+
return encryptionContextStorage.run(context, fn);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get current encryption key from AsyncLocalStorage.
|
|
63
|
+
*/
|
|
64
|
+
getKey() {
|
|
65
|
+
const asyncContext = encryptionContextStorage.getStore();
|
|
66
|
+
if (asyncContext) {
|
|
67
|
+
return asyncContext.key;
|
|
68
|
+
}
|
|
69
|
+
throw new EncryptionContextNotSetError();
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Create Redis key from vault ID
|
|
73
|
+
*/
|
|
74
|
+
redisKey(id) {
|
|
75
|
+
return `${this.namespace}${id}`;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Create credential key from appId and providerId
|
|
79
|
+
*/
|
|
80
|
+
credentialKey(appId, providerId) {
|
|
81
|
+
return `${appId}:${providerId}`;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Encrypt sensitive data
|
|
85
|
+
*/
|
|
86
|
+
async encryptSensitive(data) {
|
|
87
|
+
return this.encryption.encryptObject(data, this.getKey());
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Decrypt sensitive data
|
|
91
|
+
*/
|
|
92
|
+
async decryptSensitive(encrypted) {
|
|
93
|
+
return this.encryption.decryptObject(encrypted, this.getKey());
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Convert Redis entry to full vault entry (decrypts sensitive data)
|
|
97
|
+
*/
|
|
98
|
+
async toVaultEntry(redisEntry) {
|
|
99
|
+
const sensitive = await this.decryptSensitive(redisEntry.encrypted);
|
|
100
|
+
return {
|
|
101
|
+
id: redisEntry.id,
|
|
102
|
+
userSub: redisEntry.userSub,
|
|
103
|
+
userEmail: redisEntry.userEmail,
|
|
104
|
+
userName: redisEntry.userName,
|
|
105
|
+
clientId: redisEntry.clientId,
|
|
106
|
+
createdAt: redisEntry.createdAt,
|
|
107
|
+
lastAccessAt: redisEntry.lastAccessAt,
|
|
108
|
+
appCredentials: sensitive.appCredentials,
|
|
109
|
+
consent: sensitive.consent,
|
|
110
|
+
federated: sensitive.federated,
|
|
111
|
+
pendingAuths: sensitive.pendingAuths,
|
|
112
|
+
authorizedAppIds: redisEntry.authorizedAppIds,
|
|
113
|
+
skippedAppIds: redisEntry.skippedAppIds
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Convert vault entry to Redis entry (encrypts sensitive data)
|
|
118
|
+
*/
|
|
119
|
+
async toRedisEntry(entry) {
|
|
120
|
+
const sensitive = {
|
|
121
|
+
appCredentials: entry.appCredentials,
|
|
122
|
+
consent: entry.consent,
|
|
123
|
+
federated: entry.federated,
|
|
124
|
+
pendingAuths: entry.pendingAuths
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
id: entry.id,
|
|
128
|
+
userSub: entry.userSub,
|
|
129
|
+
userEmail: entry.userEmail,
|
|
130
|
+
userName: entry.userName,
|
|
131
|
+
clientId: entry.clientId,
|
|
132
|
+
createdAt: entry.createdAt,
|
|
133
|
+
lastAccessAt: entry.lastAccessAt,
|
|
134
|
+
authorizedAppIds: entry.authorizedAppIds,
|
|
135
|
+
skippedAppIds: entry.skippedAppIds,
|
|
136
|
+
pendingAuthIds: entry.pendingAuths.map((p) => p.id),
|
|
137
|
+
encrypted: await this.encryptSensitive(sensitive)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Save entry to Redis
|
|
142
|
+
*/
|
|
143
|
+
async saveEntry(entry) {
|
|
144
|
+
const redisEntry = await this.toRedisEntry(entry);
|
|
145
|
+
await this.redis.set(this.redisKey(entry.id), JSON.stringify(redisEntry));
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Load entry from Redis
|
|
149
|
+
*/
|
|
150
|
+
async loadEntry(id) {
|
|
151
|
+
const data = await this.redis.get(this.redisKey(id));
|
|
152
|
+
if (!data) return null;
|
|
153
|
+
try {
|
|
154
|
+
const redisEntry = redisVaultEntrySchema.parse(JSON.parse(data));
|
|
155
|
+
return await this.toVaultEntry(redisEntry);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
throw new VaultLoadError(id, error instanceof Error ? error : void 0);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// ============================================
|
|
161
|
+
// AuthorizationVault Interface Implementation
|
|
162
|
+
// ============================================
|
|
163
|
+
async create(params) {
|
|
164
|
+
const now = Date.now();
|
|
165
|
+
const entry = {
|
|
166
|
+
id: randomUUID(),
|
|
167
|
+
userSub: params.userSub,
|
|
168
|
+
userEmail: params.userEmail,
|
|
169
|
+
userName: params.userName,
|
|
170
|
+
clientId: params.clientId,
|
|
171
|
+
createdAt: now,
|
|
172
|
+
lastAccessAt: now,
|
|
173
|
+
appCredentials: {},
|
|
174
|
+
consent: params.consent,
|
|
175
|
+
federated: params.federated,
|
|
176
|
+
pendingAuths: [],
|
|
177
|
+
authorizedAppIds: params.authorizedAppIds ?? [],
|
|
178
|
+
skippedAppIds: params.skippedAppIds ?? []
|
|
179
|
+
};
|
|
180
|
+
await this.saveEntry(entry);
|
|
181
|
+
return entry;
|
|
182
|
+
}
|
|
183
|
+
async get(id) {
|
|
184
|
+
const entry = await this.loadEntry(id);
|
|
185
|
+
if (!entry) return null;
|
|
186
|
+
entry.lastAccessAt = Date.now();
|
|
187
|
+
await this.saveEntry(entry);
|
|
188
|
+
return entry;
|
|
189
|
+
}
|
|
190
|
+
async update(id, updates) {
|
|
191
|
+
const entry = await this.loadEntry(id);
|
|
192
|
+
if (!entry) {
|
|
193
|
+
throw new VaultNotFoundError("Vault entry", id);
|
|
194
|
+
}
|
|
195
|
+
Object.assign(entry, updates, { lastAccessAt: Date.now() });
|
|
196
|
+
await this.saveEntry(entry);
|
|
197
|
+
}
|
|
198
|
+
async delete(id) {
|
|
199
|
+
await this.redis.del(this.redisKey(id));
|
|
200
|
+
}
|
|
201
|
+
async updateConsent(vaultId, consent) {
|
|
202
|
+
const entry = await this.loadEntry(vaultId);
|
|
203
|
+
if (!entry) return;
|
|
204
|
+
entry.consent = consent;
|
|
205
|
+
entry.lastAccessAt = Date.now();
|
|
206
|
+
await this.saveEntry(entry);
|
|
207
|
+
}
|
|
208
|
+
async authorizeApp(vaultId, appId) {
|
|
209
|
+
const entry = await this.loadEntry(vaultId);
|
|
210
|
+
if (!entry) return;
|
|
211
|
+
entry.skippedAppIds = entry.skippedAppIds.filter((id) => id !== appId);
|
|
212
|
+
if (!entry.authorizedAppIds.includes(appId)) {
|
|
213
|
+
entry.authorizedAppIds.push(appId);
|
|
214
|
+
}
|
|
215
|
+
entry.lastAccessAt = Date.now();
|
|
216
|
+
await this.saveEntry(entry);
|
|
217
|
+
}
|
|
218
|
+
async createPendingAuth(vaultId, params) {
|
|
219
|
+
const entry = await this.loadEntry(vaultId);
|
|
220
|
+
if (!entry) {
|
|
221
|
+
throw new VaultNotFoundError("Vault", vaultId);
|
|
222
|
+
}
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
const pendingAuth = {
|
|
225
|
+
id: randomUUID(),
|
|
226
|
+
appId: params.appId,
|
|
227
|
+
toolId: params.toolId,
|
|
228
|
+
authUrl: params.authUrl,
|
|
229
|
+
requiredScopes: params.requiredScopes,
|
|
230
|
+
elicitId: params.elicitId,
|
|
231
|
+
createdAt: now,
|
|
232
|
+
expiresAt: now + (params.ttlMs ?? 10 * 60 * 1e3),
|
|
233
|
+
status: "pending"
|
|
234
|
+
};
|
|
235
|
+
entry.pendingAuths.push(pendingAuth);
|
|
236
|
+
entry.lastAccessAt = now;
|
|
237
|
+
await this.saveEntry(entry);
|
|
238
|
+
return pendingAuth;
|
|
239
|
+
}
|
|
240
|
+
async getPendingAuth(vaultId, pendingAuthId) {
|
|
241
|
+
const entry = await this.loadEntry(vaultId);
|
|
242
|
+
if (!entry) return null;
|
|
243
|
+
const pendingAuth = entry.pendingAuths.find((p) => p.id === pendingAuthId);
|
|
244
|
+
if (!pendingAuth) return null;
|
|
245
|
+
if (Date.now() > pendingAuth.expiresAt && pendingAuth.status === "pending") {
|
|
246
|
+
pendingAuth.status = "expired";
|
|
247
|
+
await this.saveEntry(entry);
|
|
248
|
+
}
|
|
249
|
+
return pendingAuth;
|
|
250
|
+
}
|
|
251
|
+
async completePendingAuth(vaultId, pendingAuthId) {
|
|
252
|
+
const entry = await this.loadEntry(vaultId);
|
|
253
|
+
if (!entry) return;
|
|
254
|
+
const pendingAuth = entry.pendingAuths.find((p) => p.id === pendingAuthId);
|
|
255
|
+
if (pendingAuth) {
|
|
256
|
+
pendingAuth.status = "completed";
|
|
257
|
+
entry.skippedAppIds = entry.skippedAppIds.filter((id) => id !== pendingAuth.appId);
|
|
258
|
+
if (!entry.authorizedAppIds.includes(pendingAuth.appId)) {
|
|
259
|
+
entry.authorizedAppIds.push(pendingAuth.appId);
|
|
260
|
+
}
|
|
261
|
+
entry.lastAccessAt = Date.now();
|
|
262
|
+
await this.saveEntry(entry);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async cancelPendingAuth(vaultId, pendingAuthId) {
|
|
266
|
+
const entry = await this.loadEntry(vaultId);
|
|
267
|
+
if (!entry) return;
|
|
268
|
+
const pendingAuth = entry.pendingAuths.find((p) => p.id === pendingAuthId);
|
|
269
|
+
if (pendingAuth) {
|
|
270
|
+
pendingAuth.status = "cancelled";
|
|
271
|
+
await this.saveEntry(entry);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async isAppAuthorized(vaultId, appId) {
|
|
275
|
+
const data = await this.redis.get(this.redisKey(vaultId));
|
|
276
|
+
if (!data) return false;
|
|
277
|
+
try {
|
|
278
|
+
const parsed = JSON.parse(data);
|
|
279
|
+
return Array.isArray(parsed.authorizedAppIds) && parsed.authorizedAppIds.includes(appId);
|
|
280
|
+
} catch {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async getPendingAuths(vaultId) {
|
|
285
|
+
const entry = await this.loadEntry(vaultId);
|
|
286
|
+
if (!entry) return [];
|
|
287
|
+
const now = Date.now();
|
|
288
|
+
let updated = false;
|
|
289
|
+
const pending = entry.pendingAuths.filter((p) => {
|
|
290
|
+
if (now > p.expiresAt && p.status === "pending") {
|
|
291
|
+
p.status = "expired";
|
|
292
|
+
updated = true;
|
|
293
|
+
}
|
|
294
|
+
return p.status === "pending";
|
|
295
|
+
});
|
|
296
|
+
if (updated) {
|
|
297
|
+
await this.saveEntry(entry);
|
|
298
|
+
}
|
|
299
|
+
return pending;
|
|
300
|
+
}
|
|
301
|
+
// ============================================
|
|
302
|
+
// App Credential Methods
|
|
303
|
+
// ============================================
|
|
304
|
+
async addAppCredential(vaultId, credential) {
|
|
305
|
+
const entry = await this.loadEntry(vaultId);
|
|
306
|
+
if (!entry) return;
|
|
307
|
+
const shouldStore = await this.shouldStoreCredential(vaultId, credential.appId);
|
|
308
|
+
if (!shouldStore) return;
|
|
309
|
+
const key = this.credentialKey(credential.appId, credential.providerId);
|
|
310
|
+
entry.appCredentials[key] = credential;
|
|
311
|
+
entry.lastAccessAt = Date.now();
|
|
312
|
+
await this.saveEntry(entry);
|
|
313
|
+
}
|
|
314
|
+
async removeAppCredential(vaultId, appId, providerId) {
|
|
315
|
+
const entry = await this.loadEntry(vaultId);
|
|
316
|
+
if (!entry) return;
|
|
317
|
+
const key = this.credentialKey(appId, providerId);
|
|
318
|
+
delete entry.appCredentials[key];
|
|
319
|
+
entry.lastAccessAt = Date.now();
|
|
320
|
+
await this.saveEntry(entry);
|
|
321
|
+
}
|
|
322
|
+
async getAppCredentials(vaultId, appId) {
|
|
323
|
+
const entry = await this.loadEntry(vaultId);
|
|
324
|
+
if (!entry) return [];
|
|
325
|
+
const prefix = `${appId}:`;
|
|
326
|
+
return Object.entries(entry.appCredentials).filter(([key]) => key.startsWith(prefix)).map(([, cred]) => cred);
|
|
327
|
+
}
|
|
328
|
+
async getCredential(vaultId, appId, providerId) {
|
|
329
|
+
const entry = await this.loadEntry(vaultId);
|
|
330
|
+
if (!entry) return null;
|
|
331
|
+
const key = this.credentialKey(appId, providerId);
|
|
332
|
+
return entry.appCredentials[key] ?? null;
|
|
333
|
+
}
|
|
334
|
+
async getAllCredentials(vaultId, filterByConsent = false) {
|
|
335
|
+
const entry = await this.loadEntry(vaultId);
|
|
336
|
+
if (!entry) return [];
|
|
337
|
+
const allCredentials = Object.values(entry.appCredentials);
|
|
338
|
+
if (!filterByConsent || !entry.consent?.enabled) {
|
|
339
|
+
return allCredentials;
|
|
340
|
+
}
|
|
341
|
+
const consentedToolIds = new Set(entry.consent.selectedToolIds);
|
|
342
|
+
return allCredentials.filter((cred) => {
|
|
343
|
+
return Array.from(consentedToolIds).some((toolId) => toolId.startsWith(`${cred.appId}:`));
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
async updateCredential(vaultId, appId, providerId, updates) {
|
|
347
|
+
const entry = await this.loadEntry(vaultId);
|
|
348
|
+
if (!entry) return;
|
|
349
|
+
const key = this.credentialKey(appId, providerId);
|
|
350
|
+
const credential = entry.appCredentials[key];
|
|
351
|
+
if (!credential) return;
|
|
352
|
+
Object.assign(credential, updates);
|
|
353
|
+
entry.lastAccessAt = Date.now();
|
|
354
|
+
await this.saveEntry(entry);
|
|
355
|
+
}
|
|
356
|
+
async shouldStoreCredential(vaultId, appId, toolIds) {
|
|
357
|
+
const entry = await this.loadEntry(vaultId);
|
|
358
|
+
if (!entry) return false;
|
|
359
|
+
if (!entry.consent?.enabled) {
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
if (toolIds && toolIds.length > 0) {
|
|
363
|
+
return toolIds.some((toolId) => entry.consent.selectedToolIds.includes(toolId));
|
|
364
|
+
}
|
|
365
|
+
const consentedToolIds = entry.consent.selectedToolIds;
|
|
366
|
+
return consentedToolIds.some((toolId) => toolId.startsWith(`${appId}:`));
|
|
367
|
+
}
|
|
368
|
+
async invalidateCredential(vaultId, appId, providerId, reason) {
|
|
369
|
+
await this.updateCredential(vaultId, appId, providerId, {
|
|
370
|
+
isValid: false,
|
|
371
|
+
invalidReason: reason
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
async refreshOAuthCredential(vaultId, appId, providerId, tokens) {
|
|
375
|
+
const entry = await this.loadEntry(vaultId);
|
|
376
|
+
if (!entry) return;
|
|
377
|
+
const key = this.credentialKey(appId, providerId);
|
|
378
|
+
const credential = entry.appCredentials[key];
|
|
379
|
+
if (!credential || credential.credential.type !== "oauth") return;
|
|
380
|
+
credential.credential.accessToken = tokens.accessToken;
|
|
381
|
+
if (tokens.refreshToken !== void 0) {
|
|
382
|
+
credential.credential.refreshToken = tokens.refreshToken;
|
|
383
|
+
}
|
|
384
|
+
if (tokens.expiresAt !== void 0) {
|
|
385
|
+
credential.credential.expiresAt = tokens.expiresAt;
|
|
386
|
+
credential.expiresAt = tokens.expiresAt;
|
|
387
|
+
}
|
|
388
|
+
credential.isValid = true;
|
|
389
|
+
credential.invalidReason = void 0;
|
|
390
|
+
entry.lastAccessAt = Date.now();
|
|
391
|
+
await this.saveEntry(entry);
|
|
392
|
+
}
|
|
393
|
+
async cleanup() {
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
function createEncryptedVault(redis, config = {}) {
|
|
397
|
+
const encryption = new VaultEncryption({ pepper: config.pepper });
|
|
398
|
+
const vault = new EncryptedRedisVault(redis, encryption, config.namespace);
|
|
399
|
+
return { vault, encryption };
|
|
400
|
+
}
|
|
401
|
+
export {
|
|
402
|
+
EncryptedRedisVault,
|
|
403
|
+
createEncryptedVault,
|
|
404
|
+
redisVaultEntrySchema
|
|
405
|
+
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// libs/auth/src/session/federated-auth.session.ts
|
|
2
|
+
import { randomUUID } from "@frontmcp/utils";
|
|
3
|
+
import { AuthFlowError } from "../errors/auth-internal.errors.mjs";
|
|
4
|
+
function toSessionRecord(session) {
|
|
5
|
+
return {
|
|
6
|
+
...session,
|
|
7
|
+
completedProviders: Array.from(session.completedProviders.entries())
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function fromSessionRecord(record) {
|
|
11
|
+
return {
|
|
12
|
+
...record,
|
|
13
|
+
completedProviders: new Map(record.completedProviders)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
var InMemoryFederatedAuthSessionStore = class {
|
|
17
|
+
sessions = /* @__PURE__ */ new Map();
|
|
18
|
+
/** Default TTL for sessions (15 minutes) */
|
|
19
|
+
sessionTtlMs = 15 * 60 * 1e3;
|
|
20
|
+
/** Cleanup interval timer */
|
|
21
|
+
cleanupTimer;
|
|
22
|
+
constructor() {
|
|
23
|
+
this.cleanupTimer = setInterval(() => {
|
|
24
|
+
void this.cleanup();
|
|
25
|
+
}, 6e4);
|
|
26
|
+
if (this.cleanupTimer.unref) {
|
|
27
|
+
this.cleanupTimer.unref();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async store(session) {
|
|
31
|
+
const record = toSessionRecord(session);
|
|
32
|
+
this.sessions.set(session.id, record);
|
|
33
|
+
}
|
|
34
|
+
async get(id) {
|
|
35
|
+
const record = this.sessions.get(id);
|
|
36
|
+
if (!record) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (Date.now() > record.expiresAt) {
|
|
40
|
+
this.sessions.delete(id);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return fromSessionRecord(record);
|
|
44
|
+
}
|
|
45
|
+
async delete(id) {
|
|
46
|
+
this.sessions.delete(id);
|
|
47
|
+
}
|
|
48
|
+
async update(session) {
|
|
49
|
+
const record = toSessionRecord(session);
|
|
50
|
+
this.sessions.set(session.id, record);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Clean up expired sessions
|
|
54
|
+
*/
|
|
55
|
+
async cleanup() {
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
for (const [id, record] of this.sessions) {
|
|
58
|
+
if (now > record.expiresAt) {
|
|
59
|
+
this.sessions.delete(id);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Stop the cleanup timer
|
|
65
|
+
*/
|
|
66
|
+
dispose() {
|
|
67
|
+
if (this.cleanupTimer) {
|
|
68
|
+
clearInterval(this.cleanupTimer);
|
|
69
|
+
this.cleanupTimer = void 0;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Create a new federated auth session
|
|
74
|
+
*/
|
|
75
|
+
createSession(params) {
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
return {
|
|
78
|
+
id: randomUUID(),
|
|
79
|
+
pendingAuthId: params.pendingAuthId,
|
|
80
|
+
clientId: params.clientId,
|
|
81
|
+
redirectUri: params.redirectUri,
|
|
82
|
+
scopes: params.scopes,
|
|
83
|
+
state: params.state,
|
|
84
|
+
resource: params.resource,
|
|
85
|
+
userInfo: params.userInfo,
|
|
86
|
+
frontmcpPkce: params.frontmcpPkce,
|
|
87
|
+
providerQueue: [...params.providerIds],
|
|
88
|
+
completedProviders: /* @__PURE__ */ new Map(),
|
|
89
|
+
skippedProviders: [],
|
|
90
|
+
signinBinding: params.signinBinding,
|
|
91
|
+
createdAt: now,
|
|
92
|
+
expiresAt: now + this.sessionTtlMs
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get count (for testing/monitoring)
|
|
97
|
+
*/
|
|
98
|
+
get size() {
|
|
99
|
+
return this.sessions.size;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Clear all sessions (for testing)
|
|
103
|
+
*/
|
|
104
|
+
clear() {
|
|
105
|
+
this.sessions.clear();
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
function createFederatedAuthSession(params, ttlMs = 15 * 60 * 1e3) {
|
|
109
|
+
const now = Date.now();
|
|
110
|
+
return {
|
|
111
|
+
id: randomUUID(),
|
|
112
|
+
pendingAuthId: params.pendingAuthId,
|
|
113
|
+
clientId: params.clientId,
|
|
114
|
+
redirectUri: params.redirectUri,
|
|
115
|
+
scopes: params.scopes,
|
|
116
|
+
state: params.state,
|
|
117
|
+
resource: params.resource,
|
|
118
|
+
userInfo: params.userInfo,
|
|
119
|
+
frontmcpPkce: params.frontmcpPkce,
|
|
120
|
+
providerQueue: [...params.providerIds],
|
|
121
|
+
completedProviders: /* @__PURE__ */ new Map(),
|
|
122
|
+
skippedProviders: [],
|
|
123
|
+
signinBinding: params.signinBinding,
|
|
124
|
+
createdAt: now,
|
|
125
|
+
expiresAt: now + ttlMs
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function isSessionComplete(session) {
|
|
129
|
+
return session.providerQueue.length === 0 && !session.currentProviderId;
|
|
130
|
+
}
|
|
131
|
+
function getNextProvider(session) {
|
|
132
|
+
if (session.currentProviderId) {
|
|
133
|
+
return session.currentProviderId;
|
|
134
|
+
}
|
|
135
|
+
return session.providerQueue[0];
|
|
136
|
+
}
|
|
137
|
+
function completeCurrentProvider(session, tokens, userInfo) {
|
|
138
|
+
if (!session.currentProviderId) {
|
|
139
|
+
throw new AuthFlowError("No current provider to complete", "AUTH_FLOW_ERROR");
|
|
140
|
+
}
|
|
141
|
+
session.completedProviders.set(session.currentProviderId, {
|
|
142
|
+
providerId: session.currentProviderId,
|
|
143
|
+
tokens,
|
|
144
|
+
userInfo,
|
|
145
|
+
completedAt: Date.now()
|
|
146
|
+
});
|
|
147
|
+
session.currentProviderId = void 0;
|
|
148
|
+
session.currentProviderPkce = void 0;
|
|
149
|
+
session.currentProviderState = void 0;
|
|
150
|
+
}
|
|
151
|
+
function startNextProvider(session, pkce, state) {
|
|
152
|
+
if (session.currentProviderId) {
|
|
153
|
+
throw new AuthFlowError("Cannot start next provider while current is in progress", "AUTH_FLOW_ERROR");
|
|
154
|
+
}
|
|
155
|
+
if (session.providerQueue.length === 0) {
|
|
156
|
+
throw new AuthFlowError("No more providers in queue", "AUTH_FLOW_ERROR");
|
|
157
|
+
}
|
|
158
|
+
const providerId = session.providerQueue.shift();
|
|
159
|
+
if (!providerId) {
|
|
160
|
+
throw new AuthFlowError("No more providers in queue", "AUTH_FLOW_ERROR");
|
|
161
|
+
}
|
|
162
|
+
session.currentProviderId = providerId;
|
|
163
|
+
session.currentProviderPkce = pkce;
|
|
164
|
+
session.currentProviderState = state;
|
|
165
|
+
return providerId;
|
|
166
|
+
}
|
|
167
|
+
export {
|
|
168
|
+
InMemoryFederatedAuthSessionStore,
|
|
169
|
+
completeCurrentProvider,
|
|
170
|
+
createFederatedAuthSession,
|
|
171
|
+
fromSessionRecord,
|
|
172
|
+
getNextProvider,
|
|
173
|
+
isSessionComplete,
|
|
174
|
+
startNextProvider,
|
|
175
|
+
toSessionRecord
|
|
176
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// libs/auth/src/session/incremental-auth-ticket.ts
|
|
2
|
+
import { randomUUID, signData, verifyData } from "@frontmcp/utils";
|
|
3
|
+
var DEFAULT_INCREMENTAL_TICKET_TTL_MS = 3e5;
|
|
4
|
+
function signIncrementalAuthTicket(input, secret) {
|
|
5
|
+
const payload = {
|
|
6
|
+
sub: input.sub,
|
|
7
|
+
appId: input.appId,
|
|
8
|
+
...input.toolId !== void 0 ? { toolId: input.toolId } : {},
|
|
9
|
+
...input.priorAppIds !== void 0 ? { priorAppIds: input.priorAppIds } : {},
|
|
10
|
+
jti: randomUUID(),
|
|
11
|
+
exp: Date.now() + (input.ttlMs ?? DEFAULT_INCREMENTAL_TICKET_TTL_MS)
|
|
12
|
+
};
|
|
13
|
+
const signedJson = signData(payload, { secret });
|
|
14
|
+
return Buffer.from(signedJson, "utf8").toString("base64url");
|
|
15
|
+
}
|
|
16
|
+
function verifyIncrementalAuthTicket(token, secret, now = Date.now()) {
|
|
17
|
+
let signedJson;
|
|
18
|
+
try {
|
|
19
|
+
signedJson = Buffer.from(token, "base64url").toString("utf8");
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const payload = verifyData(signedJson, { secret });
|
|
24
|
+
if (!payload) return null;
|
|
25
|
+
if (typeof payload.sub !== "string" || payload.sub.length === 0) return null;
|
|
26
|
+
if (typeof payload.appId !== "string" || payload.appId.length === 0) return null;
|
|
27
|
+
if (typeof payload.jti !== "string" || payload.jti.length === 0) return null;
|
|
28
|
+
if (!Number.isFinite(payload.exp)) return null;
|
|
29
|
+
if (payload.toolId !== void 0 && typeof payload.toolId !== "string") return null;
|
|
30
|
+
if (payload.priorAppIds !== void 0 && (!Array.isArray(payload.priorAppIds) || payload.priorAppIds.some((id) => typeof id !== "string"))) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (now >= payload.exp) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return payload;
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
DEFAULT_INCREMENTAL_TICKET_TTL_MS,
|
|
40
|
+
signIncrementalAuthTicket,
|
|
41
|
+
verifyIncrementalAuthTicket
|
|
42
|
+
};
|