Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 56 additions & 28 deletions src/Turnierplan.App/Endpoints/Identity/RefreshEndpoint.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -27,46 +29,72 @@ private async Task<IResult> 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
{
Expand Down
16 changes: 10 additions & 6 deletions src/Turnierplan.App/Security/ApiKeyAuthenticationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,22 @@ public ApiKeyAuthenticationHandler(

protected override async Task<AuthenticateResult> 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.");
Expand Down
81 changes: 44 additions & 37 deletions src/Turnierplan.App/Security/JwtAuthenticationHandler.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -23,58 +24,64 @@ public JwtAuthenticationHandler(

protected override async Task<AuthenticateResult> 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);
}
}
Loading