Skip to content

BankID v5 Signature, Validation and Authentication in ASP.NET Core

I am going to implement a service for BankID v5 (version 5) authentication, signatures and validation in this tutorial. The service is implemented as a form with HTML and JavaScript, requests is made to server methods in ASP.NET Core. BankID is an electronic identification solution in Sweden that is used for authentication and electronic signatures.

This code has been tested and is working with Google Chrome (75.0.3770.100), Mozilla Firefox (75.0) and Microsoft Edge (81.0.416.62), without any polyfill. It works in Internet Explorer (11.829.17134.0) with polyfills for Array.from, Promise, String.prototype.padStart, TextEncoder, WebCrypto, XMLHttpRequest, Array.prototype.includes, CustomEvent, Array.prototype.closest, Array.prototype.remove, String.prototype.endsWith and String.prototype.includes and code transpilation. If you want to support older browsers, check out our post on transpilation and polyfilling of JavaScript. This code depends on annytab.effects, Font Awesome, annytab.notifier and js-spark-md5.

Preparation and Settings

BankID certificates can be used with a computer application and/or a smartphone (mobile) application. You will need to visit the official site with BankID documentation in order to download a SSL test certificate and issue a user test certificate. A SSL test certificate is needed in order to establish a connection to the API service. BankID applications needs to be configured for testing, information about this configuration can be found here. I use an application.json file to store settings for my BankIdClient and a BankIdOptions class to access these settings.

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  },
  "BankIdOptions": {
    "BaseAddress": "https://appapi2.test.bankid.com",
    "TimeoutInMilliseconds": 90000
  }
}
using System;

namespace Annytab.Scripts
{
    public class BankIdOptions
    {
        #region Variables

        public string BaseAddress { get; set; }
        public Int32? TimeoutInMilliseconds { get; set; }

        #endregion

        #region Constructors

        public BankIdOptions()
        {
            // Set values for instance variables
            this.BaseAddress = null;
            this.TimeoutInMilliseconds = null;

        } // End of the constructor

        #endregion

    } // End of the class

} // End of the namespace

Models

using System.Security.Cryptography.X509Certificates;

namespace Annytab.Scripts.Models
{
    public class ResponseData
    {
        #region variables

        public bool success { get; set; }
        public string id { get; set; }
        public string message { get; set; }
        public string url { get; set; }

        #endregion

        #region Constructors

        public ResponseData()
        {
            // Set values for instance variables
            this.success = false;
            this.id = "";
            this.message = "";
            this.url = "";

        } // End of the constructor

        public ResponseData(bool success, string id, string message, string url = "")
        {
            // Set values for instance variables
            this.success = success;
            this.id = id;
            this.message = message;
            this.url = url;

        } // End of the constructor

        #endregion

    } // End of the class

    public class Signature
    {
        #region Variables

        public string validation_type { get; set; }
        public string algorithm { get; set; }
        public string padding { get; set; }
        public string data { get; set; }
        public string value { get; set; }
        public string certificate { get; set; }

        #endregion

        #region Constructors

        public Signature()
        {
            // Set values for instance variables
            this.validation_type = null;
            this.algorithm = null;
            this.padding = null;
            this.data = null;
            this.value = null;
            this.certificate = null;

        } // End of the constructor

        #endregion

    } // End of the class

    public class SignatureValidationResult
    {
        #region Variables

        public bool valid { get; set; }
        public string signature_data { get; set; }
        public string signatory { get; set; }
        public X509Certificate2 certificate { get; set; }

        #endregion

        #region Constructors

        public SignatureValidationResult()
        {
            // Set values for instance variables
            this.valid = false;
            this.signature_data = null;
            this.signatory = null;
            this.certificate = null;

        } // End of the constructor

        #endregion

    } // End of the class

} // End of the namespace
using System.Xml.Serialization;
using System.Collections.Generic;

namespace Annytab.Scripts.Models
{
    public class BankidResponse
    {
        #region Variables

        public string autoStartToken { get; set; }
        public string orderRef { get; set; }
        public string status { get; set; }
        public string hintCode { get; set; }
        public CompletionData completionData { get; set; }

        #endregion

        #region Constructors

        public BankidResponse()
        {
            // Set values for instance variables
            this.autoStartToken = null;
            this.orderRef = null;
            this.status = null;
            this.hintCode = null;
            this.completionData = null;

        } // End of the constructor

        #endregion

    } // End of the class

    public class CompletionData
    {
        #region Variables

        public string signature { get; set; }
        public string ocspResponse { get; set; }

        #endregion

        #region Constructors

