up
	
		
			
	
		
	
	
		
	
		
			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
				
			
		
		
	
	
				
					
				
			
		
			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
				
			This commit is contained in:
		| @@ -0,0 +1,163 @@ | ||||
| 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! | ||||
|                 ) | ||||
|             ); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user