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>