        /// <summary>
        /// Create a new post with default properties
        /// </summary>
        public CompletionData()
        {
            // Set values for instance variables
            this.signature = null;
            this.ocspResponse = null;

        } // End of the constructor

        #endregion

    } // End of the class

    [XmlRoot(ElementName = "CanonicalizationMethod", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
    public class CanonicalizationMethod
    {
        [XmlAttribute(AttributeName = "Algorithm")]
        public string Algorithm { get; set; }
    }

    [XmlRoot(ElementName = "SignatureMethod", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
    public class SignatureMethod
    {
        [XmlAttribute(AttributeName = "Algorithm")]
        public string Algorithm { get; set; }
    }

    [XmlRoot(ElementName = "Transform", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
    public class Transform
    {
        [XmlAttribute(AttributeName = "Algorithm")]
        public string Algorithm { get; set; }
    }

    [XmlRoot(ElementName = "Transforms", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
    public class Transforms
    {
        [XmlElement(ElementName = "Transform", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
        public Transform Transform { get; set; }
    }

    [XmlRoot(ElementName = "DigestMethod", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
    public class DigestMethod
    {
        [XmlAttribute(AttributeName = "Algorithm")]
        public string Algorithm { get; set; }
    }

    [XmlRoot(ElementName = "Reference", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
    public class Reference
    {
        [XmlElement(ElementName = "Transforms", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
        public Transforms Transforms { get; set; }
        [XmlElement(ElementName = "DigestMethod", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
        public DigestMethod DigestMethod { get; set; }
        [XmlElement(ElementName = "DigestValue", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
        public string DigestValue { get; set; }
        [XmlAttribute(AttributeName = "URI")]
        public string URI { get; set; }
        [XmlAttribute(AttributeName = "Type")]
        public string Type { get; set; }
    }

    [XmlRoot(ElementName = "SignedInfo", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
    public class SignedInfo
    {
        [XmlElement(ElementName = "CanonicalizationMethod", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
        public CanonicalizationMethod CanonicalizationMethod { get; set; }
        [XmlElement(ElementName = "SignatureMethod", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
        public SignatureMethod SignatureMethod { get; set; }
        [XmlElement(ElementName = "Reference", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
        public List<Reference> Reference { get; set; }
        [XmlAttribute(AttributeName = "xmlns")]
        public string Xmlns { get; set; }
    }

    [XmlRoot(ElementName = "X509Data", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
    public class X509Data
    {
        [XmlElement(ElementName = "X509Certificate", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
        public List<string> X509Certificate { get; set; }
    }

    [XmlRoot(ElementName = "KeyInfo", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
    public class KeyInfo
    {
        [XmlElement(ElementName = "X509Data", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
        public X509Data X509Data { get; set; }
        [XmlAttribute(AttributeName = "xmlns")]
        public string Xmlns { get; set; }
        [XmlAttribute(AttributeName = "Id")]
        public string Id { get; set; }
    }

    [XmlRoot(ElementName = "usrVisibleData", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
    public class UsrVisibleData
    {
        [XmlAttribute(AttributeName = "visible")]
        public string Visible { get; set; }
        [XmlAttribute(AttributeName = "charset")]
        public string Charset { get; set; }
        [XmlText]
        public string Text { get; set; }
    }

    [XmlRoot(ElementName = "srvInfo", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
    public class SrvInfo
    {
        [XmlElement(ElementName = "name", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
        public string Name { get; set; }
        [XmlElement(ElementName = "nonce", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
        public string Nonce { get; set; }
        [XmlElement(ElementName = "displayName", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
        public string DisplayName { get; set; }
    }

    [XmlRoot(ElementName = "condition", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
    public class Condition
    {
        [XmlElement(ElementName = "type", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
        public string Type { get; set; }
        [XmlElement(ElementName = "value", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
        public string Value { get; set; }
    }

    [XmlRoot(ElementName = "requirement", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
    public class Requirement
    {
        [XmlElement(ElementName = "condition", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
        public Condition Condition { get; set; }
    }

    [XmlRoot(ElementName = "ai", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
    public class Ai
    {
        [XmlElement(ElementName = "type", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
        public string Type { get; set; }
        [XmlElement(ElementName = "deviceInfo", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
        public string DeviceInfo { get; set; }
        [XmlElement(ElementName = "uhi", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
        public string Uhi { get; set; }
        [XmlElement(ElementName = "fsib", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
        public string Fsib { get; set; }
        [XmlElement(ElementName = "utb", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
        public string Utb { get; set; }
        [XmlElement(ElementName = "requirement", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
        public Requirement Requirement { get; set; }
        [XmlElement(ElementName = "uauth", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
        public string Uauth { get; set; }
    }

    [XmlRoot(ElementName = "env", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
    public class Env
    {
        [XmlElement(ElementName = "ai", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
        public Ai Ai { get; set; }
    }

    [XmlRoot(ElementName = "clientInfo", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
    public class ClientInfo
    {
        [XmlElement(ElementName = "funcId", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
        public string FuncId { get; set; }
        [XmlElement(ElementName = "version", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
        public string Version { get; set; }
        [XmlElement(ElementName = "env", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
        public Env Env { get; set; }
    }

    [XmlRoot(ElementName = "bankIdSignedData", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
    public class BankIdSignedData
    {
        [XmlElement(ElementName = "usrVisibleData", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
        public UsrVisibleData UsrVisibleData { get; set; }
        [XmlElement(ElementName = "srvInfo", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
        public SrvInfo SrvInfo { get; set; }
        [XmlElement(ElementName = "clientInfo", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
        public ClientInfo ClientInfo { get; set; }
        [XmlAttribute(AttributeName = "xmlns")]
        public string Xmlns { get; set; }
        [XmlAttribute(AttributeName = "Id")]
        public string Id { get; set; }
    }

    [XmlRoot(ElementName = "Object", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
    public class Object
    {
        [XmlElement(ElementName = "bankIdSignedData", Namespace = "http://www.bankid.com/signature/v1.0.0/types")]
        public BankIdSignedData BankIdSignedData { get; set; }
    }

    [XmlRoot(ElementName = "Signature", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
    public class XmlSignature
    {
        [XmlElement(ElementName = "SignedInfo", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
        public SignedInfo SignedInfo { get; set; }
        [XmlElement(ElementName = "SignatureValue", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
        public string SignatureValue { get; set; }
        [XmlElement(ElementName = "KeyInfo", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
        public KeyInfo KeyInfo { get; set; }
        [XmlElement(ElementName = "Object", Namespace = "http://www.w3.org/2000/09/xmldsig#")]
        public Object Object { get; set; }
        [XmlAttribute(AttributeName = "xmlns")]
        public string Xmlns { get; set; }
    }

} // End of the namespace

BankID Client

using System.Threading.Tasks;
using Annytab.Scripts.Models;

namespace Annytab.Scripts
{
    public interface IBankIdClient
    {
        Task<bool> Authenticate(string personal_id, string ip_address);
        Task<bool> Sign(string personal_id, string ip_address, Annytab.Scripts.Models.Signature signature);
        SignatureValidationResult Validate(string signature_value);

    } // End of the interface

} // End of the namespace
using System;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Xml;
using System.Xml.Serialization;
using System.Threading.Tasks;
using System.Security.Cryptography.Xml;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Logging;
using Annytab.Scripts.Models;

namespace Annytab.Scripts
{
    public class BankIdClient : IBankIdClient
    {
        #region Variables

        private readonly HttpClient client;
        private readonly BankIdOptions options;
        private readonly ILogger logger;

        #endregion

        #region Constructors

        public BankIdClient(HttpClient http_client, IOptions<BankIdOptions> options, ILogger<IBankIdClient> logger)
        {
            // Set values for instance variables
            this.client = http_client;
            this.options = options.Value;
            this.logger = logger;

            // Set values for the client
            this.client.BaseAddress = new Uri(this.options.BaseAddress);
            this.client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            
        } // End of the constructor

        #endregion

        #region Authentication

        public async Task<bool> Authenticate(string personal_id, string ip_address)
        {
            // Variables
            StringContent content = null;
            BankidResponse bankid_response = null;

            try
            {
                // Create a json string
                string json = "{\"personalNumber\":\"" + personal_id + "\", \"endUserIp\":\"" + ip_address + "\"}";

                // Create string content
                content = new StringContent(json);
                content.Headers.ContentType = new MediaTypeWithQualityHeaderValue("application/json");

                // Get the response
                HttpResponseMessage response = await this.client.PostAsync("/rp/v5/auth", content);

                // Check the status code for the response
                if (response.IsSuccessStatusCode == true)
                {
                    // Get string data
                    string data = await response.Content.ReadAsStringAsync();

                    // Convert data to a bankid response
                    bankid_response = JsonSerializer.Deserialize<BankidResponse>(data);

                    // Create json data
                    json = "{\"orderRef\": \"" + bankid_response.orderRef + "\"}";

                    // Add content
                    content = new StringContent(json);
                    content.Headers.ContentType = new MediaTypeWithQualityHeaderValue("application/json");

                    // Collect the signature
                    Int32 timeout = this.options.TimeoutInMilliseconds.GetValueOrDefault();
                    while (true)
                    {
                        // Check for a timeout
                        if (timeout <= 0)
                        {
                            // Cancel the order and return false
                            await this.client.PostAsync("/rp/v5/cancel", content);
                            return false;
                        }

                        // Sleep for 2 seconds
                        await Task.Delay(2000);
                        timeout -= 2000;

                        // Collect a signature
                        response = await this.client.PostAsync("/rp/v5/collect", content);

                        // Check the status code for the response
                        if (response.IsSuccessStatusCode == true)
                        {
                            // Get string data
                            data = await response.Content.ReadAsStringAsync();

                            // Convert data to a bankid response
                            bankid_response = JsonSerializer.Deserialize<BankidResponse>(data);

                            if (bankid_response.status == "pending")
                            {
                                // Continue to loop
                                continue;
                            }
                            else if (bankid_response.status == "failed")
                            {
                                // Return false
                                return false;
                            }
                            else
                            {
                                // Break out from the loop
                                break;
                            }
                        }
                        else
                        {
                            // Get string data
                            data = await response.Content.ReadAsStringAsync();

                            // Log the error
                            this.logger.LogError($"Authenticate: {data}");

                            // Return false
                            return false;
                        }
                    }
                }
                else
                {
                    // Get string data
                    string data = await response.Content.ReadAsStringAsync();

                    // Log the error
                    this.logger.LogError($"Authenticate: {data}");

                    // Return false
                    return false;
                }
            }
            catch (Exception ex)
            {
                // Log the exception
                this.logger.LogInformation(ex, "Authenticate", null);
                return false;
            }
            finally
            {
                if (content != null)
                {
                    content.Dispose();
                }
            }

            // Return success
            return true;

        } // End of the Authenticate method

        #endregion

        #region Signatures

        public async Task<bool> Sign(string personal_id, string ip_address, Annytab.Scripts.Models.Signature signature)
        {
            // Variables
            StringContent content = null;
            BankidResponse bankid_response = null;

            try
            {
                // Create a json string
                string json = "{\"personalNumber\":\"" + personal_id + "\", \"endUserIp\":\"" + ip_address + "\", \"userVisibleData\":\"" + Convert.ToBase64String(Encoding.UTF8.GetBytes(signature.data)) + "\"}";

                // Create string content
                content = new StringContent(json);
                content.Headers.ContentType = new MediaTypeWithQualityHeaderValue("application/json");

                // Get the response
                HttpResponseMessage response = await this.client.PostAsync("/rp/v5/sign", content);

                // Check the status code for the response
                if (response.IsSuccessStatusCode == true)
                {
                    // Get string data
                    string data = await response.Content.ReadAsStringAsync();

                    // Convert data to a bankid response
                    bankid_response = JsonSerializer.Deserialize<BankidResponse>(data);

                    // Create json data
                    json = "{\"orderRef\": \"" + bankid_response.orderRef + "\"}";

                    // Add content
                    content = new StringContent(json);
                    content.Headers.ContentType = new MediaTypeWithQualityHeaderValue("application/json");

                    // Collect the signature
                    Int32 timeout = this.options.TimeoutInMilliseconds.GetValueOrDefault();
                    while (true)
                    {
                        // Check for a timeout
                        if (timeout <= 0)
                        {
                            // Cancel the order and return false
                            await client.PostAsync("/rp/v5/cancel", content);
                            return false;
                        }

                        // Sleep for 2 seconds
                        await Task.Delay(2000);
                        timeout -= 2000;

                        // Collect a signature
                        response = await client.PostAsync("/rp/v5/collect", content);

                        // Check the status code for the response
                        if (response.IsSuccessStatusCode == true)
                        {
                            // Get string data
                            data = await response.Content.ReadAsStringAsync();

                            // Convert data to a bankid response
                            bankid_response = JsonSerializer.Deserialize<BankidResponse>(data);

                            if (bankid_response.status == "pending")
                            {
                                // Continue to loop
                                continue;
                            }
                            else if (bankid_response.status == "failed")
                            {
                                // Return false
                                return false;
                            }
                            else
                            {
                                // Break out from the loop
                                break;
                            }
                        }
                        else
                        {
                            // Get string data
                            data = await response.Content.ReadAsStringAsync();

                            // Log the error
                            this.logger.LogError($"Sign: {data}");

                            // Return false
                            return false;
                        }
                    }
                }
                else
                {
                    // Get string data
                    string data = await response.Content.ReadAsStringAsync();

                    // Log the error
                    this.logger.LogError($"Sign: {data}");

                    // Return false
                    return false;
                }

                // Get the xml signature
                //string xml = Encoding.UTF8.GetString(Convert.FromBase64String(bankid_response.completionData.signature));
                //XmlSerializer serializer = new XmlSerializer(typeof(XmlSignature));
                //XmlSignature xml_signature = null;
                //using (TextReader reader = new StringReader(xml))
                //{
                //    xml_signature = (XmlSignature)serializer.Deserialize(reader);
                //}

                // Update the signature
                signature.algorithm = null;
                signature.padding = null;
                signature.value = bankid_response.completionData.signature;
                signature.certificate = null;
            }
            catch (Exception ex)
            {
                // Log the exception
                this.logger.LogInformation(ex, $"Sign: {signature.value}", null);
                return false;
            }
            finally
            {
                if (content != null)
                {
                    content.Dispose();
                }
            }

            // Return success
            return true;

        } // End of the Sign method

        public SignatureValidationResult Validate(string signature_value)
        {
            // Create the validation result to return
            SignatureValidationResult result = new SignatureValidationResult();

            try
            {
                // Convert from Base64
                string xml = Encoding.UTF8.GetString(Convert.FromBase64String(signature_value));

                // Create an xml document
                XmlDocument doc = new XmlDocument();
                doc.LoadXml(xml);

                // Load the xml signature
                SignedXml signed_xml = new SignedXml();
                signed_xml.LoadXml((XmlElement)doc.GetElementsByTagName("Signature")[0]);

                // Get the xml signature
                XmlSignature xml_signature = null;
                XmlSerializer serializer = new XmlSerializer(typeof(XmlSignature));
                using (TextReader reader = new StringReader(xml))
                {
                    xml_signature = (XmlSignature)serializer.Deserialize(reader);
                }

                // Get the certificate
                result.certificate = new X509Certificate2(Convert.FromBase64String(xml_signature.KeyInfo.X509Data.X509Certificate[0]));

                // Get signature data
                result.signature_data = Encoding.UTF8.GetString(Convert.FromBase64String(xml_signature.Object.BankIdSignedData.UsrVisibleData.Text));

                // Check if the signature is valid
                result.valid = signed_xml.CheckSignature();
            }
            catch (Exception ex)
            {
                string exMessage = ex.Message;
                result.certificate = null;
            }

            // Return the validation result
            return result;

        } // End of the Validate method

        #endregion

    } // End of the class

} // End of the namespace

Configuration

using System;
using System.Net;
using System.Net.Http;
using System.Globalization;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Annytab.Scripts
{
    public class Startup
    {
        public IConfiguration configuration { get; set; }

        public Startup(IConfiguration configuration)
        {
            this.configuration = configuration;

        } // End of the constructor method

        public void ConfigureServices(IServiceCollection services)
        {
            // Add the mvc framework
            services.AddRazorPages();

            // Set limits for form options
            services.Configure<FormOptions>(x =>
            {
                x.BufferBody = false;
                x.KeyLengthLimit = 2048; // 2 KiB
                x.ValueLengthLimit = 4194304; // 32 MiB
                x.ValueCountLimit = 2048;// 1024
                x.MultipartHeadersCountLimit = 32; // 16
                x.MultipartHeadersLengthLimit = 32768; // 16384
                x.MultipartBoundaryLengthLimit = 256; // 128
                x.MultipartBodyLengthLimit = 134217728; // 128 MiB
            });


            // Create api options
            services.Configure<BankIdOptions>(configuration.GetSection("BankIdOptions"));

            // Create clients
            services.AddHttpClient<IBankIdClient, BankIdClient>()
                .ConfigurePrimaryHttpMessageHandler(() =>
                {
                    HttpClientHandler handler = new HttpClientHandler
                    {
                        AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
                        ClientCertificateOptions = ClientCertificateOption.Manual,
                        SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11,
                        ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => { return true; }
                    };
                    handler.ClientCertificates.Add(new X509Certificate2("C:\\DATA\\BankID\\Certificates\\FPTestcert2_20150818_102329.pfx", "qwerty123"));
                    return handler;
                });

        } // End of the ConfigureServices method

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // Use error handling
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseStatusCodePagesWithReExecute("/home/error/{0}");
            }

            // To get client ip address
            app.UseForwardedHeaders(new ForwardedHeadersOptions
            {
                ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
            });

            // Use static files
            app.UseStaticFiles(new StaticFileOptions
            {
                OnPrepareResponse = ctx =>
                {
                    // Cache static files for 30 days
                    ctx.Context.Response.Headers.Add("Cache-Control", "public,max-age=2592000");
                    ctx.Context.Response.Headers.Add("Expires", DateTime.UtcNow.AddDays(30).ToString("R", CultureInfo.InvariantCulture));
                }
            });

            // For most apps, calls to UseAuthentication, UseAuthorization, and UseCors must 
            // appear between the calls to UseRouting and UseEndpoints to be effective.
            app.UseRouting();

            // Use authentication and authorization middlewares
            app.UseAuthentication();
            app.UseAuthorization();

            // Routing endpoints
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    "default",
                    "{controller=home}/{action=index}/{id?}");
            });

        } // End of the Configure method

    } // End of the class

} // End of the namespace

Controller

using System.Threading.Tasks;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Annytab.Scripts.Models;

namespace Annytab.Scripts.Controllers
{
    public class bankidController : Controller
    {
        #region Variables

        private readonly ILogger logger;
        private readonly IBankIdClient bankid_client;

        #endregion

        #region Constructors

        public bankidController(ILogger<bankidController> logger, IBankIdClient bankid_client)
        {
            // Set values for instance variables
            this.logger = logger;
            this.bankid_client = bankid_client;

        } // End of the constructor

        #endregion

        #region Post methods

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> authentication(IFormCollection collection)
        {
            // Get form data
            string personal_id = collection["txtPersonalId"];

            // Authenticate with BankID v5
            bool success = await this.bankid_client.Authenticate(personal_id, ControllerContext.HttpContext.Connection.RemoteIpAddress.ToString());
            if (success == false)
            {
                return Json(data: new ResponseData(false, "", "Was not able to authenticate you with BankID. If you have a BankID app with a valid certificate, try again."));
            }

            // Return a response
            return Json(data: new ResponseData(success, "You were successfully authenticated!", null));

        } // End of the authentication method

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> sign(IFormCollection collection)
        {
            // Create a signature and get SSN
            Annytab.Scripts.Models.Signature signature = new Annytab.Scripts.Models.Signature();
            signature.validation_type = "BankID v5";
            signature.data = collection["txtSignatureData"];
            string personal_id = collection["txtPersonalId"];

            // Sign with bankID v5
            bool success = await this.bankid_client.Sign(personal_id, ControllerContext.HttpContext.Connection.RemoteIpAddress.ToString(), signature);
            if (success == false)
            {
                return Json(data: new ResponseData(false, "", "The file could not be signed with BankID. If you have a BankID app with a valid certificate, try again."));
            }

            // Return a response
            return Json(data: new ResponseData(success, "Signature was successfully created!", signature.value));

        } // End of the sign method

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult validate(IFormCollection collection)
        {
            // Create a signature
            Annytab.Scripts.Models.Signature signature = new Annytab.Scripts.Models.Signature();
            signature.validation_type = "BankID v5";
            signature.data = collection["txtSignatureData"];
            signature.value = collection["txtSignatureValue"];

            // Validate the signature
            SignatureValidationResult result = this.bankid_client.Validate(signature.value);

            // Set a title and a message
            string title = result.valid == false ? "Invalid Signature" : "Valid Signature";
            string message = "<b>" + title + "</b><br />" + result.signature_data + "<br />";
            message += result.certificate != null ? result.certificate.GetNameInfo(X509NameType.SimpleName, false) + ", " + result.certificate.GetNameInfo(X509NameType.SimpleName, true)
                + ", " + result.certificate.NotBefore.ToString("yyyy-MM-dd") + " to "
                + result.certificate.NotAfter.ToString("yyyy-MM-dd") : "";

            // Return a response
            return Json(data: new ResponseData(result.valid, title, message));

        } // End of the validate method

        #endregion

    } // End of the class

} // End of the namespace

HTML and JavaScript

This form has a file upload control that starts the signing process, todays date and the md5-hash of the file is the data that is signed. The person that wants to sign a file must enter his social security number (yyyymmddnnnn). The signature can also be validated.

<!DOCTYPE html>
<html>
<head>
    <title>BankID v5 Signature</title>
    <style>
        .annytab-textarea{width:300px;height:100px;}
        .annytab-textbox {width:300px;}
        .annytab-form-loading-container {display: none;width: 300px;padding: 20px 0px 20px 0px;text-align: center;}
        .annytab-basic-loading-text {margin: 20px 0px 0px 0px;font-size: 16px;line-height: 24px;}
        .annytab-cancel-link {color: #ff0000;cursor: pointer;}
    </style>
</head>
<body style="width:100%;font-family:Arial, Helvetica, sans-serif;">

    <!-- Container -->
    <div style="display:block;padding:10px;">

        <!-- Input form -->
        <form id="inputForm">

            <!-- Hidden data -->
            @Html.AntiForgeryToken()

            <div>Select file to sign <span id="loading"></span></div>
            <input id="fuFile" name="fuFile" type="file" onchange="calculateMd5();" class="annytab-textbox" /><br /><br />

            <div>Signature data</div>
            <textarea id="txtSignatureData" name="txtSignatureData" class="annytab-textarea"></textarea><br /><br />

            <div>Social Security Number (SSN)</div>
            <input name="txtPersonalId" type="text" class="annytab-textbox" placeholder="Social Security Number (SSN)" value="" /><br /><br />

            <div>Signature value</div>
            <textarea id="txtSignatureValue" name="txtSignatureValue" class="annytab-textarea"></textarea><br /><br />

            <div class="annytab-form-loading-container">
                <i class="fas fa-spinner fa-pulse fa-4x fa-fw"></i><div class="annytab-basic-loading-text">Start your BankID app on your computer, smartphone or tablet.</div>
                <div class="annytab-basic-loading-text annytab-cancel-link" onclick="cancelSignature()">Cancel</div>
            </div>

            <input type="button" value="Authenticate" class="btn-disablable" onclick="authenticate()" disabled />
            <input type="button" value="Sign file" class="btn-disablable" onclick="createSignature()" disabled />
            <input type="button" value="Validate signature" class="btn-disablable" onclick="validateSignature()" disabled />

        </form>

    </div>

    <!-- Style and scripts -->
    <link href="/css/annytab.notifier.css" rel="stylesheet" />
    <script src="/js/font-awesome/all.min.js"></script>
    <script src="/js/annytab.effects.js"></script>
    <script src="/js/annytab.notifier.js"></script>
    <script src="/js/crypto/spark-md5.js"></script>
    <script>

        // Set default focus
        document.querySelector('#fuFile').focus();

        // Authenticate
        function authenticate() {
            // Make sure that the request is secure (SSL)
            if (location.protocol !== 'https:') {
                annytab.notifier.show('error', 'You need a secure connection (SSL)!');
                return;
            }

            // Show loading animation
            annytab.effects.fadeIn(document.querySelector('.annytab-form-loading-container'), 500);

            // Disable buttons
            disableButtons();

            // Create form data
            var fd = new FormData(document.querySelector('#inputForm'));

            // Post form data
            postFormData('/bankid/authentication', fd, function (data) {
                if (data.success === true) {
                    annytab.notifier.show('success', data.id);
                    cancelSignature();
                }
                else {
                    annytab.notifier.show('error', data.message);
                    cancelSignature();
                }

            }, function (data) {
                annytab.notifier.show('error', data.message);
                cancelSignature();
            });

        } // End of the authenticate method

        // Create a signature
        function createSignature()
        {
            // Make sure that the request is secure (SSL)
            if (location.protocol !== 'https:') {
                annytab.notifier.show('error', 'You need a secure connection (SSL)!');
                return;
            }

            // Show loading animation
            annytab.effects.fadeIn(document.querySelector('.annytab-form-loading-container'), 500);

            // Disable buttons
            disableButtons();

            // Create form data
            var fd = new FormData(document.querySelector('#inputForm'));

            // Post form data
            postFormData('/bankid/sign', fd, function (data) {
                if (data.success === true) {
                    annytab.notifier.show('success', data.id);
                    document.querySelector('#txtSignatureValue').value = data.message;
                    cancelSignature();
                }
                else
                {
                    annytab.notifier.show('error', data.message);
                    cancelSignature();
                }

            }, function (data) {
                annytab.notifier.show('error', data.message);
                cancelSignature();
            });

        } // End of the createSignature method

        // Cancel a signature
        function cancelSignature()
        {
            // Hide loading container
            annytab.effects.fadeOut(document.querySelector('.annytab-form-loading-container'), 500);

            // Enable buttons
            enableButtons();

        } // End of the cancelSignature method

        // Validate signature
        function validateSignature() {

            // Disable buttons
            disableButtons();

            // Create form data
            var fd = new FormData(document.querySelector('#inputForm'));

            // Post form data
            postFormData('/bankid/validate', fd, function (data) {
                if (data.success === true) {
                    annytab.notifier.show('success', data.message);
                }
                else {
                    annytab.notifier.show('error', data.message);
                }

                // Enable buttons
                enableButtons();

            }, function (data) {
                annytab.notifier.show('error', data.message);

                // Enable buttons
                enableButtons();
            });

        } // End of the validateSignature method

        // Get a hash of a message
        async function getHash(data, algorithm)
        {
            // Hash data
            var hashBuffer = await crypto.subtle.digest(algorithm, new TextEncoder().encode(data));

            // Convert buffer to byte array
            var hashArray = Array.from(new Uint8Array(hashBuffer));

            // Convert bytes to hex string
            var hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');

            // Return hash as hex string
            return hashHex;

        } // End of the getHash method

        // #region MD5

        // Convert Md5 to C# version
        function convertMd5(str) {
            return btoa(String.fromCharCode.apply(null,
                str.replace(/\r|\n/g, "").replace(/([\da-fA-F]{2}) ?/g, "0x$1 ").replace(/ +$/, "").split(" "))
            );

        } // End of the convertMd5 method

        // Calculate a MD5 value of a file
        async function calculateMd5() {

            // Get the controls
            var data = document.querySelector("#txtSignatureData");
            var loading = document.querySelector("#loading");

            // Get the file
            var file = document.querySelector("#fuFile").files[0];

            // Make sure that a file is selected
            if (typeof file === 'undefined' || file === null) {
                return;
            }

            // Add a loading animation
            loading.innerHTML = '- 0 %';

            // Variables
            var block_size = 4 * 1024 * 1024; // 4 MiB
            var offset = 0;

            // Create a spark object
            var spark = new SparkMD5.ArrayBuffer();
            var reader = new FileReader();

            // Create blocks
            while (offset < file.size) {
                // Get the start and end indexes
                var start = offset;
                var end = Math.min(offset + block_size, file.size);

                await loadToMd5(spark, reader, file.slice(start, end));
                loading.innerHTML = '- ' + Math.round((offset / file.size) * 100) + ' %';

                // Modify the offset and increment the index
                offset = end;
            }

            // Get todays date
            var today = new Date();
            var dd = String(today.getDate()).padStart(2, '0');
            var mm = String(today.getMonth() + 1).padStart(2, '0');
            var yyyy = today.getFullYear();

            // Output signature data
            data.value = yyyy + '-' + mm + '-' + dd + ',' + convertMd5(spark.end());
            loading.innerHTML = '- 100 %';

            // Enable buttons
            enableButtons();

        } // End of the calculateMd5 method

        // Load to md5
        async function loadToMd5(spark, reader, chunk) {
            return new Promise((resolve, reject) => {
                reader.readAsArrayBuffer(chunk);
                reader.onload = function (e) {
                    resolve(spark.append(e.target.result));
                };
                reader.onerror = function () {
                    reject(reader.abort());
                };
            });

        } // End of the loadToMd5 method

        // #endregion

        // #region form methods

        // Post form data
        function postFormData(url, fd, successCallback, errorCallback) {

            var xhr = new XMLHttpRequest();
            xhr.open('POST', url, true);
            xhr.onload = function () {
                if (xhr.status === 200) {
                    // Get response
                    var data = JSON.parse(xhr.response);

                    // Check success status
                    if (data.success === true) {
                        // Callback success
                        if (successCallback !== null) { successCallback(data); }
                    }
                    else {
                        // Callback error
                        if (errorCallback !== null) { errorCallback(data); }
                    }
                }
                else {
                    // Callback error information
                    data = { success: false, id: '', message: xhr.status + " - " + xhr.statusText };
                    if (errorCallback !== null) { errorCallback(data); }
                }
            };
            xhr.onerror = function () {
                // Callback error information
                data = { success: false, id: '', message: xhr.status + " - " + xhr.statusText };
                if (errorCallback !== null) { errorCallback(data); }
            };
            xhr.send(fd);

        } // End of the postFormData method

        // Disable buttons
        function disableButtons() {
            var buttons = document.getElementsByClassName('btn-disablable');
            for (var i = 0; i < buttons.length; i++) {
                buttons[i].setAttribute('disabled', true);
            }

        } // End of the disableButtons method

        // Enable buttons
        function enableButtons() {
            var buttons = document.getElementsByClassName('btn-disablable');
            for (var i = 0; i < buttons.length; i++) {
                setTimeout(function (button) { button.removeAttribute('disabled'); }, 1000, buttons[i]);
            }

        } // End of the enableButtons method

        // #endregion

    </script>

</body>
</html>

Leave a Reply

Your email address will not be published. Required fields are marked *