From 4046ffd2063f89dfe2eaacde4fa8508412e225d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20H=C3=B6rner?= Date: Sun, 19 Apr 2026 09:20:38 +0200 Subject: [PATCH] Auth --- .../Endpoints/Identity/RefreshEndpoint.cs | 84 ++++++++++++------- .../Security/ApiKeyAuthenticationHandler.cs | 16 ++-- .../Security/JwtAuthenticationHandler.cs | 81 ++++++++++-------- 3 files changed, 110 insertions(+), 71 deletions(-) diff --git a/src/Turnierplan.App/Endpoints/Identity/RefreshEndpoint.cs b/src/Turnierplan.App/Endpoints/Identity/RefreshEndpoint.cs index ad2b2f1c..8108b1fa 100644 --- a/src/Turnierplan.App/Endpoints/Identity/RefreshEndpoint.cs +++ b/src/Turnierplan.App/Endpoints/Identity/RefreshEndpoint.cs @@ -1,9 +1,11 @@ using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using Turnierplan.App.Options; using Turnierplan.App.Security; using Turnierplan.Dal.Repositories; +using ClaimTypes = Turnierplan.App.Security.ClaimTypes; namespace Turnierplan.App.Endpoints.Identity; @@ -27,46 +29,72 @@ private async Task Handle( IUserRepository userRepository, CancellationToken cancellationToken) { - Guid userIdFromToken; - Guid securityStampFromToken; + string? token = null; - try + foreach (var (cookieName, cookieValue) in context.Request.Cookies) { - var cookie = context.Request.Cookies.Single(x => x.Key.Equals(CookieNames.RefreshTokenCookieName)); - - var tokenHandler = new JwtSecurityTokenHandler(); - - var signingKey = await _signingKeyProvider.GetSigningKeyAsync(cancellationToken); - var validationParameters = new TokenValidationParameters - { - ValidateIssuerSigningKey = true, - IssuerSigningKey = signingKey, - ValidateIssuer = false, - ValidateAudience = false, - ValidateLifetime = true, - ClockSkew = TimeSpan.Zero - }; - - var claimsPrincipal = tokenHandler.ValidateToken(cookie.Value, validationParameters, out _); - - var tokenType = claimsPrincipal.Claims.Single(x => x.Type.Equals(ClaimTypes.TokenType)).Value; - if (!tokenType.Equals(JwtTokenTypes.Refresh)) + if (cookieName.Equals(CookieNames.RefreshTokenCookieName)) { - return Results.Unauthorized(); + token = cookieValue; + break; } + } + + if (string.IsNullOrEmpty(token)) + { + return Results.Unauthorized(); + } + + var tokenHandler = new JwtSecurityTokenHandler(); + + var signingKey = await _signingKeyProvider.GetSigningKeyAsync(cancellationToken); + var validationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = signingKey, + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero + }; + + ClaimsPrincipal claimsPrincipal; + + try + { + claimsPrincipal = tokenHandler.ValidateToken(token, validationParameters, out _); + } + catch (SecurityTokenArgumentException) + { + return Results.Unauthorized(); + } + catch (SecurityTokenException) + { + return Results.Unauthorized(); + } - userIdFromToken = Guid.Parse(claimsPrincipal.Claims.Single(x => x.Type.Equals(ClaimTypes.UserId)).Value); - securityStampFromToken = Guid.Parse(claimsPrincipal.Claims.Single(x => x.Type.Equals(ClaimTypes.SecurityStamp)).Value); + var tokenType = claimsPrincipal.Claims.FirstOrDefault(x => x.Type.Equals(ClaimTypes.TokenType))?.Value; + + if (!Equals(tokenType, JwtTokenTypes.Refresh)) + { + return Results.Unauthorized(); } - catch + + var userIdFromToken = claimsPrincipal.Claims.FirstOrDefault(x => x.Type.Equals(ClaimTypes.UserId))?.Value; + var securityStampFromToken = claimsPrincipal.Claims.FirstOrDefault(x => x.Type.Equals(ClaimTypes.SecurityStamp))?.Value; + + if (string.IsNullOrWhiteSpace(userIdFromToken) + || string.IsNullOrWhiteSpace(securityStampFromToken) + || !Guid.TryParse(userIdFromToken, out var userIdFromTokenGuid) + || !Guid.TryParse(securityStampFromToken, out var securityStampFromTokenGuid)) { return Results.Unauthorized(); } - var user = await userRepository.GetByIdAsync(userIdFromToken); + var user = await userRepository.GetByIdAsync(userIdFromTokenGuid); // If the security stamp has changed, that means the user has changed their password since the reset token was issued - if (user is null || user.SecurityStamp != securityStampFromToken) + if (user is null || user.SecurityStamp != securityStampFromTokenGuid) { return Results.Ok(new RefreshEndpointResponse { diff --git a/src/Turnierplan.App/Security/ApiKeyAuthenticationHandler.cs b/src/Turnierplan.App/Security/ApiKeyAuthenticationHandler.cs index 737547a7..bc93f63a 100644 --- a/src/Turnierplan.App/Security/ApiKeyAuthenticationHandler.cs +++ b/src/Turnierplan.App/Security/ApiKeyAuthenticationHandler.cs @@ -33,18 +33,22 @@ public ApiKeyAuthenticationHandler( protected override async Task HandleAuthenticateAsync() { - string apiKeyId, apiKeySecret; + var hasApiKeyIdHeader = Request.Headers.TryGetValue(ApiKeyIdHeaderName, out var apiKeyIdHeaderValue); + var hasApiKeySecretHeader = Request.Headers.TryGetValue(ApiKeySecretHeaderName, out var apiKeySecretHeaderValue); - try + if (!hasApiKeyIdHeader && !hasApiKeySecretHeader) { - apiKeyId = Request.Headers[ApiKeyIdHeaderName][0]!; - apiKeySecret = Request.Headers[ApiKeySecretHeaderName][0]!; + return AuthenticateResult.NoResult(); } - catch + + if (apiKeyIdHeaderValue.Count != 1 || apiKeySecretHeaderValue.Count != 1) { - return AuthenticateResult.NoResult(); + return AuthenticateResult.Fail("The API key ID and secret headers must each be specified exactly once."); } + var apiKeyId = apiKeyIdHeaderValue.Single(); + var apiKeySecret = apiKeySecretHeaderValue.Single(); + if (string.IsNullOrEmpty(apiKeyId) || string.IsNullOrEmpty(apiKeySecret) || !PublicId.TryParse(apiKeyId, out var apiKeyIdParsed)) { return AuthenticateResult.Fail("There exists no valid API key with the specified ID and secret."); diff --git a/src/Turnierplan.App/Security/JwtAuthenticationHandler.cs b/src/Turnierplan.App/Security/JwtAuthenticationHandler.cs index 695fbeb8..71aac91b 100644 --- a/src/Turnierplan.App/Security/JwtAuthenticationHandler.cs +++ b/src/Turnierplan.App/Security/JwtAuthenticationHandler.cs @@ -1,4 +1,5 @@ using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; @@ -23,58 +24,64 @@ public JwtAuthenticationHandler( protected override async Task HandleAuthenticateAsync() { - if (!Request.Cookies.ContainsKey(CookieNames.AccessTokenCookieName)) - { - return AuthenticateResult.NoResult(); - } - - string token; + string? token = null; - try - { - token = Request.Cookies.Single(x => x.Key.Equals(CookieNames.AccessTokenCookieName)).Value; - } - catch + foreach (var (cookieName, cookieValue) in Request.Cookies) { - return AuthenticateResult.Fail("Missing or malformed access token cookie."); + if (cookieName.Equals(CookieNames.AccessTokenCookieName)) + { + token = cookieValue; + break; + } } - if (string.IsNullOrEmpty(token)) + if (token is null) { - return AuthenticateResult.Fail("Empty access token cookie."); + return AuthenticateResult.NoResult(); } - try + if (string.IsNullOrWhiteSpace(token)) { - var tokenHandler = new JwtSecurityTokenHandler(); + return AuthenticateResult.Fail("Invalid authentication token provided"); + } - var signingKey = await _signingKeyProvider.GetSigningKeyAsync(CancellationToken.None); - var validationParameters = new TokenValidationParameters - { - ValidateIssuerSigningKey = true, - IssuerSigningKey = signingKey, - ValidateIssuer = false, - ValidateAudience = false, - ValidateLifetime = true, - ClockSkew = TimeSpan.Zero - }; + var tokenHandler = new JwtSecurityTokenHandler(); - var claimsPrincipal = tokenHandler.ValidateToken(token, validationParameters, out _); + var signingKey = await _signingKeyProvider.GetSigningKeyAsync(CancellationToken.None); + var validationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = signingKey, + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero + }; - var tokenType = claimsPrincipal.Claims.Single(x => x.Type.Equals(ClaimTypes.TokenType)).Value; + ClaimsPrincipal claimsPrincipal; - if (!tokenType.Equals(JwtTokenTypes.Access)) - { - return AuthenticateResult.Fail("Incorrect token type."); - } + try + { + claimsPrincipal = tokenHandler.ValidateToken(token, validationParameters, out _); + } + catch (SecurityTokenArgumentException ex) + { + return AuthenticateResult.Fail($"Token validation failed: {ex.Message}"); + } + catch (SecurityTokenException ex) + { + return AuthenticateResult.Fail($"Token validation failed: {ex.Message}"); + } - var ticket = new AuthenticationTicket(claimsPrincipal, Scheme.Name); + var tokenType = claimsPrincipal.Claims.FirstOrDefault(x => x.Type.Equals(ClaimTypes.TokenType))?.Value; - return AuthenticateResult.Success(ticket); - } - catch (Exception ex) + if (!Equals(tokenType, JwtTokenTypes.Access)) { - return AuthenticateResult.Fail($"Invalid token: {ex.Message}"); + return AuthenticateResult.Fail("Incorrect token type."); } + + var ticket = new AuthenticationTicket(claimsPrincipal, Scheme.Name); + + return AuthenticateResult.Success(ticket); } }