IdentityServer3

IdentityServer3 with the Rapid.Authentication SDK

The RapID QR code authentication section describes how you can add a QR Code logon to your IdentityServer3 solution using our Rapid.Authentication SDK. This reference section includes further details.

Persistent storage

The sample class in step 2 of the RapID QR code authentication notes for IdentityServer3 shows an in-memory two-phase authentication request store. This class derives from FindAuthenticationRequest<T> and implements the interface INewAuthenticationRequest<in T>, and as such has an override for T FindByChallenge(string challenge) and an implementation of void Add(T request). The class also makes use of a System.Collections.Generic.List<TwoPhaseAuthenticationRequest>:

using System.Collections.Generic;

public class TwoPhaseAuthenticationRequestStore 
            : FindAuthenticationRequest<TwoPhaseAuthenticationRequest>
            , INewAuthenticationRequest<TwoPhaseAuthenticationRequest>
{
    protected readonly List<TwoPhaseAuthenticationRequest> Requests = 
                    new List<TwoPhaseAuthenticationRequest>();
    . 
    .
    public void Add(TwoPhaseAuthenticationRequest request)
    {
        Requests.Add(request);
    }
    .
    .
    public override TwoPhaseAuthenticationRequest FindByChallenge(string challenge)
    {
        return Requests.SingleOrDefault(x => x.Challenge == challenge);
    }
}        

For an in-memory solution, this is enough for the IdentityServer3 to be able to create new authentication requests and then find and update them when a user is authenticated since the TwoPhaseAuthenticationRequest which is returned by FindByChallenge is the same instance as is stored in the collection.

For a persistent approach, you will need to implement an update mechanism so that when a request is located on one web app, it can be updated in the shared persistent store, making it available to other web apps. Thus, the two way TLS authentication route handler is able to update an authenticated request and the separate one way TLS identity server which is showing the logon page can see the change. The code samples in this section illustrate how this can be achieved.

Note: The code uses Entity Framework 6 (EF6) to handle the SQL database where we store TwoPhaseAuthenticationRequest objects. There is a lot of EF6 documentation available on the internet should you need further information.

Persisted authentication request

We need a simple POCO to represent a request in the SQL database:

public class PersistedTwoPhaseAuthenticationRequest : TwoPhaseAuthenticationRequest
{
    public int Id { get; set; }
}

The EF6 DbContext will have a DbSet<TEntity> for these objects:

public DbSet<PersistedTwoPhaseAuthenticationRequest> TwoPhaseRequests { get; set; }

Persisted authentication request store

We can implement our request store with the required Add and FindByChallenge methods:

using Rapid.Authentication;
using Rapid.Authentication.Persistence;

public class TwoPhaseAuthenticationDbStore
            : FindAuthenticationRequest<PersistedTwoPhaseAuthenticationRequest>
            , INewAuthenticationRequest<PersistedTwoPhaseAuthenticationRequest>
{
    private readonly IRapidAuthenticationDbContextFactory contextFactory;

    public TwoPhaseAuthenticationDbStore(IRapidAuthenticationDbContextFactory contextFactory)
    {
        this.contextFactory = contextFactory;
    }


    public void Add(PersistedTwoPhaseAuthenticationRequest request)
    {
        using (var dbContext = contextFactory.CreateDbContext())
        {
            dbContext.TwoPhaseRequests.Add(request);
            CommitChanges(dbContext);
        }
    }

    public override PersistedTwoPhaseAuthenticationRequest FindByChallenge(string challenge)
    {
        using (var dbContext = contextFactory.CreateDbContext())
        {
            var request = dbContext.TwoPhaseRequests.SingleOrDefault(x => x.Challenge == challenge);

            ConfigureDbRequest(request);

            return request;
        }
     }

    .
    .
    .
}        

We don't want the persistent store to fill up with old, expired authentication requests, so the CommitChanges method called from Add above gives us an opportunity to remove expired data while the database is being updated:

private void CommitChanges(IRapidAuthenticationDbContext dbContext)
{
    RemoveExpiredData(dbContext);

    dbContext.SaveChanges();
}


