Add inbound PoP (SHR) token validation to the Entra sidecar - #4008
Merged
Merged
Conversation
Re-hosts MISE.AuthN's SignedHttpRequestHandler validation to validate inbound app-only Signed HTTP Request (SHR) Proof-of-Possession tokens at the sidecar's /Validate endpoint, additively alongside the unchanged Bearer path. - Register a second "PoP" AuthenticationHandler; the global default scheme stays "Bearer". Only /Validate opts into a [Bearer, PoP] authorization policy, so every other endpoint and the Bearer path are unchanged. - Validate the embedded access token with the same AzureAd/JwtBearer TokenValidationParameters (one source of truth for issuer/audience/ signing-keys/lifetime); clone per request and propagate the ConfigurationManager for order-independent JWKS key resolution. - Timestamp-only (ts) freshness with MISE-compatible default flags (m/u/p/ts on; q/h/b off; 5-min lifetime), operator-configurable via a new Sidecar:PopValidation options block (AOT-safe: bind DTO, map in code). - Emit WWW-Authenticate: PoP error="invalid_token" only for PoP-scheme requests; log the failure reason server-side at Information (parity with Bearer). No error_description over the wire. - Tests: ARM-shaped round-trip via an in-process mock IdP (live discovery/ JWKS), outer-SHR and inner-AT negative matrix, config default/override mapping, server-side-log assertion, and a Bearer-unchanged guard. Scope: app-only tokens, timestamp-only. Server nonce, OBO/actor, PFT/CDT, and mTLS PoP are out of scope. Throwaway spike - not for production; no PR/merge intended. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addresses review feedback on the inbound SHR PoP spike. - Positively assert the embedded access token is app-only in ShrPopValidationService: reject a token carrying "scp" (or idtyp != "app") before reporting Protocol:"PoP". SHR validation authenticates the inner token but does not distinguish app-only from delegated, so without this a valid cnf-bound *delegated* token wrapped in an SHR would be accepted - and because the /Validate PoP branch skips the delegated-scope gate, it would bypass the AzureAd:Scopes check a Bearer caller faces. Fail-closed: the handler logs the reason (PopValidationFailed) and returns 401. - Correct the /Validate comment that claimed skipping the scope check for PoP is "the same effective outcome as an app-only Bearer under ACL" - only true when AzureAd:Scopes is unset. Document the divergence and a production open item (whether PoP needs a symmetric app-permission/roles gate). - Tests: add CreateDelegatedAccessToken and a Validate_WithDelegatedEmbeddedToken_ReturnsUnauthorizedAsync negative proving a delegated token in an otherwise-valid SHR is rejected. ShrPopValidationTests 19/19; sidecar Release build 0 warnings (AOT clean). Throwaway spike - not for production. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds SidecarPopEndpointsE2ETests (design-doc S9 "E2E (pipeline)" deliverable), gated behind #if !FROM_GITHUB_ACTION like the sibling Bearer E2E suite. Each test acquires a REAL app-only token from Entra via MSIdWeb (GetAuthenticationResultForAppAsync + PoPAuthenticationConfiguration over an ARM-shaped request line) and drives it through the sidecar's real AzureAd discovery/JWKS path. Coverage mirrors the Bearer E2E surface and adds the PoP-specific guarantees that only a real token proves end-to-end (9 tests): - Happy: real PoP -> Protocol "PoP"; real Bearer control -> Protocol "Bearer". - Bad credential: garbage PoP -> 401 with PoP invalid_token challenge. - Request-line BINDING vs a real SHR: tampered method, tampered URI, and missing request-line headers -> 401 (anti-replay guarantee). - Scheme ISOLATION with real tokens: real SHR on Bearer -> 401; real Bearer on PoP -> 401 (schemes never cross-validate). - No credential -> 401 advertising Bearer. Verified: all 9 PASS against the real lab tenant. Confirms the lab agent app issues cnf-bound PoP tokens for the sidecar scope. Uses CurrentUser/My locally to avoid the LocalMachine private-key ACL/elevation requirement. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Refine the inbound Signed HTTP Request (SHR) proof-of-possession validation for the sidecar: professionalize comments and documentation to repo conventions and address review feedback. - Guard the SHR validation catch against OperationCanceledException - Remove the unused original-IP header constant - Clarify the reused JwtBearer TokenValidationParameters / ConfigurationManager fallback - Document the /Validate PoP path, trusted-header and replay-window caveats in the README - Align test certificate store and test naming with repo conventions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lients Update the sidecar documentation surface for inbound app-only PoP (Signed HTTP Request) validation on /Validate, and extend the dev-adapter sample clients to exercise the PoP path alongside Bearer. - README: note the WWW-Authenticate: PoP invalid_token challenge on PoP failure. - appsettings.json: add a commented Sidecar:PopValidation sample block. - PopValidationOptions / ValidateAuthorizationHeaderResult: clarify XML docs and document the Protocol values (Bearer or PoP). - .http: add a PoP /Validate sample request with original-method/original-uri. - Python sample client + CLI: accept optional original-method/original-uri and document a PoP validate example. - TypeScript sample: forward the request line for PoP tokens and document it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bogdan Gavril (bgavrilMS)
approved these changes
Sep 1, 2026
Bogdan Gavril (bgavrilMS)
left a comment
Member
There was a problem hiding this comment.
Approved with comments.
The sidecar receives only the request line (method + URI) from the caller, never the SHR-signed headers, so the h claim can never be validated against them. ValidateH/AcceptUnsignedHeaders were therefore removed from the Sidecar:PopValidation surface; the secure defaults (h binding off, unsigned headers accepted) are now hardcoded in the validation parameters. Query (q) binding stays configurable because the query rides inside original-uri. Updates the options doc-comments, README and appsettings sample, and reframes the mapping test to prove the removed keys are now inert. Addresses PR #4008 review feedback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ValidateTs is removed from the Sidecar:PopValidation surface and hardcoded to true. Timestamp validation is the only replay/freshness guard in the nonce-less design, so it must stay on and there is no reason to expose it. Updates the options doc-comment, the README and appsettings sample, and the mapping test, which now proves a ValidateTs=false config key is ignored. Addresses PR #4008 review feedback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add an IValidateOptions<SidecarOptions> validator that bounds SignedHttpRequestLifetime greater than zero and register it with ValidateOnStart, so a misconfigured lifetime fails the host boot instead of throwing on the first PoP request. Also move the SignedHttpRequestValidationContext construction inside the guarded try in ValidateAsync, so any residual throw returns a clean 401 rather than a 500. Adds unit tests for the validator. Addresses PR #4008 review feedback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reword the /Validate PoP-branch comment and README so it is clear the app-only restriction is a token-type admission check, not an authorization decision. AzureAd:Scopes enforcement targets delegated 'scp' claims (Bearer only); app-only PoP tokens carry none, so authorizing the returned app identity remains the caller's responsibility. Documentation-only; no behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tanujsood/sidecar-shr-pop
Ignacio Inglese (iNinja)
approved these changes
Sep 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add inbound PoP (SHR) token validation to the Entra sidecar
Summary of the changes (Less than 80 chars)
Description
Adds inbound Signed HTTP Request (SHR) proof-of-possession validation to the
Microsoft.Identity.Web.Sidecar. The/Validateendpoint now accepts either aBearertoken (unchanged) or, for app-only (client-credentials) tokens, thePoPscheme, returningprotocol: "PoP"with the embedded token's claims.Scope
App-only tokens only — no OBO/actor, no user/delegated, no PFT/CDT-over-PoP, no mTLS-PoP.
Timestamp-only freshness; no server nonce.
Tests
cnfnegatives, plus default options-mapping.
Docs & samples
Sidecar
README,appsettings.jsonsample block,.httpsample, and PoP examples addedto the Python and TypeScript dev-adapter sample clients.