Thursday, 21 March 2013

Extending ThinkTecture Identity Server to a 3rd Party Identity Source

This is the 3rd post in a 3 part series on custom claims identity management in the enterprise.  Firstly, I would like to apologise for those who have been waiting for this post.

ThinkTecture is a custom identity provider which is a great choice to enable claims authentication from existing applications.  This post will show you how to extend IdentityServer to your own identity store.

In order to follow this blog you will need to download the identity server source-code which can be found on GitHub https://github.com/thinktecture/Thinktecture.IdentityServer.v2

Once downloaded, the easiest way to extend ThinkTecture is to open the solution in Visual Studio, this will give you full debug while you make the modifications.

image

ThinkTecture is a well-organised and easy to follow solution making good use of SOLID principles.  This makes the solution very easy to read, extend and adapt.

One of the common requirements for SSO extensibility is providing custom claims and access control mechanisms.  There are many ways to achieve this, but I would recommend utilising the existing core libraries without a pass-through mechanism.  This enables you to continue to use the membership provided with ThinkTecture, but provide additional access control from your own solution.

From the above Image you will see I have created a new assembly (Gstt.Icm.Repository).  This will be my extension to identity-server.

Identity server makes use of 2 primary interfaces for user and claim management, IUserRepository and IClaimsRepository.  These types can be found in the ThinkTecture.IdentityServer.Core assembly.  As we are also extending the existing User and Claims Repositories you will need the following assembly references.

image

With these references you are now ready to setup the environment to implement this tutorial.  To execute this tutorial we will need a sample database, which will represent our 3rd party system.  For this sample I am using SQL Server 2012 and am using the sample data as below.

Database Code
CREATE DATABASE Users
GO

INSERT INTO dbo.[User] (Username, Password)
VALUES
    ('Gary',  HASHBYTES('SHA2_256', N'Test')),
    ('Dominic',  HASHBYTES('SHA2_256', N'Test2'))

INSERT INTO Roles (RoleName)
VALUES ('Admin'), ('Limited'), ('Profit and Loss')

INSERT INTO UserRoles (UserId, RoleId)
SELECT u.Id, r.Id FROM [User] u, Roles r WHERE u.Username = 'Gary' AND r.RoleName = 'Admin'
UNION
SELECT u.Id, r.Id FROM [User] u, Roles r WHERE u.Username = 'Dominic' AND r.RoleName = 'Limited'

Once the database has been configured you should see the SQL schema below:

image

With the above complete, we can now create our data access components. To do this I have created a Domain folder in the class library and generated a new Entity Framework edmx model.  This database is a representation of the schema above created in .NET EF 5.0 Database First.

image With the schema generated we will need to include the connection string generated by EF.  Identity Server stores all configuration in the configuration folder under the web application.  Open the connectionString.config settings and add the new connectionString to the SQL database as created above.  Please remember that as a web-application the identity of the application pool should have the required rights to the SQL database, alternatively configure SQL authentication in the configuration file and encrypt the config.image

Once complete you should have a new entry in the connectionString.config.

Moving on, you will need to create 2 new class files in your extension project.  These classes will derive from IUserRepository and IClaimsRepository as above.  To make life easier, copy the existing code from the Thinktecture core repositories below to these new files.  Rename the files relative to your identity store.

image This will leave you with a project that has the skeleton from Identity Server.  What we need now is an additional type to our new library which we will use as a repository to talk to *our* SQL database.  Below is a sample of the interface and implementation for the new database module.

IIcmUserRepository
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6.  
  7. namespace Gstt.Icm.Repository.Interfaces
  8. {
  9.  
  10.     public interface IIcmUserRepository
  11.     {
  12.  
  13.          IEnumerable<string> GetRoles();
  14.  
  15.          bool ValidateUser(string username, string password);
  16.  
  17.          IEnumerable<string> GetUserRoles(string username);
  18.  
  19.     }
  20. }
