Some checks failed
		
		
	
	Build Test Deploy / build-test (push) Has been cancelled
				
			Build Test Deploy / authority-container (push) Has been cancelled
				
			Build Test Deploy / docs (push) Has been cancelled
				
			Build Test Deploy / deploy (push) Has been cancelled
				
			Docs CI / lint-and-preview (push) Has been cancelled
				
			
		
			
				
	
	
		
			163 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			163 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using System;
 | |
| using System.Collections.Generic;
 | |
| using System.Linq;
 | |
| using System.Security.Claims;
 | |
| using System.Text.Encodings.Web;
 | |
| using System.Threading.Tasks;
 | |
| using Microsoft.AspNetCore.Authentication;
 | |
| using Microsoft.AspNetCore.Authentication.JwtBearer;
 | |
| using Microsoft.Extensions.Logging;
 | |
| using Microsoft.Extensions.Options;
 | |
| using OpenIddict.Abstractions;
 | |
| 
 | |
| using Ablera.Serdica.Authentication.Constants;
 | |
| using Ablera.Serdica.Authentication.Models;
 | |
| using Ablera.Serdica.Authentication.Models.Oidc;
 | |
| using Ablera.Serdica.Authority.Extensions;
 | |
| using System.Net;
 | |
| 
 | |
| using OpenIddict.Validation.AspNetCore;
 | |
| using Ablera.Serdica.Authentication.Extensions;
 | |
| 
 | |
| using static Ablera.Serdica.Authentication.Constants.ConstantsClass;
 | |
| 
 | |
| namespace Ablera.Serdica.Authentication.Services;
 | |
| 
 | |
| public sealed class SerdicaJwtBearerAuthenticationHandler : AuthenticationHandler<JwtBearerOptions>
 | |
| {
 | |
|     private readonly OidcValidation oidcValidationSettings;
 | |
|     private readonly ILogger<SerdicaJwtBearerAuthenticationHandler> logger;
 | |
| 
 | |
|     public SerdicaJwtBearerAuthenticationHandler(
 | |
|         IOptionsMonitor<JwtBearerOptions> jwtOptions,
 | |
|         ILoggerFactory loggerFactory,
 | |
|         ILogger<SerdicaJwtBearerAuthenticationHandler> logger,
 | |
|         UrlEncoder encoder,
 | |
|         IOptions<OidcValidation> oidcServerConnection)
 | |
|         : base(jwtOptions, loggerFactory, encoder)
 | |
|     {
 | |
|         this.oidcValidationSettings = oidcServerConnection.Value;
 | |
|         this.logger = logger;
 | |
|     }
 | |
|     protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
 | |
|     {
 | |
|         // 1. Internal callers detected by bypass mask → Super user
 | |
|         if (oidcValidationSettings.BypassValidationsMasks?
 | |
|                 .Any(m => m.MatchesRemote(Context)) == true)
 | |
|         {
 | |
|             return SuccessTicket(BuildDefaultRolePrincipal(SerdicaClaims.RoleSuperUser));
 | |
|         }
 | |
| 
 | |
|         // 2. What roles does the endpoint require?
 | |
|         Context.Items.TryGetValue(ConstantsClass.HttpContextEndpointRequiredRoles,
 | |
|                                   out var rolesObj);
 | |
|         var requiredRoles = rolesObj as string[];
 | |
|         if (requiredRoles is { Length: 0 }) // empty means requirement for authentication claim
 | |
|         {
 | |
|             requiredRoles =
 | |
|             [
 | |
|                 SerdicaClaims.IsAuthenticated
 | |
|             ];
 | |
|         }
 | |
| 
 | |
|         bool anonymousAllowed = requiredRoles == null ||
 | |
|                                 requiredRoles.Contains(SerdicaClaims.Anonymous,
 | |
|                                                        StringComparer.Ordinal);
 | |
| 
 | |
|         // 3. Decide whether we *need* to run AuthenticateAsync
 | |
|         bool tokenPresent =
 | |
|             Context.Request.Headers.TryGetValue("Authorization", out var authHeaders) &&
 | |
|             authHeaders.Any(h => h?.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) == true);
 | |
| 
 | |
|         bool mustAuthenticate = tokenPresent || !anonymousAllowed;
 | |
| 
 | |
|         AuthenticateResult authResult = mustAuthenticate
 | |
|             ? await Context.AuthenticateAsync(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)
 | |
|             : AuthenticateResult.NoResult();   // cheap placeholder; not succeeded, not failed
 | |
|         logger.LogInformation(
 | |
|             "Authorizing with following parameters authResult: {AuthResult}, anonymousAllowed: {anonymousAllowed}, tokenPresent: {tokenPresent}, requiredRoles: {requiredRoles}, roleClaims: {roleClaims}",
 | |
|             authResult.Succeeded,
 | |
|             anonymousAllowed,
 | |
|             tokenPresent,
 | |
|             string.Join(",", requiredRoles ?? []),
 | |
|             string.Join(",", authResult?.Principal?.Claims?.Where(c => c.Type == ClaimTypes.Role)?.Select(c => c.Value) ?? [])
 | |
|         );
 | |
| 
 | |
|         // 4. Figure out whether roles are satisfied (only matters if authenticated)
 | |
|         bool rolesSatisfied = authResult?.Succeeded == true &&
 | |
|                               !anonymousAllowed &&
 | |
|                               requiredRoles is { Length: > 0 } &&
 | |
|                               (requiredRoles.Contains(SerdicaClaims.IsAuthenticated) 
 | |
|                                 || 
 | |
|                                 (authResult?.Principal?.Claims
 | |
|                                          ?.Where(c => c.Type == ClaimTypes.Role)
 | |
|                                          ?.Select(c => c.Value)
 | |
|                                          ?.Intersect(requiredRoles!)
 | |
|                                          ?.Any() ?? false) == true);
 | |
| 
 | |
|         // 5. Switch expression drives the outcome
 | |
|         return (anonymousAllowed, authResult?.Succeeded ?? false, rolesSatisfied) switch
 | |
|         {
 | |
|             // Anonymous endpoint
 | |
|             (true, true, _) => SuccessTicket(authResult!.Principal!),              // token supplied
 | |
|             (true, false, _) => SuccessTicket(BuildDefaultRolePrincipal(
 | |
|                                    SerdicaClaims.Anonymous)),                       // no token
 | |
| 
 | |
|             // Protected endpoint but NOT authenticated
 | |
|             (false, false, _) => AuthenticateResult.Fail(
 | |
|                                     authResult!.Failure ?? new Exception("Token invalid.")),
 | |
| 
 | |
|             // Authenticated but lacks required roles
 | |
|             (_, _, false) => AuthenticateResult.Fail("Insufficient privileges"),
 | |
| 
 | |
|             // Authenticated and authorised
 | |
|             _ => SuccessTicket(authResult!.Principal!)
 | |
|         };
 | |
|     }
 | |
