Identityserver4: Authenticating Users using X.509 Certificates

Created on 18 Aug 2016  路  12Comments  路  Source: IdentityServer/IdentityServer4

Hello,
I need little clarification and help about configuring Identity server with support for "User" certificate authentication. I'm new to identity server but I have read all the documentation about Identity Server 4 and 3 and also looked and understand the samples so please be gentle with me :)

I have "users" (pc's and servers) on which I have windows service running that need to consume two "clients" or RP's one Web API and one SignalR hub (there is no human it is all machine to machine). The clients need to authenticate with their certificate. With identity server I understand how to configure the "client" and also the "users". My understanding of identity server is that is responsible for the authentication process and also for issuing tokens.

Is the terminology used above correct in relation to identity server or my "users" should actually be "clients"?

Since identity server is responsible for authentication, how to configure identity server to authenticate users with certificates. I found this document but it is for clients. Is this supported out of the box for users? Is there any sample for this?

Is it correct to first request a token from the identity server and provide the token to the client to validate or I should connect to the client and then the client will automatically redirect the request to identity server to authenticate and issue a token?

enhancement

Most helpful comment

Is there a story that tracks authenticating clients using x.509 certificates? I see that #214 covers user authentication, but I'm interested in client credentials grant using certificates.

All 12 comments

you only use the term user for humans. Clients are applications, services etc (pure silicon).

https://identityserver.github.io/Documentation/docsv2/overview/terminology.html

In idsrv3 we support both client and user authentication using x509. In idsrv4 there is still some work to do for client authentication. User authentication is not part of idsrv4 anymore and would work just as it would work in standard aspnet core (but we don't have any guidance yet).

214 tracks this

I know you closed this issue but I wanted to share my findings, I hope it will be use.

I investigate the source code from idsrv3 and idsrv4 and with some modifications I was able to successfully issue a token for client providing X509 certificate. I have reused the X509CertificateSecretParser and X509CertificateThumbprintSecretValidator class from idsrv3. I had some small issues that I have resolved for the Kestrel configuration. If needed I can provide you with step by step guide on how to configure Kestrel for https use to accept client certificates. You can see my answer here

Here are my implementations for idsrv4

X509CertificateSecretParser

    public class X509CertificateSecretParser : ISecretParser
    {
        private readonly ILogger _Logger;
        private readonly IdentityServerOptions _Options;

        public X509CertificateSecretParser(IdentityServerOptions options, ILogger<X509CertificateSecretParser> logger)
        {
            _Options = options;
            _Logger = logger;
        }

        #region Implementation of ISecretParser

        public string AuthenticationMethod => "ClientCertificate";

        public Task<ParsedSecret> ParseAsync(HttpContext context)
        {
            _Logger.LogDebug("Start parsing for X.509 certificate");

            var certificate = context.Connection.ClientCertificate;

            if (certificate == null)
            {
                _Logger.LogDebug("Client certificate is null");
                return Task.FromResult<ParsedSecret>(null);
            }

            if (!context.Request.HasFormContentType)
            {
                _Logger.LogDebug("Content type is not a form");
                return Task.FromResult<ParsedSecret>(null);
            }

            var body = context.Request.Form;

            if (body == null)
            {
                _Logger.LogDebug("No form found");
                return Task.FromResult<ParsedSecret>(null);
            }

            var id = body["client_id"].FirstOrDefault();

            if (string.IsNullOrWhiteSpace(id))
            {
                _Logger.LogDebug("No client id found");
                return Task.FromResult<ParsedSecret>(null);
            }

            if (id.Length > _Options.InputLengthRestrictions.ClientId)
            {
                _Logger.LogError("Client ID exceeds maximum lenght.");
                return Task.FromResult<ParsedSecret>(null);
            }

            return Task.FromResult(new ParsedSecret
                                   {
                                       Id = id,
                                       Type = Constants.ParsedSecretTypes.X509Certificate,
                                       Credential = certificate
                                   });
        }

        #endregion
    }

X509CertificateThumbprintSecretValidator

    public class X509CertificateThumbprintSecretValidator : ISecretValidator
    {
        #region Implementation of ISecretValidator

        public Task<SecretValidationResult> ValidateAsync(IEnumerable<Secret> secrets, ParsedSecret parsedSecret)
        {
            var fail = Task.FromResult(new SecretValidationResult {Success = false});
            var success = Task.FromResult(new SecretValidationResult {Success = true});

            if (parsedSecret.Type != Constants.ParsedSecretTypes.X509Certificate)
            {
                return fail;
            }

            var cert = parsedSecret.Credential as X509Certificate2;

            if (cert == null)
            {
                throw new ArgumentException("ParsedSecret.Credential is not an X509 Certificate");
            }

            string thumbprint = cert.Thumbprint;

            if (string.IsNullOrWhiteSpace(thumbprint))
            {
                throw new ArgumentException("ParsedSecret.Credential.Thumbprint is empty");
            }

            foreach (var secret in secrets)
            {
                if (secret.Type == Constants.SecretTypes.X509CertificateThumbprint)
                {
                    if (TimeConstantComparer.IsEqual(thumbprint.ToLowerInvariant(), secret.Value.ToLowerInvariant()))
                    {
                        return success;
                    }
                }
            }

            return fail;
        }

        #endregion
    }

Here is my Client configuration. It is very important to specify AllowedGrantTypes of type "client_credentials" otherwise it will fail.

new Client
                         {
                             ClientName = "PC01",
                             ClientId = "PC01",
                             ClientSecrets = new List<Secret>
                                             {
                                                 new Secret
                                                 {
                                                     Value = "3559FE8C8EE47E7D4DCBA77B0281215AB11A63B4", //cert Thumbprint
                                                     Type = "X509Thumbprint",
                                                 }
                                             },
                             AllowedGrantTypes = new List<string> { "X509Certificate", "client_credentials" },

                             AllowedScopes = new List<string>
                                             {
                                                 "api",
                                                 "signalr"
                                             },
                             AccessTokenLifetime = 3600

                         });

And I try to get the token with

var cert = GetCertificate();
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(cert);
var client = new TokenClient("https://identity.local:6001/connect/token/", "PC01", handler);
var result = client.RequestClientCredentialsAsync("api signalr").Result;

Thanks for researching this. I re-opened it, maybe we can get the code into RC1

While making Kestrel support client certs is definitely useful for dev - in a real deployment this will be handled by the front-end server.

Did you figure out how that works with IIS? (IIS express should work the same actually).

No problem, thanks.

While making Kestrel support client certs is definitely useful for dev - in a real deployment this will be handled by the front-end server.

I totally agree with you and this is my biggest concern right now. In production, for ASP.NET Core applications we need a front end server (IIS or nginx) that acts like proxy. So the client certificate authentication is done by this server. From here there are two options:

  1. the client certificate is forwarded to idsrv so that we can parse and extract the secret
  2. the proxy extracts the secret and modify the request and sends that to idsrv.

In case of 1) I know that IIS supports client certificate forwarding but this means that Kestrel would need to be configured for https and to accept client certificates.