IcmUserRepository
  1. using Gstt.Icm.Repository.Interfaces;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Security.Cryptography;
  7. using System.Text;
  8. using System.Threading.Tasks;
  9. using System.Web.Security;
  10.  
  11. namespace Gstt.Icm.Repository
  12. {
  13.     public class IcmUserRepository : IIcmUserRepository
  14.     {
  15.         public IEnumerable<string> GetRoles()
  16.         {
  17.             using (var context = new Domain.UsersEntities())
  18.             {
  19.                 return context.Roles.Select(p => p.RoleName).ToList();
  20.             }
  21.         }
  22.  
  23.         public bool ValidateUser(string username, string password)
  24.         {
  25.             using (var context = new Domain.UsersEntities())
  26.             {
  27.                 var user = context.Users.FirstOrDefault(p => p.Username == username);
  28.                 if (user == null)
  29.                 {
  30.                     return false;
  31.                 }
  32.  
  33.                 var sha256 = SHA256.Create();
  34.                 
  35.                 using (var ms = new MemoryStream(System.Text.Encoding.Unicode.GetBytes(password)))
  36.                 {
  37.                     var bytes = sha256.ComputeHash(ms);
  38.                     if (user.Password.SequenceEqual(bytes))
  39.                     {
  40.                         return true;
  41.                     }
  42.                 }
  43.             }
  44.  
  45.             return false;
  46.         }
  47.  
  48.         public IEnumerable<string> GetUserRoles(string username)
  49.         {
  50.             using (var context = new Domain.UsersEntities())
  51.             {
  52.                 return (from p in context.Users
  53.                         join r in context.UserRoles on p.Id equals r.UserId
  54.                         join x in context.Roles on r.RoleId equals x.Id
  55.                         where p.Username == username
  56.                         select x.RoleName).ToList();
  57.             }
  58.         }
  59.     }
  60. }

With the new types in the project you should have a class library project which looks something like below.

image

The eager eyes out there will notice that I also have 3 additional types in my project, GsttConfigurationSection and IcmExportProvider.  IcmClaimsRepository and IcmRepository are my custom implementations of IUserRepository and IClaimsRepository.

ThinkTecture makes use of the Microsoft Extensibility Framework (MEF) to enable runtime initialisation of components and dependency injection.  This provides the software with the ability to swap out components at Runtime (such as the repositories we are building).  To keep our new assembly in alignment with the conventions provided in ThinkTecture we will make use of MEF, this means creating our own configuration file and MEF Export Provider. 

Below is a sample of the Config section and IcmExportProvider.  As you can see from the code below, we are creating a new config section called gstt.repositories with an attribute of icmUserManagement.  This will provide MEF with the catalogue type entries for runtime type configuration.

Config Section
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6.  
  7. namespace Gstt.Icm.Repository.Config
  8. {
  9.     /// <summary>
  10.     /// The IcmConfigurationSection Configuration Section.
  11.     /// </summary>
  12.     public partial class GsttConfigurationSection : global::System.Configuration.ConfigurationSection
  13.     {
  14.         public const string SectionName = "gstt.repositories";
  15.  
  16.         /// <summary>
  17.         /// The XML name of the <see cref="ConfigurationProvider"/> property.
  18.         /// </summary>
  19.         internal const global::System.String IcmUserRepositoryName = "icmUserManagement";
  20.  
  21.         /// <summary>
  22.         /// Gets or sets type of the class that provides custom user validation
  23.         /// </summary>
  24.         [global::System.Configuration.ConfigurationProperty(IcmUserRepositoryName, IsRequired = false, IsKey = false, IsDefaultCollection = false, DefaultValue = "Gstt.Icm.Repository.IcmUserRepository, Gstt.Icm.Repository")]
  25.         public global::System.String IcmUserRepository
  26.         {
  27.             get
  28.             {
  29.                 return (global::System.String)base[IcmUserRepositoryName];
  30.             }
  31.             set
  32.             {
  33.                 base[IcmUserRepositoryName] = value;
  34.             }
  35.         }
  36.     }
  37. }

Out of the box ThinkTecture comes with a RepositoryExportProvider type providing the catalogue services for Core components.  To keep consistent with ThinkTecture, we create the following MEF export catalogue.  This is a direct copy of the existing code but with my custom type registered, this enables you to swap out my component and hook into your own identity store if you wish to use my extension approach.

