When you’re wrangling a distributed system, authentication can feel like herding cats while riding a unicycle. You need a solution where services can validate tokens without cozying up to share secrets like they’re swapping gossip at a high school reunion. Enter our hero: a JWT-based architecture using asymmetric cryptography and OpenID Connect (OIDC) discovery. It’s secure, it’s scalable, and it’s got just enough swagger to make lesser auth systems jealous.
The Architecture: Trust Centrally, Validate Wildly
The idea is deceptively simple: one service plays the role of token-signing overlord, while the rest of your services verify tokens like independent detectives, no secret-sharing required. This setup laughs in the face of single points of failure and scales like a tech startup’s dreams before the Series A funding runs dry.
Here’s the trifecta of components that make this magic happen:
- Token Service: The grand maestro that signs JWTs with a private key, keeping it locked away like a dragon hoarding gold.
- Authentication Middleware: The gatekeeper that verifies tokens using public keys, because who has time to hardcode secrets?
- OIDC Discovery Endpoints: The cool kids’ way of sharing public keys, enabling seamless key rotation without breaking a sweat.
1. Token Generation Service: Signing Tokens with a Smirk
This service is the VIP of our setup, wielding RSA asymmetric cryptography to sign JWTs with flair. The KeyId is the unsung hero here, ensuring key rotation doesn’t turn into a chaotic game of musical chairs. Also being registered as singleton service ensures no duplicate RsaSecurityKey generation while keeping CreateToken thread-safe too, win-win for nerds!
public class AsymmetricTokenProvider
{
private readonly RsaSecurityKey _signatureKey; // The gatekeeper with the secret handshake
private readonly JwtSecurityTokenHandler _tokenHandler = new();
private readonly TokenConfiguration _config;
private readonly JsonWebKey _publicJwk; // Cached upfront so we don’t keep reinventing the wheel
public AsymmetricTokenProvider(TokenConfiguration config)
{
_config = config;
var rsa = RSA.Create();
rsa.ImportFromPem(config.PrivateKey); // Load the private key — handle with care
_signatureKey = new RsaSecurityKey(rsa)
{
KeyId = GenerateKeyFingerprint(rsa) // Every key deserves a cool nickname
};
_publicJwk = GeneratePublicJwk(_signatureKey); // Do it once, keep it forever (singleton life)
}
public string CreateToken(IEnumerable<Claim> assertions)
{
var descriptor = new SecurityTokenDescriptor
{
Issuer = _config.Issuer,
Audience = _config.Audience,
Subject = new ClaimsIdentity(assertions),
Expires = DateTime.UtcNow.Add(_config.DefaultDuration),
SigningCredentials = new SigningCredentials(_signatureKey, SecurityAlgorithms.RsaSha256)
};
var token = _tokenHandler.CreateToken(descriptor);
return _tokenHandler.WriteToken(token); // Out comes a shiny new JWT
}
public JsonWebKey GetPublicJwk() => _publicJwk; // Nothing to hide, just the public bits
private static string GenerateKeyFingerprint(RSA rsa)
{
// Hash the modulus and truncate it: fingerprints for cryptographic celebrities
var parameters = rsa.ExportParameters(false);
using var hasher = SHA256.Create();
var hash = hasher.ComputeHash(parameters.Modulus);
return Base64UrlEncoder.Encode(hash.AsSpan(0, 16));
}
private static JsonWebKey GeneratePublicJwk(RsaSecurityKey signatureKey)
{
// Convert the public half of the key into a JSON Web Key for the outside world
var publicParams = signatureKey.Rsa.ExportParameters(false);
var publicKey = RSA.Create();
publicKey.ImportParameters(publicParams);
var jwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(
new RsaSecurityKey(publicKey) { KeyId = signatureKey.KeyId });
jwk.Alg = SecurityAlgorithms.RsaSha256;
jwk.Use = "sig"; // For signatures only — no encryption shenanigans
return jwk;
}
}
2. Authentication Middleware: The Bouncer with Trust Issues
The middleware is your system’s skeptical bouncer, checking tokens without needing to phone home to the token service. It dynamically fetches public keys from the authority server, because hardcoding keys is so 2010.
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = configuration["Jwt:AuthorityUrl"]; // Basically this server which shares OIDC, LMAO
options.RequireHttpsMetadata = false; // Flip to true in prod, unless you like living dangerously
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateSignature = true, // No fake IDs allowed
ValidateIssuer = true,
ValidIssuer = configuration["Jwt:IssuingAuthority"],
ValidateAudience = true,
ValidAudience = configuration["Jwt:Audience"],
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5) // Because clocks are never *that* synced
};
});
3. OIDC Discovery Endpoints: The Key-Sharing Party
These endpoints are the social butterflies of the system, serving up public keys via standardized OIDC routes. They make key rotation as smooth as a sunny afternoon, no manual updates required.
[ApiController]
[Route(".well-known")]
[AllowAnonymous]
public class DiscoveryEndpoints : ControllerBase
{
private readonly AsymmetricTokenProvider _tokenProvider;
private readonly LinkGenerator _urlGenerator;
public DiscoveryEndpoints(AsymmetricTokenProvider tokenProvider, LinkGenerator urlGenerator)
{
_tokenProvider = tokenProvider;
_urlGenerator = urlGenerator; // Helping us navigate the URL jungle
}
[HttpGet("openid-configuration")]
public IActionResult GetOpenIdConfiguration()
{
var keysEndpoint = _urlGenerator.GetUriByAction(
HttpContext,
nameof(GetJsonWebKeys),
controller: "DiscoveryEndpoints");
return Ok(new
{
issuer = "https://api.example.com",
jwks_uri = keysEndpoint // "Here’s where the public keys party at"
});
}
[HttpGet("jwks")]
public IActionResult GetJsonWebKeys()
{
var jwk = _tokenProvider.GetPublicJwk();
return Ok(new { keys = new[] { jwk } }); // Public keys, free to a good home
}
}
Why This Setup is the Bee’s Knees
- Decoupled Validation: Services validate tokens solo, no chit-chat with the auth service. Less latency, fewer tears.
- Automated Key Management: OIDC endpoints serve fresh public keys like a barista slinging lattes, making key rotation a breeze.
- Fort Knox Security: Private key stays locked up tight, while public keys are handed out like candy at a parade.
- Plays Nice with Standards: OIDC compatibility means it works with any client that speaks the universal auth language.
- Scales Like a Dream: Each service verifies independently, so you can scale to the moon without breaking a sweat.
This architecture is your ticket to a secure, scalable, and maintainable auth system. It’s built on cryptographic best practices and OIDC standards, ready to handle everything from your garage startup to a global empire. So go forth, authenticate with confidence, and maybe throw in a smug grin for good measure.
Top comments (0)