Certificate authentication with JWT

Certificate

The scenario:

  • Each client/user receives an SSL client certificate for authentication.
  • The subject of the certificate includes the role in the O RDN and the user name in the CN, or some other combination.
  • The client generates a JWT on behalf of the user signed with the certificate.
  • The authentication server receives the JWT, verifies it with the certificate (public) of the user and maps the certificate subject information to the user claims.


public static void AddSecureAuthentication(this IServiceCollection sp, IConfiguration configuration)
{
    sp.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
     .AddJwtBearer(options =>
     {
         options.Events = new JwtBearerEvents
         {
             OnTokenValidated = async ctx =>
             {
                 if (ctx.SecurityToken is JsonWebToken jwt)
                 {
                     if (ctx.SecurityToken.SigningKey is X509SecurityKey securityKey)
                     {
                         X509Name name = new(securityKey.Certificate.Subject);
                         string? roleName = name
                           .GetValueList(X509Name.O)
                           .OfType<string>()
                           .FirstOrDefault();

                         List<Claim> claims = new ()
                         {
                             new Claim(ClaimTypes.Role, roleName ?? "")
                         };
                         
                         ClaimsIdentity clientIdentity = new ClaimsIdentity(claims);

                         ctx.Principal?.AddIdentity(clientIdentity);
                     }
                 }

                 await Task.CompletedTask;
             }
         };

         options.Audience = configuration["Authentication:Audience"];
         options.IncludeErrorDetails = true;        

         options.TokenValidationParameters.ValidateAudience = false;
         options.TokenValidationParameters.ValidateLifetime = true;
         options.TokenValidationParameters.ValidateIssuer = false;
         options.TokenValidationParameters.ValidateActor = false;

         options.TokenValidationParameters.IssuerSigningKeyResolver = new IssuerSigningKeyResolver((_, st, kid, p) =>
         {
             if (st is not  JsonWebToken jwt)
             {
                 return null;
             }

             string? certPath = configuration[$"Authentication:Clients:{kid.ToLower()}"];

             if (certPath == null || !File.Exists(certPath))
             {
                 return null;
             }

             X509Certificate2 xcer = new(File.ReadAllBytes(certPath));

             return new[] { new X509SecurityKey(xcer) };

         });
     });
}

Here we have a signing key resolver that retreives the client certificate by kid which is the thumbprint of the certificate. Then there are JwtBearerEvents that allow us to to create a ClaimsIdentity based on the certificate information

This way we completely ignore the contents of the JWT, which is used as an ownership proof only

Post a Comment

Previous Post Next Post