@frontmcp/sdk 1.5.3 → 1.5.5

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.
Files changed (34) hide show
  1. package/agent/agent.scope.d.ts +8 -0
  2. package/agent/agent.scope.d.ts.map +1 -1
  3. package/auth/flows/oauth.authorize.flow.d.ts.map +1 -1
  4. package/auth/flows/oauth.callback.flow.d.ts +13 -0
  5. package/auth/flows/oauth.callback.flow.d.ts.map +1 -1
  6. package/auth/flows/oauth.provider-callback.flow.d.ts.map +1 -1
  7. package/auth/flows/oauth.register.flow.d.ts.map +1 -1
  8. package/auth/flows/oauth.token.flow.d.ts +2 -0
  9. package/auth/flows/oauth.token.flow.d.ts.map +1 -1
  10. package/auth/flows/session.verify.flow.d.ts.map +1 -1
  11. package/auth/flows/well-known.oauth-authorization-server.flow.d.ts +1 -0
  12. package/auth/flows/well-known.oauth-authorization-server.flow.d.ts.map +1 -1
  13. package/auth/flows/well-known.prm.flow.d.ts.map +1 -1
  14. package/auth/instances/instance.local-primary-auth.d.ts +2 -2
  15. package/auth/instances/instance.local-primary-auth.d.ts.map +1 -1
  16. package/common/metadata/app.metadata.d.ts +20 -4
  17. package/common/metadata/app.metadata.d.ts.map +1 -1
  18. package/common/metadata/flow.metadata.d.ts +1 -1
  19. package/common/metadata/front-mcp.metadata.d.ts +72 -8
  20. package/common/metadata/front-mcp.metadata.d.ts.map +1 -1
  21. package/common/metadata/tool.metadata.d.ts +1 -1
  22. package/common/utils/decide-request-intent.utils.d.ts +25 -0
  23. package/common/utils/decide-request-intent.utils.d.ts.map +1 -1
  24. package/common/utils/path.utils.d.ts +2 -0
  25. package/common/utils/path.utils.d.ts.map +1 -1
  26. package/esm/index.mjs +380 -68
  27. package/front-mcp/front-mcp.providers.d.ts +32 -0
  28. package/front-mcp/front-mcp.providers.d.ts.map +1 -1
  29. package/index.js +379 -67
  30. package/package.json +10 -10
  31. package/scope/flows/http.request.flow.d.ts.map +1 -1
  32. package/server/middleware/host-validation.middleware.d.ts.map +1 -1
  33. package/tool/flows/call-tool.flow.d.ts.map +1 -1
  34. package/transport/mcp-handlers/index.d.ts +1 -1