private void RemoveExpiredData(IRapidAuthenticationDbContext dbContext)
{
    var expiredRequests = GetExpiredRequests(dbContext);

    dbContext.TwoPhaseRequests.RemoveRange(expiredRequests);
}


private IEnumerable<PersistedTwoPhaseAuthenticationRequest> GetExpiredRequests(
            IRapidAuthenticationDbContext dbContext)
{
    return dbContext.TwoPhaseRequests.Where(r => r.Expires < DateTime.UtcNow);
}        

Also of note is the ConfigureDbRequest method called from FindByChallenge. The RapID library calls FindByChallenge both while setting up the authentication request and also when a user is attempting to authenticate and has POSTed the QR code's challenge to the /rapid/authenticate endpoint.

Since the intent is that we deploy the same IdentityServer3 web app for both the one way TLS logon page and the two way TLS authentication endpoint, ConfigureDbRequest does what is required for both scenarios: for setting up a new request, we add the request to the watch pool; and for the handling of the /rapid/authenticate route, we add an Authenticated event hook to save the anonymous ID.

In an Azure Web App scenario, the former will occur on the web app with one way TLS which presents the identity server logon page and the latter will occur on the web app with two way TLS which handles the /rapid/authenticate POSTs from the end-user's scanner.

The Authenticated event handler SaveAnonymousId modifies the supplied TwoPhaseAuthenticationRequest setting its AnonymousUserId and updating the row in the database.

private void ConfigureDbRequest(PersistedTwoPhaseAuthenticationRequest request)
{
    if (request == null) return;

    authenticationWatcher.WatchFor(request);
    request.Authenticated += (sender, args) => SaveAnonymousId(request, args.Value);
}


public void SaveAnonymousId(PersistedTwoPhaseAuthenticationRequest request, string anonId)
{
    Update(request.Id, x => x.AnonymousUserId = anonId );
}


public void Update(int requestId, Action<TwoPhaseAuthenticationRequest> update)
{
    using (var dbContext = contextFactory.CreateDbContext())
    {
        var stored = dbContext.TwoPhaseRequests.SingleOrDefault(x => x.Id == requestId);

        if (stored == null) return;

        update(stored);
        CommitChanges(dbContext);
    }
}        

Authentication watcher

The authentication watcher is a collection of requests that are awaiting authentication. The persistent store needs to instantiate an authentication watcher in its constructor for access from the ConfigureDbRequest method. In this example, we have made use of a class PersistentAuthenticatedWatcher which is described in Polling the persistent store.

In class TwoPhaseAuthenticationDbStore, declare the authentication watcher:

private readonly PersistentAuthenticatedWatcher authenticationWatcher;

And instantiate it in the constructor:

authenticationWatcher = new PersistentAuthenticatedWatcher(contextFactory);

Custom IdentityServer3 DefaultViewService

In step 4 of the RapID QR code authentication notes for IdentityServer3 for the in-memory example, we show a custom RapidViewService which makes use of a simple TwoPhaseAuthenticationFactory. Since these classes are dealing with in-memory authentication requests, the request object can be modified directly:

authenticationRequest.Authenticated += (sender, args) => challenge.AnonymousUserId = args.Value;

For a persistent solution, these two classes need to update the request in the persistent store so that the changes are available to other web apps.

The custom RapidViewService:

public class RapidViewService : DefaultViewService
{
    private readonly IdentityServerAuthenticationFactory rapid;

    public RapidViewService(
                DefaultViewServiceOptions custom, 
                IViewLoader viewLoader, 
                IdentityServerAuthenticationFactory rapid)
                : base(custom, viewLoader)
    {
        this.rapid = rapid;
    }

    public override Task<Stream> Login(LoginViewModel model, SignInMessage message)
    {
        var authenticationRequest = rapid.StartAuthenticationRequest();
        rapid.SaveTwoPhaseKey(authenticationRequest, message.ReturnUrl);

        model.Custom = new
        {
            Code = authenticationRequest.Challenge,
            RapidUrls =  new LogonUrls($"http://MyIdentityServerAddress/rapid", authenticationRequest),
        };

        return base.Login(model, message);
    }
}

The IdentityServerAuthenticationFactory:

using Rapid.Authentication;

