Skip to content

Freja eID v1.0 Signature, Validation and Authentication in ASP.NET Core

I am creating a service for Freja eID v1.0 signatures, validation of signatures and authentication in this tutorial. Freja eID is a global service for electronic identification (eID) that can be used for e-authentication and e-signatures. Users of Freja eID are using a smartphone application to login and create signatures, every user can control how their eID may be used and view history in a web portal.

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

This service is implemented with HTML, JavaScript and C-sharp in ASP.NET Core. You need to get a client test certificate by sending an email to Freja eID. The client test certificate is used to connect to the REST API. You also need to get JWS certificates to be able to validate signatures.

You need to download Freja eID Mobile Application and start the application in test mode, instructions can be found here. When you have created a test account, you can upgrade your account from BASIC to EXTENDED and then to PLUS from the Test Vetting Portal. I use an application.json file to store settings for my freja client and a FrejaOptions class to access these settings.

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  },
  "AllowedHosts": "*",
  "FrejaOptions": {
    "BaseAddress": "https://services.test.frejaeid.com",
    "JwsCertificate": "MIIEETCCAvmgAwIBAgIUTeCJ0hz3mbtyONBEiap7su74LZwwDQYJKoZIhvcNAQELBQAwgYMxCzAJBgNVBAYTAlNFMRIwEAYDVQQHEwlTdG9ja2hvbG0xFDASBgNVBGETCzU1OTExMC00ODA2MR0wGwYDVQQKExRWZXJpc2VjIEZyZWphIGVJRCBBQjENMAsGA1UECxMEVGVzdDEcMBoGA1UEAxMTUlNBIFRFU1QgSXNzdWluZyBDQTAeFw0xNzA3MTIxNTIwMTNaFw0yMDA3MTIxNTIwMTNaMIGKMQswCQYDVQQGEwJTRTESMBAGA1UEBxMJU3RvY2tob2xtMRQwEgYDVQRhEws1NTkxMTAtNDgwNjEdMBsGA1UEChMUVmVyaXNlYyBGcmVqYSBlSUQgQUIxDTALBgNVBAsTBFRlc3QxIzAhBgNVBAMTGkZyZWphIGVJRCBURVNUIE9yZyBTaWduaW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgMINs87TiouDPSSmpn05kZv9TN8XdopcHnElp6ElJLpQh3oYGIL4B71oIgF3r8zRWq8kQoJlYMugmhsld0r0EsUJbsrcjBJ5CJ1WYZg1Vu8FpYLKoaFRI/qxT6xCMvd238Q99Sdl6G6O9sQQoFq10EaYBa970Tl3nDziQQ6bbSNkZoOYIZoicx4+1XFsrGiru8o8QIyc3g0eSgrd3esbUkuk0eH65SeaaOCrsaCOpJUqEziD+el4R6d40dTz/uxWmNpGKF4BmsNWeQi9b4gDYuFqNYhs7bnahvkK6LvtDThV79395px/oUz5BEDdVwjxPJzgaAuUHE+6A1dMapkjsQIDAQABo3QwcjAOBgNVHQ8BAf8EBAMCBsAwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBRqfIoPnXAOHNpfLaA8Jl+I6BW/nDASBgNVHSAECzAJMAcGBSoDBAUKMB0GA1UdDgQWBBT7j90x8xG2Sg2p7dCiEpsq3mo5PTANBgkqhkiG9w0BAQsFAAOCAQEAaKEIpRJvhXcN3MvP7MIMzzuKh2O8kRVRQAoKCj0K0R9tTUFS5Ang1fEGMxIfLBohOlRhXgKtqJuB33IKzjyA/1IBuRUg2bEyecBf45IohG+vn4fAHWTJcwVChHWcOUH+Uv1g7NX593nugv0fFdPqt0JCnsFx2c/r9oym+VPP7p04BbXzYUk+17qmFBP/yNlltjzfeVnIOk4HauR9i94FrfynuZLuItB6ySCVmOlfA0r1pHv5sofBEirhwceIw1EtFqEDstI+7XZMXgDwSRYFc1pTjrWMaua2UktmJyWZPfIY69pi/z4u+uAnlPuQZnksaGdZiIcAyrt5IXpNCU5wyg==",
    "TimeoutInMilliseconds" : 90000
  }
}
using System;

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

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

        #endregion

        #region Constructors

        public FrejaOptions()
        {
            // Set values for instance variables
            this.BaseAddress = null;
            this.JwsCertificate = 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;
using System.Collections.Generic;

namespace Annytab.Scripts
{
    public class DataToSign
    {
        public string text { get; set; }
        public string binaryData { get; set; }

    } // End of the class

    public class PushNotification
    {
        public string title { get; set; }
        public string text { get; set; }

    } // End of the class

    public class AttributesToReturnItem
    {
        public string attribute { get; set; }

    } // End of the class

    public class FrejaRequest
    {
        public string userInfoType { get; set; }
        public string userInfo { get; set; }
        public string minRegistrationLevel { get; set; }
        public string title { get; set; }
        public PushNotification pushNotification { get; set; }
        public Int64? expiry { get; set; }
        public string dataToSignType { get; set; }
        public DataToSign dataToSign { get; set; }
        public string signatureType { get; set; }
        public IList<AttributesToReturnItem> attributesToReturn { get; set; }

    } // End of the class

    public class BasicUserInfo
    {
        public string name { get; set; }
        public string surname { get; set; }

    } // End of the class

    public class AddressesItem
    {
        public string country { get; set; }
        public string city { get; set; }
        public string postCode { get; set; }
        public string address1 { get; set; }
        public string address2 { get; set; }
        public string address3 { get; set; }
        public string validFrom { get; set; }
        public string type { get; set; }
        public string sourceType { get; set; }

    } // End of the class

    public class Ssn
    {
        public string ssn { get; set; }
        public string country { get; set; }

    } // End of the class

    public class RequestedAttributes
    {
        public BasicUserInfo basicUserInfo { get; set; }
        public string emailAddress { get; set; }
        public string dateOfBirth { get; set; }
        public List<AddressesItem> addresses { get; set; }
        public Ssn ssn { get; set; }
        public string relyingPartyUserId { get; set; }
        public string integratorSpecificUserId { get; set; }
        public string customIdentifier { get; set; }

    } // End of the class

    public class FrejaStatusResponse
    {
        public string authRef { get; set; }
        public string signRef { get; set; }
        public string status { get; set; }
        public string details { get; set; }
        public RequestedAttributes requestedAttributes { get; set; }

    } // End of the class

    public class FrejaResponseHeader
    {
        public string x5t { get; set; }
        public string alg { get; set; }

    } // End of the class

    public class FrejaPayload
    {
        public string authRef { get; set; }
        public string signRef { get; set; }
        public string status { get; set; }
        public string userInfoType { get; set; }
        public string userInfo { get; set; }
        public string minRegistrationLevel { get; set; }
        public RequestedAttributes requestedAttributes { get; set; }
        public string signatureType { get; set; }
        public SignatureData signatureData { get; set; }
        public Int64? timestamp { get; set; }

    } // End of the class

    public class SignatureData
    {
        public string userSignature { get; set; }
        public string certificateStatus { get; set; }

    } // End of the class

} // End of the namespace

Freja Client

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

namespace Annytab.Scripts
{
    public interface IFrejaClient
    {
        Task<bool> Authenticate(string userInfoType, string userInfo);
        Task<bool> Sign(string userInfoType, string userInfo, Annytab.Scripts.Models.Signature signature);
        SignatureValidationResult Validate(Signature signature);

    } // End of the interface

} // End of the namespace
using System;
using System.Text;
using System.Text.Json;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Logging;
using Annytab.Scripts.Models;

namespace Annytab.Scripts
{
    public class FrejaClient : IFrejaClient
    {
        #region Variables

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

        #endregion

        #region Constructors

        public FrejaClient(HttpClient http_client, IOptions<FrejaOptions> options, ILogger<IFrejaClient> 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 userInfoType, string userInfo)
        {
            // Variables
            StringContent content = null;
            FrejaStatusResponse status_response = null;

            try
            {
                // Create a request
                FrejaRequest request = new FrejaRequest
                {
                    userInfoType = userInfoType,
                    userInfo = userInfo,
                    minRegistrationLevel = "PLUS", // BASIC, EXTENDED or PLUS
                    attributesToReturn = new List<AttributesToReturnItem>
                    {
                        new AttributesToReturnItem
                        {
                            attribute = "BASIC_USER_INFO",
                        },
                        new AttributesToReturnItem
                        {
                            attribute = "EMAIL_ADDRESS",
                        },
                        new AttributesToReturnItem
                        {
                            attribute = "DATE_OF_BIRTH",
                        },
                        new AttributesToReturnItem
                        {
                            attribute = "ADDRESSES",
                        },
                        new AttributesToReturnItem
                        {
                            attribute = "SSN",
                        }
                    }
                };

                // Set serializer options
                var json_options = new JsonSerializerOptions
                {
                    IgnoreNullValues = true,
                    WriteIndented = true,
                    Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
                };

                // Convert request to json
                string json = JsonSerializer.Serialize(request, json_options);

                // Create string content
                content = new StringContent("initAuthRequest=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(json)));
                content.Headers.ContentType.MediaType = "application/json";
                content.Headers.ContentType.CharSet = "utf-8";

                // Get the response
                HttpResponseMessage response = await client.PostAsync("/authentication/1.0/initAuthentication", content);

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

                    // Add content
                    content = new StringContent("getOneAuthResultRequest=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(json)));
                    content.Headers.ContentType.MediaType = "application/json";
                    content.Headers.ContentType.CharSet = "utf-8";

                    // Wait for authentication
                    Int32 timeout = this.options.TimeoutInMilliseconds.GetValueOrDefault();
                    while (true)
                    {
                        // Check for a timeout
                        if (timeout <= 0)
                        {
                            // Cancel the order and return false
                            content = new StringContent("cancelAuthRequest=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(json)));
                            content.Headers.ContentType.MediaType = "application/json";
                            content.Headers.ContentType.CharSet = "utf-8";
                            response = await client.PostAsync("/authentication/1.0/cancel", content);
                            return false;
                        }

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

                        // Collect a signature
                        response = await client.PostAsync("/authentication/1.0/getOneResult", 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
                            status_response = JsonSerializer.Deserialize<FrejaStatusResponse>(data);

                            if (status_response.status == "APPROVED")
                            {
                                // Break out from the loop
                                break;

                            }
                            else if (status_response.status == "STARTED" || status_response.status == "DELIVERED_TO_MOBILE"
                                || status_response.status == "OPENED" || status_response.status == "OPENED")
                            {
                                // Continue to loop
                                continue;
                            }
                            else
                            {
                                    // CANCELED, RP_CANCELED, EXPIRED or REJECTED
                                    return false;
                            }
                        }
                        else
                        {
                            // Get string data
                            string data = await response.Content.ReadAsStringAsync();

                            // 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: {status_response.details}", null);

                // Return false
                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 userInfoType, string userInfo, Annytab.Scripts.Models.Signature signature)
        {
            // Variables
            StringContent content = null;
            FrejaStatusResponse status_response = null;

            try
            {
                // Create a request
                FrejaRequest request = new FrejaRequest
                {
                    userInfoType = userInfoType,
                    userInfo = userInfo,
                    minRegistrationLevel = "BASIC", // BASIC, EXTENDED or PLUS
                    title = "Sign File",
                    pushNotification = new PushNotification // Can not include swedish characters å,ä,ö
                    {
                        title = "Hello - Hallå",
                        text = "Please sign this file - Signera denna fil"
                    },
                    expiry = (Int64)DateTime.UtcNow.AddMinutes(5).Subtract(new DateTime(1970, 1, 1)).TotalMilliseconds,
                    //expiry = DateTimeOffset.UtcNow.AddMinutes(5).ToUnixTimeMilliseconds(),
                    dataToSignType = "SIMPLE_UTF8_TEXT",
                    dataToSign = new DataToSign { text = Convert.ToBase64String(Encoding.UTF8.GetBytes(signature.data)) },
                    signatureType = "SIMPLE",
                    attributesToReturn = new List<AttributesToReturnItem>
                    {
                        //new AttributesToReturnItem
                        //{
                        //    attribute = "BASIC_USER_INFO",
                        //},
                        new AttributesToReturnItem
                        {
                            attribute = "EMAIL_ADDRESS",
                        },
                        //new AttributesToReturnItem
                        //{
                        //    attribute = "DATE_OF_BIRTH",
                        //},
                        //new AttributesToReturnItem
                        //{
                        //    attribute = "ADDRESSES",
                        //},
                        //new AttributesToReturnItem
                        //{
                        //    attribute = "SSN",
                        //}
                    }
                };

                // Set serializer options
                var json_options = new JsonSerializerOptions
                {
                    IgnoreNullValues = true,
                    WriteIndented = true,
                    Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
                };

                // Convert request to json
                string json = JsonSerializer.Serialize(request, json_options);

                // Create string content
                content = new StringContent("initSignRequest=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(json)));
                content.Headers.ContentType.MediaType = "application/json";
                content.Headers.ContentType.CharSet = "utf-8";

                // Get the response
                HttpResponseMessage response = await client.PostAsync("/sign/1.0/initSignature", content);

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

                    // Add content
                    content = new StringContent("getOneSignResultRequest=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(json)));
                    content.Headers.ContentType.MediaType = "application/json";
                    content.Headers.ContentType.CharSet = "utf-8";

                    // Collect the signature
                    Int32 timeout = this.options.TimeoutInMilliseconds.GetValueOrDefault();
                    while (true)
                    {
                        // Check for a timeout
                        if (timeout <= 0)
                        {
                            // Cancel the order and return false
                            content = new StringContent("cancelSignRequest=" + Convert.ToBase64String(Encoding.UTF8.GetBytes(json)));
                            content.Headers.ContentType.MediaType = "application/json";
                            content.Headers.ContentType.CharSet = "utf-8";
                            response = await client.PostAsync("/sign/1.0/cancel", content);
                            return false;
                        }

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

                        // Collect a signature
                        response = await client.PostAsync("/sign/1.0/getOneResult", 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
                            status_response = JsonSerializer.Deserialize<FrejaStatusResponse>(data);

                            if (status_response.status == "APPROVED")
                            {
                                // Break out from the loop
                                break;
                                
                            }
                            else if (status_response.status == "STARTED" || status_response.status == "DELIVERED_TO_MOBILE" 
                                || status_response.status == "OPENED" || status_response.status == "OPENED")
                            {
                                // Continue to loop
                                continue;
                            }
                            else
                            {
                                // CANCELED, RP_CANCELED or EXPIRED
                                return false;
                            }
                        }
                        else
                        {
                            // Get string data
                            string data = await response.Content.ReadAsStringAsync();

                            // 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;
                }

                // Update the signature
                signature.algorithm = "SHA-256";
                signature.padding = "Pkcs1";
                signature.value = status_response.details;
                signature.certificate = this.options.JwsCertificate;
            }
            catch (Exception ex)
            {
                // Log the exception
                this.logger.LogInformation(ex, $"Sign: {signature.value}", null);

                // Return false
                return false;
            }
            finally
            {
                if (content != null)
                {
                    content.Dispose();
                }
            }

            // Return success
            return true;

        } // End of the Sign method

        public SignatureValidationResult Validate(Annytab.Scripts.Models.Signature signature)
        {
            // Create the result to return
            SignatureValidationResult result = new SignatureValidationResult();
            result.signature_data = signature.data;

            // Get JWS data (signed by Freja)
            string[] jws = signature.value.Split('.');
            byte[] data = Encoding.UTF8.GetBytes(jws[0] + "." + jws[1]);
            byte[] digest = WebEncoders.Base64UrlDecode(jws[2]);

            // Get payload data
            FrejaPayload payload = JsonSerializer.Deserialize<FrejaPayload>(Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(jws[1])));
            result.signatory = payload.userInfoType + ": " + payload.userInfo;
            string[] user_signature = payload.signatureData.userSignature.Split('.');
            string signed_data = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(user_signature[1]));

            try
            {
                // Get the certificate
                result.certificate = new X509Certificate2(Convert.FromBase64String(signature.certificate));

                // Get the public key
                using (RSA rsa = result.certificate.GetRSAPublicKey())
                {
                    // Check if the signature is valid
                    result.valid = rsa.VerifyData(data, digest, GetHashAlgorithmName(signature.algorithm), GetRSASignaturePadding(signature.padding));
                }
            }
            catch (Exception ex)
            {
                // Log the exception
                this.logger.LogInformation(ex, $"Validate: {signature.value}", null);
            }

            // Make sure that signature data conforms
            if(signature.data != signed_data)
            {
                result.valid = false;
            }

            // Return the validation result
            return result;

        } // End of the Validate method

        #endregion

        #region Helpers

        public static HashAlgorithmName GetHashAlgorithmName(string signature_algorithm)
        {
            if (signature_algorithm == "SHA-256")
            {
                return HashAlgorithmName.SHA256;
            }
            else if (signature_algorithm == "SHA-384")
            {
                return HashAlgorithmName.SHA384;
            }
            else if (signature_algorithm == "SHA-512")
            {
                return HashAlgorithmName.SHA512;
            }
            else
            {
                return HashAlgorithmName.SHA1;
            }

        } // End of the GetHashAlgorithmName method

        public static RSASignaturePadding GetRSASignaturePadding(string signature_padding)
        {
            if (signature_padding == "Pss")
            {
                return RSASignaturePadding.Pss;
            }
            else
            {
                return RSASignaturePadding.Pkcs1;
            }

        } // End of the GetRSASignaturePadding 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
{
    /// <summary>
    /// This class handles application startup
    /// </summary>
    public class Startup
    {
        /// <summary>
        /// Variables
        /// </summary>
        public IConfiguration configuration { get; set; }

        /// <summary>
        /// Create a new startup object
        /// </summary>
        public Startup(IConfiguration configuration)
        {
            this.configuration = configuration;

        } // End of the constructor method

        /// <summary>
        /// Configure services
        /// </summary>
        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<FrejaOptions>(configuration.GetSection("FrejaOptions"));

            // Create clients
            services.AddHttpClient<IFrejaClient, FrejaClient>()
                .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\\home\\Freja\\Certificates\\ANameNotYetTakenAB_1.pfx", "5iTCTp"));
                    return handler;
                });

        } // End of the ConfigureServices method

        /// <summary>
        /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        /// </summary>
        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;
using System.Text;
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 frejaController : Controller
    {
        #region Variables

        private readonly ILogger logger;
        private readonly IFrejaClient freja_client;

        #endregion

        #region Constructors

        public frejaController(ILogger<frejaController> logger, IFrejaClient freja_client)
        {
            // Set values for instance variables
            this.logger = logger;
            this.freja_client = freja_client;

        } // End of the constructor

        #endregion

        #region Post methods

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

            // Check if email is personal id
            if(userInfoType == "SSN")
            {
                userInfo = Convert.ToBase64String(Encoding.UTF8.GetBytes("{\"ssn\":" + Convert.ToInt64(userInfo) + ", \"country\":\"" + "SE" + "\"}"));
            }

            // Authenticate with freja eID v1.0
            bool success = await this.freja_client.Authenticate(userInfoType, userInfo);
            if (success == false)
            {
                return Json(data: new ResponseData(false, "", "Was not able to authenticate you with Freja eID. If you have a Freja eID 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
            Annytab.Scripts.Models.Signature signature = new Annytab.Scripts.Models.Signature();
            signature.validation_type = "Freja eID v1.0";

            // Get form data
            signature.data = collection["txtSignatureData"];
            string userInfoType = collection["userInfoType"];
            string userInfo = collection["txtUserInfo"];

            // Check if email is personal id
            if (userInfoType == "SSN")
            {
                userInfo = Convert.ToBase64String(Encoding.UTF8.GetBytes("{\"ssn\":" + Convert.ToInt64(userInfo) + ", \"country\":\"" + "SE" + "\"}"));
            }

            // Sign with freja eID
            bool success = await this.freja_client.Sign(userInfoType, userInfo, signature);
            if (success == false)
            {
                return Json(data: new ResponseData(false, "", "The file could not be signed with Freja eID. If you have a Freja eID app with a valid certificate, try again."));
            }

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

        } // 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 = "Freja eID v1.0";
            signature.algorithm = "SHA-256";
            signature.padding = "Pkcs1";
            signature.data = collection["txtSignatureData"];
            signature.value = collection["txtSignatureValue"];
            signature.certificate = collection["txtSignatureCertificate"];

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

            // Set a title and a message
            string title = result.valid == false ? "Invalid Signature" : "Valid Signature";
            string message = "<b>" + title + "</b><br />" + signature.data + "<br />" + result.signatory + "<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 authenticate or sign a file must enter his email, phone number or social security number (SSN). The signature can also be validated.

<!DOCTYPE html>
<html>
<head>
    <title>Freja eID v1.0 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>User information type</div>
            <input type="radio" name="userInfoType" value="EMAIL" checked>Email
            <input type="radio" name="userInfoType" value="PHONE">Phone
            <input type="radio" name="userInfoType" value="SSN">SSN<br /><br />

            <div>User information</div>
            <input name="txtUserInfo" type="text" class="annytab-textbox" placeholder="Email, SSN or Phone" value="" /><br /><br />

            <div>Certificate</div>
            <textarea id="txtSignatureCertificate" name="txtSignatureCertificate" class="annytab-textarea"></textarea><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 Freja eID app on your 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('/freja/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('/freja/sign', fd, function (data) {
                if (data.success === true) {
                    annytab.notifier.show('success', data.id);
                    document.querySelector('#txtSignatureValue').value = data.message;
                    document.querySelector('#txtSignatureCertificate').value = data.url;
                    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('/freja/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 *