/**
* mcp/skills/verifySsoAuth.ts — verify_sso_auth
*
* Unauthenticated reachability probe against the IDP's public endpoints.
* This tool confirms that the IDP is reachable and responding healthily
* from the endpoint — it does NOT verify that the user's new password
* works. Password propagation is verified implicitly when the user
* signs back into their apps (Outlook, VPN, Teams, …).
*
* Probe strategy
* --------------
* 1. HEAD/GET the OIDC discovery endpoint
* (.well-known/openid-configuration) — a 200 response proves DNS +
* TLS + IDP liveness.
* 2. Optionally GET the tenant-specific userinfo endpoint with NO
* Authorization header — the IDP should respond with 401 (challenge),
* proving the endpoint is live.
*
* Returns a structured { reachable, checks[] } result. Never throws.
*/
import { z } from "zod" ;
import { httpGet } from "./_shared/platform" ;
import {
buildOidcDiscoveryUrl,
idpDisplayName,
type Idp,
} from "./_shared/idp" ;
// -- Meta ---------------------------------------------------------------------
export const meta = {
name: "verify_sso_auth" ,
description:
"Verifies IDP endpoint reachability and TLS health by probing the " +
"OIDC discovery endpoint and a token endpoint (expected 401). Does NOT " +
"verify that the user's new password works — password propagation is " +
"verified implicitly when the user signs back into apps. Use as the " +
"final step in a cloud IDP password-reset workflow." ,
riskLevel: "low" ,
destructive: false ,
requiresConsent: false ,
supportsDryRun: false ,
affectedScope: [ "user" ],
auditRequired: true ,
schema: {
idp: z
. enum ([ "okta" , "entra" , "google" , "unknown" ])
. describe ( "IDP identifier from detect_identity_provider." ),
tenant: z
. string ()
. optional ()
. describe ( "IDP tenant slug (required for Okta)." ),
},
} as const ;
// -- Types --------------------------------------------------------------------
export interface VerifyCheck {
name : string ;
url : string ;
httpStatus : number ;
passed : boolean ;
note : string ;
}
export interface VerifySsoResult {
idp : Idp ;
idpLabel : string ;
reachable : boolean ;
checks : VerifyCheck [];
message : string ;
}
// -- Implementation -----------------------------------------------------------
async function probeDiscovery ( url : string ) : Promise < VerifyCheck > {
const r = await httpGet (url, { accept: "application/json" }, { timeoutMs: 5_000 });
if (r.failureReason) {
return {
name: "OIDC discovery" ,
url,
httpStatus: r.statusCode,
passed: false ,
note:
r.failureReason === "timeout"
? "Discovery request timed out (IDP unreachable)."
: "Network error contacting the IDP discovery endpoint." ,
};
}
const passed = r.statusCode === 200 ;
return {
name: "OIDC discovery" ,
url,
httpStatus: r.statusCode,
passed,
note: passed
? "Discovery endpoint responded 200 — TLS and DNS are healthy."
: `Discovery endpoint returned unexpected status ${ r . statusCode }.` ,
};
}
/**
* Hit a token / login endpoint with no credentials — the expected
* response is 400/401/405. Any 2xx here would indicate a weird
* misconfiguration; 5xx suggests the IDP is unhealthy.
*/
async function probeChallenge ( idp : Idp , tenant ?: string ) : Promise < VerifyCheck | null > {
const url = challengeUrl (idp, tenant);
if ( ! url) return null ;
const r = await httpGet (url, {}, { timeoutMs: 5_000 });
if (r.failureReason) {
return {
name: "Auth-endpoint reachability" ,
url,
httpStatus: r.statusCode,
passed: false ,
note:
r.failureReason === "timeout"
? "Auth-endpoint request timed out."
: "Network error contacting the IDP auth endpoint." ,
};
}
// 400/401/403/405 → endpoint is live and rejecting the anonymous hit.
// 200/302 → also live (many IDPs redirect unauth users to a login page).
const live = [ 200 , 302 , 400 , 401 , 403 , 405 ]. includes (r.statusCode);
return {
name: "Auth-endpoint reachability" ,
url,
httpStatus: r.statusCode,
passed: live,
note: live
? `Auth endpoint responded ${ r . statusCode } — endpoint is live.`
: `Auth endpoint returned ${ r . statusCode }; the IDP may be unhealthy.` ,
};
}
function challengeUrl ( idp : Idp , tenant ?: string ) : string | null {
switch (idp) {
case "okta" :
if ( ! tenant || ! / ^ [a-z0-9-] +$ / i . test (tenant)) return null ;
return `https://${ tenant . toLowerCase () }.okta.com/oauth2/default/v1/authorize` ;
case "entra" : {
const t = tenant && tenant. length > 0 ? tenant : "common" ;
return `https://login.microsoftonline.com/${ encodeURIComponent ( t ) }/oauth2/v2.0/authorize` ;
}
case "google" :
return "https://accounts.google.com/o/oauth2/v2/auth" ;
case "unknown" :
return null ;
}
}
// Exported for unit tests.
export const __testing = { probeDiscovery, probeChallenge, challengeUrl };
// -- Exported run function ----------------------------------------------------
export async function run ({
idp ,
tenant ,
} : {
idp : Idp ;
tenant ?: string ;
}) : Promise < VerifySsoResult > {
const idpLabel = idpDisplayName (idp);
if (idp === "unknown" ) {
return {
idp, idpLabel, reachable: false , checks: [],
message: "IDP is unknown — nothing to verify." ,
};
}
const discoveryUrl = buildOidcDiscoveryUrl (idp, tenant);
const checks : VerifyCheck [] = [];
if (discoveryUrl) {
checks. push ( await probeDiscovery (discoveryUrl));
} else {
checks. push ({
name: "OIDC discovery" ,
url: "" ,
httpStatus: 0 ,
passed: false ,
note: "Discovery URL could not be built (missing tenant for Okta?)." ,
});
}
const challenge = await probeChallenge (idp, tenant);
if (challenge) checks. push (challenge);
const reachable = checks. every (( c ) => c.passed);
const message = reachable
? `${ idpLabel } endpoints are reachable. Sign back into your apps to confirm the password propagated.`
: `${ idpLabel } reachability checks did not all pass — review the per-check notes.` ;
return { idp, idpLabel, reachable, checks, message };
}
// -- CLI smoke test -----------------------------------------------------------
if ( false ) {
run ({ idp: "google" })
. then (( r ) => console. log ( JSON . stringify (r, null , 2 )))
. catch (( err : Error ) => { console. error (err.message); process. exit ( 1 ); });
}