1
1
using Microsoft . AspNetCore . Authentication ;
2
- using Microsoft . Extensions . Caching . Memory ;
3
2
using Microsoft . Extensions . Options ;
3
+ using rubberduckvba . Server . Api . Auth ;
4
4
using rubberduckvba . Server . Services ;
5
- using System . Collections . Concurrent ;
5
+ using System . IdentityModel . Tokens . Jwt ;
6
6
using System . Security . Claims ;
7
7
using System . Text . Encodings . Web ;
8
8
9
9
namespace rubberduckvba . Server ;
10
10
11
11
public class GitHubAuthenticationHandler : AuthenticationHandler < AuthenticationSchemeOptions >
12
12
{
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" ;
14
15
15
16
private readonly IGitHubClientService _github ;
16
- private readonly IMemoryCache _cache ;
17
17
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 ;
22
21
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 )
25
24
: base ( options , logger , encoder )
26
25
{
27
26
_github = github ;
28
- _cache = cache ;
29
- }
30
27
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
+ }
32
32
33
33
protected override Task < AuthenticateResult > HandleAuthenticateAsync ( )
34
34
{
35
35
try
36
36
{
37
- var token = Context . Request . Cookies [ AuthCookie ]
38
- ?? Context . Request . Headers [ AuthCookie ] ;
39
-
40
- if ( string . IsNullOrWhiteSpace ( token ) )
37
+ if ( TryAuthenticateJWT ( out var jwtResult ) )
41
38
{
42
- return Task . FromResult ( AuthenticateResult . Fail ( "Access token was not provided" ) ) ;
39
+ return Task . FromResult ( jwtResult ! ) ;
43
40
}
44
41
45
- if ( TryAuthenticateFromCache ( token , out var cachedResult ) )
42
+ var token = Context . Request . Headers [ AuthTokenHeader ] . SingleOrDefault ( ) ;
43
+ if ( ! string . IsNullOrEmpty ( token ) )
46
44
{
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
+ }
61
51
}
62
52
63
53
return Task . FromResult ( AuthenticateResult . Fail ( "Missing or invalid access token" ) ) ;
64
54
}
65
55
catch ( InvalidOperationException e )
66
56
{
67
- Logger . LogError ( e , e . Message ) ;
57
+ Logger . LogError ( e , "{Message}" , e . Message ) ;
68
58
return Task . FromResult ( AuthenticateResult . NoResult ( ) ) ;
69
59
}
70
60
}
71
61
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 )
81
63
{
82
64
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 ) )
84
68
{
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
+ }
86
80
87
- Context . User = cachedPrincipal ;
88
- Thread . CurrentPrincipal = cachedPrincipal ;
81
+ var principal = payload . ToClaimsPrincipal ( ) ;
82
+ Context . User = principal ;
83
+ Thread . CurrentPrincipal = principal ;
89
84
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 ) ;
92
87
return true ;
93
88
}
89
+
94
90
return false ;
95
91
}
96
92
97
93
private bool TryAuthenticateGitHubToken ( string token , out AuthenticateResult ? result )
98
94
{
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 ( ) ;
105
97
106
- _authApiTask [ token ] = AuthenticateGitHubAsync ( token ) ;
107
- result = _authApiTask [ token ] . GetAwaiter ( ) . GetResult ( ) ;
108
-
109
- _authApiTask [ token ] = null ! ;
110
98
return result is not null ;
111
99
}
112
100
@@ -118,6 +106,15 @@ private bool TryAuthenticateGitHubToken(string token, out AuthenticateResult? re
118
106
Context . User = principal ;
119
107
Thread . CurrentPrincipal = principal ;
120
108
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
+
121
118
var ticket = new AuthenticationTicket ( principal , "github" ) ;
122
119
return AuthenticateResult . Success ( ticket ) ;
123
120
}
0 commit comments