In my previous post I walked through configuring ADFS and writing a small C# console app to authenticate with it and get back a ClaimsPrincipal object. This is fine when your system only needs to authenticate domain users but lets say we have external users we want to be able to authenticate using a username and password. This is where using a Secure Token Service for your authentication really shines as you can pretty much swap them in/out and add more without having to rewrite your security logic.
In this post I’m going to alter my previous ADFS project to swap out ADFS for Identity Server. I’ll first walk through configuring Identity Server then modify the console app to use this STS rather than ADFS. To follow the walkthrough you may want to first read my previous post on ADFS as I will be building on that. In a future blog post I’ll cover building a resource STS that can act as a gateway to support multiple STS’s without having to make any code changes but for now we’ll just look at Identity Server.
First things first you’re going to need a copy of Identity Server, head over to their github repository and download the latest version (2.1 at the time of this blog post).
Installing & Configuring Identity Server
- Unzip IdentityServer to a destination of your choice
- In the IdentityServer folder right click App_Data and go to properties
- On the Security tab click edit
- Click Add
- Enter “Network Service” and click OK
- Tick the allow box for modify to give the service modify permissions on the app_data directory
- Click OK on both open modal windows
- Open IIS
- Under Application Pools, create a new Application Pool called “IdentityServer” using .Net 4.0
- Highlight the new Application Pool and click “Advanced Settings”
- Set the Identity property to “NetworkService”
- Right click the Default Website and click Add Application
- Enter "”IdentityServer” into the alias
- Set the Application Pool to “IdentityServer”
- In physical path point it to where you unzipped Identity Server to
- Open a web browser and navigate to https://localhost/identityserver
- Enter a site name of your choice
- Choose a unique Issuer URI
- Select your SSL certificate
- Tick Create default roles and admin, then choose a username/password and click save
- Click sign in and use your new admin credentials
- Click administration
- Under users add a new user and make sure you tick the IdentityServerUsers role
- Click protocols and make sure WS-Trust is ticked
- Click “Relying Parties & Resources” and click new
- Tick Enabled, enter a display name, enter an identifier URI in “Realm/Scope Name” then click save. You may want to use the same URI for Realm as you gave for your ADFS relying party identifier but that’s your call
- Change the relyingPartyId to your Identity Server relying party id (Only if it’s different to the one you used for ADFS)
- Change adfsEndpoint to point at the Identity Server ws-trust endpoint
- Change the certSubject to the subject of your Identity Server certificate. Unlike ADFS, by default Identity Server uses the certificate the website is running on to sign the token, rather than using a separate signing certificate.
- Change the token handler collection to include an instance of Saml2SecurityTokenHandler as Identity Server uses SAML2
- Lastly you need to change the the binding from WindowsWSTrustBinding to UserNameWSTrustBinding and set the username/password you are authenticating. </ol>
Identity Server is now configured to authenticate users over the WS-Trust endpoint. This will be <IdentityServerUri>/issue/wstrust/mixed/username.
The following changes need to be made to our ADFS console app to get it to authenticate with Identity Server.
Below is the code with these changes made….
using System; using System.IO; using System.IdentityModel.Protocols.WSTrust; using System.IdentityModel.Tokens; using System.Linq; using System.Security.Claims; using System.ServiceModel; using System.ServiceModel.Security; using System.Xml; using Thinktecture.IdentityModel.WSTrust; namespace ConsoleApplication1 { class Program { static void Main() { const string relyingPartyId = "https://adfsserver.security.net/MyApp"; //ID of the relying party specified in Identity Server const string identityServerEndpoint = "https://adfsserver.security.net/IdentityServer/issue/wstrust/mixed/username"; const string certSubject = "CN=adfsserver.security.net"; //Setup the connection to Identity Server var factory = new WSTrustChannelFactory(new UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential), new EndpointAddress(identityServerEndpoint)) { TrustVersion = TrustVersion.WSTrust13 }; factory.Credentials.UserName.UserName = "myuser"; factory.Credentials.UserName.Password = "mypassword"; //Setup the request object var rst = new RequestSecurityToken { RequestType = RequestTypes.Issue, KeyType = KeyTypes.Bearer, AppliesTo = new EndpointReference(relyingPartyId) }; //Open a connection to Identity Server and get a token for the logged in user var channel = factory.CreateChannel(); var genericToken = channel.Issue(rst) as GenericXmlSecurityToken; if (genericToken != null) { //Setup the handlers needed to convert the generic token to a SAML Token var tokenHandlers = new SecurityTokenHandlerCollection(new SecurityTokenHandler[] { new Saml2SecurityTokenHandler() }); tokenHandlers.Configuration.AudienceRestriction = new AudienceRestriction(); tokenHandlers.Configuration.AudienceRestriction.AllowedAudienceUris.Add(new Uri(relyingPartyId)); var trusted = new TrustedIssuerNameRegistry(certSubject); tokenHandlers.Configuration.IssuerNameRegistry = trusted; //convert the generic security token to a saml token var samlToken = tokenHandlers.ReadToken(new XmlTextReader(new StringReader(genericToken.TokenXml.OuterXml))); //convert the saml token to a claims principal var claimsPrincipal = new ClaimsPrincipal(tokenHandlers.ValidateToken(samlToken).First()); //Display token information Console.WriteLine("Name : " + claimsPrincipal.Identity.Name); Console.WriteLine("Auth Type : " + claimsPrincipal.Identity.AuthenticationType); Console.WriteLine("Is Authed : " + claimsPrincipal.Identity.IsAuthenticated); foreach (var c in claimsPrincipal.Claims) Console.WriteLine(c.Type + " / " + c.Value); Console.ReadLine(); } } //The token handler calls this to check the token is from a trusted issuer before converting it to a claims principal //In this case I authenticate this by checking the certificate name used to sign the token public class TrustedIssuerNameRegistry : IssuerNameRegistry { private string _certSubject; public TrustedIssuerNameRegistry(string certSubject) { _certSubject = certSubject; } public override string GetIssuerName(SecurityToken securityToken) { var x509Token = securityToken as X509SecurityToken; if (x509Token != null && x509Token.Certificate.SubjectName.Name != null && x509Token.Certificate.SubjectName.Name.Contains(_certSubject)) return x509Token.Certificate.SubjectName.Name; throw new SecurityTokenException("Untrusted issuer."); } } } }
After seeing both these STS’s in action the next step is to combine both of them in a way they can be used in the same system for different authentication types. I’ll cover this in a future blog post…..