Skip to main content

Overview

AndanDo uses JSON Web Tokens (JWT) for API authentication and authorization. The JWT configuration controls token generation, validation, and expiration.
JWT tokens are generated by the JwtTokenService and used to authenticate users across API requests.

Configuration

JWT settings are configured in the Jwt section of appsettings.json:
appsettings.json
{
  "Jwt": {
    "Issuer": "AndanDo",
    "Audience": "AndanDo",
    "SecretKey": "your-secret-key-min-32-characters",
    "ExpirationMinutes": 120
  }
}

Configuration Properties

Issuer

The issuer (iss) claim identifies the principal that issued the JWT.
"Issuer": "AndanDo"
  • Type: string
  • Required: Yes
  • Default: "AndanDo"
  • Purpose: Identifies tokens issued by AndanDo
The issuer should match your application name or domain. This claim is validated when verifying tokens.

Audience

The audience (aud) claim identifies the recipients that the JWT is intended for.
"Audience": "AndanDo"
  • Type: string
  • Required: Yes
  • Default: "AndanDo"
  • Purpose: Ensures tokens are only valid for AndanDo services

SecretKey

The secret key used to sign and verify JWT tokens using HMAC-SHA256.
"SecretKey": "c0d3x-andando-dev-secret-key-please-change-123456"
  • Type: string
  • Required: Yes
  • Minimum Length: 32 characters (256 bits)
  • Algorithm: HMAC-SHA256 (HS256)
Critical Security Requirements:
  • Must be at least 32 characters long
  • Use a cryptographically secure random string
  • Never commit production keys to source control
  • Rotate keys regularly in production
  • Different keys for each environment (dev, staging, prod)
Generate a secure key:
# Using OpenSSL
openssl rand -base64 32

# Using PowerShell
[Convert]::ToBase64String((1..32 | ForEach-Object { Get-Random -Maximum 256 }))

# Using .NET
dotnet user-secrets set "Jwt:SecretKey" "$(openssl rand -base64 32)"

ExpirationMinutes

The token expiration time in minutes.
"ExpirationMinutes": 120
  • Type: int
  • Required: No
  • Default: 60 (defined in JwtOptions.cs:61)
  • Recommended: 120 (2 hours)
Token Expiration Guidelines:
  • Development: 120-480 minutes (2-8 hours)
  • Production: 15-60 minutes
  • Mobile Apps: 60-240 minutes (with refresh tokens)
  • Public APIs: 5-15 minutes (with refresh tokens)

JwtOptions Class

The configuration is bound to the JwtOptions class:
Services/JWT/JwtTokenService.cs
public sealed class JwtOptions
{
    public string Issuer { get; set; } = string.Empty;
    public string Audience { get; set; } = string.Empty;
    public string SecretKey { get; set; } = string.Empty;
    public int ExpirationMinutes { get; set; } = 60;
}

JwtTokenService Implementation

The JwtTokenService generates JWT tokens based on the configuration:
Services/JWT/JwtTokenService.cs
public sealed class JwtTokenService : IJwtTokenService
{
    private readonly JwtOptions _options;
    private readonly TimeProvider _timeProvider;
    private readonly JwtSecurityTokenHandler _tokenHandler = new();

    public JwtTokenService(IOptions<JwtOptions> options, TimeProvider? timeProvider = null)
    {
        _options = options.Value;
        _timeProvider = timeProvider ?? TimeProvider.System;
    }

    public string GenerateToken(UserIdentity identity)
    {
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.SecretKey));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature);

        var claims = new[]
        {
            new Claim(JwtRegisteredClaimNames.Sub, identity.Id.ToString()),
            new Claim(JwtRegisteredClaimNames.Email, identity.Email),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
        };

        var now = _timeProvider.GetUtcNow();
        var descriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(claims),
            NotBefore = now.UtcDateTime,
            IssuedAt = now.UtcDateTime,
            Expires = now.AddMinutes(_options.ExpirationMinutes).UtcDateTime,
            Issuer = _options.Issuer,
            Audience = _options.Audience,
            SigningCredentials = credentials
        };

        var token = _tokenHandler.CreateToken(descriptor);
        return _tokenHandler.WriteToken(token);
    }
}

Token Claims

Each generated token includes the following claims:
ClaimDescriptionExample
subSubject - User ID"12345"
emailUser’s email address"[email protected]"
jtiJWT ID - Unique token identifier"3fa85f64-5717-4562-b3fc-2c963f66afa6"
issIssuer"AndanDo"
audAudience"AndanDo"
nbfNot Before - Token valid start time1709744400
iatIssued At - Token creation time1709744400
expExpiration - Token expiry time1709751600
Claims are encoded in the JWT payload and can be read without validation, but cannot be tampered with due to the signature.

Service Registration

The JWT service is registered in Program.cs:
Program.cs
// Bind JWT configuration
builder.Services.Configure<JwtOptions>(builder.Configuration.GetSection("Jwt"));