public class IdentityServerAuthenticationFactory : 
        AuthenticationFactory<PersistedTwoPhaseAuthenticationRequest>
{
    private TwoPhaseAuthenticationDbStore Database { get; set; }

    public IdentityServerAuthenticationFactory(
                TwoPhaseAuthenticationDbStore store) 
                : base(store)
    {
        Database = store;
    }


    public IdentityServerAuthenticationFactory(
                RapidContext context, 
                TwoPhaseAuthenticationDbStore store) 
                : base(context, store)
    {
        Database = store;
    }


    public void SaveTwoPhaseKey(PersistedTwoPhaseAuthenticationRequest request, string key)
    {
        Database.SaveTwoPhaseKey(request, key);
    }
}

Note: This makes use a SaveTwoPhaseKey method on the TwoPhaseAuthenticationDbStore class:

public void SaveTwoPhaseKey(PersistedTwoPhaseAuthenticationRequest request, string key)
{
    Update(request.Id, x => x.TwoPhaseAuthenticationKey = key );
}

OWIN Startup

As per step 5 of the RapID QR code authentication notes for IdentityServer3 your identity server's OWIN Startup class Configuration method needs to instantiate this persistent store and register it with the RapID library and identity server.

Polling the persistent store

If you are using a pair of Azure Web Apps to host your identity server, the one way TLS web app that shows the identity server's logon page will need to poll your persistent authentication store to see when the authentication request has its anonymous Id updated, indicating that an end-user scanned the QR code and POSTed to your two way TLS web app's /rapid/authenticate end point.

The following sample shows the PersistentAuthenticatedWatcher class used by the TwoPhaseAuthenticationDbStore in the sample code above (see Authentication watcher).

internal class PersistentAuthenticatedWatcher
{
    private readonly IRapidAuthenticationDbContextFactory contextFactory;

    private readonly SynchronizedCollection<PersistedTwoPhaseAuthenticationRequest> watching =
        new SynchronizedCollection<PersistedTwoPhaseAuthenticationRequest>();


    public PersistentAuthenticatedWatcher(IRapidAuthenticationDbContextFactory contextFactory)
    {
        this.contextFactory = contextFactory;
        PeriodicTask.Run(TriggerNewAuthentications, TimeSpan.FromSeconds(1));
    }


    private void TriggerNewAuthentications()
    {
        using (var dbContext = contextFactory.CreateDbContext())
        {
            var requestIds = watching.Select(x => x.Id);

            var newlyAuthenticated = dbContext.TwoPhaseRequests
                .Where(x => requestIds.Contains(x.Id))
                .Where(x => x.AnonymousUserId != null);

            TriggerEvents(newlyAuthenticated);
        }
    }


    private void TriggerEvents(IEnumerable<PersistedTwoPhaseAuthenticationRequest> authenticated)
    {
        foreach (var request in authenticated)
        {
            var objectsWatchingRequest = watching.Where(x => x.Id == request.Id).ToList();

            foreach (var instance in objectsWatchingRequest)
            {
                watching.Remove(instance);
                instance.AuthenticationSuccess(request.AnonymousUserId);
            }
        }
     }


    public void WatchFor(PersistedTwoPhaseAuthenticationRequest request)
    {
        watching.Add(request);
    }
}

Note: In its constructor it starts a periodic task using the following class:

using System;
using System.Threading;
using System.Threading.Tasks;

public static class PeriodicTask
{
    public static void Run(Action action, TimeSpan interval)
    {
        var never = new CancellationToken();
        Run(action, interval, never);
    }


    public static void Run(Action action, TimeSpan interval, CancellationToken cancel)
    {
        var leaveRunning = RunAsync(action, interval, cancel);
    }


    private static async Task RunAsync(Action action, TimeSpan interval, CancellationToken cancel)
    {
        while (!cancel.IsCancellationRequested)
        {
            action();
            await Task.Delay(interval, cancel);
        }
    }
}

With this periodic task set up, the authentication DB is polled every second to see if there are any newly authenticated requests. If there are, it calls the AuthenticationSuccess method on the TwoPhaseAuthenticationRequest object. Since this is running in the web app that is hosting the identity server logon page, this triggers the authentication of the end-user who scanned the QR code and leads to the authorisation page being displayed.