1+ using System . Security . Claims ;
2+ using System . Text . Encodings . Web ;
3+ using System . Text . Json ;
4+ using System . Text . RegularExpressions ;
5+ using Microsoft . AspNetCore . Authentication ;
6+ using Microsoft . AspNetCore . Http ;
7+ using Microsoft . AspNetCore . Mvc ;
8+ using Microsoft . AspNetCore . WebUtilities ;
9+ using Microsoft . Extensions . Logging ;
10+ using Microsoft . Extensions . Options ;
11+ using Shuttle . Core . Contract ;
12+
13+ namespace Shuttle . Access . AspNetCore ;
14+
15+ public class AccessAuthenticationHandler : AuthenticationHandler < AuthenticationSchemeOptions >
16+ {
17+ private readonly IAccessService _accessService ;
18+ public static readonly string AuthenticationScheme = "Shuttle.Access" ;
19+ public const string SessionTokenClaimType = "http://shuttle.org/claims/session/token" ;
20+ public static readonly Regex TokenExpression = new ( @"token\s*=\s*(?<token>[0-9a-fA-F-]{36})" , RegexOptions . IgnoreCase ) ;
21+ private const string Type = "https://tools.ietf.org/html/rfc9110#section-15.5.2" ;
22+
23+ public AccessAuthenticationHandler ( IOptionsMonitor < AuthenticationSchemeOptions > options , ILoggerFactory logger , UrlEncoder encoder , IAccessService accessService ) : base ( options , logger , encoder )
24+ {
25+ _accessService = Guard . AgainstNull ( accessService ) ;
26+ }
27+
28+ protected override async Task < AuthenticateResult > HandleAuthenticateAsync ( )
29+ {
30+ await Task . CompletedTask ;
31+
32+ var header = Request . Headers [ "Authorization" ] . FirstOrDefault ( ) ;
33+
34+ if ( header == null )
35+ {
36+ return AuthenticateResult . NoResult ( ) ;
37+ }
38+
39+ if ( ! header . StartsWith ( "Shuttle.Access " , StringComparison . OrdinalIgnoreCase ) )
40+ {
41+ return AuthenticateResult . NoResult ( ) ;
42+ }
43+
44+ var match = TokenExpression . Match ( header [ "Shuttle.Access " . Length ..] . Trim ( ) ) ;
45+
46+ if ( ! match . Success ||
47+ ! Guid . TryParse ( match . Groups [ "token" ] . Value , out var sessionToken ) )
48+ {
49+ return AuthenticateResult . Fail ( Resources . InvalidAuthenticationHeader ) ;
50+ }
51+
52+ var session = await _accessService . FindSessionAsync ( sessionToken ) ;
53+
54+ if ( session == null )
55+ {
56+ return AuthenticateResult . Fail ( Resources . InvalidAuthenticationHeader ) ;
57+ }
58+
59+ Context . SetPrincipalAccessSessionToken ( sessionToken ) ;
60+
61+ List < Claim > claims =
62+ [
63+ new ( ClaimTypes . NameIdentifier , session . IdentityName ) ,
64+ new ( ClaimTypes . Name , session . IdentityName ) ,
65+ new ( nameof ( session . IdentityName ) , session . IdentityName ) ,
66+ new ( SessionTokenClaimType , $ "{ session . Token : D} ")
67+ ] ;
68+
69+ return AuthenticateResult . Success ( new ( new ( new ClaimsIdentity ( claims , Scheme . Name ) ) , Scheme . Name ) ) ;
70+ }
71+
72+ protected override async Task HandleChallengeAsync ( AuthenticationProperties properties )
73+ {
74+ var authenticateResult = await HandleAuthenticateOnceAsync ( ) ;
75+
76+ if ( authenticateResult . Succeeded )
77+ {
78+ return ;
79+ }
80+
81+ Response . StatusCode = StatusCodes . Status401Unauthorized ;
82+ Response . Headers . WWWAuthenticate = "Shuttle.Access" ;
83+
84+ var problemDetails = new ProblemDetails
85+ {
86+ Type = Type ,
87+ Title = ReasonPhrases . GetReasonPhrase ( StatusCodes . Status401Unauthorized ) ,
88+ Status = StatusCodes . Status401Unauthorized ,
89+ Detail = authenticateResult . Failure ? . Message
90+ } ;
91+
92+ await Response . WriteAsJsonAsync ( problemDetails , ( JsonSerializerOptions ? ) null , "application/problem+json" ) ;
93+ }
94+ }
0 commit comments