Skip to content

Commit be4c529

Browse files
committed
remove ticket caching
1 parent e625943 commit be4c529

File tree

5 files changed

+103
-67
lines changed

5 files changed

+103
-67
lines changed

rubberduckvba.Server/Api/Auth/AuthController.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,9 @@ public IActionResult SessionSignIn(SignInViewModel vm)
115115
[HttpPost("auth/github")]
116116
[EnableCors(CorsPolicies.AllowAll)]
117117
[AllowAnonymous]
118-
public IActionResult OnGitHubCallback(SignInViewModel vm)
118+
public async Task<IActionResult> OnGitHubCallback(SignInViewModel vm)
119119
{
120-
return GuardInternalAction(() =>
120+
return await GuardInternalAction(async () =>
121121
{
122122
Logger.LogInformation("OAuth code was received. State: {state}", vm.State);
123123
var clientId = configuration.Value.ClientId;
@@ -127,7 +127,7 @@ public IActionResult OnGitHubCallback(SignInViewModel vm)
127127
var github = new GitHubClient(new ProductHeaderValue(agent));
128128
var request = new OauthTokenRequest(clientId, clientSecret, vm.Code);
129129

130-
var token = github.Oauth.CreateAccessToken(request).GetAwaiter().GetResult();
130+
var token = await github.Oauth.CreateAccessToken(request);
131131

132132
if (token is null)
133133
{
@@ -136,7 +136,7 @@ public IActionResult OnGitHubCallback(SignInViewModel vm)
136136
}
137137

138138
Logger.LogInformation("OAuth access token was created. Authorizing...");
139-
var authorizedToken = AuthorizeAsync(token.AccessToken).GetAwaiter().GetResult();
139+
var authorizedToken = await AuthorizeAsync(token.AccessToken);
140140

141141
return authorizedToken is null ? Unauthorized() : Ok(vm with { Token = authorizedToken });
142142
});
@@ -176,7 +176,7 @@ public IActionResult OnGitHubCallback(SignInViewModel vm)
176176
Thread.CurrentPrincipal = HttpContext.User;
177177

178178
Logger.LogInformation("GitHub user with login {login} has signed in with role authorizations '{role}'.", githubUser.Login, configuration.Value.OwnerOrg);
179-
Response.Cookies.Append(GitHubAuthenticationHandler.AuthCookie, token, new CookieOptions
179+
Response.Cookies.Append(GitHubAuthenticationHandler.AuthTokenHeader, token, new CookieOptions
180180
{
181181
IsEssential = true,
182182
HttpOnly = true,

rubberduckvba.Server/Api/Auth/ClaimsPrincipalExtensions.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,19 @@ namespace rubberduckvba.Server.Api.Auth;
88

99
public static class ClaimsPrincipalExtensions
1010
{
11-
public static string AsJWT(this ClaimsPrincipal principal, string secret, string issuer, string audience)
11+
public static ClaimsPrincipal ToClaimsPrincipal(this JwtPayload payload)
12+
{
13+
var identity = new ClaimsIdentity(payload.Claims, "github");
14+
return new ClaimsPrincipal(identity);
15+
}
16+
17+
public static string ToJWT(this ClaimsPrincipal principal, string secret, string issuer, string audience, int expirationMinutes = 60)
1218
{
1319
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
1420
var token = new JwtSecurityToken(issuer, audience,
1521
claims: principal?.Claims,
1622
notBefore: new DateTimeOffset(DateTime.UtcNow).UtcDateTime,
17-
expires: new DateTimeOffset(DateTime.UtcNow.AddMinutes(60)).UtcDateTime,
23+
expires: new DateTimeOffset(DateTime.UtcNow.AddMinutes(expirationMinutes)).UtcDateTime,
1824
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
1925

2026
return new JwtSecurityTokenHandler().WriteToken(token);

rubberduckvba.Server/GitHubAuthenticationHandler.cs

Lines changed: 56 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,100 @@
11
using Microsoft.AspNetCore.Authentication;
2-
using Microsoft.Extensions.Caching.Memory;
32
using Microsoft.Extensions.Options;
3+
using rubberduckvba.Server.Api.Auth;
44
using rubberduckvba.Server.Services;
5-
using System.Collections.Concurrent;
5+
using System.IdentityModel.Tokens.Jwt;
66
using System.Security.Claims;
77
using System.Text.Encodings.Web;
88

99
namespace rubberduckvba.Server;
1010

1111
public 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
}

rubberduckvba.Server/GitHubSettings.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ namespace rubberduckvba.Server;
55
public record class ApiSettings
66
{
77
public string SymetricKey { get; set; } = default!;
8+
9+
public string Audience { get; set; } = default!;
10+
public string Issuer { get; set; } = default!;
811
}
912

1013
public record class ConnectionSettings

rubberduckvba.Server/RubberduckApiController.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,37 @@ protected RubberduckApiController(ILogger logger)
1818
}
1919

2020
protected ILogger Logger => _logger;
21+
protected async Task<IActionResult> GuardInternalAction(Func<Task<IActionResult>> method, [CallerMemberName] string name = default!)
22+
{
23+
var sw = Stopwatch.StartNew();
24+
IActionResult result = NoContent();
25+
var success = false;
26+
try
27+
{
28+
_logger.LogTrace("GuardInternalAction:{name} | ▶ Invoking controller action", name);
29+
result = await method.Invoke();
30+
success = true;
31+
}
32+
catch (Exception exception)
33+
{
34+
_logger.LogError(exception, "GuardInternalAction:{name} | ❌ An exception was caught", name);
35+
throw;
36+
}
37+
finally
38+
{
39+
sw.Stop();
40+
if (success)
41+
{
42+
_logger.LogTrace("GuardInternalAction:{name} | ✔️ Controller action completed | ⏱️ {elapsed}", name, sw.Elapsed);
43+
}
44+
else
45+
{
46+
_logger.LogWarning("GuardInternalAction:{name} | ⚠️ Controller action completed with errors", name);
47+
}
48+
}
49+
50+
return result;
51+
}
2152

2253
protected IActionResult GuardInternalAction(Func<IActionResult> method, [CallerMemberName] string name = default!)
2354
{
@@ -48,7 +79,6 @@ protected IActionResult GuardInternalAction(Func<IActionResult> method, [CallerM
4879
}
4980
}
5081

51-
//Response.Headers.AccessControlAllowOrigin = "*";
5282
return result;
5383
}
5484
}

0 commit comments

Comments
 (0)