MEF Export Provider
  1. using Gstt.Icm.Repository.Config;
  2. using Gstt.Icm.Repository.Interfaces;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.ComponentModel.Composition.Hosting;
  6. using System.ComponentModel.Composition.Primitives;
  7. using System.Configuration;
  8. using System.Linq;
  9. using System.Text;
  10. using System.Threading.Tasks;
  11. using Thinktecture.IdentityServer.Configuration;
  12.  
  13. namespace Gstt.Icm.Repository
  14. {
  15.     public class IcmExportProvider : ExportProvider
  16.     {
  17.         private Dictionary<string, string> _mappings;
  18.  
  19.         public IcmExportProvider()
  20.         {
  21.             var section = ConfigurationManager.GetSection(GsttConfigurationSection.SectionName) as GsttConfigurationSection;
  22.  
  23.             _mappings = new Dictionary<string, string>
  24.             {
  25.                 { typeof(IIcmUserRepository).FullName, section.IcmUserRepository }
  26.             };
  27.         }
  28.  
  29.         protected override IEnumerable<Export> GetExportsCore(ImportDefinition definition, AtomicComposition atomicComposition)
  30.         {
  31.             var exports = new List<Export>();
  32.  
  33.             string implementingType;
  34.             if (_mappings.TryGetValue(definition.ContractName, out implementingType))
  35.             {
  36.                 var t = Type.GetType(implementingType);
  37.                 if (t == null)
  38.                 {
  39.                     throw new InvalidOperationException("Type not found for interface: " + definition.ContractName);
  40.                 }
  41.  
  42.                 var instance = t.GetConstructor(Type.EmptyTypes).Invoke(null);
  43.                 var exportDefintion = new ExportDefinition(definition.ContractName, new Dictionary<string, object>());
  44.                 var toAdd = new Export(exportDefintion, () => instance);
  45.  
  46.                 exports.Add(toAdd);
  47.             }
  48.  
  49.             return exports;
  50.         }
  51.     }
  52. }

As you can see from the above the ExportProvider is used to provide a dynamic type registration from our new config section.

With the config section complete and the MEF export catalogue configured we can now modify identity server to include the additional files and configuration.  To start this process we need to modify the Global.ascx.cs file and register the new MEF Export Provider.  Inside the global ascx file, we need to register the new Export Provider, this is achieved via the CompositionContainer constructor (SetupCompositionContainer method):

Global.ascx.cs
  1. public class MvcApplication : System.Web.HttpApplication
  2. {
  3.     [Import]
  4.     public IConfigurationRepository ConfigurationRepository { get; set; }
  5.  
  6.     [Import]
  7.     public IUserRepository UserRepository { get; set; }
  8.  
  9.     [Import]
  10.     public IRelyingPartyRepository RelyingPartyRepository { get; set; }
  11.  
  12.  
  13.     protected void Application_Start()
  14.     {
  15.         // create empty config database if it not exists
  16.         Database.SetInitializer(new ConfigurationDatabaseInitializer());
  17.         
  18.         // set the anti CSRF for name (that's a unqiue claim in our system)
  19.         AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.Name;
  20.  
  21.         // setup MEF
  22.         SetupCompositionContainer();
  23.         Container.Current.SatisfyImportsOnce(this);
  24.  
  25.         AreaRegistration.RegisterAllAreas();
  26.  
  27.         FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
  28.         RouteConfig.RegisterRoutes(RouteTable.Routes, ConfigurationRepository, UserRepository);
  29.         ProtocolConfig.RegisterProtocols(GlobalConfiguration.Configuration, RouteTable.Routes, ConfigurationRepository, UserRepository, RelyingPartyRepository);
  30.         BundleConfig.RegisterBundles(BundleTable.Bundles);
  31.     }
  32.  
  33.     private void SetupCompositionContainer()
  34.     {
  35.         var container = new CompositionContainer(new RepositoryExportProvider(), new IcmExportProvider());
  36.         
  37.         Container.Current = container;
  38.     }
  39. }

Now the Global configuration has been set you will be able to use the MEF [Import] attribute to include your types dynamically via configuration at runtime.  To complete this part of the tutorial you will need to include the config file, this is done via modification of the web.config, including the new config section.  The section below shows the GSTT configuration section registration xml element.

Web.Config
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <configuration>
  3.   <configSections>
  4.     <section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
  5.     <section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
  6.     
  7.     <section name="thinktecture.identityServer.repositories" type="Thinktecture.IdentityServer.Configuration.RepositoryConfigurationSection, Thinktecture.IdentityServer.Core" />
  8.     <section name="gstt.repositories" type="Gstt.Icm.Repository.Config.GsttConfigurationSection, Gstt.Icm.Repository" />
  9.  
  10.     <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
  11.     <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
  12.   </configSections>
  13.   <!-- ............................................... -->
  14.   <!-- start of identity server relevant configuration -->
  15.   <thinktecture.identityServer.repositories configSource="configuration\repositories.config" />
  16.   <gstt.repositories configSource="configuration\gsttCustom.config" />
gsttConfig.config
  1. <gstt.repositories icmUserManagement="Gstt.Icm.Repository.IcmUserRepository, Gstt.Icm.Repository" />

