Invalidate JWTs on profile changes

This commit is contained in:
Holden
2026-06-28 12:46:12 -05:00
parent fefa01cf3f
commit e194bbf9c4
15 changed files with 395 additions and 47 deletions

View File

@@ -6,11 +6,28 @@ namespace API.Contracts.Profiles.Responses;
public class LoginResponse
{
[JsonPropertyName("Profile")]
public required ProfileDTO Profile { get; set; }
public ProfileDTO? Profile { get; set; }
[JsonPropertyName("AccessToken")]
public required string AccessToken { get; set; }
public string AccessToken { get; set; } = string.Empty;
[JsonPropertyName("ExpiresIn")]
public required int ExpiresIn { get; set; }
}
public int ExpiresIn { get; set; }
[JsonPropertyName("Error")]
public string Error { get; set; } = string.Empty;
public static LoginResponse Success(ProfileDTO profile, string accessToken, int expiresIn)
=> new()
{
Profile = profile,
AccessToken = accessToken,
ExpiresIn = expiresIn,
};
public static LoginResponse Fail(string? error)
=> new()
{
Error = error ?? "An unexpected error occured"
};
}

View File

@@ -1,4 +1,5 @@
using API.Contracts.Profiles.Responses;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using RecNet.Application.Profiles;
using RecNet.Application.Profiles.Login;
@@ -6,8 +7,9 @@ using LoginRequest = API.Contracts.Profiles.Requests.LoginRequest;
namespace API.Controllers.Profiles.V1;
[Route("api/[controller]/v1")]
[Authorize]
[ApiController]
[Route("api/[controller]/v1")]
public class ProfilesController(IProfileService profileService) : ControllerBase
{
[HttpGet("{id:guid}")]
@@ -28,6 +30,7 @@ public class ProfilesController(IProfileService profileService) : ControllerBase
CancellationToken ct)
=> await profileService.GetProfilesAsync(ids, ct);
[AllowAnonymous]
[HttpPost("login")]
public async Task<ActionResult<LoginResponse>> Login(
[FromBody] LoginRequest request,
@@ -42,13 +45,8 @@ public class ProfilesController(IProfileService profileService) : ControllerBase
request.PlatformAuthentication
), ct);
if (!result.Succeeded)
return BadRequest(result.Message);
return LoginResponse.Fail(result.Message);
return new LoginResponse
{
Profile = result.Profile!,
AccessToken = result.AccessToken!,
ExpiresIn = result.ExpiresIn
};
return LoginResponse.Success(result.Profile!, result.AccessToken!, result.ExpiresIn);
}
}

View File

@@ -4,6 +4,8 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.IdentityModel.Tokens;
using RecNet.Application;
using RecNet.Application.Common.Security;
using RecNet.Domain.Profiles;
using RecNet.Infrastructure;
using RecNet.Infrastructure.Services.Tokens;
using RecNet.ServiceDefaults;
@@ -15,17 +17,17 @@ public class Program
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddOptions<NeutrinoOptions>()
.Bind(builder.Configuration.GetSection("Neutrino"))
.ValidateOnStart();
var recNetOptions = builder.Configuration.GetSection("RecNet").Get<RecNetOptions>()
?? new RecNetOptions();
?? new RecNetOptions();
var jwtOptions = builder.Configuration.GetSection("Jwt").Get<JwtOptions>()
?? new JwtOptions();
?? new JwtOptions();
builder.AddServiceDefaults();
@@ -57,8 +59,28 @@ public class Program
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(1)
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
var profileRepository = context.HttpContext.RequestServices
.GetRequiredService<IProfileRepository>();
var userId = context.Principal?.GetProfileId();
var tokenVersion = context.Principal?.GetTokenVersion();
if (userId.HasValue)
{
var profileTokenVersion = await profileRepository.GetProfileTokenVersion(userId.Value);
if (profileTokenVersion != tokenVersion)
context.Fail("Unauthorized");
}
}
};
});
builder.Services.AddAuthorization();
builder.Services.AddControllers();
@@ -76,4 +98,4 @@ public class Program
app.Run();
}
}
}