Repository navigation
Add inbound PoP (SHR) token validation to the Entra sidecar #4008
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 6 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
5492207
Add inbound SHR PoP validation to the Entra Sidecar (throwaway spike)
soodt 6905c8b
Enforce app-only on the PoP path + fix misleading scope comment (spike)
soodt 2b692d2
Add real-Entra E2E pipeline tests for inbound SHR PoP validation
soodt b4b1eb1
Harden inbound SHR proof-of-possession validation in the sidecar
soodt 3e0c510
Document inbound PoP (SHR) validation in the sidecar and add sample c…
soodt 2b472cb
Merge branch 'master' into tanujsood/sidecar-shr-pop
soodt 0670112
Merge branch 'master' into tanujsood/sidecar-shr-pop
iNinja 73ad1be
Remove non-functional PoP header-binding knobs from the sidecar
soodt c6a59d4
Make PoP timestamp validation always-on in the sidecar
soodt 2a109f5
Validate sidecar PoP options at startup and harden the validation path
soodt 3ed2d59
Clarify app-only PoP is a token-type check, not an authorization gate
soodt 62248fa
Merge remote-tracking branch 'origin/tanujsood/sidecar-shr-pop' into …
soodt e3396eb
Merge branch 'master' into tanujsood/sidecar-shr-pop
iNinja File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
99 changes: 99 additions & 0 deletions
99
src/Microsoft.Identity.Web.Sidecar/Configuration/PopValidationOptions.cs
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System.Collections.ObjectModel; | ||
| using System.ComponentModel; | ||
|
|
||
| namespace Microsoft.Identity.Web.Sidecar.Configuration; | ||
|
|
||
| /// <summary> | ||
| /// Tunable flags for inbound Signed HTTP Request (SHR) Proof-of-Possession validation, bound | ||
| /// from the <c>Sidecar:PopValidation</c> configuration subsection. Every property defaults to the | ||
| /// secure Microsoft.IdentityModel <c>SignedHttpRequestValidationParameters</c> default, so when the | ||
| /// subsection is absent PoP validation runs with method/URI/path/timestamp binding on, query/header | ||
| /// binding off, unsigned headers and query parameters accepted, and a five-minute lifetime. | ||
| /// <para> | ||
| /// Only members exposed as simple configuration values are surfaced here; body-hash (<c>b</c>) | ||
| /// binding is not configurable and remains off. | ||
| /// </para> | ||
| /// </summary> | ||
| public class PopValidationOptions | ||
| { | ||
| /// <summary> | ||
| /// Gets or sets a value indicating whether the <c>ts</c> (timestamp) claim is validated - the | ||
| /// replay/freshness guard. Default <c>true</c>; disabling it removes replay protection. | ||
| /// </summary> | ||
| [DefaultValue(true)] | ||
| public bool ValidateTs { get; set; } = true; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a value indicating whether the <c>m</c> (HTTP method) claim is validated - request | ||
| /// binding. Default <c>true</c>; disabling it lets a captured SHR be replayed against another method. | ||
| /// </summary> | ||
| [DefaultValue(true)] | ||
| public bool ValidateM { get; set; } = true; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a value indicating whether the <c>u</c> (host + scheme) claim is validated - request | ||
| /// binding. Default <c>true</c>; disabling it lets a captured SHR be replayed against another host. | ||
| /// </summary> | ||
| [DefaultValue(true)] | ||
| public bool ValidateU { get; set; } = true; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a value indicating whether the <c>p</c> (path) claim is validated - request binding. | ||
| /// Default <c>true</c>; disabling it lets a captured SHR be replayed against another path. | ||
| /// </summary> | ||
| [DefaultValue(true)] | ||
| public bool ValidateP { get; set; } = true; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a value indicating whether the <c>q</c> (query parameters) claim is validated - | ||
| /// request binding. Default <c>false</c>; enabling it hardens binding but the caller must sign | ||
| /// <c>q</c> or every request fails. | ||
| /// </summary> | ||
| [DefaultValue(false)] | ||
| public bool ValidateQ { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a value indicating whether the <c>h</c> (headers) claim is validated - request | ||
| /// binding. Default <c>false</c>; enabling it hardens binding but the caller must sign <c>h</c> or | ||
| /// every request fails. | ||
| /// </summary> | ||
| [DefaultValue(false)] | ||
| public bool ValidateH { get; set; } | ||
|
iNinja marked this conversation as resolved.
Resolved
|
||
|
|
||
| /// <summary> | ||
| /// Gets or sets a value indicating whether headers not covered by the signature are accepted. | ||
| /// Default <c>true</c>. | ||
| /// </summary> | ||
| [DefaultValue(true)] | ||
| public bool AcceptUnsignedHeaders { get; set; } = true; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a value indicating whether query parameters not covered by the signature are | ||
| /// accepted. Default <c>true</c>. | ||
| /// </summary> | ||
| [DefaultValue(true)] | ||
| public bool AcceptUnsignedQueryParameters { get; set; } = true; | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets a value indicating whether the claims listed in <see cref="ClaimsToValidateWhenPresent"/> | ||
| /// are validated when present (even if their individual flag is off). Default <c>false</c>; while | ||
| /// <c>false</c> the <see cref="ClaimsToValidateWhenPresent"/> list is inert. | ||
| /// </summary> | ||
| [DefaultValue(false)] | ||
| public bool ValidatePresentClaims { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the signed HTTP request lifetime - the <c>ts</c> clock-skew tolerance. Default | ||
| /// 5 minutes. | ||
| /// </summary> | ||
| public TimeSpan SignedHttpRequestLifetime { get; set; } = TimeSpan.FromMinutes(5); | ||
|
|
||
| /// <summary> | ||
| /// Gets or sets the claims validated only when present, gated by <see cref="ValidatePresentClaims"/>. | ||
| /// Default <c>{ "m", "p" }</c>. Inert unless <see cref="ValidatePresentClaims"/> is <c>true</c>. | ||
| /// </summary> | ||
| public IList<string> ClaimsToValidateWhenPresent { get; set; } = new Collection<string> { "m", "p" }; | ||
| } | ||
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
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
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
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
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
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
43 changes: 43 additions & 0 deletions
43
src/Microsoft.Identity.Web.Sidecar/Pop/PopAuthenticationExtensions.cs
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using Microsoft.AspNetCore.Authentication; | ||
| using Microsoft.AspNetCore.Authentication.JwtBearer; | ||
| using Microsoft.AspNetCore.Authorization; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
|
|
||
| namespace Microsoft.Identity.Web.Sidecar.Pop; | ||
|
|
||
| /// <summary> | ||
| /// Wires inbound SHR PoP into the sidecar's authentication pipeline without changing the Bearer scheme | ||
| /// or the global default authentication scheme. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Registers a second authentication handler under the "PoP" scheme and a named authorization policy | ||
| /// (<see cref="PopConstants.ValidatePolicyName"/>) that accepts both "Bearer" and "PoP". Only the | ||
| /// <c>/Validate</c> endpoint opts into that policy, so every other endpoint - and the default scheme - | ||
| /// is unchanged. A Bearer request is validated by JwtBearer while the PoP handler no-ops; a PoP request | ||
| /// is validated by the PoP handler while JwtBearer no-ops. | ||
| /// </remarks> | ||
| internal static class PopAuthenticationExtensions | ||
| { | ||
| public static AuthenticationBuilder AddInboundShrPop(this AuthenticationBuilder builder) | ||
| { | ||
| builder.Services.AddSingleton<ShrPopValidationService>(); | ||
|
|
||
| builder.AddScheme<PopAuthenticationSchemeOptions, PopAuthenticationHandler>( | ||
| PopConstants.SchemeName, | ||
| static _ => { }); | ||
|
|
||
| // Accept both Bearer and PoP on /Validate. The global default scheme stays "Bearer". | ||
| builder.Services | ||
| .AddAuthorizationBuilder() | ||
| .AddPolicy(PopConstants.ValidatePolicyName, policy => | ||
| { | ||
| policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme, PopConstants.SchemeName); | ||
| policy.RequireAuthenticatedUser(); | ||
| }); | ||
|
|
||
| return builder; | ||
| } | ||
| } |
85 changes: 85 additions & 0 deletions
85
src/Microsoft.Identity.Web.Sidecar/Pop/PopAuthenticationHandler.cs
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System.Net.Http.Headers; | ||
| using System.Security.Claims; | ||
| using System.Text.Encodings.Web; | ||
| using Microsoft.AspNetCore.Authentication; | ||
| using Microsoft.Extensions.Logging; | ||
| using Microsoft.Extensions.Options; | ||
| using Microsoft.Identity.Web.Sidecar.Logging; | ||
|
|
||
| namespace Microsoft.Identity.Web.Sidecar.Pop; | ||
|
|
||
| /// <summary>Options for the PoP authentication scheme.</summary> | ||
| internal sealed class PopAuthenticationSchemeOptions : AuthenticationSchemeOptions | ||
| { | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// ASP.NET Core authentication handler for inbound SHR PoP. It recognises the "PoP" Authorization | ||
| /// scheme, runs <see cref="ShrPopValidationService"/>, and on success builds a | ||
| /// <see cref="ClaimsPrincipal"/> from the validated inner access token. The Bearer (JwtBearer) handler | ||
| /// is untouched; the <c>/Validate</c> endpoint opts into a policy that accepts both schemes. | ||
| /// </summary> | ||
| internal sealed class PopAuthenticationHandler : AuthenticationHandler<PopAuthenticationSchemeOptions> | ||
| { | ||
| private readonly ShrPopValidationService _validationService; | ||
|
|
||
| public PopAuthenticationHandler( | ||
| IOptionsMonitor<PopAuthenticationSchemeOptions> options, | ||
| ILoggerFactory logger, | ||
| UrlEncoder encoder, | ||
| ShrPopValidationService validationService) | ||
| : base(options, logger, encoder) | ||
| { | ||
| _validationService = validationService; | ||
| } | ||
|
|
||
| protected override async Task<AuthenticateResult> HandleAuthenticateAsync() | ||
| { | ||
| string authorization = Request.Headers.Authorization.ToString(); | ||
| if (string.IsNullOrEmpty(authorization)) | ||
| { | ||
| return AuthenticateResult.NoResult(); | ||
| } | ||
|
|
||
| if (!AuthenticationHeaderValue.TryParse(authorization, out AuthenticationHeaderValue? headerValue) || | ||
| !string.Equals(headerValue.Scheme, PopConstants.SchemeName, StringComparison.OrdinalIgnoreCase) || | ||
| string.IsNullOrEmpty(headerValue.Parameter)) | ||
| { | ||
| return AuthenticateResult.NoResult(); | ||
| } | ||
|
|
||
| ShrPopValidationResult result = await _validationService | ||
| .ValidateAsync(headerValue.Parameter, Request, Context.RequestAborted) | ||
| .ConfigureAwait(false); | ||
|
|
||
| if (!result.Succeeded) | ||
| { | ||
| string error = result.Error ?? "PoP validation failed."; | ||
| Logger.PopValidationFailed(error); | ||
| return AuthenticateResult.Fail(error); | ||
| } | ||
|
|
||
| // Expose the validated inner access token to the /Validate endpoint seam. | ||
| Context.Items[PopConstants.ValidatedAccessTokenItemKey] = result.ValidatedAccessToken; | ||
|
|
||
| var principal = new ClaimsPrincipal(result.ClaimsIdentity!); | ||
| var ticket = new AuthenticationTicket(principal, Scheme.Name); | ||
| return AuthenticateResult.Success(ticket); | ||
| } | ||
| protected override Task HandleChallengeAsync(AuthenticationProperties properties) | ||
| { | ||
| if (AuthenticationHeaderValue.TryParse(Request.Headers.Authorization.ToString(), out AuthenticationHeaderValue? headerValue) && | ||
| string.Equals(headerValue.Scheme, PopConstants.SchemeName, StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| Response.StatusCode = StatusCodes.Status401Unauthorized; | ||
| Response.Headers.Append( | ||
| Microsoft.Net.Http.Headers.HeaderNames.WWWAuthenticate, | ||
| $"{PopConstants.SchemeName} error=\"invalid_token\""); | ||
| } | ||
|
|
||
| return Task.CompletedTask; | ||
| } | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using Microsoft.IdentityModel.Protocols.SignedHttpRequest; | ||
|
|
||
| namespace Microsoft.Identity.Web.Sidecar.Pop; | ||
|
|
||
| /// <summary> | ||
| /// Constants for the inbound Signed HTTP Request (SHR) Proof-of-Possession path: the "PoP" | ||
| /// Authorization scheme token and the request-line contract header names. | ||
| /// </summary> | ||
| internal static class PopConstants | ||
| { | ||
| /// <summary>The Authorization scheme token for SHR PoP ("PoP"). Sourced from Microsoft.IdentityModel so it never drifts.</summary> | ||
| public const string SchemeName = SignedHttpRequestConstants.AuthorizationHeaderSchemeName; | ||
|
|
||
| /// <summary>Protocol label returned to the caller for a validated PoP request.</summary> | ||
| public const string ProtocolName = "PoP"; | ||
|
|
||
| /// <summary> | ||
| /// Name of the authorization policy applied to the <c>/Validate</c> endpoint. It accepts both the | ||
| /// "Bearer" and "PoP" schemes so a single endpoint handles either credential without changing the | ||
| /// global default scheme (which stays "Bearer"). | ||
| /// </summary> | ||
| public const string ValidatePolicyName = "SidecarValidateBearerOrPoP"; | ||
|
|
||
| /// <summary>Header carrying the original (signed) request absolute URI.</summary> | ||
| public const string OriginalUriHeaderName = "original-uri"; | ||
|
|
||
| /// <summary>Header carrying the original (signed) request HTTP method.</summary> | ||
| public const string OriginalMethodHeaderName = "original-method"; | ||
|
|
||
| /// <summary>HttpContext.Items key holding the validated inner access token (a JsonWebToken).</summary> | ||
| public const string ValidatedAccessTokenItemKey = "Sidecar.Pop.ValidatedAccessToken"; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.