Fixing slow logins

Written by Bill Boga

At Ritter Insurance Marketing, we use IdentityServer as part of our authentication strategy. It works great for our current needs. However, we noticed initial logins took longer than expected. We did some quick tests to confirm one-page had a response-time of ~500 ms (on average). This delay was made worse when many people would login at the same time (which happens every morning). The particular page is part of Identity Server’s authorize endpoint controller (i.e. /connect/authorize). Not really sure where to begin further debugging, we started hacking on the URI’s query string, and noticed when removing nonce and state-keys, the response-time went down. However, the generated HTML did not contain the expected id-token hidden form-tag. We used this info. and traversed through the controller to see where this tag was generated and eventually ended up in the DefaultTokenSigningService class. This is responsible for signing the jwt payload.

Before long, we realized this process involves the certificate provided to Identity Server, by the app., when it starts up. So, we pulled the certificate and looked to see if anything looked unfamiliar:

> openssl x509 -in cert.crt -text -noout

Two things stood out: Signature Algorithm: sha512WithRSAEncryption and Public-Key: (8192 bit). These are overkill for our purposes (along with almost everyone else) and definitely contributed to the long page load-time. We fairly quickly went about issuing a new certificate at 2048-bits using SHA256 and confirmed login times improved (~150 ms TTFB). However, we were still curious about various bit-sizes, signature algorithms, and how long each would take. So, a quick benchmark app. was constructed using BenchmarkDotNet. What follows is both the source and results of running the app.

CertSigningBenchmarks.cs

using BenchmarkDotNet.Attributes;
using IdentityServer3.Core;
using IdentityServer3.Core.Models;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;

namespace IdentityServerTokenGenerationBenchmarks
{
    public class CertSigningBenchmarks : IDisposable
    {
        public CertSigningBenchmarks()
        {
            var certPassword = "notsecure";

            certificate_256_2048 = new X509Certificate2("certs/idsrv-256-2048.pfx", certPassword);
            certificate_256_4096 = new X509Certificate2("certs/idsrv-256-4096.pfx", certPassword);
            certificate_256_8192 = new X509Certificate2("certs/idsrv-256-8192.pfx", certPassword);

            certificate_512_2048 = new X509Certificate2("certs/idsrv-512-2048.pfx", certPassword);
            certificate_512_4096 = new X509Certificate2("certs/idsrv-512-4096.pfx", certPassword);
            certificate_512_8192 = new X509Certificate2("certs/idsrv-512-8192.pfx", certPassword);
        }

        private readonly string payload = TokenFactory.CreateIdentityToken().CreateJwtPayload();
        private readonly X509Certificate2 certificate_256_2048;
        private readonly X509Certificate2 certificate_256_4096;
        private readonly X509Certificate2 certificate_256_8192;
        private readonly X509Certificate2 certificate_512_2048;
        private readonly X509Certificate2 certificate_512_4096;
        private readonly X509Certificate2 certificate_512_8192;

        [Benchmark(Description = "SHA256 - 2048-bit")]
        public void Sha256_2048_Bit()
        {
            var credentials = new X509SigningCredentials(certificate_256_2048);

            Sign(payload, credentials);
        }

        [Benchmark(Description = "SHA256 - 4096-bit")]
        public void Sha256_4096_Bit()
        {
            var credentials = new X509SigningCredentials(certificate_256_4096);

            Sign(payload, credentials);
        }

        [Benchmark(Description = "SHA256 - 8192-bit")]
        public void Sha256_8192_Bit()
        {
            var credentials = new X509SigningCredentials(certificate_256_8192);

            Sign(payload, credentials);
        }

        [Benchmark(Description = "SHA512 - 2048-bit")]
        public void Sha512_2048_Bit()
        {
            var credentials = new X509SigningCredentials(certificate_512_2048);

            Sign(payload, credentials);
        }

        [Benchmark(Description = "SHA512 - 4096-bit")]
        public void Sha512_4096_Bit()
        {
            var credentials = new X509SigningCredentials(certificate_512_4096);

            Sign(payload, credentials);
        }