| 
 | |
|     protected override async Task HandleChallengeAsync(AuthenticationProperties props)
 | |
|     {
 | |
|         var proxy = new ProxyResult
 | |
|         {
 | |
|             StatusCode = HttpStatusCode.Unauthorized,               // 401
 | |
|             TraceId = Context.TraceIdentifier,
 | |
|             Title = "Unauthorized",
 | |
|             Type = "https://datatracker.ietf.org/doc/html/rfc9110#section-15.5.2",
 | |
|             Errors = new Dictionary<string, string>
 | |
|             {
 | |
|                 ["authentication"] = "Missing or invalid credentials."
 | |
|             }
 | |
|         };
 | |
|         await proxy.ReturnHttpRessponse(Response);
 | |
|     }
 | |
| 
 | |
|     protected override async Task HandleForbiddenAsync(AuthenticationProperties props)
 | |
|     {
 | |
|         var proxy = new ProxyResult
 | |
|         {
 | |
|             StatusCode = HttpStatusCode.Forbidden,                  // 403
 | |
|             TraceId = Context.TraceIdentifier,
 | |
|             Title = "Forbidden",
 | |
|             Type = "https://datatracker.ietf.org/doc/html/rfc9110#section-15.5.3",
 | |
|             Errors = new Dictionary<string, string>
 | |
|             {
 | |
|                 ["authorization"] = "Insufficient privileges."
 | |
|             }
 | |
|         };
 | |
|         await proxy.ReturnHttpRessponse(Response);
 | |
|     }
 | |
| 
 | |
|     // ──────────────────────────────────────────────────────────────────
 | |
|     private ClaimsPrincipal BuildDefaultRolePrincipal(string role) => 
 | |
|         PrincipalBuilder.Build($"{DefaultRolePrincipalPrefix}_{role}", ConstantsClass.AuthenticationScheme)
 | |
|         .AddClaim(ClaimTypes.NameIdentifier, $"{DefaultRolePrincipalPrefix}_{role}")
 | |
|         .AddClaim(ClaimTypes.Role, role);
 | |
| 
 | |
|     private static AuthenticateResult SuccessTicket(ClaimsPrincipal principal)
 | |
|         => AuthenticateResult.Success(
 | |
|                 new AuthenticationTicket(
 | |
|                     principal,
 | |
|                     principal.Identity!.AuthenticationType!
 | |
|                 )
 | |
|             );
 | |
| } |