Embed user roles as JWT claims in SSO token

IssueToken is now async so it can query app.user_roles before signing
the JWT. Role names are added as ClaimTypes.Role claims, enabling
role-based authorization in downstream apps (e.g. retirementAdmin).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
2026-05-18 19:29:54 -06:00
co-authored by Claude Sonnet 4.6
parent 7bbd679171
commit 0393dacf63
+18 -3
View File
@@ -56,7 +56,7 @@ public class SsoApi(DbExecutor db, IConfiguration config, ILogger<SsoApi> logger
return BadRequest(new { error = "user no longer exists" }); return BadRequest(new { error = "user no longer exists" });
} }
var jwt = IssueToken(user, request.ClientId); var jwt = await IssueTokenAsync(user, request.ClientId);
return Ok(new SsoTokenResponse return Ok(new SsoTokenResponse
{ {
AccessToken = jwt, AccessToken = jwt,
@@ -118,7 +118,7 @@ public class SsoApi(DbExecutor db, IConfiguration config, ILogger<SsoApi> logger
new { userId }); new { userId });
} }
private string IssueToken(SsoUser user, string audience) private async Task<string> IssueTokenAsync(SsoUser user, string audience)
{ {
var jwtKey = config["Jwt:Key"] ?? throw new InvalidOperationException("JWT Key not configured"); var jwtKey = config["Jwt:Key"] ?? throw new InvalidOperationException("JWT Key not configured");
var jwtIssuer = config["Jwt:Issuer"] ?? throw new InvalidOperationException("JWT Issuer not configured"); var jwtIssuer = config["Jwt:Issuer"] ?? throw new InvalidOperationException("JWT Issuer not configured");
@@ -126,7 +126,9 @@ public class SsoApi(DbExecutor db, IConfiguration config, ILogger<SsoApi> logger
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)); var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new[] var roles = await LoadUserRolesAsync(user.Id);
var claims = new List<Claim>
{ {
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()), new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
@@ -136,6 +138,8 @@ public class SsoApi(DbExecutor db, IConfiguration config, ILogger<SsoApi> logger
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N")) new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N"))
}; };
claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r)));
var token = new JwtSecurityToken( var token = new JwtSecurityToken(
issuer: jwtIssuer, issuer: jwtIssuer,
audience: audience, audience: audience,
@@ -146,6 +150,17 @@ public class SsoApi(DbExecutor db, IConfiguration config, ILogger<SsoApi> logger
return new JwtSecurityTokenHandler().WriteToken(token); return new JwtSecurityTokenHandler().WriteToken(token);
} }
private async Task<List<string>> LoadUserRolesAsync(long userId)
{
return await db.ExecuteListReaderAsync(
@"SELECT r.name
FROM app.user_roles ur
JOIN app.roles r ON ur.role_id = r.id
WHERE ur.user_id = @userId",
reader => reader.GetString(0),
new { userId });
}
private static string HashCode(string code) private static string HashCode(string code)
{ {
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(code)); var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(code));