        [Benchmark(Description = "SHA512 - 8192-bit")]
        public void Sha512_8192_Bit()
        {
            var credentials = new X509SigningCredentials(certificate_512_8192);

            Sign(payload, credentials);
        }

        /// <summary>
        /// Modified from https://github.com/IdentityServer/IdentityServer3/blob/dd07bd88f64b437b2c261cc76c71917ec5a0eb03/source/Core/Services/Default/DefaultTokenSigningService.cs#L103-L130
        /// </summary>
        /// <returns></returns>
        private static string Sign(string payload, SigningCredentials credentials)
        {
            var header = new JwtHeader(credentials);
            var jwtPayload = JwtPayload.Deserialize(payload);
            var token = new JwtSecurityToken(header, jwtPayload);
            var handler = new JwtSecurityTokenHandler();

            return handler.WriteToken(token);
        }

        private static class TokenFactory
        {
            public static Token CreateIdentityToken()
            {
                var client = new Client()
                {
                    ClientId = "test-client",
                    Flow = Flows.Implicit
                };

                var claims = new List<Claim>
                {
                    new Claim("sub", "valid")
                };

                var token = new Token(Constants.TokenTypes.IdentityToken)
                {
                    Claims = claims,
                    Client = client,
                    Lifetime = 60,
                };

                return token;
            }
        }

        public void Dispose()
        {
            certificate_256_2048.Dispose();
            certificate_256_4096.Dispose();
            certificate_256_8192.Dispose();
            certificate_512_2048.Dispose();
            certificate_512_4096.Dispose();
            certificate_512_8192.Dispose();
        }
    }
}

Program.cs

using BenchmarkDotNet.Running;

namespace IdentityServerTokenGenerationBenchmarks
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BenchmarkRunner.Run<CertSigningBenchmarks>();
        }
    }
}

project.json

{
    "version": "1.0.0-*",
    "buildOptions": {
        "emitEntryPoint": true
    },
    "dependencies": {
        "BenchmarkDotNet": "0.9.8",
        "System.IdentityModel.Tokens.Jwt": "4.0.0",
        "IdentityModel": "1.11.0",
        "IdentityServer3": "2.5.1"
    },
    "frameworks": {
        "net461": {}
    }
}

Results


Host Process Environment Information:
BenchmarkDotNet=v0.9.8.0
OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel(R) Core(TM) i7-5600U CPU 2.60GHz, ProcessorCount=4
Frequency=2533198 ticks, Resolution=394.7579 ns, Timer=TSC
CLR=MS.NET 4.0.30319.42000, Arch=64-bit RELEASE [RyuJIT]
GC=Concurrent Workstation
JitModules=clrjit-v4.6.1080.0

Type=CertSigningBenchmarks  Mode=Throughput  GarbageCollection=Concurrent Workstation  

{: .table .table-striped .table-fluid}

MethodMedianMeanStdErrorStdDevOp/sMinMax
SHA256 - 2048-bit2.4120 ms2.4276 ms0.0164 ms0.0837 ms411.922.3309 ms2.6418 ms
SHA256 - 4096-bit11.4094 ms11.4642 ms0.0423 ms0.1892 ms87.2311.2163 ms11.8987 ms
SHA256 - 8192-bit79.2107 ms81.1363 ms1.2031 ms6.1348 ms12.3276.7397 ms106.1459 ms
SHA512 - 2048-bit2.4356 ms2.4679 ms0.0210 ms0.1258 ms405.22.3434 ms2.8646 ms
SHA512 - 4096-bit11.4812 ms11.6003 ms0.0923 ms0.4128 ms86.211.2208 ms12.9738 ms
SHA512 - 8192-bit77.9870 ms78.2887 ms0.2996 ms1.3398 ms12.7776.5296 ms81.6495 ms

We were a bit surprised about the insignificant difference between SHA256 and SHA512. Granted, these results are coming from my dev. machine and not how the app. runs in production, they still give us a good baseline. More importantly, we now understand why the login originally took so long.

Published August 09, 2016 by

undefined avatar
Bill Boga Github Senior Software Developer

Suggested Reading