In case of 2) I'm clueless right now and further investigation need to be done.

I will try to make it work with with IIS and let you know of my findings.

IIS express should work the same actually

Actually I think it will not work out of the box for IIS Express and client certificate, too. There is some configurations you need to made in order for IIS Express to accept the certificates. So for dev I would recommend to run Kestrel as stand alone app and installing the Microsoft.AspNetCore.Server.Kestrel.Https package to support https. Then you just configure with

var host = new WebHostBuilder()
                .UseKestrel(x =>
                            {
                                HttpsConnectionFilterOptions httpsoptions = new HttpsConnectionFilterOptions
                                           {
                                                 ServerCertificate = GetCertificate(),
                                                  ClientCertificateMode = ClientCertificateMode.AllowCertificate,
                                                 CheckCertificateRevocation = false,
                                                 SslProtocols = SslProtocols.Default,
                                                 ClientCertificateValidation = ClientCertificateValidation
                                            };
                                x.UseHttps(httpsoptions);
                            })
                .UseUrls("https://identity.local:6001")
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseIISIntegration()
                .UseStartup<Startup>()
                .Build();
host.Run();

So my understanding is, that you only need to do the Kestrel specific config if you want to use the built-in client certs feature.

If you are using IIS - then the AspNetCore module from IIS should forward the cert.

Any progress?

We have a PR now for client signed JWTs - that's actually my preferred method. Will merge soon.

Still tracking here: #214. Will close now, but feel free to update us if necessary.

@techgeek03 Did you have to modify the source code to work with X509 certificate authentication? I'm stuck on this I'll appreciate any help on this.

Is there a story that tracks authenticating clients using x.509 certificates? I see that #214 covers user authentication, but I'm interested in client credentials grant using certificates.

Hi, I recently implemented private/public certificate client credentials based on this stack overflow entry: https://stackoverflow.com/questions/49686262/identityserver-client-authentication-with-public-private-keys-instead-of-shared

The thing that was not mentioned there was that I had to add the JwtBearerClientAssertionSecretParser and PrivateKeyJwtSecretValidator in the IdentityServer configuration.

My question is: Is that the right approach to do private/public certificate authentication? Should you add additional validations that the private/public certificate match? And if this approach is right, should this be part of the documentation? I would be happy to contribute that.

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

Was this page helpful?
0 / 5 - 0 ratings