Skip to content

Add sso-setup-typescript plugin (Entra SSO for ui-widget agents) + expand authentication.md#156

Open
nayanaramakanth wants to merge 14 commits into
microsoft:mainfrom
nayanaramakanth:users/naramaka/sso-uiwidget-skill
Open

Add sso-setup-typescript plugin (Entra SSO for ui-widget agents) + expand authentication.md#156
nayanaramakanth wants to merge 14 commits into
microsoft:mainfrom
nayanaramakanth:users/naramaka/sso-uiwidget-skill

Conversation

@nayanaramakanth

@nayanaramakanth nayanaramakanth commented Jul 6, 2026

Copy link
Copy Markdown

Summary

Adds a setup-sso-ui-widget skill to the microsoft-365-agents-toolkit plugin: one-command Microsoft Entra SSO (single sign-on, no OBO) for a Microsoft 365 Copilot declarative agent whose tools are served by an MCP server. It supports both widget standards — MCP Apps (Express) and OAI Apps (raw-http) — and defaults to MCP Apps. Also expands declarative-agent-developer/references/authentication.md and adds an SSO hand-off to ui-widget-developer.

What's included

New skill — setup-sso-ui-widget/

  • scripts/*.ps1 (14) — the phases as real, testable PowerShell (detect → register Entra app → ATK OAuth → wire manifest → inject guard → write env → build/sideload → verify 401 → cleanup), plus resync-tunnel-url.ps1 and _lib.ps1. The .md files invoke the scripts (deterministic — works well with smaller models, not just Opus).
  • references/wire-and-guard.md, detect-and-inputs.md, register-app.md, build-verify-cleanup.md, auth.ts (hardened JWKS guard), sso-explained.md (token-flow deep dive).

Dual-layout support

  • Auto-detects the layout: MCP Apps (readiness_plugin.json + Express main.ts) vs OAI Apps (mcpPlugin.json + raw-http). Defaults to MCP Apps when ambiguous.
  • Patches the resolved manifest's runtimes[] auth → OAuthPluginVault (filename-agnostic).
  • Injects the same hardened auth.ts guard, wired per layout: an Express middleware for MCP Apps, or a raw-http handler edit for OAI Apps (no express-jwt, no server rewrite).

Updated

  • ui-widget-developer/SKILL.md — required "Add SSO" hand-off at completion.
  • declarative-agent-developer/references/authentication.md — expanded from OAuth-only to a full auth guide (Entra SSO S1–S6, auth-pattern chooser, 401-vs-403 consent-UX guidance).

Security

Single-tenant JWKS bearer-token guard: validates signature / aud / iss / exp via jose, and additionally enforces scp = access_as_user, tenant match, and rejects app-only tokens. Accepts every aud form Entra emits (bare client-id GUID + api://…) to avoid the endless consent loop, while still rejecting tokens minted for a different app. Includes an Azure Easy Auth deployment note (the custom guard is for local dev).

Tunnel resilience

Reuses the existing named devtunnel (never creates a second). If the tunnel URL later changes, resync-tunnel-url.ps1 (+ a Phase-2 drift check) re-aligns the OAuth base URL / App ID URI so auth doesn't break.

Tenant handling

No hardcoded tenant IDs or internal portals. Defaults to the standard single-tenant registration; managed-enterprise steps (--service-management-reference, co-owners, admin-consent portal) trigger reactively only if app creation requires them.

Testing

Validated on a public Microsoft Entra tenant across both layouts:

  • OAI Apps / raw-http — detect + wire + guard in src/ + 401 enforcement.
  • MCP Apps / Express — detect + wire + guard at root + 401 + valid Copilot SSO tokens accepted ([auth] Valid SSO token accepted).

Notes

  • SSO only (no OBO). For Microsoft Graph / downstream APIs, a separate OBO flow is out of scope.
  • Gap (toolkit, not this skill): create-mcp-app emits a standalone MCP server with no M365 agent wrapping; this skill assumes the project is already wrapped as a declarative agent. A clean "create-mcp-app → M365 agent" wrap step is a follow-up.
  • Copilot tool selection (match vs execute) is orchestration/model-dependent — independent of SSO wiring.

naramaka added 3 commits July 7, 2026 10:01
…pand authentication.md

New plugin sso-setup-typescript: setup-sso base skill (+4 references) and setup-sso-ui-widget, which adds Entra SSO to ui-widget-developer agents (MCP server + mcpPlugin.json), reusing the existing devtunnel; SSO only, no OBO. Includes a new sso-explained.md reference (token flow, client-id-GUID audience, 401-vs-403 consent loop).

Audience fix: the auth.ts guard accepts [clientId, api://clientId, appIdUri] because a real M365 Copilot SSO token's aud is the bare client-id GUID.

Public/external-only: no hardcoded tenant IDs or internal portals; standard registration path with generic admin-consent guidance. Conversation starters added conditionally. Update declarative-agent-developer/references/authentication.md with the full Entra SSO section.
… temp file

- Pre-authorized apps: remove the 'add Teams/Office/Outlook client IDs later' note; a DA only needs M365 Copilot (ab3be6b7). Keeps the preAuth list to a single entry.
- NO SCRATCH FILES ban: add the same permitted-exception carve-out as internal for the short-lived az rest --body @file temp file.
@nayanaramakanth nayanaramakanth force-pushed the users/naramaka/sso-uiwidget-skill branch from 88acc18 to 53efe18 Compare July 7, 2026 09:59
@sebastienlevert

Copy link
Copy Markdown
Collaborator

Thorough review — sso-setup-typescript + expanded authentication.md

Nice work — this is a genuinely useful, well-documented plugin, and the write-ups (sso-explained.md, the expanded authentication.md) are excellent. I reviewed the full diff in depth and cross-checked with multiple models (a security-focused pass and a correctness/library pass) to reduce blind spots. Findings are grouped by severity; the security items in the injected auth.ts guard are the ones I'd want addressed before this leaves draft.

I've also included the explicit duplication verdict that was requested.


🔴 Security (the injected auth.ts is the only runtime enforcement — worth scrutiny)

1. [High] The token guard never validates scp / rejects app‑only tokens.
setup-sso-ui-widget/SKILL.md → Phase 7a validateBearerToken calls jwtVerify with only { audience, issuer, algorithms }. It verifies signature/aud/iss/exp but does not check that scp contains access_as_user, nor does it distinguish delegated vs app‑only tokens. sso-explained.md §3 itself states "scp must contain access_as_user" — but the code doesn't enforce it. Any token minted for this app's audience (including a client‑credentials token with roles/no scp) would be accepted, and downstream tools reading claims.oid would get undefined.
Fix: after jwtVerify, assert String(payload.scp ?? "").split(" ").includes("access_as_user") (and optionally reject idtyp: "app"), throwing otherwise.

2. [High] Multi‑tenant registration vs single‑tenant issuer pinning.
The shared entra-app-registration.md offers AzureADMultipleOrgs, but auth.ts hard‑pins both the JWKS URL and issuer allowlist to a single TENANT_ID (https://login.microsoftonline.com/${tenantId}/v2.0). A user from any other tenant would present iss = .../<theirTenant>/v2.0 and get a 401 → the exact endless sign‑in loop the docs warn about.
Fix: either force single‑tenant for the SSO path, or implement tid‑aware issuer validation (validate iss matches .../<payload.tid>/v2.0) with the appropriate common/organizations JWKS strategy. At minimum, document that this skill only supports single‑tenant.

3. [Medium] Base setup-sso skill — "validation illusion".
Unlike the ui‑widget skill (which injects a jose guard), the base setup-sso skill only checks that express-jwt is a dependency (Phase 0) and writes .env audience values (Phase 7c). It never inspects or injects the actual /mcp auth middleware. So SSO can be fully "configured" on the Copilot side while the server still accepts unauthenticated requests if the template's middleware is missing/misconfigured. The Phase 9 401 probe only runs for local MCP backends and merely prints a warning.
Fix: inject/verify route protection deterministically (as the ui‑widget path does), or hard‑gate success on the 401 probe with an explicit failure rather than a soft warning.

4. [Medium] Auth error text leaked to the caller.
Phase 7b returns "Authentication failed: " + (err as Error).message to the client, exposing verifier internals (e.g. "unexpected audience", key‑id issues).
Fix: return a generic 401 message to the caller; log the detailed reason server‑side only.

5. [Low] Accepting the bare client‑id GUID aud is fine — once #1 lands. The [clientId, api://clientId, appIdUri] audience list is the correct workaround for real Copilot tokens and is safe because the issuer is tenant‑scoped. I only flag it to note it must be paired with the scp check (#1) so audience acceptance isn't the only gate.

6. [Low] v1 issuer accepted despite requiring v2 tokens. auth.ts also allows https://sts.windows.net/${tenantId}/, but S5 / requestedAccessTokenVersion=2 guarantees v2 tokens. Dropping the v1 issuer tightens the surface without cost.

7. [Low] Least privilege — User.Read + admin consent added unconditionally. entra-app-update.md Step 4 always adds Graph User.Read and requests admin consent, even though pure SSO identity validation (no OBO) needs no Graph permission. Consider making the Graph permission opt‑in for the downstream/OBO case.

8. [Nit] Claims logged on every call. console.log("[auth] Valid SSO token accepted:", { sid, aud, tid, iss }) logs identifiers on each request. Fine as a dev proof signal (and it's not the raw token), but worth a note to gate/remove for production.


📚 Common‑library usage

  • jose is used correctly and idiomatically — createRemoteJWKSet (with key caching, so no per‑request Entra call) + jwtVerify with explicit algorithms: ["RS256"] (good — blocks alg‑confusion/none). ✅
  • AsyncLocalStorage (claimsStore) is the right pattern for per‑request identity and correctly avoids the cross‑request leakage a module‑level global would cause. The lazy ensureConfig() note (avoiding a top‑level process.env read before dotenv loads) is a nice touch. ✅
  • Inconsistency: the two skills use different validation stacks — jose (ui‑widget, injected) vs express-jwt/jwks-rsa (base, assumed but never injected — see Adding Microsoft SECURITY.MD #3). Worth calling out so users of the base skill know the security‑critical piece is on them.

🧭 Correctness / robustness

  • [Medium] YAML patched with regex/string ops. Both setup-sso Phase 5a and setup-sso-ui-widget Phase 4a locate/insert oauth/register via -match/IndexOf and grab the first configurationId:/applicationIdUri:. This can bind to the wrong block, and the provision:‑insertion path breaks if provision: is the file's last line with no trailing newline (IndexOf("\n", …)-1 → inserts at offset 0, corrupting the file). Prefer structural YAML parse, or guard the -1 case.
  • [Low] az ... 2>$null swallows creation errors. In entra-app-registration.md Step 4, a failed az ad app create yields a null $app/empty $ClientId, and downstream steps fail with confusing errors instead of a clear message.

🧩 Documentation consistency

  • [Medium] authentication.md contradicts itself on the redirect URI. S1 correctly uses oAuthConsentRedirect, but the pre‑existing troubleshooting row still says "Ensure redirect URI is exactly …/oAuthRedirect". Update the troubleshooting row to oAuthConsentRedirect.
  • [Low] Env‑var naming drift. authentication.md documents <PREFIX>_SSO_AUTH_ID / <PREFIX>_SSO_APP_ID_URI, but both skills hardcode MCP_DA_OAUTH_AUTH_ID / MCP_DA_OAUTH_APP_ID_URI. Align the doc and the automation.

✅ Duplication verdict — sso-explained.md vs authentication.md

Not a duplicate. They serve distinct purposes and cross‑reference each other:

  • authentication.md (declarative‑agent‑developer) = procedural registration/wiring reference spanning Entra SSO and third‑party OAuth, for both MCP and API‑plugin agents (the auth‑pattern chooser, S1–S6, oauth/register).
  • sso-explained.md = conceptual/runtime deep dive tied to the ui‑widget layout (end‑to‑end token flow, a decoded token anatomy, claimsStore, failure‑mode table, OBO next steps).

However, two sections are near‑verbatim in both files: the "audience = bare client‑id GUID" rule and the "401 vs 403 consent loop" table/behavior. That's a maintenance/drift risk. Recommendation: keep one canonical copy (I'd vote authentication.md) and have sso-explained.md link to it for those two topics, rather than restating them.


Overall

Solid, ship‑worthy direction with great docs. I'd treat #1 (scp enforcement) and #2 (multi‑tenant issuer) as blocking for the injected guard, #3/#4 as strong follow‑ups, and the doc/robustness items as quick cleanups. Happy to pair on the auth.ts hardening if useful.

Reviewed with multiple models (security pass + correctness/library pass) cross‑checked against a manual read of the full diff.

naramaka added 5 commits July 7, 2026 21:46
…5-agents-toolkit

Addresses reviewer direction (Eric/Sebastien): don't ship a separate plugin (avoids a discovery issue), and drop the base setup-sso skill (its 'validation illusion' - it never injected/verified the guard).

- Move setup-sso-ui-widget into plugins/microsoft-365-agents-toolkit/skills/ (auto-discovered via the plugin's skills:./skills/).
- Fold the former base setup-sso references (entra-app-registration, entra-app-update, dev-tunnel, admin-consent) into the skill so it is fully self-contained.
- Delete the standalone plugins/sso-setup-typescript plugin (plugin.json, README, base setup-sso skill).
- Stays on PowerShell (de-PowerShell rewrite parked per Sebastien: the model adapts to PS).
Addresses Sebastien's security review of the injected guard:
- microsoft#1 Enforce scp contains access_as_user and reject app-only (idtyp=app) tokens, so audience acceptance is no longer the only gate and a real signed-in user is always present.
- microsoft#2 Single-tenant only: pin the issuer to the tenant's v2 endpoint + defense-in-depth tid match; documented single-tenant-only.
- microsoft#6 Drop the v1 sts.windows.net issuer (app issues v2 tokens).
- microsoft#4 Return a generic 401 (Authentication required) to the caller; log the detailed reason server-side only.
- microsoft#8 Gate the per-request [auth] identity log behind SSO_DEBUG=1 so it never logs identifiers in prod (test steps set it).

- Rip the guard source out of SKILL.md into references/auth.ts (keeps the skill lightweight, per Sebastien); Phase 7a now copies that file in.
- YAML provision:-insert: guard the IndexOf newline -1 case (provision: as the file's last line with no trailing newline) so it no longer inserts at offset 0 and corrupts the yml.
- entra-app-registration: force single-tenant (AzureADMyOrg) to match the single-tenant guard, and document that multi-tenant is unsupported (microsoft#2). Drop 2>\ on az ad app create and fail clearly on an empty ClientId instead of confusing downstream errors.
- microsoft#7 Least privilege: make Graph User.Read + admin consent OPT-IN (only for OBO/Graph); pure SSO needs neither. Updated Step 5 + the verify expectation + the SKILL Phase 5 summary.
- authentication.md: the 'Invalid redirect URI' troubleshooting row now distinguishes SSO (oAuthConsentRedirect) from third-party OAuth (oAuthRedirect), resolving the contradiction with S1.
- authentication.md: document the concrete key names the setup-sso-ui-widget skill uses (MCP_DA_OAUTH_AUTH_ID / MCP_DA_OAUTH_APP_ID_URI) so the doc and automation align.
- sso-explained.md: point the audience rule (3.2) and the 401-vs-403 table (6.1) at authentication.md as the canonical source instead of restating them near-verbatim (drift risk), keeping only the skill-specific auth.ts detail.
…toolkit

authentication.md pointed at the retired sso-setup-typescript plugin; update it to reference the setup-sso-ui-widget skill in this same plugin and link sso-explained.md relatively.
@nayanaramakanth

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree company="Microsoft"

naramaka added 2 commits July 9, 2026 19:35
…t resync + Azure/Easy Auth note

- Move phase logic from reference .md into real scripts/*.ps1 (13 scripts + _lib), .md files invoke them via pwsh -File $SsoScripts/x.ps1.
- Group references into detect-and-inputs / register-app / wire-and-guard / build-verify-cleanup; delete old per-phase .md.
- wire-mcpplugin.ps1: copy the widget's own conversation_starters from mcpPlugin.json into declarativeAgent.json when the DA defines none (no synthetic starters); fix @($null).Count==1 guard.
- Add scripts/resync-tunnel-url.ps1 + Phase 2 drift check: keep OAuth baseUrl/App ID URI aligned when the dev tunnel URL changes.
- SKILL.md: Azure Easy Auth deployment note (Learn links), tunnel-changed recovery in Notes & Error Handling + FINAL SUMMARY caveat.
- ui-widget-developer: add optional 'Add SSO' handoff to setup-sso-ui-widget.
@nayanaramakanth nayanaramakanth marked this pull request as ready for review July 9, 2026 17:53
naramaka added 4 commits July 10, 2026 16:04
…w-http) layouts

- Broaden skill description to cover both widget standards (default MCP Apps) so Copilot auto-selects it for create-mcp-app projects, not just ui-widget-developer.
- _lib.ps1: add Get-PluginManifest (filename-agnostic via declarativeAgent.json actions[].file -> mcpPlugin.json OR readiness_plugin.json) + Get-ServerStyle (express vs rawhttp).
- detect-project.ps1: accept either manifest, report layout + server style, persist SSO_SERVER_STYLE.
- wire-mcpplugin.ps1: patch the resolved manifest (not hardcoded mcpPlugin.json).
- inject-guard.ps1: jose + dotenv (Express); place auth.ts next to the server entry (src/ for raw-http, root for Express main.ts).
- wire-and-guard.md: Phase 7b split into Variant A (raw-http) + Variant B (Express app.all('/mcp')), both reuse auth.ts; 7c CORS for both.
Validated end-to-end on an Express/MCP Apps project (readiness_plugin.json): guard enforces 401 and accepts valid Copilot SSO tokens.
…lugin integration

After adding an MCP server as a plugin to a DA, offer to run setup-sso-ui-widget. Completes the create-mcp-app -> add MCP plugin -> SSO path (no separate wrap step). Validated end-to-end: DA-dev emits {name}-plugin.json, which the SSO skill resolves filename-agnostically and patches to OAuthPluginVault.
…asy Auth config)

Per Eric's deployment feedback: codify the field-by-field Easy Auth blade config (App Service -> Authentication -> Microsoft Entra) so the model configures it right first time instead of iterating. Includes the two gotchas: audience must be the bare client-id (else 401), and Allowed client applications must include the M365 Copilot host client id ab3be6b7 (else 403). Maps placeholders to this skill's CLIENT_ID/TENANT_ID + the Copilot host id from Phase 5. Linked from the Azure notes in wire-and-guard.md and SKILL.md FINAL SUMMARY.
…d so it always prints

The model was paraphrasing the FINAL SUMMARY deployment note and dropping the easy-auth.md reference. Rewrite it as a reproduce-verbatim block with concrete values (ClientId, TenantId, M365 Copilot host id ab3be6b7) + the two gotchas inline + easy-auth.md cited by name, so the Easy Auth guidance surfaces reliably in every run.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants