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!
|
|
)
|
|
);
|
|
} |