package/esm/index.mjs CHANGED
@@ -20916,18 +20916,28 @@ var init_call_tool_flow = __esm({
20916
20916
  const consentCtx = this.input.ctx;
20917
20917
  if (!consentCtx?.internalCall) {
20918
20918
  const consentedToolIds = getConsentedToolIds(authInfo);
20919
- if (consentedToolIds && tool && !isToolConsented(consentedToolIds, tool.name, tool.fullName)) {
20920
- this.logger.info(
20921
- `checkToolAuthorization: tool "${tool.fullName || tool.name}" not in consented set \u2014 rejecting`
20922
- );
20923
- throw new ToolNotConsentedError(tool.fullName || tool.name);
20919
+ if (consentedToolIds && tool) {
20920
+ const bareNameCount = this.scope.tools.getTools(true).filter((t) => t.name === tool.name).length;
20921
+ const consented = bareNameCount > 1 ? isToolConsented(consentedToolIds, tool.fullName) : isToolConsented(consentedToolIds, tool.name, tool.fullName);
20922
+ if (!consented) {
20923
+ this.logger.info(
20924
+ `checkToolAuthorization: tool "${tool.fullName || tool.name}" not in consented set \u2014 rejecting`
20925
+ );
20926
+ throw new ToolNotConsentedError(tool.fullName || tool.name);
20927
+ }
20924
20928
  }
20925
20929
  }
20926
20930
  const claimAuthorizedApps = getAuthorizedAppIds(authInfo);
20927
20931
  const legacyAuthorization = authInfo?.extra?.["authorization"];
20928
20932
  if (!claimAuthorizedApps && !legacyAuthorization) {
20929
- this.logger.verbose("checkToolAuthorization:skip (no auth context)");
20930
- return;
20933
+ const incrementalEnabled = !consentCtx?.internalCall && !!authInfo?.token && this.scope.auth?.options?.incrementalAuth?.enabled === true;
20934
+ if (!incrementalEnabled) {
20935
+ this.logger.verbose("checkToolAuthorization:skip (no auth context)");
20936
+ return;
20937
+ }
20938
+ this.logger.warn(
20939
+ "checkToolAuthorization: incrementalAuth enabled but the verified token carries no authorized_apps claim \u2014 failing closed"
20940
+ );
20931
20941
  }
20932
20942
  const appId = tool?.owner?.id;
20933
20943
  if (!appId) {
@@ -34253,7 +34263,7 @@ var init_oauth_authorize_flow = __esm({
34253
34263
  if (!result.success) {
34254
34264
  const errors = this.formatZodErrors(result.error);
34255
34265
  this.logger.warn(`Authorization request validation failed: ${errors.join(", ")}`);
34256
- this.respondWithError(errors, rawRedirectUri, rawState);
34266
+ this.respondWithError(errors, void 0, rawState);
34257
34267
  return;
34258
34268
  }
34259
34269
  this.state.set("validatedRequest", result.data);
@@ -34308,6 +34318,22 @@ var init_oauth_authorize_flow = __esm({
34308
34318
  this.respondWithError(["redirect_uri is not registered for this client"], void 0, rawState);
34309
34319
  return;
34310
34320
  }
34321
+ if (!registered) {
34322
+ const authOptions = this.scope.auth?.options;
34323
+ const requireRegistered = authOptions?.requireRegisteredClients === true;
34324
+ const redirectAllowlisted = registry?.hasRedirectAllowlist?.() === true;
34325
+ if (requireRegistered && !redirectAllowlisted) {
34326
+ this.logger.warn(
34327
+ `OAuth authorize: rejecting unregistered client_id "${client_id}" (requireRegisteredClients)`
34328
+ );
34329
+ this.respondWithError(
34330
+ ["Unknown client_id: register the client (DCR / pre-registered) or use a CIMD client-id URL"],
34331
+ void 0,
34332
+ rawState
34333
+ );
34334
+ return;
34335
+ }
34336
+ }
34311
34337
  }
34312
34338
  }
34313
34339
  async checkIfAuthorized() {
@@ -35034,6 +35060,11 @@ var init_oauth_callback_flow = __esm({
35034
35060
  return;
35035
35061
  }
35036
35062
  }
35063
+ if (this.isCrossOriginSubmission()) {
35064
+ this.logger.warn("Cross-origin login submission rejected (CSRF)");
35065
+ this.respond(httpRespond.html(this.renderErrorPage("invalid_request", "Cross-origin request blocked"), 400));
35066
+ return;
35067
+ }
35037
35068
  let userSub;
35038
35069
  if (email) {
35039
35070
  userSub = this.generateUserSub(email);
@@ -35490,6 +35521,33 @@ var init_oauth_callback_flow = __esm({
35490
35521
  }
35491
35522
  return void 0;
35492
35523
  }
35524
+ /**
35525
+ * Same-origin (CSRF) check for a state-changing login/consent submission.
35526
+ *
35527
+ * Returns true when the request carries an `Origin` or `Referer` header whose
35528
+ * host does NOT match the request's own host — the signature of a cross-site
35529
+ * login-CSRF/fixation. `Origin`/`Referer` are set by the browser and cannot
35530
+ * be forged by a malicious page, so this reliably distinguishes a legitimate
35531
+ * same-origin submission (the built-in form is served by THIS server) from a
35532
+ * cross-site one. Returns false (do NOT block) when neither header is present
35533
+ * or the request's own host cannot be determined — defense-in-depth, not a
35534
+ * hard gate, so header-stripped GET navigations and existing flows keep working.
35535
+ */
35536
+ isCrossOriginSubmission() {
35537
+ const headers = this.rawInput?.request?.headers ?? {};
35538
+ const first = (v) => Array.isArray(v) ? v[0] : typeof v === "string" ? v : void 0;
35539
+ const source = first(headers["origin"]) ?? first(headers["referer"]);
35540
+ if (!source) return false;
35541
+ let sourceHost;
35542
+ try {
35543
+ sourceHost = new URL(source).host.toLowerCase();
35544
+ } catch {
35545
+ return true;
35546
+ }
35547
+ const selfHosts = [first(headers["host"]), first(headers["x-forwarded-host"])].filter((h2) => !!h2).map((h2) => h2.toLowerCase());
35548
+ if (selfHosts.length === 0) return false;
35549
+ return !selfHosts.includes(sourceHost);
35550
+ }
35493
35551
  collectLoginFields(request) {
35494
35552
  const fields = {};
35495
35553
  const absorb = (source) => {
@@ -36262,19 +36320,21 @@ var init_oauth_provider_callback_flow = __esm({
36262
36320
  );
36263
36321
  return;
36264
36322
  }
36265
- const stateValidation = this.getStateValidation();
36266
- if (stateValidation === "strict") {
36267
- const expectedState = session.currentProviderState;
36268
- if (!expectedState || expectedState !== providerState) {
36269
- this.logger.warn("State mismatch for provider callback");
36270
- this.respond(
36271
- httpRespond.html(
36272
- this.renderErrorPage("invalid_request", "Invalid state parameter. Please restart authentication."),
36273
- 400
36274
- )
36323
+ const expectedState = session.currentProviderState;
36324
+ if (!expectedState || expectedState !== providerState) {
36325
+ if (this.getStateValidation() === "format") {
36326
+ this.logger.warn(
36327
+ 'federatedAuth.stateValidation:"format" no longer skips provider-state nonce verification (security hardening) \u2014 the callback state must match the value issued for this session'
36275
36328
  );
36276
- return;
36277
36329
  }
36330
+ this.logger.warn("State mismatch for provider callback");
36331
+ this.respond(
36332
+ httpRespond.html(
36333
+ this.renderErrorPage("invalid_request", "Invalid state parameter. Please restart authentication."),
36334
+ 400
36335
+ )
36336
+ );
36337
+ return;
36278
36338
  }
36279
36339
  this.state.set("federatedSession", session);
36280
36340
  }
@@ -36361,11 +36421,19 @@ var init_oauth_provider_callback_flow = __esm({
36361
36421
  this.state.set("providerUserInfo", userInfo);
36362
36422
  } catch (err) {
36363
36423
  const errMsg = err instanceof Error ? err.message : "Unknown error";
36364
- this.logger.warn(`Failed to get user info from provider ${providerId}: ${errMsg}`);
36424
+ this.logger.warn(`Failed to resolve user identity from provider ${providerId}: ${errMsg}`);
36425
+ this.respond(
36426
+ httpRespond.html(
36427
+ this.renderErrorPage("access_denied", "Could not determine your identity from the provider"),
36428
+ 400
36429
+ )
36430
+ );
36431
+ return;
36365
36432
  }
36366
36433
  }
36367
36434
  this.logger.info(`Successfully exchanged code for tokens with provider: ${providerId}`);
36368
36435
  } catch (err) {
36436
+ if (err instanceof FlowControl) throw err;
36369
36437
  const errMsg = err instanceof Error ? err.message : "Unknown error";
36370
36438
  this.logger.error(`Provider token exchange error: ${errMsg}`);
36371
36439
  this.respond(
@@ -36745,6 +36813,24 @@ function extractBearer(headers) {
36745
36813
  const match = /^Bearer\s+(.+)$/i.exec(raw.trim());
36746
36814
  return match ? match[1].trim() : void 0;
36747
36815
  }
36816
+ function isLoopbackRedirectUri(uri) {
36817
+ let url;
36818
+ try {
36819
+ url = new URL(uri);
36820
+ } catch {
36821
+ return false;
36822
+ }
36823
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
36824
+ if (url.username || url.password) return false;
36825
+ const host = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
36826
+ if (host === "localhost" || host === "::1") return true;
36827
+ const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
36828
+ if (m) {
36829
+ const octets = m.slice(1).map((n) => Number(n));
36830
+ return octets.every((o) => o >= 0 && o <= 255) && octets[0] === 127;
36831
+ }
36832
+ return false;
36833
+ }
36748
36834
  var inputSchema20, outputSchema19, registrationRequestSchema, stateSchema19, plan19, name19, Stage19, OauthRegisterFlow;
36749
36835
  var init_oauth_register_flow = __esm({
36750
36836
  "libs/sdk/src/auth/flows/oauth.register.flow.ts"() {
@@ -36871,13 +36957,13 @@ var init_oauth_register_flow = __esm({
36871
36957
  return;
36872
36958
  }
36873
36959
  } else {
36874
- const bad = redirect_uris.find((u) => !/^https?:\/\/(localhost|\d+\.\d+\.\d+\.\d+|127\.0\.0\.1)/.test(u));
36960
+ const bad = redirect_uris.find((u) => !isLoopbackRedirectUri(u));
36875
36961
  if (bad) {
36876
36962
  this.respond(
36877
36963
  httpRespond.json(
36878
36964
  {
36879
36965
  error: "invalid_redirect_uri",
36880
- error_description: `Registration allows only localhost-style redirect_uris; got ${bad}`
36966
+ error_description: `Registration allows only loopback redirect_uris (localhost, 127.0.0.0/8, ::1) without an allowlist; got ${bad}`
36881
36967
  },
36882
36968
  { status: 400 }
36883
36969
  )
@@ -36918,7 +37004,20 @@ var init_oauth_register_flow = __esm({
36918
37004
  created_at: now,
36919
37005
  dev: true
36920
37006
  };
36921
- this.localAuth.dcrClientRegistry.register(this.registered);
37007
+ const accepted = this.localAuth.dcrClientRegistry.register(this.registered);
37008
+ if (!accepted) {
37009
+ this.scopeLogger.warn("OAuth register: dynamic client capacity reached \u2014 rejecting registration");
37010
+ this.respond(
37011
+ httpRespond.json(
37012
+ {
37013
+ error: "temporarily_unavailable",
37014
+ error_description: "Client registration capacity reached; please try again later."
37015
+ },
37016
+ { status: 503 }
37017
+ )
37018
+ );
37019
+ return;
37020
+ }
36922
37021
  }
36923
37022
  async respondRegistration() {
36924
37023
  const c = this.registered;
@@ -36984,6 +37083,23 @@ var init_oauth_register_flow = __esm({
36984
37083
  // libs/sdk/src/auth/flows/oauth.token.flow.ts
36985
37084
  import { z as z69 } from "@frontmcp/lazy-zod";
36986
37085
  import { randomUUID as randomUUID19 } from "@frontmcp/utils";
37086
+ function parseBasicAuthorization(header) {
37087
+ if (typeof header !== "string") return void 0;
37088
+ const m = /^Basic\s+(.+)$/i.exec(header.trim());
37089
+ if (!m) return void 0;
37090
+ let decoded;
37091
+ try {
37092
+ const b64 = m[1].trim();
37093
+ if (typeof Buffer !== "undefined") decoded = Buffer.from(b64, "base64").toString("utf8");
37094
+ else if (typeof atob === "function") decoded = atob(b64);
37095
+ else return void 0;
37096
+ } catch {
37097
+ return void 0;
37098
+ }
37099
+ const idx = decoded.indexOf(":");
37100
+ if (idx < 0) return void 0;
37101
+ return { clientId: decoded.slice(0, idx), clientSecret: decoded.slice(idx + 1) };
37102
+ }
36987
37103
  function coerceTokenRequestBody(request) {
36988
37104
  const body = request.body;
36989
37105
  if (body && typeof body === "object" && !Array.isArray(body) && Object.keys(body).length > 0) {
@@ -37028,6 +37144,8 @@ var init_oauth_token_flow = __esm({
37028
37144
  redirect_uri: z69.string().url(),
37029
37145
  /** Public client identifier */
37030
37146
  client_id: z69.string().min(1),
37147
+ /** Client secret (confidential clients; also accepted via HTTP Basic). */
37148
+ client_secret: z69.string().optional(),
37031
37149
  /** PKCE verifier bound to the code */
37032
37150
  code_verifier: z69.string().regex(pkceVerifierRegex, "code_verifier must be 43\u2013128 chars of A\u2013Z, a\u2013z, 0\u20139, '-', '.', '_' or '~'")
37033
37151
  });
@@ -37036,7 +37154,9 @@ var init_oauth_token_flow = __esm({
37036
37154
  /** The refresh token */
37037
37155
  refresh_token: z69.string().min(1, "refresh_token is required"),
37038
37156
  /** Public client identifier */
37039
- client_id: z69.string().min(1)
37157
+ client_id: z69.string().min(1),
37158
+ /** Client secret (confidential clients; also accepted via HTTP Basic). */
37159
+ client_secret: z69.string().optional()
37040
37160
  });
37041
37161
  anonymousGrant = z69.object({
37042
37162
  grant_type: z69.literal("anonymous"),
@@ -37083,6 +37203,12 @@ var init_oauth_token_flow = __esm({
37083
37203
  const isDefaultAuthProvider = !metadata.auth;
37084
37204
  const isOrchestrated = !isDefaultAuthProvider;
37085
37205
  const rawBody = coerceTokenRequestBody(request);
37206
+ const basic = parseBasicAuthorization(request.headers?.["authorization"]);
37207
+ if (basic && rawBody && typeof rawBody === "object") {
37208
+ const b = rawBody;
37209
+ if (b["client_id"] === void 0) b["client_id"] = basic.clientId;
37210
+ if (b["client_secret"] === void 0) b["client_secret"] = basic.clientSecret;
37211
+ }
37086
37212
  const parsed = tokenRequestSchema.safeParse(rawBody);
37087
37213
  if (parsed.success) {
37088
37214
  this.state.set({
@@ -37133,7 +37259,13 @@ var init_oauth_token_flow = __esm({
37133
37259
  return;
37134
37260
  }
37135
37261
  const localAuth = this.scope.auth;
37136
- const result = await localAuth.exchangeCode(body.code, body.client_id, body.redirect_uri, body.code_verifier);
37262
+ const result = await localAuth.exchangeCode(
37263
+ body.code,
37264
+ body.client_id,
37265
+ body.redirect_uri,
37266
+ body.code_verifier,
37267
+ body.client_secret
37268
+ );
37137
37269
  if ("error" in result) {
37138
37270
  this.logger.warn(`Code exchange failed: ${result.error}`);
37139
37271
  this.respond(
@@ -37180,7 +37312,7 @@ var init_oauth_token_flow = __esm({
37180
37312
  return;
37181
37313
  }
37182
37314
  const localAuth = this.scope.auth;
37183
- const result = await localAuth.refreshAccessToken(body.refresh_token, body.client_id);
37315
+ const result = await localAuth.refreshAccessToken(body.refresh_token, body.client_id, body.client_secret);
37184
37316
  if ("error" in result) {
37185
37317
  this.logger.warn(`Refresh token failed: ${result.error}`);
37186
37318
  this.respond(
@@ -37408,15 +37540,17 @@ import {
37408
37540
  buildInsufficientScopeHeader,
37409
37541
  buildInvalidTokenHeader,
37410
37542
  buildUnauthorizedHeader,
37543
+ deriveExpectedAudience,
37411
37544
  deriveTypedUser,
37412
37545
  encryptJson as encryptJson2,
37413
37546
  extractBearerToken as extractBearerToken2,
37414
37547
  isJwt,
37415
- JwksService
37548
+ JwksService,
37549
+ validateAudience
37416
37550
  } from "@frontmcp/auth";
37417
37551
  import { z as z71 } from "@frontmcp/lazy-zod";
37418
37552
  import { getMachineId as getMachineId2, randomUUID as randomUUID20 } from "@frontmcp/utils";
37419
- var inputSchema23, stateSchema22, UnauthorizedSchema, AuthorizedSchema, ForbiddenSchema, sessionVerifyOutputSchema, plan22, name22, Stage22, SessionVerifyFlow;
37553
+ var inputSchema23, stateSchema22, UnauthorizedSchema, AuthorizedSchema, ForbiddenSchema, sessionVerifyOutputSchema, plan22, name22, Stage22, warnedUnpinnedAudience, SessionVerifyFlow;
37420
37554
  var init_session_verify_flow = __esm({
37421
37555
  "libs/sdk/src/auth/flows/session.verify.flow.ts"() {
37422
37556
  "use strict";
@@ -37461,6 +37595,7 @@ var init_session_verify_flow = __esm({
37461
37595
  };
37462
37596
  name22 = "session:verify";
37463
37597
  Stage22 = StageHookOf(name22);
37598
+ warnedUnpinnedAudience = false;
37464
37599
  SessionVerifyFlow = class extends FlowBase {
37465
37600
  logger = this.scopeLogger.child("SessionVerifyFlow");
37466
37601
  maskSub(sub) {
@@ -37478,20 +37613,20 @@ var init_session_verify_flow = __esm({
37478
37613
  const machineId = getMachineId2();
37479
37614
  if (sessionIdHeader) {
37480
37615
  const existingPayload = decryptPublicSession(sessionIdHeader);
37481
- const user2 = existingPayload ? { sub: `anon:${existingPayload.iat * 1e3}`, iss: issuer, name: "Anonymous", scope: scopes.join(" ") } : { sub: `anon:${randomUUID20()}`, iss: issuer, name: "Anonymous", scope: scopes.join(" ") };
37482
- const finalPayload = existingPayload && existingPayload.nodeId === machineId ? existingPayload : void 0;
37483
- this.respond({
37484
- kind: "authorized",
37485
- authorization: {
37486
- token: "",
37487
- user: user2,
37488
- session: {
37489
- id: sessionIdHeader,
37490
- payload: finalPayload
37616
+ if (existingPayload && existingPayload.nodeId === machineId) {
37617
+ const anonId = existingPayload.uuid ?? `${existingPayload.iat * 1e3}`;
37618
+ const user2 = { sub: `anon:${anonId}`, iss: issuer, name: "Anonymous", scope: scopes.join(" ") };
37619
+ this.respond({
37620
+ kind: "authorized",
37621
+ authorization: {
37622
+ token: "",
37623
+ user: user2,
37624
+ session: { id: sessionIdHeader, payload: existingPayload }
37491
37625
  }
37492
- }
37493
- });
37494
- return;
37626
+ });
37627
+ return;
37628
+ }
37629
+ this.logger.verbose("createAnonymousSession: ignoring unrecognized client session id; minting a fresh one");
37495
37630
  }
37496
37631
  const now = Date.now();
37497
37632
  const user = {
@@ -37629,6 +37764,8 @@ var init_session_verify_flow = __esm({
37629
37764
  {
37630
37765
  id: primary.providerConfig?.id ?? "default",
37631
37766
  issuerUrl: issuer,
37767
+ additionalIssuers: primary.providerConfig?.additionalIssuers,
37768
+ verifyIssuer: primary.providerConfig?.verifyIssuer,
37632
37769
  jwks: primary.providerConfig?.jwks,
37633
37770
  jwksUri: primary.providerConfig?.jwksUri
37634
37771
  }
@@ -37640,6 +37777,37 @@ var init_session_verify_flow = __esm({
37640
37777
  const result = await verify;
37641
37778
  if (result.ok) {
37642
37779
  this.state.set({ jwtPayload: result.payload });
37780
+ if (isTransparentMode(authOptions)) {
37781
+ const configuredAudience = authOptions.expectedAudience;
37782
+ if (!configuredAudience && !isPublicUrlPinned() && !warnedUnpinnedAudience) {
37783
+ warnedUnpinnedAudience = true;
37784
+ this.logger.warn(
37785
+ "transparent audience validation is deriving the expected audience from the request Host: set auth.expectedAudience or FRONTMCP_PUBLIC_URL to bind tokens to a fixed resource. X-Forwarded-Host is ignored unless FRONTMCP_TRUST_PROXY is enabled."
37786
+ );
37787
+ }
37788
+ const expectedAudiences = configuredAudience ? Array.isArray(configuredAudience) ? configuredAudience : [configuredAudience] : deriveExpectedAudience(this.state.required.baseUrl);
37789
+ const audResult = validateAudience(result.payload?.["aud"], {
37790
+ expectedAudiences,
37791
+ // By default accept tokens with no `aud` claim: many IdPs omit it, and
37792
+ // rejecting them here would break existing transparent deployments.
37793
+ // Tokens that DO carry an `aud` for another service are still rejected,
37794
+ // which blocks the cross-service replay in GHSA-hvvp-67p3-j379.
37795
+ // Operators whose IdP always sets `aud` can flip `requireAudience:true`
37796
+ // to reject audience-less tokens outright.
37797
+ allowNoAudience: authOptions.requireAudience !== true
37798
+ });
37799
+ if (!audResult.valid) {
37800
+ this.logger.warn("verifyIfJwt: audience validation failed", { error: audResult.error });
37801
+ this.respond({
37802
+ kind: "unauthorized",
37803
+ prmMetadataHeader: buildInvalidTokenHeader(
37804
+ this.state.required.prmUrl,
37805
+ audResult.error ?? "Token audience is not valid for this resource"
37806
+ )
37807
+ });
37808
+ return;
37809
+ }
37810
+ }
37643
37811
  const requiredScopes = authOptions?.["requiredScopes"];
37644
37812
  if (requiredScopes && requiredScopes.length > 0) {
37645
37813
  const scopeClaim = result.payload?.["scope"];
@@ -37859,6 +38027,10 @@ var init_well_known_oauth_authorization_server_flow = __esm({
37859
38027
  kind: z73.literal("json"),
37860
38028
  status: z73.literal(200),
37861
38029
  contentType: z73.literal("application/json; charset=utf-8"),
38030
+ // Response headers (e.g. `Cache-Control: no-store`). This flow uses a custom
38031
+ // output schema rather than `HttpJsonSchema`, so `headers` must be declared
38032
+ // explicitly for the renderer to emit it.
38033
+ headers: z73.record(z73.string(), z73.string()).optional(),
37862
38034
  body: z73.object({
37863
38035
  issuer: z73.string().min(1),
37864
38036
  authorization_endpoint: z73.string().min(1),
@@ -37940,6 +38112,10 @@ var init_well_known_oauth_authorization_server_flow = __esm({
37940
38112
  kind: "json",
37941
38113
  contentType: "application/json; charset=utf-8",
37942
38114
  status: 200,
38115
+ // Never cache: the advertised endpoints are derived from the request
38116
+ // origin, so a shared cache could serve a poisoned authorization/token
38117
+ // endpoint to another client (OAuth mix-up).
38118
+ headers: { "cache-control": "no-store" },
37943
38119
  body: {
37944
38120
  issuer: baseIssuer,
37945
38121
  authorization_endpoint: `${oauthBaseUrl}/oauth/authorize`,
@@ -38043,6 +38219,10 @@ var init_well_known_prm_flow = __esm({
38043
38219
  kind: "json",
38044
38220
  contentType: "application/json; charset=utf-8",
38045
38221
  status: 200,
38222
+ // Never cache: the body embeds the request-derived origin, so a shared
38223
+ // cache keyed only on (Host, path) could otherwise serve a poisoned
38224
+ // `authorization_servers` value to another client (OAuth mix-up).
38225
+ headers: { "cache-control": "no-store" },
38046
38226
  body: {
38047
38227
  resource,
38048
38228
  authorization_servers: [baseUrl],
@@ -38056,6 +38236,7 @@ var init_well_known_prm_flow = __esm({
38056
38236
  kind: "json",
38057
38237
  status: 200,
38058
38238
  contentType: "application/json; charset=utf-8",
38239
+ headers: { "cache-control": "no-store" },
38059
38240
  body: {
38060
38241
  resource,
38061
38242
  authorization_servers: [baseUrl],
@@ -38645,7 +38826,11 @@ var init_instance_local_primary_auth = __esm({
38645
38826
  /**
38646
38827
  * Exchange an authorization code for tokens
38647
38828
  */
38648
- async exchangeCode(code, clientId, redirectUri, codeVerifier) {
38829
+ async exchangeCode(code, clientId, redirectUri, codeVerifier, clientSecret) {
38830
+ if (this.dcrClientRegistryImpl.verifyClientSecret(clientId, clientSecret) === "invalid") {
38831
+ this.logger.warn("Client authentication failed at token endpoint (authorization_code)");
38832
+ return { error: "invalid_client", error_description: "Client authentication failed" };
38833
+ }
38649
38834
  const codeRecord = await this.authorizationStore.getAuthorizationCode(code);
38650
38835
  if (!codeRecord) {
38651
38836
  this.logger.warn(`Authorization code not found or expired: ${code.substring(0, 8)}...`);
@@ -38656,6 +38841,13 @@ var init_instance_local_primary_auth = __esm({
38656
38841
  }
38657
38842
  if (codeRecord.used) {
38658
38843
  this.logger.warn(`Authorization code already used: ${code.substring(0, 8)}...`);
38844
+ if (codeRecord.issuedRefreshToken) {
38845
+ try {
38846
+ await this.authorizationStore.revokeRefreshToken(codeRecord.issuedRefreshToken);
38847
+ } catch (err) {
38848
+ this.logger.warn(`Failed to revoke refresh token on authorization-code replay: ${err}`);
38849
+ }
38850
+ }
38659
38851
  await this.authorizationStore.deleteAuthorizationCode(code);
38660
38852
  return {
38661
38853
  error: "invalid_grant",
@@ -38717,9 +38909,19 @@ var init_instance_local_primary_auth = __esm({
38717
38909
  clientId,
38718
38910
  userSub: user.sub,
38719
38911
  scopes: codeRecord.scopes,
38720
- resource: codeRecord.resource
38912
+ resource: codeRecord.resource,
38913
+ userEmail: user.email,
38914
+ userName: user.name,
38915
+ consentEnabled: codeRecord.consentEnabled,
38916
+ selectedToolIds: codeRecord.selectedToolIds,
38917
+ authorizedAppIds: codeRecord.authorizedAppIds,
38918
+ customClaims: codeRecord.customClaims,
38919
+ federatedLoginUsed: codeRecord.federatedLoginUsed,
38920
+ selectedProviderIds: codeRecord.selectedProviderIds,
38921
+ skippedProviderIds: codeRecord.skippedProviderIds
38721
38922
  });
38722
38923
  await this.authorizationStore.storeRefreshToken(refreshTokenRecord);
38924
+ await this.authorizationStore.markCodeUsed(code, refreshTokenRecord.token);
38723
38925
  this.logger.info(`Tokens issued for user: ${user.sub}`);
38724
38926
  return {
38725
38927
  access_token: accessToken,
@@ -38732,7 +38934,11 @@ var init_instance_local_primary_auth = __esm({
38732
38934
  /**
38733
38935
  * Refresh an access token using a refresh token
38734
38936
  */
38735
- async refreshAccessToken(refreshToken, clientId) {
38937
+ async refreshAccessToken(refreshToken, clientId, clientSecret) {
38938
+ if (this.dcrClientRegistryImpl.verifyClientSecret(clientId, clientSecret) === "invalid") {
38939
+ this.logger.warn("Client authentication failed at token endpoint (refresh_token)");
38940
+ return { error: "invalid_client", error_description: "Client authentication failed" };
38941
+ }
38736
38942
  const tokenRecord = await this.authorizationStore.getRefreshToken(refreshToken);
38737
38943
  if (!tokenRecord) {
38738
38944
  this.logger.warn("Refresh token not found or expired");
@@ -38748,13 +38954,37 @@ var init_instance_local_primary_auth = __esm({
38748
38954
  error_description: "Client ID does not match"
38749
38955
  };
38750
38956
  }
38751
- const user = { sub: tokenRecord.userSub };
38752
- const accessToken = await this.signAccessToken(user, tokenRecord.scopes, tokenRecord.resource);
38957
+ const user = {
38958
+ sub: tokenRecord.userSub,
38959
+ email: tokenRecord.userEmail,
38960
+ name: tokenRecord.userName
38961
+ };
38962
+ const hasAuthorizedApps = Array.isArray(tokenRecord.authorizedAppIds);
38963
+ const hasCustomClaims = !!tokenRecord.customClaims && Object.keys(tokenRecord.customClaims).length > 0;
38964
+ const consentMetadata = tokenRecord.consentEnabled || tokenRecord.federatedLoginUsed || hasCustomClaims || hasAuthorizedApps ? {
38965
+ selectedToolIds: tokenRecord.selectedToolIds,
38966
+ selectedProviderIds: tokenRecord.selectedProviderIds,
38967
+ skippedProviderIds: tokenRecord.skippedProviderIds,
38968
+ consentEnabled: tokenRecord.consentEnabled,
38969
+ federatedLoginUsed: tokenRecord.federatedLoginUsed,
38970
+ authorizedAppIds: tokenRecord.authorizedAppIds,
38971
+ customClaims: tokenRecord.customClaims
38972
+ } : void 0;
38973
+ const accessToken = await this.signAccessToken(user, tokenRecord.scopes, tokenRecord.resource, consentMetadata);
38753
38974
  const newRefreshRecord = this.authorizationStore.createRefreshTokenRecord({
38754
38975
  clientId,
38755
38976
  userSub: tokenRecord.userSub,
38756
38977
  scopes: tokenRecord.scopes,
38757
- resource: tokenRecord.resource
38978
+ resource: tokenRecord.resource,
38979
+ userEmail: tokenRecord.userEmail,
38980
+ userName: tokenRecord.userName,
38981
+ consentEnabled: tokenRecord.consentEnabled,
38982
+ selectedToolIds: tokenRecord.selectedToolIds,
38983
+ authorizedAppIds: tokenRecord.authorizedAppIds,
38984
+ customClaims: tokenRecord.customClaims,
38985
+ federatedLoginUsed: tokenRecord.federatedLoginUsed,
38986
+ selectedProviderIds: tokenRecord.selectedProviderIds,
38987
+ skippedProviderIds: tokenRecord.skippedProviderIds
38758
38988
  });
38759
38989
  await this.authorizationStore.rotateRefreshToken(refreshToken, newRefreshRecord);
38760
38990
  this.logger.info(`Tokens refreshed for user: ${user.sub}`);
@@ -39054,13 +39284,22 @@ var init_instance_local_primary_auth = __esm({
39054
39284
  try {
39055
39285
  const [, payloadB64] = idToken.split(".");
39056
39286
  const payload = JSON.parse(new TextDecoder().decode(base64urlDecode2(payloadB64)));
39057
- return {
39058
- sub: payload["sub"],
39059
- email: payload["email"],
39060
- name: payload["name"],
39061
- picture: payload["picture"],
39062
- claims: payload
39063
- };
39287
+ const exp = typeof payload["exp"] === "number" ? payload["exp"] : void 0;
39288
+ if (exp !== void 0 && exp * 1e3 < Date.now()) {
39289
+ this.logger.warn(`ID token for ${providerId} is expired; falling back to userinfo`);
39290
+ } else {
39291
+ const sub = typeof payload["sub"] === "string" && payload["sub"].trim() ? payload["sub"] : void 0;
39292
+ if (sub) {
39293
+ return {
39294
+ sub,
39295
+ email: payload["email"],
39296
+ name: payload["name"],
39297
+ picture: payload["picture"],
39298
+ claims: payload
39299
+ };
39300
+ }
39301
+ this.logger.warn(`ID token for ${providerId} has no usable sub; falling back to userinfo`);
39302
+ }
39064
39303
  } catch (err) {
39065
39304
  this.logger.warn(`Failed to parse ID token for ${providerId}: ${err}`);
39066
39305
  }
@@ -39075,21 +39314,25 @@ var init_instance_local_primary_auth = __esm({
39075
39314
  });
39076
39315
  if (response.ok) {
39077
39316
  const userInfo = await response.json();
39078
- return {
39079
- sub: userInfo["sub"],
39080
- email: userInfo["email"],
39081
- name: userInfo["name"],
39082
- picture: userInfo["picture"],
39083
- claims: userInfo
39084
- };
39317
+ const sub = typeof userInfo["sub"] === "string" && userInfo["sub"].trim() ? userInfo["sub"] : void 0;
39318
+ if (sub) {
39319
+ return {
39320
+ sub,
39321
+ email: userInfo["email"],
39322
+ name: userInfo["name"],
39323
+ picture: userInfo["picture"],
39324
+ claims: userInfo
39325
+ };
39326
+ }
39327
+ this.logger.warn(`userinfo for ${providerId} returned no usable sub`);
39085
39328
  }
39086
39329
  } catch (err) {
39087
39330
  this.logger.warn(`Failed to get userinfo from ${providerId}: ${err}`);
39088
39331
  }
39089
39332
  }
39090
- return {
39091
- sub: `${providerId}:unknown`
39092
- };
39333
+ throw new Error(
39334
+ `Unable to determine a stable user identity from provider "${providerId}" (no id_token sub and no usable userinfo endpoint)`
39335
+ );
39093
39336
  }
39094
39337
  /**
39095
39338
  * Refresh tokens from upstream provider
@@ -49313,6 +49556,16 @@ function decideIntent(req, cfg) {
49313
49556
  debug: { key, channel: channel2, flags }
49314
49557
  };
49315
49558
  }
49559
+ function authorizeSessionTermination(verifyResult, requestedSessionId) {
49560
+ if (!verifyResult || verifyResult.kind !== "authorized" || !verifyResult.authorization) {
49561
+ return { kind: "unauthorized" };
49562
+ }
49563
+ const ownedSessionId = verifyResult.authorization.session?.id;
49564
+ if (!ownedSessionId || !requestedSessionId || requestedSessionId !== ownedSessionId) {
49565
+ return { kind: "forbidden" };
49566
+ }
49567
+ return { kind: "ok", sessionId: ownedSessionId };
49568
+ }
49316
49569
  var intentSchema, decisionSchema, CH_OTHER, CH_GET_SSE, CH_POST_INIT_JSON, CH_POST_INIT_SSE, CH_POST_JSON, CH_POST_SSE, CH_POST_MESSAGE, CH_GET_SSE_PATH, CH_MASK, B_HAS_SESSION, B_STATELESS_EN, B_REQ_SESSION, B_LEGACY_HINT, B_SSE_LISTENER_EN, B_STREAMABLE_EN, B_STATEFUL_EN, B_LEGACY_EN, h, wantsSSE, wantsJSON, isInitialize, RULES;
49317
49570
  var init_decide_request_intent_utils = __esm({
49318
49571
  "libs/sdk/src/common/utils/decide-request-intent.utils.ts"() {
@@ -49576,13 +49829,29 @@ function readPublicUrlPin() {
49576
49829
  }
49577
49830
  return void 0;
49578
49831
  }
49832
+ function isPublicUrlPinned() {
49833
+ return readPublicUrlPin() !== void 0;
49834
+ }
49835
+ function isProxyTrusted() {
49836
+ try {
49837
+ if (typeof process !== "undefined" && process.env) {
49838
+ const v = process.env["FRONTMCP_TRUST_PROXY"];
49839
+ return typeof v === "string" && /^(1|true|yes|on)$/i.test(v.trim());
49840
+ }
49841
+ } catch {
49842
+ }
49843
+ return false;
49844
+ }
49579
49845
  function getRequestBaseUrl(req, entryPath) {
49580
49846
  const pin = readPublicUrlPin();
49581
49847
  if (pin) {
49582
49848
  return `${pin}${entryPath ?? ""}`;
49583
49849
  }
49584
- const proto = req.headers["x-forwarded-proto"] || req.protocol || "http";
49585
- const host = req.headers["x-forwarded-host"] || req.headers["host"];
49850
+ const trustProxy = isProxyTrusted();
49851
+ const fwdProto = trustProxy ? req.headers["x-forwarded-proto"] : void 0;
49852
+ const fwdHost = trustProxy ? req.headers["x-forwarded-host"] : void 0;
49853
+ const proto = fwdProto || req.protocol || "http";
49854
+ const host = fwdHost || req.headers["host"];
49586
49855
  return `${proto}://${host}${entryPath ?? ""}`;
49587
49856
  }
49588
49857
  function computeIssuer(req, entryPath, scopeBase) {
@@ -54348,6 +54617,10 @@ import {
54348
54617
  } from "@frontmcp/auth";
54349
54618
  import { z as z103 } from "@frontmcp/lazy-zod";
54350
54619
  import { randomUUID as randomUUID27 } from "@frontmcp/utils";
54620
+ function parseUserScopes(user) {
54621
+ const s = user?.scope;
54622
+ return typeof s === "string" ? s.split(/\s+/).filter(Boolean) : [];
54623
+ }
54351
54624
  var plan39, httpRequestStateSchema, name40, Stage40, HttpRequestFlow;
54352
54625
  var init_http_request_flow = __esm({
54353
54626
  "libs/sdk/src/scope/flows/http.request.flow.ts"() {
@@ -54489,7 +54762,10 @@ var init_http_request_flow = __esm({
54489
54762
  ctx.updateAuthInfo({
54490
54763
  token,
54491
54764
  clientId: user.sub,
54492
- scopes: [],
54765
+ // Populate scopes from the verified token's `scope` claim instead of
54766
+ // hardcoding `[]`, so any consumer that authorizes on
54767
+ // `authInfo.scopes` sees the real grant (same accessor as the web path).
54768
+ scopes: parseUserScopes(user),
54493
54769
  // JWT exp is in seconds, SDK uses milliseconds throughout (e.g., Date.now())
54494
54770
  expiresAt: user.exp ? user.exp * 1e3 : void 0,
54495
54771
  extra: {
@@ -54608,6 +54884,27 @@ var init_http_request_flow = __esm({
54608
54884
  this.next();
54609
54885
  return;
54610
54886
  }
54887
+ const deleteVerify = this.state.required.verifyResult;
54888
+ const requestedSessionId = request[ServerRequestTokens.sessionId] ?? request.headers["mcp-session-id"] ?? request.query?.["sessionId"];
54889
+ const deleteAuthz = authorizeSessionTermination(deleteVerify, requestedSessionId);
54890
+ if (deleteAuthz.kind === "unauthorized") {
54891
+ this.logger.warn(`[${this.requestId}] DELETE session denied: request is not authenticated`);
54892
+ this.respond(
54893
+ httpRespond.unauthorized({
54894
+ headers: {
54895
+ "WWW-Authenticate": deleteVerify.prmMetadataHeader ?? "Bearer"
54896
+ }
54897
+ })
54898
+ );
54899
+ return;
54900
+ }
54901
+ if (deleteAuthz.kind === "forbidden") {
54902
+ this.logger.warn(`[${this.requestId}] DELETE session denied: caller does not own the requested session id`);
54903
+ this.respond(httpRespond.notFound("Session not found"));
54904
+ return;
54905
+ }
54906
+ request[ServerRequestTokens.auth] = deleteVerify.authorization;
54907
+ request[ServerRequestTokens.sessionId] = deleteAuthz.sessionId;
54611
54908
  this.state.set("intent", decision.intent);
54612
54909
  return;
54613
54910
  }
@@ -54720,7 +55017,9 @@ var init_http_request_flow = __esm({
54720
55017
  const authInfo = authorization ? {
54721
55018
  token: authorization.token,
54722
55019
  clientId: authorization.user?.sub,
54723
- scopes: [],
55020
+ // Same scope parsing as the Node path so scope-aware handlers behave
55021
+ // identically on the web transport.
55022
+ scopes: parseUserScopes(authorization.user),
54724
55023
  expiresAt: authorization.user?.exp ? authorization.user.exp * 1e3 : void 0,
54725
55024
  extra: {
54726
55025
  user: authorization.user,
@@ -57686,6 +57985,17 @@ function createHostValidationMiddleware(options) {
57686
57985
  });
57687
57986
  return;
57688
57987
  }
57988
+ const forwardedHost = req.headers?.["x-forwarded-host"];
57989
+ if (forwardedHost) {
57990
+ const candidates = forwardedHost.split(",").map((h2) => h2.trim()).filter(Boolean);
57991
+ if (candidates.some((h2) => !allowedHostsSet.has(h2))) {
57992
+ res.status(403).json({
57993
+ error: "Forbidden",
57994
+ message: "Invalid X-Forwarded-Host header"
57995
+ });
57996
+ return;
57997
+ }
57998
+ }
57689
57999
  }
57690
58000
  if (allowedOriginsSet) {
57691
58001
  const origin = req.headers?.["origin"];
@@ -61522,6 +61832,7 @@ export {
61522
61832
  authorizationRequiredMetaSchema,
61523
61833
  authorizationRequiredParamsSchema,
61524
61834
  authorizationSchema,
61835
+ authorizeSessionTermination,
61525
61836
  buildChannelInstructions,
61526
61837
  buildEsmPromptRecord,
61527
61838
  buildEsmResourceRecord,
@@ -61695,6 +62006,7 @@ export {
61695
62006
  isPackageSpecifier,
61696
62007
  isPublicError,
61697
62008
  isPublicMode,
62009
+ isPublicUrlPinned,
61698
62010
  isPubsubConfigured,
61699
62011
  isRedisProvider,
61700
62012
  isRemoteMode,