PixelPin Developer Documentation

.NET Framework

Updated July 14th 2020

Tested with .NET Framework Version: 4.8

Add PixelPin for a simple and secure way to access your .NET Framework site

  1. Create a New .NET Framework Web Application Project
  2. Modify the Web Application Project
  3. Testing the integration

Create a New .NET Framework Web Application Project

Screenshots from Visual Studio 2019

  1. From Visual Studio, create a new "ASP.NET Web Application (.NET Framework)". Press Next.

  1. Give the project a name and be sure to select the appropriate .NET Framework version (this guide was tested with .NET Framework 4.8). Press Create.
  2. Select MVC (PixelPin should work for any template, but this guide will use MVC), and be sure to change the Authentication method to "Individual User Accounts". Press Create.

Modify the Web Application Project

Code flow isn't properly supported by the Microsoft.Owin.Security.OpenIdConnect library, however it's relatively trivial to roll our own implementation with a little help from the IdentityModel library. We DO NOT recommend using Implict Flow due to its security shortcomings.

  1. First, you'll need to create a PixelPin developer account to obtain your Client ID and secret. A guide on how to do this can be found following this link. For this tutorial, your Redirect URI will be http(s)://{BASE_URL}/signin-pixelpin, for example http://localhost:54654/signin-pixelpin.
    • While creating the PixelPin developer account, make sure you set the OpenID flow to Authorisation Code.
  2. Install the following NuGet packages:
    • Microsoft.Owin.Security.OpenIdConnect - Tested with 4.1.0
    • IdentityModel - Tested with 4.3.1
  3. Add a new file to the project "/Extensions/AppBuilderExtensions.cs" and add the following code:
    using System.Collections.Generic;
    using System.Net.Http;
    using System.Security.Claims;
    using IdentityModel.Client;
    using Microsoft.IdentityModel.Protocols.OpenIdConnect;
    using Microsoft.Owin.Security.OpenIdConnect;
    using Net4Test.Configuration;
    using Owin;
    
    namespace Net4Test.Extensions
    {
        /// <summary>
        /// Extension methods for the <see cref="IAppBuilder"/> type
        /// </summary>
        public static class AppBuilderExtensions
        {
            /// <summary>
            /// Adds the <see cref="T:Microsoft.Owin.Security.OpenIdConnect.OpenIdConnectAuthenticationMiddleware" /> into
            /// the OWIN runtime, pre-configured to support PixelPin's IdP
            /// </summary>
            /// <param name="app">The <see cref="T:Owin.IAppBuilder" /> passed to the configuration method</param>
            /// <param name="config">The PixelPin auth configuration</param>
            /// <returns>The updated <see cref="T:Owin.IAppBuilder" /></returns>
            public static IAppBuilder UsePixelPinAuthentication(this IAppBuilder app, PixelPinConfiguration config)
            {
                // Validate the config and throw if invalid
                config.ThrowIfInvalid();
    
                // Set up a discovery client in order to automatically determine certain endpoints. We need to add our
                // front-end URI here too to allow us to redirect users to there for authorise requests, whilst still
                // requiring backchannel requests direct to the API.
                var discoveryPolicy = new DiscoveryPolicy();
                discoveryPolicy.AdditionalEndpointBaseAddresses.Add(config.FrontEndUrl);
    
                var discoveryCache = new DiscoveryCache(config.Authority, discoveryPolicy);
                if (config.DiscoveryCacheDuration.HasValue)
                {
                    discoveryCache.CacheDuration = config.DiscoveryCacheDuration.Value;
                }
    
                return app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
                {
                    ClientId = config.ClientId,
                    ClientSecret = config.ClientSecret,
                    Authority = config.Authority,
                    RedirectUri = config.RedirectUri,
                    ResponseType = OpenIdConnectResponseType.CodeIdToken,
                    Scope = "openid profile email",
                    UseTokenLifetime = false, // Read more about this flag at the end of the article
                    Notifications = new OpenIdConnectAuthenticationNotifications
                    {
                        AuthorizationCodeReceived = async n =>
                        {
                            // Check discovery for /token and /userInfo endpoints as ours are not standard.
                            // Ideally we'd use discovery /authorize too but as we use the standard endpoint
                            // for this, it's extra complexity without much gain in this case.
                            var discoveryResponse = await discoveryCache.GetAsync();
                            if (discoveryResponse.IsError)
                            {
                                config.DiscoveryErrorHandler?.Invoke(n, discoveryResponse);
                                return;
                            }
    
                            var httpClient = new HttpClient();
    
                            // Exchange code for access and ID tokens
                            var tokenResponse = await httpClient.RequestAuthorizationCodeTokenAsync(
                                new AuthorizationCodeTokenRequest
                                {
                                    Address = discoveryResponse.TokenEndpoint,
                                    ClientId = config.ClientId,
                                    ClientSecret = config.ClientSecret,
                                    RedirectUri = config.RedirectUri,
                                    Code = n.Code
                                });
                            if (tokenResponse.IsError)
                            {
                                config.TokenErrorHandler?.Invoke(n, tokenResponse);
                                return;
                            }
    
                            var claims = new List<Claim>
                            {
                                new Claim("id_token", tokenResponse.IdentityToken),
                                new Claim("access_token", tokenResponse.AccessToken)
                            };
    
                            // Use access token to require user profile
                            var userInfoResponse = await httpClient.GetUserInfoAsync(new UserInfoRequest
                            {
                                Address = discoveryResponse.UserInfoEndpoint,
                                Token = tokenResponse.AccessToken
                            });
                            if (userInfoResponse.IsError)
                            {
                                config.UserInfoErrorHandler?.Invoke(n, userInfoResponse);
                                return;
                            }
                            claims.AddRange(userInfoResponse.Claims);
    
                            n.AuthenticationTicket.Identity.AddClaims(claims);
                        }
                    }
                });
            }
        }
    }
    
  4. Add a new file to the project "/Configuration/PixelPinConfiguration.cs" and add the following code:
    using System;
    using IdentityModel.Client;
    using Microsoft.Owin.Security.Notifications;
    
    namespace Net4Test.Configuration
    {
        /// <summary>
        /// Configuration settings for PixelPin OpenIdConnect authentication
        /// </summary>
        public class PixelPinConfiguration
        {
            /// <summary>
            /// The Authority to use when making OpenIdConnect calls. This should be the PixelPin API URL.
            /// Defaults to sandbox.
            /// </summary>
            /// <remarks>
            /// Sandbox: https://api.sandbox.pixelpin.net
            /// Production: https://api.login.pixelpin.io
            /// </remarks>
            public string Authority { get; set; } = "https://api.sandbox.pixelpin.net";
    
            /// <summary>
            /// The host the user is redirected to for authentication. This should be the PixelPin front-end URL
            /// Defaults to sandbox.
            /// </summary>
            /// <remarks>
            /// Sandbox: https://sandbox.pixelpin.net
            /// Production: https://login.pixelpin.io
            /// </remarks>
            public string FrontEndUrl { get; set; } = "https://sandbox.pixelpin.net";
    
            /// <summary>
            /// The OpenIdConnect 'client_id'
            /// </summary>
            public string ClientId { get; set; }
    
            /// <summary>
            /// The OpenIdConnect 'client_secret'
            /// </summary>
            public string ClientSecret { get; set; }
    
            /// <summary>
            /// The OpenIdConnect 'redirect_uri'
            /// </summary>
            public string RedirectUri { get; set; }
    
            /// <summary>
            /// Frequency to refresh discovery document. Leave null to default to the underlying default of
            /// <see cref="DiscoveryCache.CacheDuration"/>
            /// </summary>
            public TimeSpan? DiscoveryCacheDuration { get; set; }
    
            /// <summary>
            /// Handler for if discovery of /token and /userInfo endpoints fails.
            /// Could potentially fall back to hard-coded endpoints if discovery fails, making this non-fatal.
            /// By default an exception is thrown.
            /// </summary>
            public Action<AuthorizationCodeReceivedNotification, DiscoveryDocumentResponse> DiscoveryErrorHandler { get; set; } =
                (n, r) => throw new Exception(r.Error);
    
            /// <summary>
            /// Handler for if the call to /token fails. This is usually fatal as it means authentication has failed.
            /// By default an exception is thrown.
            /// </summary>
            public Action<AuthorizationCodeReceivedNotification, TokenResponse> TokenErrorHandler { get; set; } =
                (n, r) => throw new Exception(r.Error);
    
            /// <summary>
            /// Handler for if the call to /userInfo fails. This may not be fatal as authentication is already complete
            /// from /token, this is just extra profile information.
            /// By default an exception is thrown.
            /// </summary>
            public Action<AuthorizationCodeReceivedNotification, UserInfoResponse> UserInfoErrorHandler { get; set; } =
                (n, r) => throw new Exception(r.Error);
    
            /// <summary>
            /// Validates the provided config, throwing if invalid
            /// </summary>
            public void ThrowIfInvalid()
            {
                if (!Uri.IsWellFormedUriString(Authority, UriKind.Absolute))
                {
                    throw new ArgumentOutOfRangeException(
                        nameof(Authority),
                        $"{nameof(Authority)} is not a well formed absolute URI");
                }
    
                if (!Uri.IsWellFormedUriString(FrontEndUrl, UriKind.Absolute))
                {
                    throw new ArgumentOutOfRangeException(
                        nameof(FrontEndUrl),
                        $"{nameof(FrontEndUrl)} is not a well formed absolute URI");
                }
    
                if (string.IsNullOrWhiteSpace(ClientId))
                {
                    throw new ArgumentNullException(nameof(ClientId), $"{nameof(ClientId)} is missing");
                }
    
                if (string.IsNullOrWhiteSpace(ClientSecret))
                {
                    throw new ArgumentNullException(nameof(ClientId), $"{nameof(ClientId)} is missing");
                }
    
                if (!Uri.IsWellFormedUriString(RedirectUri, UriKind.Absolute))
                {
                    throw new ArgumentOutOfRangeException(
                        nameof(RedirectUri),
                        $"{nameof(RedirectUri)} is not a well formed absolute URI");
                }
            }
        }
    }
    
  5. Open up App_Start/Startup.Auth.cs and:
    • Add a using declaration for your new extensions class, e.g.
      using Net4Test.Extensions;
    • At the bottom of the ConfigureAuth method add a call to
      var config = new PixelPinConfiguration
      {
          // Authority = Defaults to sandbox. See docs
          // FrontEndUrl = Defaults to sandbox. See docs
          ClientId = "{CLIENT ID}",
          ClientSecret = "{CLIENT_SECRET}",
          RedirectUri = "https://localhost:44338/signin-pixelpin"
      };
      
      app.UsePixelPinAuthentication(config);
      
      The path at the end of the RedirectUri should NOT be a current endpoint in your app, the system will attach a handler to this endpoint for continuing authentication manually and so it should be a path that is not already in use.

      In a full application these values would not be hardcoded and would instead be retrieved from configuration, however this is out of the scope of this tutorial.
  6. Open up Controllers/HomeController.cs and:
    • Add the following using statement:
      using System.Security.Claims;
    • Modify the About method to look like the following:
      public ActionResult About()
      {
          return View((User as ClaimsPrincipal).Claims);
      }
      
  7. Open up Views/Home/About.cshtml and add the following code to the html. This will display the claims sent from PixelPin:
     @model IEnumerable<System.Security.Claims.Claim>
    
     <dl>
         @foreach (var claim in Model)
         {
             <dt>@claim.Type</dt>
             <dd>@claim.Value</dd>
         }
     </dl>
    

