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.

20 comments:

  1. This is my first introduction to MEF, but doesn't modifying the Global.ascx.cs file require a build and re-deployment? Doesn't that defeat the purpose of the abstraction. The SimpleCalculator3 sample from MS has the same issue, except its hard-coded path for the DirectoryCatalog is in the actual Program. The reason I'm looking into this is because I want to be able to create a different implementation of the IClaimsTransformationRulesRepository to handle custom claims transformations.

    ReplyDelete
  2. Hi Brad,

    You are correct and this is a current implementation issue I have raised with Dominic.

    At the moment the repository catalogue is a "rip and replace" only implementation, you cannot add additional types to the registry without re-compiling.

    I have tried to limit the behavioural changes and impact to ThinkTeckture by allowing a TT\ convention... you can achieve this via a rip and replace, but you'd need more code in your repository implementation.

    To ensure that User-Repositories do not become bloated, I wanted to highlight that this was possible.

    Regards

    Gary

    ReplyDelete
  3. Hey Gary!

    Thanks for sharing.
    Could you please make the sample available? Thanks.

    ReplyDelete
  4. Dear Gary,

    Thanks for this very needed article. I think there are very few people who are actually implementing this particular scenario so it is hard to find many example in internet. I haven't tried your example yet but i believe it will work without any exceptions errors (I trust you). Am so happy to at least found your link in Google about ThinkTecture extensibility.

    Could you also please share the source code if not a problem. It will help in case we land up with errors after all these setups :-)

    Thanks a ton!!
    Raj

    ReplyDelete
  5. Hi yes it could be a great idea to have the code, i think your MEF export provider is not complete.

    Thanks

    ReplyDelete
  6. I would appreciate the source for this as well. Thanks in advance.

    ReplyDelete
  7. This is an interesting article
    Hi, is there source available for download?

    ReplyDelete
  8. Getting Following Error after taking all actions:

    Type not found for interface: Thinktecture.IdentityServer.Repositories.IUserRepository

    ReplyDelete
  9. Getting Following Error after taking all actions:

    Type not found for interface: Thinktecture.IdentityServer.Repositories.IUserRepository


    ????

    ReplyDelete
  10. next error


    The composition produced a single composition error. The root cause is provided below. Review the CompositionException.Errors property for more detailed information.

    1) The export 'Thinktecture.IdentityServer.Repositories.IUserRepository' is not assignable to type 'Thinktecture.IdentityServer.Repositories.IUserRepository'.

    Resulting in: Cannot set import 'Thinktecture.IdentityServer.Web.MvcApplication.UserRepository (ContractName="Thinktecture.IdentityServer.Repositories.IUserRepository")' on part 'ASP.global_asax'.
    Element: Thinktecture.IdentityServer.Web.MvcApplication.UserRepository (ContractName="Thinktecture.IdentityServer.Repositories.IUserRepository") --> ASP.global_asax

    ReplyDelete
    Replies
    1. Wandelson I'm having the same problem. Do you still remember how you fixed this?
      I managed to make it work for the first time, but then I had to go back and undo some stuff, re do them later, but then i got stuck with this error.

      I appreciate any help.
      Jaime

      Delete
    2. Getting error .... any idea?
      The composition produced a single composition error. The root cause is provided below. Review the CompositionException.Errors property for more detailed information.

      1) No exports were found that match the constraint:
      ContractName Grocery.ICM.Repository.Interfaces.IIcmUserRepository
      RequiredTypeIdentity Grocery.ICM.Repository.Interfaces.IIcmUserRepository

      Resulting in: Cannot set import 'Grocery.ICM.Repository.IcmRepository.IcmUserRepository (ContractName="Grocery.ICM.Repository.Interfaces.IIcmUserRepository")' on part 'Grocery.ICM.Repository.IcmRepository'.
      Element: Grocery.ICM.Repository.IcmRepository.IcmUserRepository (ContractName="Grocery.ICM.Repository.Interfaces.IIcmUserRepository") --> Grocery.ICM.Repository.IcmRepository

      Delete
  11. Hi, is there source available for download? plisss

    ReplyDelete
  12. after some adjustments it worked.

    ReplyDelete
    Replies
    1. Can you please let me know what adjustments you made to work?

      Delete
  13. Hi, I was wondering if there is source available to download please?

    ReplyDelete
  14. Hi
    I was confused at the config section, you have created gsttConfig.config file and given reference in web.config as gsttCustom.config


    I have followed this link but I got this error after running the WebSite:


    HTTP Error 500.19 - Internal Server Error
    The requested page cannot be accessed because the related configuration data for the page is invalid.
    Detailed Error Information:
    Module IIS Web Core
    Notification Unknown
    Handler Not yet determined
    Error Code 0x80070032
    Config Error The configuration section 'gstt.repositories' cannot be read because it is missing a section declaration
    Config File \\?\E:\New IdPServer\Thinktecture.IdentityServer.v2-master\src\OnPremise\WebSite\web.config
    Requested URL http://localhost:53340/
    Physical Path
    Logon Method Not yet determined
    Logon User Not yet determined

    Config Source:

    17:
    18:
    19:

    ReplyDelete
  15. Gary,

    I'm looking to extend identityserver, but to AD LDS instead of a database. Is this roughly the same process. Looking for a starting point.

    Thanks,

    Ken

    ReplyDelete