// Register JWT token service
builder.Services.AddScoped<IJwtTokenService, JwtTokenService>();

Usage Example

Generating a JWT token for a user:
public class AuthService : IAuthService
{
    private readonly IJwtTokenService _jwtTokenService;

    public AuthService(IJwtTokenService jwtTokenService)
    {
        _jwtTokenService = jwtTokenService;
    }

    public async Task<string> AuthenticateAsync(string email, string password)
    {
        // Validate credentials (simplified)
        var user = await GetUserByEmailAsync(email);
        if (user == null || !VerifyPassword(password, user.PasswordHash))
        {
            throw new UnauthorizedAccessException("Invalid credentials");
        }

        // Generate JWT token
        var identity = new UserIdentity
        {
            Id = user.Id,
            Email = user.Email
        };

        var token = _jwtTokenService.GenerateToken(identity);
        return token;
    }
}

Environment-Specific Configuration

Store JWT settings in User Secrets for development:
dotnet user-secrets init
dotnet user-secrets set "Jwt:SecretKey" "dev-secret-key-min-32-characters-long"
dotnet user-secrets set "Jwt:ExpirationMinutes" "480"
Verify secrets:
dotnet user-secrets list
Use environment variables in production:
export Jwt__SecretKey="prod-secret-key-min-32-characters-long"
export Jwt__ExpirationMinutes="30"
export Jwt__Issuer="AndanDo"
export Jwt__Audience="AndanDo"
Or in appsettings.Production.json:
{
  "Jwt": {
    "ExpirationMinutes": 30
  }
}
Never put the SecretKey in appsettings.Production.json. Always use environment variables or Azure Key Vault.
Store the secret key in Azure Key Vault:
# Add secret to Key Vault
az keyvault secret set \
  --vault-name "your-keyvault" \
  --name "JwtSecretKey" \
  --value "prod-secret-key-min-32-characters-long"
Reference in App Service configuration:
@Microsoft.KeyVault(SecretUri=https://your-keyvault.vault.azure.net/secrets/JwtSecretKey/)

Testing JWT Configuration

Verify JWT configuration is working:
[Test]
public void GenerateToken_ValidIdentity_ReturnsToken()
{
    // Arrange
    var options = Options.Create(new JwtOptions
    {
        Issuer = "AndanDo",
        Audience = "AndanDo",
        SecretKey = "test-secret-key-min-32-chars-long",
        ExpirationMinutes = 60
    });
    var service = new JwtTokenService(options);
    var identity = new UserIdentity { Id = 1, Email = "[email protected]" };

    // Act
    var token = service.GenerateToken(identity);

    // Assert
    Assert.IsNotEmpty(token);
    
    // Verify token structure
    var handler = new JwtSecurityTokenHandler();
    var jwtToken = handler.ReadJwtToken(token);
    Assert.AreEqual("AndanDo", jwtToken.Issuer);
    Assert.AreEqual("[email protected]", jwtToken.Claims.First(c => c.Type == "email").Value);
}

Troubleshooting

Error: Token signature validation failedCause: The SecretKey used to sign the token doesn’t match the key used to validate it.Solutions:
  1. Ensure the same SecretKey is used across all instances
  2. Verify no extra whitespace in the configuration
  3. Check environment-specific configuration is loading correctly
  4. Verify the secret key is at least 32 characters
Error: Token has expiredCause: The current time is past the token’s exp claim.Solutions:
  1. Increase ExpirationMinutes if tokens expire too quickly
  2. Implement token refresh mechanism
  3. Ensure server clocks are synchronized (NTP)
  4. Check for time zone issues
Error: IDX10603: The algorithm requires the SecurityKey.KeySize to be greater than 256 bitsCause: The SecretKey is less than 32 characters (256 bits).Solution: Use a secret key with at least 32 characters:
openssl rand -base64 32
Error: Options are empty or not boundSolutions:
  1. Verify appsettings.json contains the Jwt section
  2. Check Program.cs includes the configuration binding
  3. Ensure environment-specific files are properly named
  4. Verify JSON syntax is valid

Security Best Practices

JWT Security Checklist:
  • Use a strong, randomly generated secret key (minimum 32 characters)
  • Never commit secret keys to source control
  • Use different keys for each environment
  • Rotate secret keys regularly (every 90 days in production)
  • Keep expiration times short (15-60 minutes)
  • Implement token refresh mechanism for better UX
  • Store tokens securely on the client (HttpOnly cookies preferred)
  • Validate all claims on the server side
  • Consider using asymmetric keys (RS256) for multi-service architectures
  • Implement token revocation for logout functionality
Token Refresh Pattern: Consider implementing refresh tokens to allow users to stay logged in without compromising security:
  1. Issue short-lived access tokens (15-30 minutes)
  2. Issue long-lived refresh tokens (7-30 days)
  3. Store refresh tokens securely (HttpOnly cookie or database)
  4. Allow clients to request new access tokens using refresh tokens

Build docs developers (and LLMs) love