Configuration sections and types are registered using the Gstt.Icm.Repository.IcmUserRepository, Gstt.Icm.Repository (Fully Qualified Type Name, Assembly Name).

Now we are ready to use all the types and modifications we have made and change the ThinkTecture configuration.  As we are only extending the role system and login system I add a domain level qualifier to the ThinkTecture UserRepository and ClaimRepository.  The code snippet below shows my custom implementation of the UserRepository service, this service utilises a domain qualifier of TT\* to identify ThinkTecture users and allows backwards compatibility.

Code Snippet
  1. public class IcmRepository : IUserRepository
  2. {
  3.     [Import]
  4.     public IClientCertificatesRepository Repository { get; set; }
  5.  
  6.     [Import]
  7.     public IIcmUserRepository IcmUserRepository { get; set; }
  8.  
  9.     public IcmRepository()
  10.     {
  11.         Container.Current.SatisfyImportsOnce(this);
  12.     }
  13.  
  14.     public bool ValidateUser(string userName, string password)
  15.     {
  16.         // Convention that ThinkTecture Users always begin with TT
  17.         if (userName.StartsWith("TT\\") && userName.Length > 3)
  18.         {
  19.             userName = userName.Substring(3);
  20.             return Membership.ValidateUser(userName, password);
  21.         }
  22.         else
  23.         {
  24.             return IcmUserRepository.ValidateUser(userName, password);
  25.         }
  26.     }

If you login to ThinkTecture using TT\Admin we direct to the ASP.NET membership system, alternatively we use the MEF imported repository to check against our custom SQL store.

The same logic applies to the claims repository, we swap out the roles with the roles from the SQL database:

Code Snippet
  1. public class IcmClaimsRepository : IClaimsRepository
  2. {
  3.     private const string ProfileClaimPrefix = "http://identityserver.thinktecture.com/claims/profileclaims/";
  4.  
  5.     [Import]
  6.     public IIcmUserRepository UserRepository { get; set; }
  7.  
  8.     public IEnumerable<Claim> GetClaims(ClaimsPrincipal principal, RequestDetails requestDetails)
  9.     {
  10.         var userName = principal.Identity.Name;
  11.         var claims = new List<Claim>(from c in principal.Claims select c);
  12.  
  13.         if (!userName.StartsWith("TT\\"))
  14.         {
  15.             UserRepository.GetUserRoles(userName).ToList().ForEach(p => claims.Add(new Claim(ClaimTypes.Role, p)));
  16.             return claims;
  17.         }

With all the above complete and our new database referenced the final step is to modify the configuration/repositories.config in the web project, registering our new User and Claims Repository.

repositories.config
  1. <thinktecture.identityServer.repositories tokenServiceConfiguration="Thinktecture.IdentityServer.Repositories.Sql.ConfigurationRepository, Thinktecture.IdentityServer.Core.Repositories"
  2.                                           userManagement="Thinktecture.IdentityServer.Repositories.ProviderUserManagementRepository, Thinktecture.IdentityServer.Core.Repositories"
  3.                                           userValidation="Gstt.Icm.Repository.IcmRepository, Gstt.Icm.Repository"
  4.                                           claimsRepository="Gstt.Icm.Repository.IcmClaimsRepository, Gstt.Icm.Repository"
  5.                                           relyingParties="Thinktecture.IdentityServer.Repositories.Sql.RelyingPartyRepository, Thinktecture.IdentityServer.Core.Repositories"
  6.                                           claimsTransformationRules="Thinktecture.IdentityServer.Repositories.PassThruTransformationRuleRepository, Thinktecture.IdentityServer.Core.Repositories"
  7.                                           clientCertificates="Thinktecture.IdentityServer.Repositories.Sql.ClientCertificatesRepository, Thinktecture.IdentityServer.Core.Repositories"
  8.                                           clientsRepository="Thinktecture.IdentityServer.Repositories.Sql.ClientsRepository, Thinktecture.IdentityServer.Core.Repositories"
  9.                                           identityProvider="Thinktecture.IdentityServer.Repositories.Sql.IdentityProviderRepository, Thinktecture.IdentityServer.Core.Repositories"
  10.                                           delegation="Thinktecture.IdentityServer.Repositories.Sql.DelegationRepository, Thinktecture.IdentityServer.Core.Repositories"
  11.                                           caching="Thinktecture.IdentityServer.Repositories.MemoryCacheRepository, Thinktecture.IdentityServer.Core.Repositories" />

One thing to remember, in order to get the best developer experience bind the VS project to IIS root and on the first run, ensure you visit the /InitialConfiguration controller.