Claude-skill-registry configure-jwt

Set up JWT Bearer authentication with token service and authorization policies

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/configure-jwt" ~/.claude/skills/majiayu000-claude-skill-registry-configure-jwt && rm -rf "$T"
manifest: skills/data/configure-jwt/SKILL.md
source content

Configure JWT Authentication Skill

Set up JWT Bearer authentication for NovaTune API.

Project Context

  • Configuration:
    src/NovaTuneApp/NovaTuneApp.ApiService/appsettings.json
  • Auth setup:
    src/NovaTuneApp/NovaTuneApp.ApiService/Program.cs
  • Token service:
    src/NovaTuneApp/NovaTuneApp.ApiService/Services/TokenService.cs

Steps

1. Add Configuration Section

Location:

appsettings.json

{
  "Jwt": {
    "Issuer": "https://novatune.example",
    "Audience": "novatune-api",
    "AccessTokenExpirationMinutes": 15,
    "RefreshTokenExpirationMinutes": 60,
    "SigningAlgorithm": "HS256"
  }
}

Note: SigningKey should be in environment variable

JWT_SIGNING_KEY
or secrets manager.

2. Create JWT Configuration Class

Location:

src/NovaTuneApp/NovaTuneApp.ApiService/Configuration/JwtSettings.cs

public class JwtSettings
{
    public const string SectionName = "Jwt";

    public required string Issuer { get; set; }
    public required string Audience { get; set; }
    public int AccessTokenExpirationMinutes { get; set; } = 15;
    public int RefreshTokenExpirationMinutes { get; set; } = 60;
    public string SigningAlgorithm { get; set; } = "HS256";
}

3. Create Token Service

Location:

src/NovaTuneApp/NovaTuneApp.ApiService/Services/TokenService.cs

public interface ITokenService
{
    string GenerateAccessToken(ApplicationUser user);
    string GenerateRefreshToken();
    ClaimsPrincipal? ValidateToken(string token);
}

public class TokenService : ITokenService
{
    private readonly JwtSettings _settings;
    private readonly byte[] _signingKey;

    public TokenService(IOptions<JwtSettings> settings, IConfiguration config)
    {
        _settings = settings.Value;
        _signingKey = Encoding.UTF8.GetBytes(
            config["JWT_SIGNING_KEY"]
            ?? throw new InvalidOperationException("JWT_SIGNING_KEY not configured"));
    }

    public string GenerateAccessToken(ApplicationUser user)
    {
        var claims = new List<Claim>
        {
            new(JwtRegisteredClaimNames.Sub, user.UserId),
            new(JwtRegisteredClaimNames.Email, user.Email),
            new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            new("status", user.Status.ToString())
        };

        // Add role claims
        foreach (var role in user.Roles)
        {
            claims.Add(new Claim("roles", role.ToLowerInvariant()));
        }

        var key = new SymmetricSecurityKey(_signingKey);
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            issuer: _settings.Issuer,
            audience: _settings.Audience,
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(_settings.AccessTokenExpirationMinutes),
            signingCredentials: credentials);

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

    public string GenerateRefreshToken()
    {
        var randomBytes = new byte[32];
        using var rng = RandomNumberGenerator.Create();
        rng.GetBytes(randomBytes);
        return Convert.ToBase64String(randomBytes);
    }

    public ClaimsPrincipal? ValidateToken(string token)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = new SymmetricSecurityKey(_signingKey);

        try
        {
            var principal = tokenHandler.ValidateToken(token, new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidIssuer = _settings.Issuer,
                ValidateAudience = true,
                ValidAudience = _settings.Audience,
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = key,
                ValidateLifetime = true,
                ClockSkew = TimeSpan.Zero
            }, out _);

            return principal;
        }
        catch
        {
            return null;
        }
    }
}

4. Configure Authentication in Program.cs

// Bind settings
builder.Services.Configure<JwtSettings>(
    builder.Configuration.GetSection(JwtSettings.SectionName));

// Register token service
builder.Services.AddSingleton<ITokenService, TokenService>();

// Configure JWT authentication
var jwtSettings = builder.Configuration
    .GetSection(JwtSettings.SectionName)
    .Get<JwtSettings>()!;

var signingKey = Encoding.UTF8.GetBytes(
    builder.Configuration["JWT_SIGNING_KEY"]
    ?? throw new InvalidOperationException("JWT_SIGNING_KEY not configured"));

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = jwtSettings.Issuer,
            ValidateAudience = true,
            ValidAudience = jwtSettings.Audience,
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(signingKey),
            ValidateLifetime = true,
            ClockSkew = TimeSpan.Zero
        };
    });

builder.Services.AddAuthorization();

// After app.Build()
app.UseAuthentication();
app.UseAuthorization();

5. Configure Authorization Policies

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("Listener", policy =>
        policy.RequireAuthenticatedUser());

    options.AddPolicy("Admin", policy =>
        policy.RequireClaim("roles", "admin"));

    options.AddPolicy("ActiveUser", policy =>
        policy.Requirements.Add(new ActiveUserRequirement()));
});

6. Create ActiveUser Authorization Requirement

Location:

src/NovaTuneApp/NovaTuneApp.ApiService/Authorization/ActiveUserRequirement.cs

public class ActiveUserRequirement : IAuthorizationRequirement { }

public class ActiveUserHandler : AuthorizationHandler<ActiveUserRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        ActiveUserRequirement requirement)
    {
        var statusClaim = context.User.FindFirstValue("status");

        if (statusClaim == UserStatus.Active.ToString())
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

Required NuGet Packages

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package System.IdentityModel.Tokens.Jwt

Environment Variables

VariableDescriptionRequired
JWT_SIGNING_KEY
Secret key for token signing (min 32 chars)Yes

Security Checklist

  • Signing key is NOT in appsettings.json
  • Signing key is at least 256 bits (32 characters)
  • ClockSkew
    is set to
    TimeSpan.Zero
  • Access token TTL is short (15 minutes)
  • jti
    claim is included for revocation support
  • Token never logged (add to redaction policy)

Testing

[Fact]
public void GenerateAccessToken_IncludesRequiredClaims()
{
    var user = new ApplicationUser
    {
        UserId = "01HQ3K...",
        Email = "test@example.com",
        Roles = ["Listener"]
    };

    var token = _tokenService.GenerateAccessToken(user);
    var principal = _tokenService.ValidateToken(token);

    principal.Should().NotBeNull();
    principal!.FindFirstValue(JwtRegisteredClaimNames.Sub).Should().Be(user.UserId);
    principal.FindFirstValue("roles").Should().Be("listener");
}