Testing the integration

  1. Run the Web Application up and head to the log in page by selecting the Log In link in the Nav Bar.
  2. Select the "OpenIdConnect" Button.

  1. You should be taken to PixelPin to complete your authentication
  2. On return, select About and you should see details of the user you authenticated as

Follow this link to see how to use our official PixelPin styling in your web application following.

A note on token lifetimes

Normally when authenticating with an OIDC provider, you are requesting access to one or more resources owned by your user. This is why the provider will often hand you a long-lived access token (and possibly a refresh token for renewing the access token).

In our case however we provide very short lived tokens that are intended to last just long enough to provide your application access to the user's profile for the purpose of authentication only. It is not an intended use case for an application to store the access token for long periods. Because of this, our tokens last 30 seconds. This should be long enough for your application to make a call to the /userinfo endpoint immediately after /token and gather the requested information.

In the case of the .NET Framework integration however there is an extra complication. .NET will create an authentication ticket for the external authentication and store this in a cookie. This allows the application to present the user with a form to complete before finalising their registration and linking the external auth details to that account.

This is fine, however by default the cookie will inherit its lifetime from the lifetime of the token we return (in this case 30 seconds). This may well be long enough for the user to complete their form but it's far from ideal. This is where the UseTokenLifetime flag that we set in the OpenIdConnectAuthenticationOptions comes in. If we set this to false, .NET will instead use the default lifetime configured in the external cookie configuration. By default this is 5 minutes which is much better!

