11using Microsoft . AspNetCore . Authentication ;
2- using Microsoft . Extensions . Caching . Memory ;
32using Microsoft . Extensions . Options ;
3+ using rubberduckvba . Server . Api . Auth ;
44using rubberduckvba . Server . Services ;
5- using System . Collections . Concurrent ;
5+ using System . IdentityModel . Tokens . Jwt ;
66using System . Security . Claims ;
77using System . Text . Encodings . Web ;
88
99namespace rubberduckvba . Server ;
1010
1111public class GitHubAuthenticationHandler : AuthenticationHandler < AuthenticationSchemeOptions >
1212{
13- public static readonly string AuthCookie = "x-access-token" ;
13+ public static readonly string AuthTokenHeader = "x-access-token" ;
14+ public static readonly string AuthCookie = "x-auth" ;
1415
1516 private readonly IGitHubClientService _github ;
16- private readonly IMemoryCache _cache ;
1717
18- private static readonly MemoryCacheEntryOptions _options = new MemoryCacheEntryOptions
19- {
20- SlidingExpiration = TimeSpan . FromMinutes ( 60 ) ,
21- } ;
18+ private readonly string _audience ;
19+ private readonly string _issuer ;
20+ private readonly string _secret ;
2221
23- public GitHubAuthenticationHandler ( IGitHubClientService github , IMemoryCache cache ,
24- IOptionsMonitor < AuthenticationSchemeOptions > options , ILoggerFactory logger , UrlEncoder encoder )
22+ public GitHubAuthenticationHandler ( IGitHubClientService github , IOptionsMonitor < AuthenticationSchemeOptions > options , ILoggerFactory logger ,
23+ UrlEncoder encoder , IOptions < ApiSettings > apiOptions )
2524 : base ( options , logger , encoder )
2625 {
2726 _github = github ;
28- _cache = cache ;
29- }
3027
31- private static readonly ConcurrentDictionary < string , Task < AuthenticateResult ? > > _authApiTask = new ( ) ;
28+ _audience = apiOptions . Value . Audience ;
29+ _issuer = apiOptions . Value . Issuer ;
30+ _secret = apiOptions . Value . SymetricKey ;
31+ }
3232
3333 protected override Task < AuthenticateResult > HandleAuthenticateAsync ( )
3434 {
3535 try
3636 {
37- var token = Context . Request . Cookies [ AuthCookie ]
38- ?? Context . Request . Headers [ AuthCookie ] ;
39-
40- if ( string . IsNullOrWhiteSpace ( token ) )
37+ if ( TryAuthenticateJWT ( out var jwtResult ) )
4138 {
42- return Task . FromResult ( AuthenticateResult . Fail ( "Access token was not provided" ) ) ;
39+ return Task . FromResult ( jwtResult ! ) ;
4340 }
4441
45- if ( TryAuthenticateFromCache ( token , out var cachedResult ) )
42+ var token = Context . Request . Headers [ AuthTokenHeader ] . SingleOrDefault ( ) ;
43+ if ( ! string . IsNullOrEmpty ( token ) )
4644 {
47- return Task . FromResult ( cachedResult ) ! ;
48- }
49-
50- if ( TryAuthenticateGitHubToken ( token , out var result )
51- && result is AuthenticateResult
52- && result . Ticket is AuthenticationTicket ticket )
53- {
54- CacheAuthenticatedTicket ( token , ticket ) ;
55- return Task . FromResult ( result ! ) ;
56- }
57-
58- if ( TryAuthenticateFromCache ( token , out cachedResult ) )
59- {
60- return Task . FromResult ( cachedResult ! ) ;
45+ if ( TryAuthenticateGitHubToken ( token , out var result )
46+ && result is AuthenticateResult
47+ && result . Ticket is AuthenticationTicket )
48+ {
49+ return Task . FromResult ( result ! ) ;
50+ }
6151 }
6252
6353 return Task . FromResult ( AuthenticateResult . Fail ( "Missing or invalid access token" ) ) ;
6454 }
6555 catch ( InvalidOperationException e )
6656 {
67- Logger . LogError ( e , e . Message ) ;
57+ Logger . LogError ( e , "{Message}" , e . Message ) ;
6858 return Task . FromResult ( AuthenticateResult . NoResult ( ) ) ;
6959 }
7060 }
7161
72- private void CacheAuthenticatedTicket ( string token , AuthenticationTicket ticket )
73- {
74- if ( ! string . IsNullOrWhiteSpace ( token ) && ticket . Principal . Identity ? . IsAuthenticated == true )
75- {
76- _cache . Set ( token , ticket , _options ) ;
77- }
78- }
79-
80- private bool TryAuthenticateFromCache ( string token , out AuthenticateResult ? result )
62+ private bool TryAuthenticateJWT ( out AuthenticateResult ? result )
8163 {
8264 result = null ;
83- if ( _cache . TryGetValue ( token , out var cached ) && cached is AuthenticationTicket cachedTicket )
65+
66+ var jsonContent = Context . Request . Cookies [ AuthCookie ] ;
67+ if ( ! string . IsNullOrEmpty ( jsonContent ) )
8468 {
85- var cachedPrincipal = cachedTicket . Principal ;
69+ var payload = JwtPayload . Deserialize ( jsonContent ) ;
70+ if ( ! payload . Iss . Equals ( _issuer , StringComparison . OrdinalIgnoreCase ) )
71+ {
72+ Logger . LogWarning ( "Invalid issuer in JWT payload: {Issuer}" , payload . Iss ) ;
73+ return false ;
74+ }
75+ if ( ! payload . Aud . Contains ( _audience ) )
76+ {
77+ Logger . LogWarning ( "Invalid audience in JWT payload: {Audience}" , payload . Aud ) ;
78+ return false ;
79+ }
8680
87- Context . User = cachedPrincipal ;
88- Thread . CurrentPrincipal = cachedPrincipal ;
81+ var principal = payload . ToClaimsPrincipal ( ) ;
82+ Context . User = principal ;
83+ Thread . CurrentPrincipal = principal ;
8984
90- Logger . LogInformation ( $ "Successfully retrieved authentication ticket from cached token for { cachedPrincipal . Identity ! . Name } ; token will not be revalidated. ") ;
91- result = AuthenticateResult . Success ( cachedTicket ) ;
85+ var ticket = new AuthenticationTicket ( principal , "github ") ;
86+ result = AuthenticateResult . Success ( ticket ) ;
9287 return true ;
9388 }
89+
9490 return false ;
9591 }
9692
9793 private bool TryAuthenticateGitHubToken ( string token , out AuthenticateResult ? result )
9894 {
99- result = null ;
100- if ( _authApiTask . TryGetValue ( token , out var task ) && task is not null )
101- {
102- result = task . GetAwaiter ( ) . GetResult ( ) ;
103- return result is not null ;
104- }
95+ var task = AuthenticateGitHubAsync ( token ) ;
96+ result = task . GetAwaiter ( ) . GetResult ( ) ;
10597
106- _authApiTask [ token ] = AuthenticateGitHubAsync ( token ) ;
107- result = _authApiTask [ token ] . GetAwaiter ( ) . GetResult ( ) ;
108-
109- _authApiTask [ token ] = null ! ;
11098 return result is not null ;
11199 }
112100
@@ -118,6 +106,15 @@ private bool TryAuthenticateGitHubToken(string token, out AuthenticateResult? re
118106 Context . User = principal ;
119107 Thread . CurrentPrincipal = principal ;
120108
109+ var jwt = principal . ToJWT ( _secret , _issuer , _audience ) ;
110+ Context . Response . Cookies . Append ( AuthCookie , jwt , new CookieOptions
111+ {
112+ IsEssential = true ,
113+ HttpOnly = true ,
114+ Secure = true ,
115+ Expires = DateTimeOffset . UtcNow . AddHours ( 1 )
116+ } ) ;
117+
121118 var ticket = new AuthenticationTicket ( principal , "github" ) ;
122119 return AuthenticateResult . Success ( ticket ) ;
123120 }
0 commit comments