157 lines
6.3 KiB
C#
157 lines
6.3 KiB
C#
using System.Security.Claims;
|
|
using System.Text.Encodings.Web;
|
|
using System.Text.RegularExpressions;
|
|
using Abstractions;
|
|
using HashidsNet;
|
|
using JWT.Algorithms;
|
|
using JWT.Builder;
|
|
using JWT.Serializers;
|
|
using Microsoft.AspNetCore.Authentication;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using W542.GandalfReborn.Data.Database;
|
|
|
|
namespace Security.Scheme;
|
|
|
|
public class GandalfRebornJwtBody
|
|
{
|
|
public string? Id { get; init; }
|
|
public string? Sub { get; init; }
|
|
public DateTimeOffset? Iat { get; init; }
|
|
public DateTimeOffset? Exp { get; init; }
|
|
public string? Iss { get; init; }
|
|
public string? Aud { get; init; }
|
|
}
|
|
|
|
public partial class GandalfRebornJwtTokenAuthSchemeHandler(
|
|
IOptionsMonitor<GandalfRebornJwtTokenAuthSchemeOptions> options,
|
|
ILoggerFactory logger,
|
|
UrlEncoder encoder,
|
|
IHashids hashIds,
|
|
ApplicationContext context,
|
|
IHttpContextAccessor httpContextAccessor,
|
|
InvokerContext invokerContext,
|
|
TimeProvider timeProvider)
|
|
: AuthenticationHandler<GandalfRebornJwtTokenAuthSchemeOptions>(options, logger, encoder)
|
|
{
|
|
[GeneratedRegex(@"Bearer\s+(?<token>[A-Za-z0-9\-._~+\/]+=*\.[A-Za-z0-9\-._~+\/]+=*\.[A-Za-z0-9\-._~+\/]+=*)")]
|
|
private static partial Regex BearerTokenRegex();
|
|
|
|
private string FailReason { get; set; } = "";
|
|
|
|
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
|
{
|
|
try
|
|
{
|
|
var jwtBody = GetJwtTokenBody(httpContextAccessor.HttpContext);
|
|
|
|
if (!hashIds.TryDecodeSingleLong(jwtBody.Sub, out var subjectId))
|
|
{
|
|
throw new UnauthorizedAccessException("One does not simply authenticate with invalid id.");
|
|
}
|
|
|
|
var subjectExists = context.Subjects.Any(x => x.Id == subjectId);
|
|
|
|
if (!subjectExists)
|
|
{
|
|
throw new UnauthorizedAccessException("One does not simply authenticate with a not existing subject.");
|
|
}
|
|
|
|
var claims = new List<Claim>
|
|
{
|
|
new(Invoker.SubType, subjectId.ToString())
|
|
};
|
|
|
|
var tenantAuthorities = await context.TenantSubjectRelations
|
|
.AsNoTracking()
|
|
.Where(x => x.SubjectId == subjectId)
|
|
.Include(x => x.InternalAuthorities)
|
|
.ToDictionaryAsync(x => x.TenantId, x => x.InternalAuthorities.Select(authority => authority.Name));
|
|
|
|
var appAuthorities = await context.AppSubjectRelations
|
|
.AsNoTracking()
|
|
.Where(x => x.SubjectId == subjectId)
|
|
.Include(x => x.InternalAuthorities)
|
|
.ToDictionaryAsync(x => x.AppId, x => x.InternalAuthorities.Select(authority => authority.Name));
|
|
|
|
var tenantClaims = tenantAuthorities
|
|
.SelectMany(x => x.Value.Select(authority => new Claim($"{Invoker.TenantAuthorityPrefix}{Invoker.AuthoritySeparator}{hashIds.EncodeLong(x.Key)}", authority)));
|
|
|
|
var appClaims = appAuthorities
|
|
.SelectMany(x => x.Value.Select(authority => new Claim($"{Invoker.AppAuthorityPrefix}{Invoker.AuthoritySeparator}{hashIds.EncodeLong(x.Key)}", authority)));
|
|
|
|
claims.AddRange(tenantClaims);
|
|
claims.AddRange(appClaims);
|
|
|
|
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "Tokens"));
|
|
|
|
invokerContext.Invoker = principal;
|
|
|
|
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
|
return AuthenticateResult.Success(ticket);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
FailReason = ex.Message;
|
|
return AuthenticateResult.Fail(ex);
|
|
}
|
|
}
|
|
|
|
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
|
|
{
|
|
await base.HandleChallengeAsync(properties);
|
|
if (Response.StatusCode == StatusCodes.Status401Unauthorized &&
|
|
!string.IsNullOrWhiteSpace(FailReason))
|
|
{
|
|
Response.ContentType = "application/json";
|
|
var executor = Context.RequestServices.GetRequiredService<IActionResultExecutor<ObjectResult>>();
|
|
var actionContext = new ActionContext(Context, Context.GetRouteData(), new ActionDescriptor());
|
|
await executor.ExecuteAsync(actionContext, new UnauthorizedObjectResult(new
|
|
{
|
|
status = Response.StatusCode,
|
|
error = FailReason,
|
|
}));
|
|
}
|
|
}
|
|
|
|
private GandalfRebornJwtBody GetJwtTokenBody(HttpContext? httpContext)
|
|
{
|
|
var authHeader = httpContext?.Request.Headers.Authorization.ToString();
|
|
|
|
if (authHeader is null)
|
|
throw new UnauthorizedAccessException("One does not simply not provide an authorization header.");
|
|
|
|
var tokenRegexMatch = BearerTokenRegex().Match(authHeader);
|
|
|
|
if (!tokenRegexMatch.Success)
|
|
throw new UnauthorizedAccessException("One does not simply provide an invalid authorization header.");
|
|
|
|
var token = tokenRegexMatch.Groups["token"].Value;
|
|
|
|
var decodedToken = JwtBuilder.Create()
|
|
.WithAlgorithm(new HMACSHA512Algorithm())
|
|
.WithSecret(Options.JwtSecret)
|
|
.MustVerifySignature()
|
|
.WithJsonSerializer(new JsonNetSerializer())
|
|
.Decode<GandalfRebornJwtBody>(token);
|
|
|
|
if (decodedToken.Aud is null || !decodedToken.Aud.StartsWith(Options.BaseUrl))
|
|
throw new UnauthorizedAccessException("One does not simply provide a token for a different audience.");
|
|
|
|
if (decodedToken.Iss is null || !decodedToken.Iss.StartsWith(Options.BaseUrl))
|
|
throw new UnauthorizedAccessException("One does not simply provide a token from a unknown issuer.");
|
|
|
|
if (decodedToken.Exp is null || decodedToken.Exp?.ToUniversalTime() <= timeProvider.GetUtcNow())
|
|
throw new UnauthorizedAccessException("One does not simply provide an expired token.");
|
|
|
|
return decodedToken;
|
|
}
|
|
} |