Skip to content

usage with older dotnet versions

Mihkel Kivisild edited this page Oct 7, 2024 · 2 revisions

How to use the validation library with .Net Framework

To use this library with .NET Framework 4.6.1 - 4.8.1, you need to compile the library targeting .NET Standard 2.0 instead of .NET Standard 2.1, as .NET Standard 2.1 removes support for .NET Framework. .NET Standard 2.0 also lacks some features available in .NET Standard 2.1, which requires some refactoring. For example, .NET Standard 2.0 does not support default interface implementation.

For this you will need to download the source code, make the necessary code changes, compile the solution and then import the library as a DLL into your application.

1. Targeting .Net Standard 2.0

Set the target framework in WebEid.Security.csproj file to .Net Standard 2.0. To do this, open the WebEid.Security.csproj file and modify the TargetFramework element value as follows:

<TargetFramework>netstandard2.0</TargetFramework>

2. Changing the implementation of the interfaces

2.1. Change IAuthTokenValidator interface and AuthTokenValidator class implementation

We need to move CURRENT_TOKEN_FORMAT_VERSION to AuthTokenValidator class. In IAuthTokenValidator interface we need to remove the following line (ln32):

        const string CURRENT_TOKEN_FORMAT_VERSION = "web-eid:1";

Then add the constant to the AuthTokenValidator class and change the references:

...
    public sealed class AuthTokenValidator : IAuthTokenValidator
    {
        private readonly ILogger logger;
        private readonly AuthTokenValidationConfiguration configuration;
        private readonly AuthTokenSignatureValidator authTokenSignatureValidator;
        private readonly SubjectCertificateValidatorBatch simpleSubjectCertificateValidators;
        private readonly OcspClient ocspClient;
        private readonly OcspServiceProvider ocspServiceProvider;

        private const int TokenMinLength = 100;
        private const int TokenMaxLength = 10000;

        /// <summary>
        /// The current token format version
        /// </summary>
        public const string CURRENT_TOKEN_FORMAT_VERSION = "web-eid:1";

...
        private async Task<X509Certificate2> ValidateToken(WebEidAuthToken token, string currentChallengeNonce)
        {
            if (token.Format == null || !token.Format.StartsWith(CURRENT_TOKEN_FORMAT_VERSION))
            {
                throw new AuthTokenParseException($"Only token format version '{CURRENT_TOKEN_FORMAT_VERSION}' is currently supported");
            }
            if (string.IsNullOrEmpty(token.UnverifiedCertificate))
            {
                throw new AuthTokenParseException("'unverifiedCertificate' field is missing, null or empty");
            }
...

2.2. Change IChallengeNonceGenerator interface and ChallengeNonceGenerator class implementation

We need to move NonceLength from IChallengeNonceGenerator to ChallengeNonceGenerator class. In IChallengeNonceGenerator interface we need to remove the following lines (ln31-34):

        /// <summary>
        /// Challenge Nonce length in bytes.
        /// </summary>
        const int NonceLength = 32;

Then add the constant to the ChallengeNonceGenerator class and change the references:

...
    public sealed class ChallengeNonceGenerator : IChallengeNonceGenerator
    {
        private readonly IChallengeNonceStore store;
        private readonly RandomNumberGenerator randomNumberGenerator;

        /// <summary>
        /// Challenge Nonce length in bytes.
        /// </summary>
        public const int NonceLength = 32;

...
        public ChallengeNonce GenerateAndStoreNonce(TimeSpan ttl)
        {
            if (ttl.IsNegativeOrZero())
            {
                throw new ArgumentOutOfRangeException(nameof(ttl), "Nonce time-to-live duration must be greater than zero");
            }
            var nonceBytes = new byte[NonceLength];
            this.randomNumberGenerator.GetBytes(nonceBytes);
            var base64StringNonce = Convert.ToBase64String(nonceBytes);
            var expirationTime = DateTimeProvider.UtcNow.Add(ttl);
            var challengeNonce = new ChallengeNonce(base64StringNonce, expirationTime);
            this.store.Put(challengeNonce);
            return challengeNonce;
        }
    }
}

2.3. Change the IChallengeNonceStore interface implementation and create an abstract ChallengeNonceStoreBase class

Since .Net Standard 2.0 does not support default interface implementation, we need to create a new abstract class, that will be referenced by our application instead. Change the IChallengeNonceStore interface as follows:

    /// <summary>
    /// Interface representing a store for storing generated challenge nonces and accessing their generation time.
    /// </summary>
    public interface IChallengeNonceStore
    {
        /// <summary>
        /// Inserts given <see cref="ChallengeNonce"/> object into the store.
        /// </summary>
        /// <param name="challengeNonce">The nonce object to be stored.</param>
        void Put(ChallengeNonce challengeNonce);

        /// <summary>
        /// Internal implementation of GetAndRemove method.
        /// </summary>
        /// <returns>Stored <see cref="ChallengeNonce"/> object.</returns>
        ChallengeNonce GetAndRemoveImpl();

        /// <summary>
        /// Removes and returns the <see cref="ChallengeNonce"/> object being stored.
        /// </summary>
        /// <returns>Stored <see cref="ChallengeNonce"/> object.</returns>
        /// <exception cref="ChallengeNonceNotFoundException">Thrown if the stored is empty.</exception>
        /// <exception cref="ChallengeNonceExpiredException">Thrown if the stored <see cref="ChallengeNonce"/> object has been expired.</exception>
        /// <remarks>
        /// The method checks if there is any <see cref="ChallengeNonce"/> stored, and if so then it also check if it is not expired before returning it.
        /// If the Challenge Nonce has been expired then <see cref="ChallengeNonceExpiredException"/> exception is thrown and it is removed from the store.
        /// If there is no Challenge Nonce stored, either no <see cref="Put(ChallengeNonce)"/> method was not called or it was removed previously
        /// due expiration then ChallengeNonceNotFoundException exception is thrown.
        /// </remarks>
        ChallengeNonce GetAndRemove();
    }

We are removing the GetAndRemove() implementation from the interface and moving it to a new abstract class ChallengeNonceStoreBase:

namespace WebEid.Security.Challenge
{
    using WebEid.Security.Exceptions;
    using WebEid.Security.Util;

    /// <summary>
    /// Base class for challenge nonce store.
    /// </summary>
    public abstract class ChallengeNonceStoreBase : IChallengeNonceStore
    {
        /// <summary>
        /// Internal implementation of <see cref="GetAndRemove"/> method.
        /// </summary>
        /// <returns>Stored <see cref="ChallengeNonce"/> object.</returns>
        public abstract ChallengeNonce GetAndRemoveImpl();

        /// <summary>
        /// Inserts given <see cref="ChallengeNonce"/> object into the store.
        /// </summary>
        /// <param name="challengeNonce">The nonce object to be stored.</param>
        public abstract void Put(ChallengeNonce challengeNonce);

        /// <summary>
        /// Removes and returns the <see cref="ChallengeNonce"/> object being stored.
        /// </summary>
        /// <returns>Stored <see cref="ChallengeNonce"/> object.</returns>
        /// <exception cref="ChallengeNonceNotFoundException">Thrown if the stored is empty.</exception>
        /// <exception cref="ChallengeNonceExpiredException">Thrown if the stored <see cref="ChallengeNonce"/> object has been expired.</exception>
        /// <remarks>
        /// The method checks if there is any <see cref="ChallengeNonce"/> stored, and if so then it also check if it is not expired before returning it.
        /// If the Challenge Nonce has been expired then <see cref="ChallengeNonceExpiredException"/> exception is thrown and it is removed from the store.
        /// If there is no Challenge Nonce stored, either no <see cref="Put(ChallengeNonce)"/> method was not called or it was removed previously
        /// due expiration then ChallengeNonceNotFoundException exception is thrown.
        /// </remarks>
        public ChallengeNonce GetAndRemove()
        {
            var challengeNonce = GetAndRemoveImpl() ?? throw new ChallengeNonceNotFoundException();

            if (DateTimeProvider.UtcNow >= challengeNonce.ExpirationTime)
            {
                throw new ChallengeNonceExpiredException();
            }
            return challengeNonce;
        }
    }
}

In this abstract class we want to make sure, that the GetAndRemove() implementation is not overridable while requiring the GetAndRemoveImpl() and Put(ChallengeNonce challengeNonce) to be overridden.

3. Minor code changes to make the code compilable

In X509CertificateExtensions.cs file we need to modify the line nr 159 as follows:

return valueList.Count == 0 ? null : string.Join(" ", valueList.Cast<object>().Select(i => i.ToString()));

We are change the ' ' to " ", since .Net Standard 2.0 requires the string.Join separator argument to be of string type and not char.

In AuthTokenSignatureValidator.cs file we need to modify the line nr 105 as follows:

var hashAlgorithmName = "SHA" + algorithmName.Substring(algorithmName.Length - 3);

.Net Standard 2.0 does not support the ^ (hat) and .. (range) operators.

4. Changing the InMemoryChallengeNonceStore implementation to ensure that tests are working as intended

In the test project we need the test Challenge Nonce Store InMemoryChallengeNonceStore to use the newly created base class instead of the interface. Change the code as follows:

namespace WebEid.Security.Tests.Nonce
{
    using WebEid.Security.Challenge;

    internal class InMemoryChallengeNonceStore : ChallengeNonceStoreBase
    {
        private ChallengeNonce challengeNonce;

        public override ChallengeNonce GetAndRemoveImpl()
        {
            var result = this.challengeNonce;
            this.challengeNonce = null;
            return result;
        }

        public override void Put(ChallengeNonce challengeNonce) => this.challengeNonce = challengeNonce;
    }
}

2. Changes to the Quickstart guide steps 1 and 2

Instead of adding the Web eID GitLab package source to NuGet Package Manager and installing the library from there as described in the Quickstart guide, we need to compile the modified solution and import the output DLL.

2.1. Compile the WebEid.Security project and copy the output files to your solution

Compile the WebEid.Security project with Release profile. After a successful compilation copy the output files from src\WebEid.Security\bin\Release\netstandard2.0 to your solution. We recommend making a new folder WebEid in your solution folder and paste the output files there. Then import the WebEid.Security.dll from the newly created folder to your solution. You can achieve this in MS Visual Studio by adding a new reference to the project. This can be achieved by first selecting the project you want to add the reference to in Solution Explorer. When the project is selected, you can go to the Project menu from the top bar and click on the Add reference... option. From there you can select Browse and browse to the WebEid.Security.dll we copied before. The DLL will then be visible in the list with a checked checkbox in front of it. Next, you can complete adding the reference by clicking OK.

If you are not using MS Visual Studio, you can achieve the same result by adding the following to your .csproj file under the ItemGroup with references:

<Reference Include="WebEid.Security">
  <HintPath>..\WebEid\WebEid.Security.dll</HintPath>
</Reference>

The HintPath needs to match the location of the WebEid.Security.dll file path.

2.2. Configure the challenge nonce store

The validation library needs a store for saving the issued challenge nonces. As it must be guaranteed that the authentication token is received from the same browser to which the corresponding challenge nonce was issued, using a session-backed challenge nonce store is the most natural choice.

Implement the session-backed challenge nonce store as follows, using the new abstract base class ChallengeNonceStoreBase instead of the IChallengeNonceStore interface:

using System.Web;
using Newtonsoft.Json;
using WebEid.Security.Challenge;

public class SessionBackedChallengeNonceStore : ChallengeNonceStoreBase
{
    private const string ChallengeNonceKey = "challenge-nonce";
    private readonly HttpContext httpContextAccessor;

    public override void Put(ChallengeNonce challengeNonce)
    {
        HttpContext.Current.Session[ChallengeNonceKey] = JsonConvert.SerializeObject(challengeNonce);
    }

    public override ChallengeNonce GetAndRemoveImpl()
    {
        var challenceNonceJson = HttpContext.Current.Session[ChallengeNonceKey] as string;
        if (!string.IsNullOrWhiteSpace(challenceNonceJson))
        {
            HttpContext.Current.Session.Remove(ChallengeNonceKey);
            return JsonConvert.DeserializeObject<ChallengeNonce>(challenceNonceJson);
        }
        return null;
    }
}

3. Follow the Quickstart guide from 3rd step onwards taking into consideration the differences between Asp .Net Core and Asp .Net with .Net Framework