Most of the authorization servers which support Oauth, Like Azure AD, IdentityServer4 , ForgeRock, WSO2 allow us to sign the JWT with a public key encryption algorithm like RS 256. The advantage of using a public key(asymmetric) algorithm is you can encrypt JWT with a private key and decode it with a public key. Here you don’t need to share your private key to your clients to decode JWT.
There are basically 4 methods to validate a JWT token at your resource server.
- You share your public key to resource server. So that they can use that public key to decrypt JWT and validate it.
- The disadvantage of above method is your resource servers need to hard-code the public key. Also what if you need to rotate your keys weekly. To resolve this problem all of the Oauth providers expose a Metadata Uri as per RFC8414. This Uri is called JWKS_URI and which expose a set of public keys used by your authorization server. Your resource server need to get public keys from this url to validate the JWT. Asp.net core doesn’t support this in it’s identity model, so you need to write your own code.
- What if your JWT token is revoked?. Above two methods can’t detect whether your token is revoked or not. So Oauth support an Introspection endpoint (RFC 7762). You can send your token to this endpoint and it will give a response valid or not valid. This endpoint is protected one and you need to use client-id and secret .
- Using a private key to sign and validate JWT. This is the least preferred method.
Method 1 & 3 is directly supported by Asp.net core. So we can look both first and the 2nd method last.
- Hard-coded public key.
public void ConfigureServices(IServiceCollection services) {
services.AddControllers();
/*
* We'll use a public key to validate if the token was signed
* with the corresponding private key.
*/
services.AddSingleton<RsaSecurityKey>(provider => {
// It's required to register the RSA key with depedency injection.
// If you don't do this, the RSA instance will be prematurely disposed.
RSA rsa = RSA.Create();
rsa.ImportRSAPublicKey(
source: Convert.FromBase64String(configuration["PublicKey"]),
bytesRead: out int _
);
return new RsaSecurityKey(rsa);
});
services.AddAuthentication()
.AddJwtBearer("Asymmetric", options => {
SecurityKey rsa = services.BuildServiceProvider().GetRequiredService<RsaSecurityKey>();
options.IncludeErrorDetails = true; // <- great for debugging
// Configure the actual Bearer validation
options.TokenValidationParameters = new TokenValidationParameters {
IssuerSigningKey = rsa,
ValidAudience = "jwt-test",
ValidIssuer = "jwt-test",
RequireSignedTokens = true,
RequireExpirationTime = true, // <- JWTs are required to have "exp" property set
ValidateLifetime = true, // <- the "exp" will be validated
ValidateAudience = true,
ValidateIssuer = true,
};
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
app.UseDeveloperExceptionPage();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization(); // <- allows the use of [Authorize] on controllers and actions
app.UseEndpoints(endpoints => { endpoints.MapDefaultControllerRoute(); });
}
2. Introspection endpoint.
Install this library https://github.com/IdentityModel/IdentityModel.AspNetCore.OAuth2Introspection
services.AddAuthentication(OAuth2IntrospectionDefaults.AuthenticationScheme)
.AddOAuth2Introspection(options =>
{
options.Authority = "https://base_address_of_token_service";
options.IntrospectionEndpoint = "";
options.ClientId = "client_id_for_introspection_endpoint";
options.ClientSecret = "client_secret_for_introspection_endpoint";
});
4. Hard-coded private key.
public void ConfigureServices(IServiceCollection services) {
services.AddControllers();
/*
* Configure validation of regular JWT signed with a symmetric key
*/
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) // Set default to 'Bearer'
.AddJwtBearer(options => { // Configure how the Bearer token is validated
var symmetricKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["privatekey"]));
options.IncludeErrorDetails = true; // <- great for debugging
// Configure the actual Bearer validation
options.TokenValidationParameters = new TokenValidationParameters {
IssuerSigningKey = symmetricKey,
ValidAudience = "jwt-test",
ValidIssuer = "jwt-test",
RequireSignedTokens = true,
RequireExpirationTime = true, // <- JWTs are required to have "exp" property set
ValidateLifetime = true, // <- the "exp" will be validated
ValidateAudience = true,
ValidateIssuer = true,
};
});
}
3. JWKS_URI.
This method is little complex. You need to write below code.
using Microsoft.IdentityModel.Tokens;
using System;
namespace JwtExtensions
{
public sealed class JwkList
{
public JwkList(JsonWebKeySet jwkTaskResult)
{
Jwks = jwkTaskResult;
When = DateTime.Now;
}
public DateTime When { get; set; }
public JsonWebKeySet Jwks { get; set; }
}
}
using System;
namespace JwtExtensions
{
public class JwkOptions
{
public JwkOptions(string jwksUri)
{
JwksUri = new Uri(jwksUri);
Issuer = $"{JwksUri.Scheme}://{JwksUri.Authority}";
KeepFor = TimeSpan.FromMinutes(15);
}
public JwkOptions(string jwksUri, TimeSpan cacheTime)
{
JwksUri = new Uri(jwksUri);
Issuer = $"{JwksUri.Scheme}://{JwksUri.Authority}";
KeepFor = cacheTime;
}
public JwkOptions(string jwksUri, string issuer, TimeSpan cacheTime)
{
JwksUri = new Uri(jwksUri);
Issuer = issuer;
KeepFor = cacheTime;
}
public string Issuer { get; private set; }
public Uri JwksUri { get; private set; }
public TimeSpan KeepFor { get; private set; }
}
}
using System;
using System.Collections.Generic;
using System.Net.Http;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace JwtExtensions
{
public static class JwksExtension
{
public static void SetJwksOptions(this JwtBearerOptions options, JwkOptions jwkOptions)
{
var httpClient = new HttpClient(options.BackchannelHttpHandler ?? new HttpClientHandler())
{
Timeout = options.BackchannelTimeout,
MaxResponseContentBufferSize = 1024 * 1024 * 10 // 10 MB
};
options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
jwkOptions.JwksUri.OriginalString,
new JwksRetriever(),
new HttpDocumentRetriever(httpClient) { RequireHttps = options.RequireHttpsMetadata });
options.TokenValidationParameters.ValidateAudience = false;
options.TokenValidationParameters.ValidIssuer = jwkOptions.Issuer;
}
}
}
using System.Threading;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Logging;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
namespace JwtExtensions
{
public class JwksRetriever : IConfigurationRetriever<OpenIdConnectConfiguration>
{
public Task<OpenIdConnectConfiguration> GetConfigurationAsync(string address, IDocumentRetriever retriever, CancellationToken cancel)
{
return GetAsync(address, retriever, cancel);
}
/// <summary>
/// Retrieves a populated <see cref="OpenIdConnectConfiguration"/> given an address and an <see cref="IDocumentRetriever"/>.
/// </summary>
/// <param name="address">address of the jwks uri.</param>
/// <param name="retriever">the <see cref="IDocumentRetriever"/> to use to read the jwks</param>
/// <param name="cancel"><see cref="CancellationToken"/>.</param>
/// <returns>A populated <see cref="OpenIdConnectConfiguration"/> instance.</returns>
public static async Task<OpenIdConnectConfiguration> GetAsync(string address, IDocumentRetriever retriever, CancellationToken cancel)
{
if (string.IsNullOrWhiteSpace(address))
throw LogHelper.LogArgumentNullException(nameof(address));
if (retriever == null)
throw LogHelper.LogArgumentNullException(nameof(retriever));
var doc = await retriever.GetDocumentAsync(address, cancel).ConfigureAwait(false);
LogHelper.LogVerbose("IDX21811: Deserializing the string: '{0}' obtained from metadata endpoint into openIdConnectConfiguration object.", doc);
var jwks = new JsonWebKeySet(doc);
var openIdConnectConfiguration = new OpenIdConnectConfiguration()
{
JsonWebKeySet = jwks,
JwksUri = address,
};
foreach (var securityKey in jwks.GetSigningKeys())
openIdConnectConfiguration.SigningKeys.Add(securityKey);
return openIdConnectConfiguration;
}
}
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = true;
options.SaveToken = true;
// API Authenticator Endpoint
options.SetJwksOptions(new JwkOptions(Configuration["JwksUri"]));
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
You must be logged in to post a comment.