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>