| File: Authentication\AspirePolicyEvaluator.cs | Web Access |
| Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization.Policy; using Microsoft.AspNetCore.Authorization; using System.Security.Claims; namespace Aspire.Dashboard.Authentication; /// <summary> /// Mostly a copy of https://github.com/dotnet/aspnetcore/blob/e9647658dc46260cce21e980ec324565a4d39293/src/Security/Authorization/Policy/src/PolicyEvaluator.cs /// The logic has been updated to support an auth scheme requesting that challenge is suppressed. /// Used to avoid challenging to authenticate when rejecting requests sent on the wrong endpoint connection. /// </summary> public class AspirePolicyEvaluator : IPolicyEvaluator { public const string SuppressChallengeKey = "SuppressChallenge"; private readonly IAuthorizationService _authorization; /// <summary> /// Constructor /// </summary> /// <param name="authorization">The authorization service.</param> public AspirePolicyEvaluator(IAuthorizationService authorization) { _authorization = authorization; } /// <summary> /// Does authentication for <see cref="AuthorizationPolicy.AuthenticationSchemes"/> and sets the resulting /// <see cref="ClaimsPrincipal"/> to <see cref="HttpContext.User"/>. If no schemes are set, this is a no-op. /// </summary> /// <param name="policy">The <see cref="AuthorizationPolicy"/>.</param> /// <param name="context">The <see cref="HttpContext"/>.</param> /// <returns><see cref="AuthenticateResult.Success"/> unless all schemes specified by <see cref="AuthorizationPolicy.AuthenticationSchemes"/> failed to authenticate.</returns> public virtual async Task<AuthenticateResult> AuthenticateAsync(AuthorizationPolicy policy, HttpContext context) { if (policy.AuthenticationSchemes != null && policy.AuthenticationSchemes.Count > 0) { ClaimsPrincipal? newPrincipal = null; DateTimeOffset? minExpiresUtc = null; foreach (var scheme in policy.AuthenticationSchemes) { var result = await context.AuthenticateAsync(scheme).ConfigureAwait(false); if (result != null) { if (result.Succeeded) { newPrincipal = MergeUserPrincipal(newPrincipal, result.Principal); if (minExpiresUtc is null || result.Properties?.ExpiresUtc < minExpiresUtc) { minExpiresUtc = result.Properties?.ExpiresUtc; } } else if (result.Properties?.GetParameter<bool>(SuppressChallengeKey) ?? false) { return result; } } } if (newPrincipal != null) { context.User = newPrincipal; var ticket = new AuthenticationTicket(newPrincipal, string.Join(';', policy.AuthenticationSchemes)); // ExpiresUtc is the easiest property to reason about when dealing with multiple schemes // SignalR will use this property to evaluate auth expiration for long running connections ticket.Properties.ExpiresUtc = minExpiresUtc; return AuthenticateResult.Success(ticket); } else { context.User = new ClaimsPrincipal(new ClaimsIdentity()); return AuthenticateResult.NoResult(); } } // No modifications made to the HttpContext so let's use the existing result if it exists return context.Features.Get<IAuthenticateResultFeature>()?.AuthenticateResult ?? DefaultAuthenticateResult(context); static AuthenticateResult DefaultAuthenticateResult(HttpContext context) { return (context.User?.Identity?.IsAuthenticated ?? false) ? AuthenticateResult.Success(new AuthenticationTicket(context.User, "context.User")) : AuthenticateResult.NoResult(); } } /// <summary> /// Attempts authorization for a policy using <see cref="IAuthorizationService"/>. /// </summary> /// <param name="policy">The <see cref="AuthorizationPolicy"/>.</param> /// <param name="authenticationResult">The result of a call to <see cref="AuthenticateAsync(AuthorizationPolicy, HttpContext)"/>.</param> /// <param name="context">The <see cref="HttpContext"/>.</param> /// <param name="resource"> /// An optional resource the policy should be checked with. /// If a resource is not required for policy evaluation you may pass null as the value. /// </param> /// <returns>Returns <see cref="PolicyAuthorizationResult.Success"/> if authorization succeeds. /// Otherwise returns <see cref="PolicyAuthorizationResult.Forbid(AuthorizationFailure)"/> if <see cref="AuthenticateResult.Succeeded"/>, otherwise /// returns <see cref="PolicyAuthorizationResult.Challenge"/></returns> public virtual async Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object? resource) { ArgumentNullException.ThrowIfNull(policy); if (authenticationResult.Failure != null && (authenticationResult.Properties?.GetParameter<bool>(SuppressChallengeKey) ?? false)) { return PolicyAuthorizationResult.Forbid(); } var result = await _authorization.AuthorizeAsync(context.User, resource, policy).ConfigureAwait(false); if (result.Succeeded) { return PolicyAuthorizationResult.Success(); } // If authentication was successful, return forbidden, otherwise challenge return (authenticationResult.Succeeded) ? PolicyAuthorizationResult.Forbid(result.Failure) : PolicyAuthorizationResult.Challenge(); } private static ClaimsPrincipal MergeUserPrincipal(ClaimsPrincipal? existingPrincipal, ClaimsPrincipal? additionalPrincipal) { // For the first principal, just use the new principal rather than copying it if (existingPrincipal == null && additionalPrincipal != null) { return additionalPrincipal; } var newPrincipal = new ClaimsPrincipal(); // New principal identities go first if (additionalPrincipal != null) { newPrincipal.AddIdentities(additionalPrincipal.Identities); } // Then add any existing non empty or authenticated identities if (existingPrincipal != null) { newPrincipal.AddIdentities(existingPrincipal.Identities.Where(i => i.IsAuthenticated || i.Claims.Any())); } return newPrincipal; } }