@frontmcp/auth 1.10.16 → 1.11.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/browser/authorities/authorities.context.mjs +115 -0
- package/browser/authorities/authorities.engine.mjs +272 -0
- package/browser/authorities/authorities.errors.mjs +59 -0
- package/browser/authorities/authorities.evaluator.mjs +225 -0
- package/browser/authorities/authorities.metadata-augment.mjs +18 -0
- package/browser/authorities/authorities.registry.mjs +112 -0
- package/browser/authorities/authorities.schema.mjs +92 -0
- package/browser/authorities/authorities.scope-mapping.mjs +30 -0
- package/browser/authorities/authorities.validation.mjs +208 -0
- package/browser/authorities/index.mjs +43 -0
- package/browser/authorization/authorization.class.mjs +252 -0
- package/browser/authorization/authorization.types.mjs +64 -0
- package/browser/authorization/dcr-client.registry.mjs +222 -0
- package/browser/authorization/index.mjs +41 -0
- package/browser/authorization/orchestrated.accessor.mjs +86 -0
- package/browser/authorization/orchestrated.authorization.mjs +289 -0
- package/browser/authorization/public.authorization.mjs +124 -0
- package/browser/authorization/transparent.authorization.mjs +130 -0
- package/browser/cimd/cimd-redis.cache.mjs +231 -0
- package/browser/cimd/cimd.cache.mjs +215 -0
- package/browser/cimd/cimd.errors.mjs +142 -0
- package/browser/cimd/cimd.logger.mjs +15 -0
- package/browser/cimd/cimd.service.mjs +376 -0
- package/browser/cimd/cimd.types.mjs +228 -0
- package/browser/cimd/cimd.validator.mjs +217 -0
- package/browser/cimd/index.mjs +63 -0
- package/browser/common/auth-logger.interface.mjs +14 -0
- package/browser/common/index.mjs +6 -0
- package/browser/common/jwt.types.mjs +38 -0
- package/browser/common/session-user.types.mjs +0 -0
- package/browser/common/session.types.mjs +58 -0
- package/browser/common/zod-utils.mjs +0 -0
- package/browser/consent/consent.types.mjs +94 -0
- package/browser/consent/index.mjs +17 -0
- package/browser/context/frontmcp-auth-context.factory.mjs +29 -0
- package/browser/context/frontmcp-auth-context.impl.mjs +112 -0
- package/browser/context/index.mjs +7 -0
- package/browser/detection/auth-provider-detection.mjs +176 -0
- package/browser/detection/index.mjs +19 -0
- package/browser/errors/auth-internal.error.mjs +42 -0
- package/browser/errors/auth-internal.errors.mjs +150 -0
- package/browser/errors/index.mjs +53 -0
- package/browser/fetch/fetch-credential-middleware.mjs +91 -0
- package/browser/fetch/index.mjs +19 -0
- package/browser/index.mjs +573 -0
- package/browser/jwks/index.mjs +9 -0
- package/browser/jwks/jose.loader.mjs +28 -0
- package/browser/jwks/jwks.service.mjs +521 -0
- package/browser/jwks/jwks.utils.mjs +33 -0
- package/browser/options/app-auth.schema.mjs +18 -0
- package/browser/options/index.mjs +89 -0
- package/browser/options/orchestrated.schema.mjs +128 -0
- package/browser/options/public.schema.mjs +39 -0
- package/browser/options/schema.mjs +16 -0
- package/browser/options/shared.schemas.mjs +506 -0
- package/browser/options/static.schema.mjs +41 -0
- package/browser/options/transparent.schema.mjs +43 -0
- package/browser/options/utils.mjs +47 -0
- package/browser/people/index.mjs +15 -0
- package/browser/people/loopback-token.auth.mjs +115 -0
- package/browser/people/people-auth.options.mjs +37 -0
- package/browser/session/authorization-vault.mjs +279 -0
- package/browser/session/authorization.store.mjs +336 -0
- package/browser/session/consent.store.mjs +34 -0
- package/browser/session/credential-resume-link.mjs +43 -0
- package/browser/session/credentials-accessor.impl.mjs +54 -0
- package/browser/session/credentials-accessor.mjs +5 -0
- package/browser/session/encrypted-authorization-vault.mjs +405 -0
- package/browser/session/federated-auth.session.mjs +176 -0
- package/browser/session/incremental-auth-ticket.mjs +42 -0
- package/browser/session/index.mjs +266 -0
- package/browser/session/orchestrated-token.crypto.mjs +47 -0
- package/browser/session/orchestrated-token.store.mjs +286 -0
- package/browser/session/pending-login.mjs +37 -0
- package/browser/session/redis-session.store.mjs +255 -0
- package/browser/session/secure-store-accessor.impl.mjs +78 -0
- package/browser/session/secure-store-accessor.mjs +5 -0
- package/browser/session/secure-store-backends.mjs +111 -0
- package/browser/session/secure-store.factory.mjs +77 -0
- package/browser/session/session-credential-vault.factory.mjs +18 -0
- package/browser/session/session-credential-vault.mjs +243 -0
- package/browser/session/session-crypto.mjs +39 -0
- package/browser/session/session-rate-limiter.mjs +126 -0
- package/browser/session/session.transport.mjs +16 -0
- package/browser/session/signin-binding.mjs +30 -0
- package/browser/session/storage/in-memory-authorization-vault.mjs +28 -0
- package/browser/session/storage/index.mjs +14 -0
- package/browser/session/storage/storage-authorization-vault.mjs +301 -0
- package/browser/session/storage/storage-token-store.mjs +83 -0
- package/browser/session/storage-authorization.store.mjs +134 -0
- package/browser/session/storage-consent.store.mjs +33 -0
- package/browser/session/storage-federated-auth.session.mjs +55 -0
- package/browser/session/storage-orchestrated-token.store.mjs +197 -0
- package/browser/session/token-storage.factory.mjs +58 -0
- package/browser/session/token.refresh.mjs +36 -0
- package/browser/session/token.vault.mjs +61 -0
- package/browser/session/transport-session.types.mjs +108 -0
- package/browser/session/utils/auth-token.utils.mjs +58 -0
- package/browser/session/utils/index.mjs +23 -0
- package/browser/session/utils/session-crypto.utils.mjs +49 -0
- package/browser/session/utils/tiny-ttl-cache.mjs +46 -0
- package/browser/session/vault-encryption.mjs +187 -0
- package/browser/session/vercel-kv-session.store.mjs +230 -0
- package/browser/shared-server/index.mjs +17 -0
- package/browser/shared-server/shared-server-identity.mjs +55 -0
- package/browser/ui/base-layout.mjs +184 -0
- package/browser/ui/index.mjs +51 -0
- package/browser/ui/local-login.helper.mjs +46 -0
- package/browser/ui/templates.mjs +482 -0
- package/browser/utils/audience.validator.mjs +110 -0
- package/browser/utils/authorization-id.utils.mjs +10 -0
- package/browser/utils/index.mjs +45 -0
- package/browser/utils/redirect-uri.utils.mjs +22 -0
- package/browser/utils/scope-grant.utils.mjs +36 -0
- package/browser/utils/token-scopes.utils.mjs +12 -0
- package/browser/utils/www-authenticate.utils.mjs +164 -0
- package/browser/vault/auth-providers.accessor.impl.mjs +236 -0
- package/browser/vault/auth-providers.accessor.mjs +5 -0
- package/browser/vault/auth-providers.registry.mjs +140 -0
- package/browser/vault/auth-providers.types.mjs +47 -0
- package/browser/vault/auth-providers.vault.mjs +206 -0
- package/browser/vault/credential-cache.mjs +187 -0
- package/browser/vault/credential-helpers.mjs +15 -0
- package/browser/vault/credential-loaders/eager-loader.mjs +91 -0
- package/browser/vault/credential-loaders/lazy-loader.mjs +120 -0
- package/browser/vault/index.mjs +35 -0
- package/cimd/cimd.types.d.ts +1 -1
- package/cimd/cimd.types.d.ts.map +1 -1
- package/common/jwt.types.d.ts +1 -1
- package/common/jwt.types.d.ts.map +1 -1
- package/common/session.types.d.ts +9 -1
- package/common/session.types.d.ts.map +1 -1
- package/consent/consent.types.d.ts +1 -1
- package/consent/consent.types.d.ts.map +1 -1
- package/detection/auth-provider-detection.d.ts +1 -1
- package/detection/auth-provider-detection.d.ts.map +1 -1
- package/errors/auth-internal.errors.d.ts +6 -0
- package/errors/auth-internal.errors.d.ts.map +1 -1
- package/errors/index.d.ts +1 -1
- package/errors/index.d.ts.map +1 -1
- package/esm/index.mjs +2 -2
- package/esm/package.json +21 -5
- package/index.d.ts +5 -1
- package/index.d.ts.map +1 -1
- package/index.js +2 -2
- package/options/app-auth.schema.d.ts +19 -1
- package/options/app-auth.schema.d.ts.map +1 -1
- package/options/orchestrated.schema.d.ts +37 -1
- package/options/orchestrated.schema.d.ts.map +1 -1
- package/options/public.schema.d.ts +1 -1
- package/options/public.schema.d.ts.map +1 -1
- package/options/schema.d.ts +19 -1
- package/options/schema.d.ts.map +1 -1
- package/options/shared.schemas.d.ts +10 -14
- package/options/shared.schemas.d.ts.map +1 -1
- package/options/static.schema.d.ts +1 -1
- package/options/static.schema.d.ts.map +1 -1
- package/options/transparent.schema.d.ts +1 -1
- package/options/transparent.schema.d.ts.map +1 -1
- package/package.json +21 -5
- package/people/index.d.ts +3 -0
- package/people/index.d.ts.map +1 -0
- package/people/loopback-token.auth.d.ts +75 -0
- package/people/loopback-token.auth.d.ts.map +1 -0
- package/people/people-auth.options.d.ts +36 -0
- package/people/people-auth.options.d.ts.map +1 -0
- package/session/authorization-vault.d.ts +1 -1
- package/session/authorization-vault.d.ts.map +1 -1
- package/session/encrypted-authorization-vault.d.ts +1 -1
- package/session/encrypted-authorization-vault.d.ts.map +1 -1
- package/session/secure-store.factory.d.ts.map +1 -1
- package/shared-server/index.d.ts +3 -0
- package/shared-server/index.d.ts.map +1 -0
- package/shared-server/shared-server-identity.d.ts +26 -0
- package/shared-server/shared-server-identity.d.ts.map +1 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// libs/auth/src/utils/audience.validator.ts
|
|
2
|
+
function validateAudience(tokenAudience, options) {
|
|
3
|
+
const { expectedAudiences, allowNoAudience = false, caseSensitive = true, allowWildcards = false } = options;
|
|
4
|
+
if (tokenAudience === void 0 || tokenAudience === null) {
|
|
5
|
+
if (allowNoAudience) {
|
|
6
|
+
return { valid: true };
|
|
7
|
+
}
|
|
8
|
+
return {
|
|
9
|
+
valid: false,
|
|
10
|
+
error: "Token is missing audience claim"
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
if (expectedAudiences.length === 0) {
|
|
14
|
+
return {
|
|
15
|
+
valid: false,
|
|
16
|
+
error: "No expected audiences configured - cannot validate token"
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const tokenAuds = Array.isArray(tokenAudience) ? tokenAudience : [tokenAudience];
|
|
20
|
+
for (const tokenAud of tokenAuds) {
|
|
21
|
+
for (const expectedAud of expectedAudiences) {
|
|
22
|
+
if (matchesAudience(tokenAud, expectedAud, caseSensitive, allowWildcards)) {
|
|
23
|
+
return { valid: true, matchedAudience: tokenAud };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
valid: false,
|
|
29
|
+
error: `Token audience does not match expected audiences. Got: ${tokenAuds.join(
|
|
30
|
+
", "
|
|
31
|
+
)}. Expected one of: ${expectedAudiences.join(", ")}`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function matchesAudience(tokenAud, expectedAud, caseSensitive, allowWildcards) {
|
|
35
|
+
if (caseSensitive) {
|
|
36
|
+
if (tokenAud === expectedAud) return true;
|
|
37
|
+
} else {
|
|
38
|
+
if (tokenAud.toLowerCase() === expectedAud.toLowerCase()) return true;
|
|
39
|
+
}
|
|
40
|
+
if (allowWildcards && expectedAud.includes("*")) {
|
|
41
|
+
const wildcardCount = (expectedAud.match(/\*/g) || []).length;
|
|
42
|
+
if (wildcardCount > 2) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const pattern = expectedAud.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^.]*");
|
|
46
|
+
const regex = new RegExp(`^${pattern}$`, caseSensitive ? "" : "i");
|
|
47
|
+
if (regex.test(tokenAud)) return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
function createAudienceValidator(options) {
|
|
52
|
+
return (audience) => validateAudience(audience, options);
|
|
53
|
+
}
|
|
54
|
+
function deriveExpectedAudience(resourceUrl) {
|
|
55
|
+
const audiences = [];
|
|
56
|
+
try {
|
|
57
|
+
const url = new URL(resourceUrl);
|
|
58
|
+
audiences.push(resourceUrl.replace(/\/$/, ""));
|
|
59
|
+
if (url.pathname !== "/" && url.pathname !== "") {
|
|
60
|
+
audiences.push(url.origin);
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
audiences.push(resourceUrl);
|
|
64
|
+
}
|
|
65
|
+
return [...new Set(audiences)];
|
|
66
|
+
}
|
|
67
|
+
var AudienceValidator = class _AudienceValidator {
|
|
68
|
+
options;
|
|
69
|
+
constructor(options = {}) {
|
|
70
|
+
this.options = {
|
|
71
|
+
expectedAudiences: [...options.expectedAudiences ?? []],
|
|
72
|
+
allowNoAudience: options.allowNoAudience ?? false,
|
|
73
|
+
caseSensitive: options.caseSensitive ?? true,
|
|
74
|
+
allowWildcards: options.allowWildcards ?? false
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Validate an audience claim
|
|
79
|
+
*/
|
|
80
|
+
validate(audience) {
|
|
81
|
+
return validateAudience(audience, this.options);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Add expected audiences
|
|
85
|
+
*/
|
|
86
|
+
addAudiences(...audiences) {
|
|
87
|
+
this.options.expectedAudiences.push(...audiences);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Set expected audiences (replace existing)
|
|
91
|
+
*/
|
|
92
|
+
setAudiences(audiences) {
|
|
93
|
+
this.options.expectedAudiences = audiences;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Create validator from resource URL
|
|
97
|
+
*/
|
|
98
|
+
static fromResourceUrl(resourceUrl, options = {}) {
|
|
99
|
+
return new _AudienceValidator({
|
|
100
|
+
...options,
|
|
101
|
+
expectedAudiences: deriveExpectedAudience(resourceUrl)
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
export {
|
|
106
|
+
AudienceValidator,
|
|
107
|
+
createAudienceValidator,
|
|
108
|
+
deriveExpectedAudience,
|
|
109
|
+
validateAudience
|
|
110
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// libs/auth/src/utils/authorization-id.utils.ts
|
|
2
|
+
import { sha256Hex } from "@frontmcp/utils";
|
|
3
|
+
function deriveAuthorizationId(token) {
|
|
4
|
+
const parts = token.split(".");
|
|
5
|
+
const signature = parts[2] || token;
|
|
6
|
+
return sha256Hex(signature).substring(0, 16);
|
|
7
|
+
}
|
|
8
|
+
export {
|
|
9
|
+
deriveAuthorizationId
|
|
10
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// libs/auth/src/utils/index.ts
|
|
2
|
+
import {
|
|
3
|
+
buildWwwAuthenticate,
|
|
4
|
+
buildPrmUrl,
|
|
5
|
+
buildUnauthorizedHeader,
|
|
6
|
+
buildInvalidTokenHeader,
|
|
7
|
+
buildInsufficientScopeHeader,
|
|
8
|
+
buildInvalidRequestHeader,
|
|
9
|
+
parseWwwAuthenticate
|
|
10
|
+
} from "./www-authenticate.utils.mjs";
|
|
11
|
+
import {
|
|
12
|
+
validateAudience,
|
|
13
|
+
createAudienceValidator,
|
|
14
|
+
deriveExpectedAudience,
|
|
15
|
+
AudienceValidator
|
|
16
|
+
} from "./audience.validator.mjs";
|
|
17
|
+
import { deriveAuthorizationId } from "./authorization-id.utils.mjs";
|
|
18
|
+
import {
|
|
19
|
+
advertisedScopes,
|
|
20
|
+
DEFAULT_ALLOWED_SCOPES,
|
|
21
|
+
grantScopes,
|
|
22
|
+
resourceScopesFor
|
|
23
|
+
} from "./scope-grant.utils.mjs";
|
|
24
|
+
import { isLoopbackRedirectUri } from "./redirect-uri.utils.mjs";
|
|
25
|
+
import { scopesFromClaims } from "./token-scopes.utils.mjs";
|
|
26
|
+
export {
|
|
27
|
+
AudienceValidator,
|
|
28
|
+
DEFAULT_ALLOWED_SCOPES,
|
|
29
|
+
advertisedScopes,
|
|
30
|
+
buildInsufficientScopeHeader,
|
|
31
|
+
buildInvalidRequestHeader,
|
|
32
|
+
buildInvalidTokenHeader,
|
|
33
|
+
buildPrmUrl,
|
|
34
|
+
buildUnauthorizedHeader,
|
|
35
|
+
buildWwwAuthenticate,
|
|
36
|
+
createAudienceValidator,
|
|
37
|
+
deriveAuthorizationId,
|
|
38
|
+
deriveExpectedAudience,
|
|
39
|
+
grantScopes,
|
|
40
|
+
isLoopbackRedirectUri,
|
|
41
|
+
parseWwwAuthenticate,
|
|
42
|
+
resourceScopesFor,
|
|
43
|
+
scopesFromClaims,
|
|
44
|
+
validateAudience
|
|
45
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// libs/auth/src/utils/redirect-uri.utils.ts
|
|
2
|
+
function isLoopbackRedirectUri(uri) {
|
|
3
|
+
let url;
|
|
4
|
+
try {
|
|
5
|
+
url = new URL(uri);
|
|
6
|
+
} catch {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
10
|
+
if (url.username || url.password) return false;
|
|
11
|
+
const host = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
12
|
+
if (host === "localhost" || host === "::1") return true;
|
|
13
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
14
|
+
if (m) {
|
|
15
|
+
const octets = m.slice(1).map((n) => Number(n));
|
|
16
|
+
return octets.every((o) => o >= 0 && o <= 255) && octets[0] === 127;
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
isLoopbackRedirectUri
|
|
22
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// libs/auth/src/utils/scope-grant.utils.ts
|
|
2
|
+
import { globMatch } from "../authorization/dcr-client.registry.mjs";
|
|
3
|
+
var DEFAULT_ALLOWED_SCOPES = Object.freeze([
|
|
4
|
+
"openid",
|
|
5
|
+
"profile",
|
|
6
|
+
"email",
|
|
7
|
+
"offline_access"
|
|
8
|
+
]);
|
|
9
|
+
function grantScopes(requested, allowed) {
|
|
10
|
+
const patterns = allowed ?? DEFAULT_ALLOWED_SCOPES;
|
|
11
|
+
const granted = [];
|
|
12
|
+
for (const scope of requested) {
|
|
13
|
+
if (!scope || granted.includes(scope)) continue;
|
|
14
|
+
if (patterns.some((pattern) => globMatch(pattern, scope))) granted.push(scope);
|
|
15
|
+
}
|
|
16
|
+
return granted;
|
|
17
|
+
}
|
|
18
|
+
function advertisedScopes(allowed) {
|
|
19
|
+
const scopes = [];
|
|
20
|
+
for (const scope of allowed ?? DEFAULT_ALLOWED_SCOPES) {
|
|
21
|
+
if (scope && !scope.includes("*") && !scopes.includes(scope)) scopes.push(scope);
|
|
22
|
+
}
|
|
23
|
+
return scopes;
|
|
24
|
+
}
|
|
25
|
+
function resourceScopesFor(options) {
|
|
26
|
+
const mode = options?.mode ?? "public";
|
|
27
|
+
if (mode === "local" || mode === "remote") return advertisedScopes(options?.allowedScopes);
|
|
28
|
+
const listed = mode === "public" ? options?.anonymousScopes ?? ["anonymous"] : mode === "static" ? options?.scopes ?? ["static"] : mode === "transparent" ? [...options?.requiredScopes ?? [], ...options?.scopes ?? []] : [];
|
|
29
|
+
return advertisedScopes(listed);
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
DEFAULT_ALLOWED_SCOPES,
|
|
33
|
+
advertisedScopes,
|
|
34
|
+
grantScopes,
|
|
35
|
+
resourceScopesFor
|
|
36
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// libs/auth/src/utils/token-scopes.utils.ts
|
|
2
|
+
function scopesFromClaims(claims) {
|
|
3
|
+
const scopes = [claims?.scope, claims?.scp].flatMap((claim) => {
|
|
4
|
+
if (typeof claim === "string") return claim.split(/\s+/).filter(Boolean);
|
|
5
|
+
if (Array.isArray(claim)) return claim.filter((scope) => typeof scope === "string");
|
|
6
|
+
return [];
|
|
7
|
+
});
|
|
8
|
+
return [...new Set(scopes)];
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
scopesFromClaims
|
|
12
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// libs/auth/src/utils/www-authenticate.utils.ts
|
|
2
|
+
function buildWwwAuthenticate(options = {}) {
|
|
3
|
+
const parts = ["Bearer"];
|
|
4
|
+
const params = [];
|
|
5
|
+
if (options.resourceMetadataUrl) {
|
|
6
|
+
params.push(`resource_metadata="${escapeQuotedString(options.resourceMetadataUrl)}"`);
|
|
7
|
+
}
|
|
8
|
+
if (options.realm) {
|
|
9
|
+
params.push(`realm="${escapeQuotedString(options.realm)}"`);
|
|
10
|
+
}
|
|
11
|
+
if (options.error) {
|
|
12
|
+
params.push(`error="${options.error}"`);
|
|
13
|
+
}
|
|
14
|
+
if (options.errorDescription) {
|
|
15
|
+
params.push(`error_description="${escapeQuotedString(options.errorDescription)}"`);
|
|
16
|
+
}
|
|
17
|
+
if (options.errorUri) {
|
|
18
|
+
params.push(`error_uri="${escapeQuotedString(options.errorUri)}"`);
|
|
19
|
+
}
|
|
20
|
+
if (options.scope) {
|
|
21
|
+
const scopeValue = Array.isArray(options.scope) ? options.scope.join(" ") : options.scope;
|
|
22
|
+
params.push(`scope="${escapeQuotedString(scopeValue)}"`);
|
|
23
|
+
}
|
|
24
|
+
if (params.length > 0) {
|
|
25
|
+
parts.push(params.join(", "));
|
|
26
|
+
}
|
|
27
|
+
return parts.join(" ");
|
|
28
|
+
}
|
|
29
|
+
function buildPrmUrl(baseUrl, entryPath, routeBase) {
|
|
30
|
+
const normalizedEntry = normalizePathSegment(entryPath);
|
|
31
|
+
const normalizedRoute = normalizePathSegment(routeBase);
|
|
32
|
+
return `${baseUrl}/.well-known/oauth-protected-resource${normalizedEntry}${normalizedRoute}`;
|
|
33
|
+
}
|
|
34
|
+
function buildUnauthorizedHeader(prmUrl) {
|
|
35
|
+
return buildWwwAuthenticate({
|
|
36
|
+
resourceMetadataUrl: prmUrl
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function buildInvalidTokenHeader(prmUrl, description) {
|
|
40
|
+
return buildWwwAuthenticate({
|
|
41
|
+
resourceMetadataUrl: prmUrl,
|
|
42
|
+
error: "invalid_token",
|
|
43
|
+
errorDescription: description ?? "The access token is invalid or expired"
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function buildInsufficientScopeHeader(prmUrl, requiredScopes, description) {
|
|
47
|
+
return buildWwwAuthenticate({
|
|
48
|
+
resourceMetadataUrl: prmUrl,
|
|
49
|
+
error: "insufficient_scope",
|
|
50
|
+
scope: requiredScopes,
|
|
51
|
+
errorDescription: description ?? "The request requires higher privileges"
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function buildInvalidRequestHeader(prmUrl, description) {
|
|
55
|
+
return buildWwwAuthenticate({
|
|
56
|
+
resourceMetadataUrl: prmUrl,
|
|
57
|
+
error: "invalid_request",
|
|
58
|
+
errorDescription: description ?? "The request is missing required parameters"
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function parseWwwAuthenticate(header) {
|
|
62
|
+
const result = {};
|
|
63
|
+
if (!header.toLowerCase().startsWith("bearer")) {
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
const paramString = header.substring(6).trim();
|
|
67
|
+
const params = parseQuotedParams(paramString);
|
|
68
|
+
for (const [key, value] of params) {
|
|
69
|
+
const unescapedValue = unescapeQuotedString(value);
|
|
70
|
+
switch (key.toLowerCase()) {
|
|
71
|
+
case "resource_metadata":
|
|
72
|
+
result.resourceMetadataUrl = unescapedValue;
|
|
73
|
+
break;
|
|
74
|
+
case "realm":
|
|
75
|
+
result.realm = unescapedValue;
|
|
76
|
+
break;
|
|
77
|
+
case "error":
|
|
78
|
+
result.error = unescapedValue;
|
|
79
|
+
break;
|
|
80
|
+
case "error_description":
|
|
81
|
+
result.errorDescription = unescapedValue;
|
|
82
|
+
break;
|
|
83
|
+
case "error_uri":
|
|
84
|
+
result.errorUri = unescapedValue;
|
|
85
|
+
break;
|
|
86
|
+
case "scope":
|
|
87
|
+
result.scope = unescapedValue;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
function parseQuotedParams(input) {
|
|
94
|
+
const result = [];
|
|
95
|
+
let i = 0;
|
|
96
|
+
const len = input.length;
|
|
97
|
+
while (i < len) {
|
|
98
|
+
while (i < len && (input[i] === " " || input[i] === "," || input[i] === " ")) {
|
|
99
|
+
i++;
|
|
100
|
+
}
|
|
101
|
+
if (i >= len) break;
|
|
102
|
+
const keyStart = i;
|
|
103
|
+
while (i < len && /\w/.test(input[i])) {
|
|
104
|
+
i++;
|
|
105
|
+
}
|
|
106
|
+
const key = input.slice(keyStart, i);
|
|
107
|
+
if (!key) break;
|
|
108
|
+
while (i < len && (input[i] === " " || input[i] === " ")) {
|
|
109
|
+
i++;
|
|
110
|
+
}
|
|
111
|
+
if (i >= len || input[i] !== "=") {
|
|
112
|
+
while (i < len && input[i] !== ",") i++;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
i++;
|
|
116
|
+
while (i < len && (input[i] === " " || input[i] === " ")) {
|
|
117
|
+
i++;
|
|
118
|
+
}
|
|
119
|
+
if (i >= len || input[i] !== '"') {
|
|
120
|
+
while (i < len && input[i] !== ",") i++;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
i++;
|
|
124
|
+
let value = "";
|
|
125
|
+
while (i < len && input[i] !== '"') {
|
|
126
|
+
if (input[i] === "\\" && i + 1 < len) {
|
|
127
|
+
value += input[i + 1];
|
|
128
|
+
i += 2;
|
|
129
|
+
} else {
|
|
130
|
+
value += input[i];
|
|
131
|
+
i++;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (i < len && input[i] === '"') {
|
|
135
|
+
i++;
|
|
136
|
+
}
|
|
137
|
+
result.push([key, value]);
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
function escapeQuotedString(value) {
|
|
142
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
143
|
+
}
|
|
144
|
+
function unescapeQuotedString(value) {
|
|
145
|
+
return value.replace(/\\(.)/g, "$1");
|
|
146
|
+
}
|
|
147
|
+
function normalizePathSegment(path) {
|
|
148
|
+
if (!path || path === "/") return "";
|
|
149
|
+
const normalized = path.startsWith("/") ? path : `/${path}`;
|
|
150
|
+
let end = normalized.length;
|
|
151
|
+
while (end > 0 && normalized[end - 1] === "/") {
|
|
152
|
+
end--;
|
|
153
|
+
}
|
|
154
|
+
return normalized.slice(0, end);
|
|
155
|
+
}
|
|
156
|
+
export {
|
|
157
|
+
buildInsufficientScopeHeader,
|
|
158
|
+
buildInvalidRequestHeader,
|
|
159
|
+
buildInvalidTokenHeader,
|
|
160
|
+
buildPrmUrl,
|
|
161
|
+
buildUnauthorizedHeader,
|
|
162
|
+
buildWwwAuthenticate,
|
|
163
|
+
parseWwwAuthenticate
|
|
164
|
+
};
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// libs/auth/src/vault/auth-providers.accessor.impl.ts
|
|
2
|
+
import { extractCredentialExpiry } from "./credential-helpers.mjs";
|
|
3
|
+
import { base64Encode } from "@frontmcp/utils";
|
|
4
|
+
var AuthProvidersAccessorImpl = class {
|
|
5
|
+
constructor(registry, vault, cache, loader, context, logger) {
|
|
6
|
+
this.registry = registry;
|
|
7
|
+
this.vault = vault;
|
|
8
|
+
this.cache = cache;
|
|
9
|
+
this.loader = loader;
|
|
10
|
+
this.context = context;
|
|
11
|
+
this.logger = logger;
|
|
12
|
+
}
|
|
13
|
+
registry;
|
|
14
|
+
vault;
|
|
15
|
+
cache;
|
|
16
|
+
loader;
|
|
17
|
+
context;
|
|
18
|
+
logger;
|
|
19
|
+
async get(providerName, options) {
|
|
20
|
+
const config = this.registry.get(providerName);
|
|
21
|
+
if (!config) {
|
|
22
|
+
this.logger?.debug(`Provider "${providerName}" is not registered`);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
if (!options?.forceRefresh) {
|
|
26
|
+
const cached = this.cache.get(providerName);
|
|
27
|
+
if (cached && this.isValid(cached)) {
|
|
28
|
+
this.logger?.debug(`Cache hit for "${providerName}"`);
|
|
29
|
+
return cached;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (!options?.forceRefresh) {
|
|
33
|
+
const fromVault = await this.loadFromVault(providerName, config);
|
|
34
|
+
if (fromVault && this.isValid(fromVault)) {
|
|
35
|
+
this.cache.set(providerName, fromVault, config.cacheTtl);
|
|
36
|
+
this.logger?.debug(`Vault hit for "${providerName}"`);
|
|
37
|
+
return fromVault;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const loaded = await this.loader.load(config, this.context);
|
|
41
|
+
if (!loaded) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
await this.storeInVault(providerName, loaded, config);
|
|
45
|
+
this.cache.set(providerName, loaded, config.cacheTtl);
|
|
46
|
+
return loaded;
|
|
47
|
+
}
|
|
48
|
+
async getMany(providerNames, options) {
|
|
49
|
+
const results = /* @__PURE__ */ new Map();
|
|
50
|
+
const promises = providerNames.map(async (name) => {
|
|
51
|
+
const result = await this.get(name, options);
|
|
52
|
+
return { name, result };
|
|
53
|
+
});
|
|
54
|
+
const settled = await Promise.allSettled(promises);
|
|
55
|
+
for (const outcome of settled) {
|
|
56
|
+
if (outcome.status === "fulfilled") {
|
|
57
|
+
results.set(outcome.value.name, outcome.value.result);
|
|
58
|
+
} else {
|
|
59
|
+
this.logger?.warn("Failed to load credential:", outcome.reason);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return results;
|
|
63
|
+
}
|
|
64
|
+
async headers(providerName) {
|
|
65
|
+
const resolved = await this.get(providerName);
|
|
66
|
+
if (!resolved) {
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
const config = this.registry.get(providerName);
|
|
70
|
+
if (config?.toHeaders) {
|
|
71
|
+
return config.toHeaders(resolved.credential);
|
|
72
|
+
}
|
|
73
|
+
return this.defaultHeaders(resolved.credential);
|
|
74
|
+
}
|
|
75
|
+
async headersMany(providerNames) {
|
|
76
|
+
const allHeaders = {};
|
|
77
|
+
for (const name of providerNames) {
|
|
78
|
+
const headers = await this.headers(name);
|
|
79
|
+
Object.assign(allHeaders, headers);
|
|
80
|
+
}
|
|
81
|
+
return allHeaders;
|
|
82
|
+
}
|
|
83
|
+
async refresh(providerName) {
|
|
84
|
+
const config = this.registry.get(providerName);
|
|
85
|
+
if (!config) {
|
|
86
|
+
this.logger?.debug(`Cannot refresh: provider "${providerName}" not registered`);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const existing = this.cache.get(providerName);
|
|
90
|
+
const existingCredential = existing?.credential;
|
|
91
|
+
try {
|
|
92
|
+
const refreshed = await this.loader.refresh(config, {
|
|
93
|
+
...this.context,
|
|
94
|
+
existingCredential
|
|
95
|
+
});
|
|
96
|
+
if (!refreshed) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
await this.storeInVault(providerName, refreshed, config);
|
|
100
|
+
this.cache.set(providerName, refreshed, config.cacheTtl);
|
|
101
|
+
return refreshed;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
this.logger?.warn(`Failed to refresh credential "${providerName}":`, error);
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async has(providerName) {
|
|
108
|
+
const cached = this.cache.get(providerName);
|
|
109
|
+
if (cached) {
|
|
110
|
+
if (this.isValid(cached)) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
this.cache.invalidate(providerName);
|
|
114
|
+
}
|
|
115
|
+
const config = this.registry.get(providerName);
|
|
116
|
+
if (!config) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
const fromVault = await this.loadFromVault(providerName, config);
|
|
120
|
+
return fromVault !== null && this.isValid(fromVault);
|
|
121
|
+
}
|
|
122
|
+
isRegistered(providerName) {
|
|
123
|
+
return this.registry.has(providerName);
|
|
124
|
+
}
|
|
125
|
+
invalidate(providerName) {
|
|
126
|
+
this.cache.invalidate(providerName);
|
|
127
|
+
this.logger?.debug(`Invalidated cache for "${providerName}"`);
|
|
128
|
+
}
|
|
129
|
+
invalidateAll() {
|
|
130
|
+
this.cache.invalidateAll();
|
|
131
|
+
this.logger?.debug("Invalidated all cached credentials");
|
|
132
|
+
}
|
|
133
|
+
listProviders() {
|
|
134
|
+
return this.registry.getNames();
|
|
135
|
+
}
|
|
136
|
+
async listAvailable() {
|
|
137
|
+
const available = [];
|
|
138
|
+
for (const name of this.registry.getNames()) {
|
|
139
|
+
if (await this.has(name)) {
|
|
140
|
+
available.push(name);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return available;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Load credential from vault storage
|
|
147
|
+
*/
|
|
148
|
+
async loadFromVault(providerName, config) {
|
|
149
|
+
try {
|
|
150
|
+
const credential = await this.vault.getCredential(
|
|
151
|
+
this.context.sessionId,
|
|
152
|
+
providerName,
|
|
153
|
+
config.scope,
|
|
154
|
+
this.context.userSub
|
|
155
|
+
);
|
|
156
|
+
if (!credential) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
credential,
|
|
161
|
+
providerId: providerName,
|
|
162
|
+
acquiredAt: Date.now(),
|
|
163
|
+
// We don't store acquiredAt in vault
|
|
164
|
+
expiresAt: extractCredentialExpiry(credential),
|
|
165
|
+
isValid: true,
|
|
166
|
+
scope: config.scope
|
|
167
|
+
};
|
|
168
|
+
} catch (error) {
|
|
169
|
+
this.logger?.warn(`Failed to load from vault "${providerName}":`, error);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Store credential in vault storage
|
|
175
|
+
*/
|
|
176
|
+
async storeInVault(providerName, resolved, config) {
|
|
177
|
+
try {
|
|
178
|
+
await this.vault.storeCredential(
|
|
179
|
+
this.context.sessionId,
|
|
180
|
+
providerName,
|
|
181
|
+
resolved.credential,
|
|
182
|
+
config.scope,
|
|
183
|
+
this.context.userSub
|
|
184
|
+
);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
this.logger?.warn(`Failed to store in vault "${providerName}":`, error);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Check if a resolved credential is still valid
|
|
191
|
+
*/
|
|
192
|
+
isValid(resolved) {
|
|
193
|
+
if (!resolved.isValid) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
if (resolved.expiresAt && Date.now() >= resolved.expiresAt) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Generate default headers for a credential type
|
|
203
|
+
*/
|
|
204
|
+
defaultHeaders(credential) {
|
|
205
|
+
switch (credential.type) {
|
|
206
|
+
case "oauth":
|
|
207
|
+
case "oauth_pkce":
|
|
208
|
+
return { Authorization: `${credential.tokenType} ${credential.accessToken}` };
|
|
209
|
+
case "bearer":
|
|
210
|
+
return { Authorization: `Bearer ${credential.token}` };
|
|
211
|
+
case "api_key":
|
|
212
|
+
if (credential.headerPrefix) {
|
|
213
|
+
return { [credential.headerName]: `${credential.headerPrefix}${credential.key}` };
|
|
214
|
+
}
|
|
215
|
+
return { [credential.headerName]: credential.key };
|
|
216
|
+
case "basic": {
|
|
217
|
+
const value = `${credential.username}:${credential.password}`;
|
|
218
|
+
const encoded = credential.encodedValue ?? base64Encode(new TextEncoder().encode(value));
|
|
219
|
+
return { Authorization: `Basic ${encoded}` };
|
|
220
|
+
}
|
|
221
|
+
case "custom":
|
|
222
|
+
return credential.headers ?? {};
|
|
223
|
+
// SSH keys, mTLS, private keys, service accounts don't have standard HTTP headers
|
|
224
|
+
case "ssh_key":
|
|
225
|
+
case "mtls":
|
|
226
|
+
case "private_key":
|
|
227
|
+
case "service_account":
|
|
228
|
+
return {};
|
|
229
|
+
default:
|
|
230
|
+
return {};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
export {
|
|
235
|
+
AuthProvidersAccessorImpl
|
|
236
|
+
};
|