For some reason there is no simple way to override this lifetime, but it is possible by looking into the implementation of the UseExternalSignInCookie extension method, in here we can see the default lifetime being set. We can therefore duplicate this method into our own code and pass in a different lifetime. Below is an extension method that can be used to allow configuration of the external auth cookie (including, but not limited to, the lifetime):

/// <summary>
/// Configure the app to use owin middleware based cookie authentication for external identities
/// </summary>
/// <param name="app">App builder passed to the application startup code</param>
/// <param name="externalAuthenticationType">
/// AuthenticationType that external middleware should sign in as
/// </param>
/// <param name="configureOptions">An action for configuring the cookie options</param>
public static void UseExternalSignInCookie(
    this IAppBuilder app,
    string externalAuthenticationType,
    Action<CookieAuthenticationOptions> configureOptions = null)
{
    if (app == null)
    {
        throw new ArgumentNullException(nameof(app));
    }

    var options = new CookieAuthenticationOptions
    {
        AuthenticationType = externalAuthenticationType,
        AuthenticationMode = AuthenticationMode.Passive,
        CookieName = ".AspNet." + externalAuthenticationType,
        // Here is the default lifetime, but we can override this with the configureOptions parameter
        ExpireTimeSpan = TimeSpan.FromMinutes(5.0)
    };

    configureOptions?.Invoke(options);

    app.SetDefaultSignInAsAuthenticationType(externalAuthenticationType);
    app.UseCookieAuthentication(options);
}