@frontmcp/auth 1.11.1-rc.1 → 1.11.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/errors/auth-internal.errors.mjs +6 -2
- package/browser/index.mjs +2 -0
- package/browser/jwks/jwks.service.mjs +53 -13
- package/browser/session/credentials-accessor.impl.mjs +13 -9
- package/browser/session/session-credential-vault.mjs +8 -0
- package/browser/vault/auth-providers.accessor.impl.mjs +3 -2
- package/browser/vault/auth-providers.vault.mjs +98 -55
- package/browser/vault/credential-loaders/lazy-loader.mjs +13 -8
- package/browser/vault/index.mjs +6 -1
- package/errors/auth-internal.errors.d.ts +2 -2
- package/errors/auth-internal.errors.d.ts.map +1 -1
- package/esm/index.mjs +3 -3
- package/esm/package.json +5 -5
- package/index.d.ts +2 -2
- package/index.d.ts.map +1 -1
- package/index.js +3 -3
- package/jwks/index.d.ts +1 -1
- package/jwks/index.d.ts.map +1 -1
- package/jwks/jwks.service.d.ts.map +1 -1
- package/jwks/jwks.types.d.ts +7 -0
- package/jwks/jwks.types.d.ts.map +1 -1
- package/options/interfaces.d.ts +2 -1
- package/options/interfaces.d.ts.map +1 -1
- package/package.json +5 -5
- package/session/authorization-vault.d.ts +2 -2
- package/session/credentials-accessor.d.ts +8 -2
- package/session/credentials-accessor.d.ts.map +1 -1
- package/session/session-credential-vault.d.ts +2 -0
- package/session/session-credential-vault.d.ts.map +1 -1
- package/vault/auth-providers.accessor.impl.d.ts +3 -3
- package/vault/auth-providers.accessor.impl.d.ts.map +1 -1
- package/vault/auth-providers.types.d.ts +1 -1
- package/vault/auth-providers.types.d.ts.map +1 -1
- package/vault/auth-providers.vault.d.ts +33 -14
- package/vault/auth-providers.vault.d.ts.map +1 -1
- package/vault/credential-loaders/lazy-loader.d.ts +8 -7
- package/vault/credential-loaders/lazy-loader.d.ts.map +1 -1
- package/vault/index.d.ts +1 -1
- package/vault/index.d.ts.map +1 -1
|
@@ -64,8 +64,12 @@ var CredentialProviderAlreadyRegisteredError = class extends AuthInternalError {
|
|
|
64
64
|
}
|
|
65
65
|
};
|
|
66
66
|
var AuthProvidersNotConfiguredError = class extends AuthInternalError {
|
|
67
|
-
constructor() {
|
|
68
|
-
|
|
67
|
+
constructor(providerName) {
|
|
68
|
+
const register = "Register it in @FrontMcp({ authProvidersVault: { providers: [...] } }).";
|
|
69
|
+
super(
|
|
70
|
+
providerName === void 0 ? "Auth providers are not configured: this server registers no auth providers." : `Auth provider "${providerName}" is not configured: this server registers no auth providers. ${register}`,
|
|
71
|
+
"AUTH_PROVIDERS_NOT_CONFIGURED"
|
|
72
|
+
);
|
|
69
73
|
}
|
|
70
74
|
};
|
|
71
75
|
var OrchestratedAuthNotAvailableError = class extends AuthInternalError {
|
package/browser/index.mjs
CHANGED
|
@@ -242,6 +242,7 @@ import {
|
|
|
242
242
|
AUTH_PROVIDERS_REGISTRY,
|
|
243
243
|
AuthProvidersVault,
|
|
244
244
|
AUTH_PROVIDERS_VAULT,
|
|
245
|
+
UNIDENTIFIED_SESSION_PREFIX,
|
|
245
246
|
EagerCredentialLoader,
|
|
246
247
|
LazyCredentialLoader
|
|
247
248
|
} from "./vault/index.mjs";
|
|
@@ -383,6 +384,7 @@ export {
|
|
|
383
384
|
TransparentAuthorization,
|
|
384
385
|
TransportIdGenerator,
|
|
385
386
|
TypedStorage,
|
|
387
|
+
UNIDENTIFIED_SESSION_PREFIX,
|
|
386
388
|
VaultEncryption,
|
|
387
389
|
VaultLoadError,
|
|
388
390
|
VaultNotFoundError,
|
|
@@ -42,6 +42,40 @@ function isMissingExpError(error) {
|
|
|
42
42
|
const { code, claim, reason } = error;
|
|
43
43
|
return code === "ERR_JWT_CLAIM_VALIDATION_FAILED" && claim === "exp" && reason === "missing";
|
|
44
44
|
}
|
|
45
|
+
var FAILURE_DEPTH = [
|
|
46
|
+
"jwks_unavailable",
|
|
47
|
+
"malformed",
|
|
48
|
+
"unsupported_algorithm",
|
|
49
|
+
"unknown_key",
|
|
50
|
+
"bad_signature",
|
|
51
|
+
"missing_claim",
|
|
52
|
+
"wrong_issuer",
|
|
53
|
+
"not_yet_valid",
|
|
54
|
+
"expired"
|
|
55
|
+
];
|
|
56
|
+
function tokenVerifyFailureOf(error) {
|
|
57
|
+
const { code, claim, reason } = typeof error === "object" && error !== null ? error : {};
|
|
58
|
+
switch (code) {
|
|
59
|
+
case "ERR_JWT_EXPIRED":
|
|
60
|
+
return "expired";
|
|
61
|
+
case "ERR_JWS_SIGNATURE_VERIFICATION_FAILED":
|
|
62
|
+
return "bad_signature";
|
|
63
|
+
case "ERR_JWKS_NO_MATCHING_KEY":
|
|
64
|
+
return "unknown_key";
|
|
65
|
+
case "ERR_JOSE_ALG_NOT_ALLOWED":
|
|
66
|
+
case "ERR_JOSE_NOT_SUPPORTED":
|
|
67
|
+
return "unsupported_algorithm";
|
|
68
|
+
case "ERR_JWT_CLAIM_VALIDATION_FAILED":
|
|
69
|
+
if (claim === "iss") return "wrong_issuer";
|
|
70
|
+
if (reason === "missing") return "missing_claim";
|
|
71
|
+
return claim === "nbf" && reason === "check_failed" ? "not_yet_valid" : "malformed";
|
|
72
|
+
default:
|
|
73
|
+
return "malformed";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function deeperFailure(current, next) {
|
|
77
|
+
return current && FAILURE_DEPTH.indexOf(current) >= FAILURE_DEPTH.indexOf(next) ? current : next;
|
|
78
|
+
}
|
|
45
79
|
var WEAK_KEY_WARNING = `
|
|
46
80
|
\u26A0\uFE0F SECURITY WARNING: OAuth provider is using an RSA key smaller than 2048 bits.
|
|
47
81
|
This is considered insecure and should be updated.
|
|
@@ -115,6 +149,7 @@ var JwksService = class {
|
|
|
115
149
|
} catch {
|
|
116
150
|
}
|
|
117
151
|
let missingExp = false;
|
|
152
|
+
let failure;
|
|
118
153
|
for (const p of candidates) {
|
|
119
154
|
let jwks;
|
|
120
155
|
try {
|
|
@@ -122,7 +157,10 @@ var JwksService = class {
|
|
|
122
157
|
if (kid && !p.jwks?.keys?.length && !jwks?.keys?.some((key) => key.kid === kid)) {
|
|
123
158
|
jwks = await this.refetchForUnknownKid(p) ?? jwks;
|
|
124
159
|
}
|
|
125
|
-
if (!jwks?.keys?.length)
|
|
160
|
+
if (!jwks?.keys?.length) {
|
|
161
|
+
failure = deeperFailure(failure, "jwks_unavailable");
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
126
164
|
const JWKS = createLocalJWKSet(jwks);
|
|
127
165
|
const issuerConstraint = this.issuerConstraintFor(p);
|
|
128
166
|
const { payload, protectedHeader } = await jwtVerify(token, JWKS, {
|
|
@@ -150,14 +188,16 @@ var JwksService = class {
|
|
|
150
188
|
return fallbackResult;
|
|
151
189
|
}
|
|
152
190
|
if (fallbackResult.error === "missing_exp") missingExp = true;
|
|
191
|
+
if (fallbackResult.failure) failure = deeperFailure(failure, fallbackResult.failure);
|
|
153
192
|
}
|
|
154
193
|
}
|
|
155
194
|
if (isMissingExpError(e)) missingExp = true;
|
|
195
|
+
failure = deeperFailure(failure, tokenVerifyFailureOf(e));
|
|
156
196
|
this.logger.debug("[JwksService] Failed to verify token for provider: %s", p.id, e);
|
|
157
197
|
}
|
|
158
198
|
}
|
|
159
|
-
if (missingExp) return { ok: false, error: MISSING_EXP_ERROR };
|
|
160
|
-
return { ok: false, error: `no_provider_verified${kid ? ` (kid=${kid})` : ""}
|
|
199
|
+
if (missingExp) return { ok: false, error: MISSING_EXP_ERROR, failure };
|
|
200
|
+
return { ok: false, error: `no_provider_verified${kid ? ` (kid=${kid})` : ""}`, failure };
|
|
161
201
|
}
|
|
162
202
|
/**
|
|
163
203
|
* Issuer constraint passed to `jose` for a provider: the configured issuer
|
|
@@ -199,20 +239,20 @@ var JwksService = class {
|
|
|
199
239
|
try {
|
|
200
240
|
const parts = token.split(".");
|
|
201
241
|
if (parts.length !== 3) {
|
|
202
|
-
return { ok: false, error: "invalid_token_format" };
|
|
242
|
+
return { ok: false, error: "invalid_token_format", failure: "malformed" };
|
|
203
243
|
}
|
|
204
244
|
const [headerB64, payloadB64, signatureB64] = parts;
|
|
205
245
|
const header = JSON.parse(Buffer.from(headerB64, "base64url").toString("utf8"));
|
|
206
246
|
const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
|
|
207
247
|
const matchingKey = this.findMatchingKey(jwks, header);
|
|
208
248
|
if (!matchingKey) {
|
|
209
|
-
return { ok: false, error: "no_matching_key" };
|
|
249
|
+
return { ok: false, error: "no_matching_key", failure: "unknown_key" };
|
|
210
250
|
}
|
|
211
251
|
const signatureInput = `${headerB64}.${payloadB64}`;
|
|
212
252
|
const signature = Buffer.from(signatureB64, "base64url");
|
|
213
253
|
const jwtAlg = typeof header.alg === "string" ? header.alg : void 0;
|
|
214
254
|
if (!jwtAlg) {
|
|
215
|
-
return { ok: false, error: "missing_alg" };
|
|
255
|
+
return { ok: false, error: "missing_alg", failure: "malformed" };
|
|
216
256
|
}
|
|
217
257
|
const isValid = await rsaVerify(
|
|
218
258
|
jwtAlg,
|
|
@@ -221,30 +261,30 @@ var JwksService = class {
|
|
|
221
261
|
signature
|
|
222
262
|
);
|
|
223
263
|
if (!isValid) {
|
|
224
|
-
return { ok: false, error: "signature_invalid" };
|
|
264
|
+
return { ok: false, error: "signature_invalid", failure: "bad_signature" };
|
|
225
265
|
}
|
|
226
266
|
if (provider.verifyIssuer !== false) {
|
|
227
267
|
const payloadIssuerRaw = typeof payload.iss === "string" ? payload.iss : void 0;
|
|
228
268
|
if (!payloadIssuerRaw) {
|
|
229
|
-
return { ok: false, error: "issuer_mismatch" };
|
|
269
|
+
return { ok: false, error: "issuer_mismatch", failure: "wrong_issuer" };
|
|
230
270
|
}
|
|
231
271
|
const trustedIssuers = new Set(
|
|
232
272
|
[provider.issuerUrl, ...provider.additionalIssuers ?? []].map((issuer) => normalizeIssuer(issuer))
|
|
233
273
|
);
|
|
234
274
|
const payloadIssuer = normalizeIssuer(payloadIssuerRaw);
|
|
235
275
|
if (!trustedIssuers.has(payloadIssuer)) {
|
|
236
|
-
return { ok: false, error: "issuer_mismatch" };
|
|
276
|
+
return { ok: false, error: "issuer_mismatch", failure: "wrong_issuer" };
|
|
237
277
|
}
|
|
238
278
|
}
|
|
239
279
|
const nowSec = Math.floor(Date.now() / 1e3);
|
|
240
280
|
if (payload.exp === void 0) {
|
|
241
|
-
return { ok: false, error: "missing_exp" };
|
|
281
|
+
return { ok: false, error: "missing_exp", failure: "missing_claim" };
|
|
242
282
|
}
|
|
243
283
|
if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp) || payload.exp <= nowSec) {
|
|
244
|
-
return { ok: false, error: "token_expired" };
|
|
284
|
+
return { ok: false, error: "token_expired", failure: "expired" };
|
|
245
285
|
}
|
|
246
286
|
if (payload.nbf !== void 0 && (typeof payload.nbf !== "number" || !Number.isFinite(payload.nbf) || payload.nbf > nowSec)) {
|
|
247
|
-
return { ok: false, error: "token_not_yet_valid" };
|
|
287
|
+
return { ok: false, error: "token_not_yet_valid", failure: "not_yet_valid" };
|
|
248
288
|
}
|
|
249
289
|
if (!this.warnedProviders.has(provider.id)) {
|
|
250
290
|
this.warnedProviders.add(provider.id);
|
|
@@ -261,7 +301,7 @@ var JwksService = class {
|
|
|
261
301
|
};
|
|
262
302
|
} catch (err) {
|
|
263
303
|
const message = err instanceof Error ? err.message : "unknown";
|
|
264
|
-
return { ok: false, error: `weak_key_verification_failed: ${message}
|
|
304
|
+
return { ok: false, error: `weak_key_verification_failed: ${message}`, failure: "malformed" };
|
|
265
305
|
}
|
|
266
306
|
}
|
|
267
307
|
/**
|
|
@@ -29,16 +29,20 @@ var CredentialsAccessorImpl = class {
|
|
|
29
29
|
async requireConnect(options) {
|
|
30
30
|
const { key, context } = options;
|
|
31
31
|
const sub = this.resolveSub();
|
|
32
|
-
if (sub) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
if (!sub) {
|
|
33
|
+
return {
|
|
34
|
+
connected: false,
|
|
35
|
+
key,
|
|
36
|
+
resumeUrl: "",
|
|
37
|
+
signInRequired: true,
|
|
38
|
+
message: `Sign in to this server to connect credential "${key}".`
|
|
39
|
+
};
|
|
37
40
|
}
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
const existing = await this.vault.get(sub, key);
|
|
42
|
+
if (existing) {
|
|
43
|
+
return { connected: true, key, credential: existing };
|
|
44
|
+
}
|
|
45
|
+
const token = signCredentialResumeToken({ sub, key, context, ttlMs: this.resumeTtlMs }, this.signingSecret);
|
|
42
46
|
const resumeUrl = buildCredentialResumeUrl(this.basePath, token);
|
|
43
47
|
this.logger.debug(`CredentialsAccessor: credential "${key}" not connected; issued resume link`);
|
|
44
48
|
return {
|
|
@@ -83,6 +83,14 @@ var SessionCredentialVault = class {
|
|
|
83
83
|
await this.storage.set(this.indexKey(sub), vaultId, this.credentialSetOpts);
|
|
84
84
|
return vaultId;
|
|
85
85
|
}
|
|
86
|
+
/** The subject's vaultId, or a new vault when it has none; unlike {@link rotateVault}, it never replaces one. */
|
|
87
|
+
async ensureVault(sub) {
|
|
88
|
+
const currentVaultId = await this.getVaultId(sub);
|
|
89
|
+
if (currentVaultId) return currentVaultId;
|
|
90
|
+
const createdVaultId = randomUUID();
|
|
91
|
+
await this.storage.set(this.indexKey(sub), createdVaultId, { ...this.credentialSetOpts, ifNotExists: true });
|
|
92
|
+
return await this.getVaultId(sub) ?? createdVaultId;
|
|
93
|
+
}
|
|
86
94
|
// ============================================
|
|
87
95
|
// Credential CRUD (committed vault)
|
|
88
96
|
// ============================================
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// libs/auth/src/vault/auth-providers.accessor.impl.ts
|
|
2
|
-
import { extractCredentialExpiry } from "./credential-helpers.mjs";
|
|
3
2
|
import { base64Encode } from "@frontmcp/utils";
|
|
3
|
+
import { extractCredentialExpiry } from "./credential-helpers.mjs";
|
|
4
4
|
var AuthProvidersAccessorImpl = class {
|
|
5
5
|
constructor(registry, vault, cache, loader, context, logger) {
|
|
6
6
|
this.registry = registry;
|
|
@@ -180,7 +180,8 @@ var AuthProvidersAccessorImpl = class {
|
|
|
180
180
|
providerName,
|
|
181
181
|
resolved.credential,
|
|
182
182
|
config.scope,
|
|
183
|
-
this.context.userSub
|
|
183
|
+
this.context.userSub,
|
|
184
|
+
config.cacheTtl
|
|
184
185
|
);
|
|
185
186
|
} catch (error) {
|
|
186
187
|
this.logger?.warn(`Failed to store in vault "${providerName}":`, error);
|
|
@@ -1,26 +1,52 @@
|
|
|
1
1
|
// libs/auth/src/vault/auth-providers.vault.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
EncryptedTypedStorage,
|
|
4
|
+
hkdfSha256,
|
|
5
|
+
MAX_TTL_SECONDS,
|
|
6
|
+
randomBytes,
|
|
7
|
+
sha256Hex
|
|
8
|
+
} from "@frontmcp/utils";
|
|
3
9
|
import { AuthInvalidInputError, CredentialStorageError } from "../errors/auth-internal.errors.mjs";
|
|
10
|
+
import { extractCredentialExpiry } from "./credential-helpers.mjs";
|
|
11
|
+
var UNIDENTIFIED_SESSION_PREFIX = "unidentified:";
|
|
12
|
+
var DEFAULT_CREDENTIAL_TTL_MS = 36e5;
|
|
13
|
+
var ENCRYPTION_KEY_INFO = new TextEncoder().encode("frontmcp-auth-providers-vault-v1");
|
|
14
|
+
function positiveOr(value, fallback) {
|
|
15
|
+
return value !== void 0 && value > 0 ? value : fallback;
|
|
16
|
+
}
|
|
17
|
+
function encryptionKeyFrom(pepper) {
|
|
18
|
+
if (!pepper) return randomBytes(32);
|
|
19
|
+
return hkdfSha256(new TextEncoder().encode(pepper), new Uint8Array(0), ENCRYPTION_KEY_INFO, 32);
|
|
20
|
+
}
|
|
4
21
|
var AuthProvidersVault = class {
|
|
5
|
-
|
|
6
|
-
this.baseVault = baseVault;
|
|
7
|
-
this.namespace = namespace;
|
|
8
|
-
this.logger = logger;
|
|
9
|
-
}
|
|
10
|
-
baseVault;
|
|
22
|
+
credentials;
|
|
11
23
|
namespace;
|
|
24
|
+
defaultCacheTtl;
|
|
12
25
|
logger;
|
|
26
|
+
constructor(storage, options = {}) {
|
|
27
|
+
this.credentials = new EncryptedTypedStorage(storage, {
|
|
28
|
+
keys: [{ kid: "authproviders-v1", key: encryptionKeyFrom(options.pepper) }]
|
|
29
|
+
});
|
|
30
|
+
this.namespace = options.namespace ?? "authproviders:";
|
|
31
|
+
this.defaultCacheTtl = positiveOr(options.defaultCacheTtl, DEFAULT_CREDENTIAL_TTL_MS);
|
|
32
|
+
this.logger = options.logger;
|
|
33
|
+
}
|
|
13
34
|
/**
|
|
14
|
-
* Store a credential in the vault
|
|
35
|
+
* Store a credential in the vault, until it expires; one without an expiry for `cacheTtl`, else `defaultCacheTtl`
|
|
15
36
|
*
|
|
16
37
|
* @param sessionId - Current session ID
|
|
17
38
|
* @param providerId - Provider name
|
|
18
39
|
* @param credential - Credential to store
|
|
19
40
|
* @param scope - Credential scope
|
|
20
41
|
* @param userId - User ID (required for user scope)
|
|
42
|
+
* @param cacheTtl - How long to keep a credential without an expiry, in ms
|
|
21
43
|
*/
|
|
22
|
-
async storeCredential(sessionId, providerId, credential, scope, userId) {
|
|
23
|
-
|
|
44
|
+
async storeCredential(sessionId, providerId, credential, scope, userId, cacheTtl) {
|
|
45
|
+
if (scope === "session" && sessionId.startsWith(UNIDENTIFIED_SESSION_PREFIX)) {
|
|
46
|
+
this.logger?.debug(`Not storing credential "${providerId}": the caller has no verified session`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const key = this.credentialKey(sessionId, providerId, scope, userId);
|
|
24
50
|
const appCredential = {
|
|
25
51
|
appId: this.namespace,
|
|
26
52
|
providerId,
|
|
@@ -30,7 +56,7 @@ var AuthProvidersVault = class {
|
|
|
30
56
|
expiresAt: extractCredentialExpiry(credential)
|
|
31
57
|
};
|
|
32
58
|
try {
|
|
33
|
-
await this.
|
|
59
|
+
await this.write(key, appCredential, cacheTtl);
|
|
34
60
|
this.logger?.debug(`Stored credential "${providerId}" with scope "${scope}"`);
|
|
35
61
|
} catch (error) {
|
|
36
62
|
this.logger?.warn(`Failed to store credential "${providerId}":`, error);
|
|
@@ -47,9 +73,9 @@ var AuthProvidersVault = class {
|
|
|
47
73
|
* @returns Credential or null if not found
|
|
48
74
|
*/
|
|
49
75
|
async getCredential(sessionId, providerId, scope, userId) {
|
|
50
|
-
const
|
|
76
|
+
const key = this.credentialKey(sessionId, providerId, scope, userId);
|
|
51
77
|
try {
|
|
52
|
-
const appCredential = await this.
|
|
78
|
+
const appCredential = await this.read(key);
|
|
53
79
|
if (!appCredential) {
|
|
54
80
|
return null;
|
|
55
81
|
}
|
|
@@ -81,9 +107,9 @@ var AuthProvidersVault = class {
|
|
|
81
107
|
* @param userId - User ID (required for user scope)
|
|
82
108
|
*/
|
|
83
109
|
async removeCredential(sessionId, providerId, scope, userId) {
|
|
84
|
-
const
|
|
110
|
+
const key = this.credentialKey(sessionId, providerId, scope, userId);
|
|
85
111
|
try {
|
|
86
|
-
await this.
|
|
112
|
+
await this.credentials.delete(key);
|
|
87
113
|
this.logger?.debug(`Removed credential "${providerId}"`);
|
|
88
114
|
} catch (error) {
|
|
89
115
|
this.logger?.warn(`Failed to remove credential "${providerId}":`, error);
|
|
@@ -99,9 +125,11 @@ var AuthProvidersVault = class {
|
|
|
99
125
|
* @param userId - User ID (required for user scope)
|
|
100
126
|
*/
|
|
101
127
|
async invalidateCredential(sessionId, providerId, scope, reason, userId) {
|
|
102
|
-
const
|
|
128
|
+
const key = this.credentialKey(sessionId, providerId, scope, userId);
|
|
103
129
|
try {
|
|
104
|
-
await this.
|
|
130
|
+
const appCredential = await this.read(key);
|
|
131
|
+
if (!appCredential) return;
|
|
132
|
+
await this.write(key, { ...appCredential, isValid: false, invalidReason: reason });
|
|
105
133
|
this.logger?.debug(`Invalidated credential "${providerId}": ${reason}`);
|
|
106
134
|
} catch (error) {
|
|
107
135
|
this.logger?.warn(`Failed to invalidate credential "${providerId}":`, error);
|
|
@@ -117,9 +145,25 @@ var AuthProvidersVault = class {
|
|
|
117
145
|
* @param userId - User ID (required for user scope)
|
|
118
146
|
*/
|
|
119
147
|
async refreshOAuthCredential(sessionId, providerId, scope, tokens, userId) {
|
|
120
|
-
const
|
|
148
|
+
const key = this.credentialKey(sessionId, providerId, scope, userId);
|
|
121
149
|
try {
|
|
122
|
-
await this.
|
|
150
|
+
const appCredential = await this.read(key);
|
|
151
|
+
if (!appCredential) return;
|
|
152
|
+
const credential = appCredential.credential;
|
|
153
|
+
if (credential.type !== "oauth" && credential.type !== "oauth_pkce") return;
|
|
154
|
+
const refreshed = {
|
|
155
|
+
...appCredential,
|
|
156
|
+
credential: {
|
|
157
|
+
...credential,
|
|
158
|
+
accessToken: tokens.accessToken,
|
|
159
|
+
refreshToken: tokens.refreshToken ?? credential.refreshToken,
|
|
160
|
+
expiresAt: tokens.expiresAt ?? credential.expiresAt
|
|
161
|
+
},
|
|
162
|
+
expiresAt: tokens.expiresAt ?? appCredential.expiresAt,
|
|
163
|
+
isValid: true,
|
|
164
|
+
invalidReason: void 0
|
|
165
|
+
};
|
|
166
|
+
await this.write(key, refreshed);
|
|
123
167
|
this.logger?.debug(`Refreshed OAuth credential "${providerId}"`);
|
|
124
168
|
} catch (error) {
|
|
125
169
|
this.logger?.warn(`Failed to refresh OAuth credential "${providerId}":`, error);
|
|
@@ -135,49 +179,47 @@ var AuthProvidersVault = class {
|
|
|
135
179
|
*/
|
|
136
180
|
async getAllCredentials(sessionId, scope, userId) {
|
|
137
181
|
if (scope) {
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
return await this.baseVault.getAppCredentials(vaultKey, this.namespace);
|
|
141
|
-
} catch {
|
|
142
|
-
return [];
|
|
143
|
-
}
|
|
182
|
+
return this.listCredentials(this.buildVaultKey(sessionId, scope, userId));
|
|
144
183
|
}
|
|
145
|
-
const
|
|
184
|
+
const scopes = userId ? ["session", "user", "global"] : ["session", "global"];
|
|
185
|
+
const credentialsPerScope = await Promise.all(
|
|
186
|
+
scopes.map((eachScope) => this.listCredentials(this.buildVaultKey(sessionId, eachScope, userId)))
|
|
187
|
+
);
|
|
188
|
+
return credentialsPerScope.flat();
|
|
189
|
+
}
|
|
190
|
+
async listCredentials(vaultKey) {
|
|
146
191
|
try {
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
);
|
|
151
|
-
credentials.push(...sessionCreds);
|
|
192
|
+
const keys = await this.credentials.keys(`${vaultKey}:*`);
|
|
193
|
+
const records = await this.credentials.mget(keys);
|
|
194
|
+
return records.flatMap((record, index) => record?.storageKey === keys[index] ? [record.appCredential] : []);
|
|
152
195
|
} catch {
|
|
196
|
+
return [];
|
|
153
197
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const globalCreds = await this.baseVault.getAppCredentials(
|
|
166
|
-
this.buildVaultKey(sessionId, "global"),
|
|
167
|
-
this.namespace
|
|
168
|
-
);
|
|
169
|
-
credentials.push(...globalCreds);
|
|
170
|
-
} catch {
|
|
198
|
+
}
|
|
199
|
+
async read(key) {
|
|
200
|
+
const record = await this.credentials.get(key);
|
|
201
|
+
return record?.storageKey === key ? record.appCredential : null;
|
|
202
|
+
}
|
|
203
|
+
/** Writes until the credential expires, else for `cacheTtl` or `defaultCacheTtl`; an expired one is deleted. */
|
|
204
|
+
async write(key, appCredential, cacheTtl) {
|
|
205
|
+
const ttlMs = appCredential.expiresAt === void 0 ? positiveOr(cacheTtl, this.defaultCacheTtl) : appCredential.expiresAt - Date.now();
|
|
206
|
+
if (ttlMs <= 0) {
|
|
207
|
+
await this.credentials.delete(key);
|
|
208
|
+
return;
|
|
171
209
|
}
|
|
172
|
-
|
|
210
|
+
const ttlSeconds = Math.min(Math.ceil(ttlMs / 1e3), MAX_TTL_SECONDS);
|
|
211
|
+
await this.credentials.set(key, { storageKey: key, appCredential }, { ttlSeconds });
|
|
212
|
+
}
|
|
213
|
+
credentialKey(sessionId, providerId, scope, userId) {
|
|
214
|
+
return `${this.buildVaultKey(sessionId, scope, userId)}:${providerId}`;
|
|
173
215
|
}
|
|
174
216
|
/**
|
|
175
|
-
* Build vault key based on scope
|
|
217
|
+
* Build vault key based on scope; user and session ids are hashed, so no id can reach into another's keys
|
|
176
218
|
*
|
|
177
219
|
* Key patterns:
|
|
178
220
|
* - global: `authproviders:global`
|
|
179
|
-
* - user: `authproviders:user:{userId}`
|
|
180
|
-
* - session: `authproviders:session:{sessionId}`
|
|
221
|
+
* - user: `authproviders:user:{sha256(userId)}`
|
|
222
|
+
* - session: `authproviders:session:{sha256(sessionId)}`
|
|
181
223
|
*/
|
|
182
224
|
buildVaultKey(sessionId, scope, userId) {
|
|
183
225
|
switch (scope) {
|
|
@@ -189,9 +231,9 @@ var AuthProvidersVault = class {
|
|
|
189
231
|
`userId is required for user-scoped credentials (namespace: ${this.namespace})`
|
|
190
232
|
);
|
|
191
233
|
}
|
|
192
|
-
return `${this.namespace}user:${userId}`;
|
|
234
|
+
return `${this.namespace}user:${sha256Hex(userId)}`;
|
|
193
235
|
case "session":
|
|
194
|
-
return `${this.namespace}session:${sessionId}`;
|
|
236
|
+
return `${this.namespace}session:${sha256Hex(sessionId)}`;
|
|
195
237
|
default: {
|
|
196
238
|
const _exhaustive = scope;
|
|
197
239
|
throw new AuthInvalidInputError(`Unknown credential scope: ${_exhaustive}`);
|
|
@@ -202,5 +244,6 @@ var AuthProvidersVault = class {
|
|
|
202
244
|
var AUTH_PROVIDERS_VAULT = /* @__PURE__ */ Symbol.for("frontmcp:AUTH_PROVIDERS_VAULT");
|
|
203
245
|
export {
|
|
204
246
|
AUTH_PROVIDERS_VAULT,
|
|
205
|
-
AuthProvidersVault
|
|
247
|
+
AuthProvidersVault,
|
|
248
|
+
UNIDENTIFIED_SESSION_PREFIX
|
|
206
249
|
};
|
|
@@ -1,32 +1,36 @@
|
|
|
1
1
|
// libs/auth/src/vault/credential-loaders/lazy-loader.ts
|
|
2
2
|
import { extractCredentialExpiry } from "../credential-helpers.mjs";
|
|
3
|
+
function inFlightKey(providerName, context) {
|
|
4
|
+
return JSON.stringify([providerName, context.sessionId, context.userSub ?? null]);
|
|
5
|
+
}
|
|
3
6
|
var LazyCredentialLoader = class {
|
|
4
7
|
constructor(logger) {
|
|
5
8
|
this.logger = logger;
|
|
6
9
|
}
|
|
7
10
|
logger;
|
|
8
|
-
/** In-flight loading promises for deduplication */
|
|
11
|
+
/** In-flight loading promises for deduplication, by provider and caller */
|
|
9
12
|
loading = /* @__PURE__ */ new Map();
|
|
10
13
|
/**
|
|
11
14
|
* Load a credential lazily.
|
|
12
|
-
* If already loading, returns the in-flight promise (deduplication).
|
|
15
|
+
* If already loading for the same caller, returns the in-flight promise (deduplication).
|
|
13
16
|
*
|
|
14
17
|
* @param config - Provider configuration
|
|
15
18
|
* @param context - Factory context
|
|
16
19
|
* @returns Resolved credential or null
|
|
17
20
|
*/
|
|
18
21
|
async load(config, context) {
|
|
19
|
-
const
|
|
22
|
+
const key = inFlightKey(config.name, context);
|
|
23
|
+
const existing = this.loading.get(key);
|
|
20
24
|
if (existing) {
|
|
21
25
|
this.logger?.debug(`Waiting for in-flight load: ${config.name}`);
|
|
22
26
|
return existing;
|
|
23
27
|
}
|
|
24
28
|
const promise = this.doLoad(config, context);
|
|
25
|
-
this.loading.set(
|
|
29
|
+
this.loading.set(key, promise);
|
|
26
30
|
try {
|
|
27
31
|
return await promise;
|
|
28
32
|
} finally {
|
|
29
|
-
this.loading.delete(
|
|
33
|
+
this.loading.delete(key);
|
|
30
34
|
}
|
|
31
35
|
}
|
|
32
36
|
/**
|
|
@@ -100,13 +104,14 @@ var LazyCredentialLoader = class {
|
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
106
|
/**
|
|
103
|
-
* Check if a credential is currently being loaded
|
|
107
|
+
* Check if a credential is currently being loaded for a caller
|
|
104
108
|
*
|
|
105
109
|
* @param name - Provider name
|
|
110
|
+
* @param context - Factory context of the caller
|
|
106
111
|
* @returns true if loading is in progress
|
|
107
112
|
*/
|
|
108
|
-
isLoading(name) {
|
|
109
|
-
return this.loading.has(name);
|
|
113
|
+
isLoading(name, context) {
|
|
114
|
+
return this.loading.has(inFlightKey(name, context));
|
|
110
115
|
}
|
|
111
116
|
/**
|
|
112
117
|
* Cancel all in-flight loads (for cleanup)
|
package/browser/vault/index.mjs
CHANGED
|
@@ -12,7 +12,11 @@ import { CredentialCache } from "./credential-cache.mjs";
|
|
|
12
12
|
import { AUTH_PROVIDERS_ACCESSOR } from "./auth-providers.accessor.mjs";
|
|
13
13
|
import { AuthProvidersAccessorImpl } from "./auth-providers.accessor.impl.mjs";
|
|
14
14
|
import { AuthProvidersRegistry, AUTH_PROVIDERS_REGISTRY } from "./auth-providers.registry.mjs";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
AuthProvidersVault,
|
|
17
|
+
AUTH_PROVIDERS_VAULT,
|
|
18
|
+
UNIDENTIFIED_SESSION_PREFIX
|
|
19
|
+
} from "./auth-providers.vault.mjs";
|
|
16
20
|
import { EagerCredentialLoader } from "./credential-loaders/eager-loader.mjs";
|
|
17
21
|
import { LazyCredentialLoader } from "./credential-loaders/lazy-loader.mjs";
|
|
18
22
|
export {
|
|
@@ -25,6 +29,7 @@ export {
|
|
|
25
29
|
CredentialCache,
|
|
26
30
|
EagerCredentialLoader,
|
|
27
31
|
LazyCredentialLoader,
|
|
32
|
+
UNIDENTIFIED_SESSION_PREFIX,
|
|
28
33
|
authProviderMappingSchema,
|
|
29
34
|
authProvidersVaultOptionsSchema,
|
|
30
35
|
credentialProviderConfigSchema,
|
|
@@ -67,10 +67,10 @@ export declare class CredentialProviderAlreadyRegisteredError extends AuthIntern
|
|
|
67
67
|
constructor(name: string);
|
|
68
68
|
}
|
|
69
69
|
/**
|
|
70
|
-
* Thrown when auth
|
|
70
|
+
* Thrown when a tool reads an auth provider's credential on a server that registers no auth providers.
|
|
71
71
|
*/
|
|
72
72
|
export declare class AuthProvidersNotConfiguredError extends AuthInternalError {
|
|
73
|
-
constructor();
|
|
73
|
+
constructor(providerName?: string);
|
|
74
74
|
}
|
|
75
75
|
/**
|
|
76
76
|
* Thrown when orchestrated auth is not available.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-internal.errors.d.ts","sourceRoot":"","sources":["../../src/errors/auth-internal.errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE1D;;GAEG;AACH,qBAAa,4BAA6B,SAAQ,iBAAiB;;CAIlE;AAED;;GAEG;AACH,qBAAa,cAAe,SAAQ,iBAAiB;IACnD,QAAQ,CAAC,aAAa,CAAC,EAAE,KAAK,CAAC;gBAEnB,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,KAAK;CAInD;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,iBAAiB;gBAC3C,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM;CAG3C;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,iBAAiB;gBAC/C,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,iBAAiB;gBAChD,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,iBAAiB;gBAC1C,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,iBAAiB;gBAC/C,MAAM,EAAE,MAAM;CAG3B;AAED;;GAEG;AACH,qBAAa,0BAA2B,SAAQ,iBAAiB;gBACnD,SAAS,EAAE,MAAM;CAG9B;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,iBAAiB;gBAC/C,IAAI,EAAE,MAAM;CAQzB;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,iBAAiB;gBAC3C,UAAU,EAAE,MAAM;CAQ/B;AAED;;GAEG;AACH,qBAAa,wCAAyC,SAAQ,iBAAiB;gBACjE,IAAI,EAAE,MAAM;CAGzB;AAED;;GAEG;AACH,qBAAa,+BAAgC,SAAQ,iBAAiB
|
|
1
|
+
{"version":3,"file":"auth-internal.errors.d.ts","sourceRoot":"","sources":["../../src/errors/auth-internal.errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE1D;;GAEG;AACH,qBAAa,4BAA6B,SAAQ,iBAAiB;;CAIlE;AAED;;GAEG;AACH,qBAAa,cAAe,SAAQ,iBAAiB;IACnD,QAAQ,CAAC,aAAa,CAAC,EAAE,KAAK,CAAC;gBAEnB,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,KAAK;CAInD;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,iBAAiB;gBAC3C,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM;CAG3C;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,iBAAiB;gBAC/C,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,uBAAwB,SAAQ,iBAAiB;gBAChD,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,iBAAiB;gBAC1C,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,iBAAiB;gBAC/C,MAAM,EAAE,MAAM;CAG3B;AAED;;GAEG;AACH,qBAAa,0BAA2B,SAAQ,iBAAiB;gBACnD,SAAS,EAAE,MAAM;CAG9B;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,iBAAiB;gBAC/C,IAAI,EAAE,MAAM;CAQzB;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,iBAAiB;gBAC3C,UAAU,EAAE,MAAM;CAQ/B;AAED;;GAEG;AACH,qBAAa,wCAAyC,SAAQ,iBAAiB;gBACjE,IAAI,EAAE,MAAM;CAGzB;AAED;;GAEG;AACH,qBAAa,+BAAgC,SAAQ,iBAAiB;gBACxD,YAAY,CAAC,EAAE,MAAM;CASlC;AAED;;GAEG;AACH,qBAAa,iCAAkC,SAAQ,iBAAiB;;CAIvE;AAED;;GAEG;AACH,qBAAa,+BAAgC,SAAQ,iBAAiB;;CAIrE;AAED;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,iBAAiB;gBAC5C,SAAS,EAAE,MAAM;CAG9B;AAED;;GAEG;AACH,qBAAa,8BAA+B,SAAQ,iBAAiB;;CAIpE;AAED;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,iBAAiB;gBACzC,UAAU,EAAE,MAAM;CAG/B;AAED;;GAEG;AACH,qBAAa,0BAA2B,SAAQ,iBAAiB;gBACnD,SAAS,EAAE,MAAM;CAG9B;AAED;;GAEG;AACH,qBAAa,iCAAkC,SAAQ,iBAAiB;;CAIvE;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,iBAAiB;gBAC/C,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,iBAAiB;gBAC9C,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,iBAAiB;gBAC/C,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,aAAc,SAAQ,iBAAiB;gBACtC,OAAO,EAAE,MAAM,EAAE,IAAI,SAAoB;CAGtD"}
|