Sunday, 20 January 2013

ASP.NET MVC 4, ADFS 2.0 and 3rd party STS integration (IdentityServer2) – Part 2

This the 2nd part of a 2 part blog series of which we will extend ADFS 2.0 to allow alternative login credentials.  If you would like more information of the objectives of this series please refer to part 1.

Moving forward….

With your application now successfully integrated we can focus on the objective of uplifting ADFS 2.0 to allow 3rd party STS integration.  As part of this series we are utilising Dominick Baier (@leastprivilege) Identity Server which provides us with the extensibility to integrate a with a custom authentication store.

Installing Identity Server

Hopefully by now you will have already configured identity server by following the instructions at http://vimeo.com/51088126, this should leave you with a working understanding of the STS user configuration process.  One thing to be mindful when installing IdentityServer and ADFS on the same server is that the federation-metadata addresses are registered to the ADFS proxy service, this means that the request is sent directly to the ADFS windows service. 

Due to this you are unlikely to be able to obtain the IdentityServer federation metadata required to create the Trusted claims provider exchange automatically.  This makes configuring the Trust a manual process, I would recommend hosting the services on different servers for this reason.

Creating the test account

In order to test the ADFS integration and limit the changes we will create and configure IdentityServer v2 with an out of the box account and configure ADFS as a relying party Trust.  To begin this process we need to create a test user account within Identity Server. 

Browse to the IdentityServer home page and sign in, click the administration link:

image Click Users on the configuration menu.imageClick New and Provide the test user details, ensuring you add the user to the IdentityServerUsers group (this enables the ability for users to login to the STS).imageNow the user is added we need to enable the WS-Trust protocol, WS-Trust provides the ability to connect directly to the STS and is an extension of WS-Security.  WS-Trust enables us to connect directly via a SOAP channel and request tokens.

To enable WS-Trust click Protocols, select WS-Trust and Save Changes.imageTo validate WS-Trust is enabled, click the home link and select Application Integration.  You should now see the addition WS-Trust meta-data and mixed mode security endpoints.imageIdentity Server provides several out of the box claims.  These can be viewed at the federation metadata endpoint address above.

<fed:ClaimTypesOffered>
<auth:ClaimType Uri="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"/>
<auth:ClaimType Uri="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"/>
<auth:ClaimType Uri="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"/>
<auth:ClaimType Uri="http://identityserver.thinktecture.com/claims/profileclaims/twittername" xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"/>
<auth:ClaimType Uri="http://identityserver.thinktecture.com/claims/profileclaims/city" xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"/>
<auth:ClaimType Uri="http://identityserver.thinktecture.com/claims/profileclaims/homepage" xmlns:auth="http://docs.oasis-open.org/wsfed/authorization/200706"/>
</fed:ClaimTypesOffered>



As you can see we have access to standard claims (such as name, emailaddress and role).  The final 3 claims are obtained via configuration twittername, city and homepage.  The configuration for these can be found in the configuration folder in the profile.config file.

<profile automaticSaveEnabled="false"
defaultProvider="Profile">
<providers>
<add name="Profile"
type="System.Web.Providers.DefaultProfileProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
connectionStringName="ProviderDB"
applicationName="/" />
</providers>

<!-- properties that should get turned into claims go here -->
<properties>
<add name="City" />
<add name="HomePage" />
<add name="TwitterName" />
</properties>
</profile>



User profile settings are added using the profile link on the users configuration page.imageWith the User configured we need to configure ADFS as a relying party.  This is achieved via the Relying Parties and Resources Tab.  To configure ADFS we will use the federation passive endpoint address (https://<<ADFSDOMAIN>>/adfs/ls).imageThis concludes the configuration of IdentityServer for processing of claims from within ADFS (for now).  In order for us to complete our mission, we need to uplift our ADFS installation to accept claims from our new STS.


Before we progress, please ensure you have the ability to access to the federation meta-data from your ADFS STS.


ADFS Configuration


Open up the ADFS MMC and Browse to the Trust Relationships, Claims Provider Trusts. imageSelect Add Claims Provider Trust, this will start the Trust Wizard, click Start.  You now have 2 options, import from the Federation Metadata or Browse directly depending on your configuration.  Either upload or browse directly to the meta-data address as below:imageClick Next, Provide a Display Name, Next and Finish.  Ensure you open the Edit Claims Rules Dialog at this point.imageAdd a rule which passes the Name through to ADFS from the Trusted Provider.imageimage Click Finish.


The last step to configure and allow remote Trusts is to configure ADFS as a valid realm audience, this is achieved via Windows Powershell.  Open up the powershell console and run the following script (Replace <<ADFSDOMAIN>> with your ADFS domain.

Add-PSSnapin Microsoft.Adfs.PowerShell
set-ADFSProperties -AcceptableIdentifier "https://<<ADFSDOMAIN>>/adfs/ls"



We now have a fully configured Trust between ADFS and Identity Server.  This trust is pretty useless as we now need to modify FormsSignIn.aspx to establish a login to our 3rd party STS. 


Modifying ADFS FormsSignIn.aspx


ADFS 2.0 on Windows Server 2012 utilises .NET 4.5 and therefore WIF baked into the Framework.  This is the use-case I am working against in this post.  Unfortunately, .NET 4.5 removes the WS Channels which are fundamental to talk to WS-Trust services.  Luckily for us Dominick Baier has captured these from the WIF 3.5 source and exposed them in the IdentityServer GitHub source. 


This source has been refactored into a component dedicated for the purpose of remote Username and Password STS contact.  ADFS Helper (and Source) can be downloaded below.


ADFS Helper.zip


To progress, copy the bin folder to C:\inetpub\adfs\ls.imageimage



Gm.Adfs.Helper includes a method which provides the ability to login to the remote STS without overly modifying the FormsSignIn.aspx.cs.  The core method in the helper is shown below.  Gm.Adfs.Helper.RemoteSignIn.SignIn accepts 7 parameters:



wsTrustAddress = The address of the remote STS (WS-Trust address from application integration).


applicationRealm = The application realm configured in the relying party trust.


username = Authenticating user.


password = Authentication password.


ignoreCertificateErrors = Whether to globally ignore invalid certificates (Non-Trusted publishers).


authenticationCertificateMode = The authentication validation certificate mode.


claims = A parameter array of RequestClaim requests.

        public static SecurityToken SignIn(
string wsTrustAddress,
string applicationRealm,
string username,
string password,
bool ignoreCertificateErrors,
X509CertificateValidationMode authenticationCertificateMode,
params RequestClaim[] claims)
{
if (ignoreCertificateErrors)
{
ServicePointManager.ServerCertificateValidationCallback = delegate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
return true;
};
}

var binding = new UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential);
using (var factory = new WSTrustChannelFactory(binding, wsTrustAddress))
{
factory.TrustVersion = TrustVersion.WSTrust13;
factory.Credentials.UserName.UserName = username;
factory.Credentials.UserName.Password = password;
factory.Credentials.ServiceCertificate.Authentication.CertificateValidationMode = authenticationCertificateMode;

// create token request
var rst = new RequestSecurityToken
{
KeyType = KeyTypes.Bearer,
RequestType = RequestTypes.Issue,
AppliesTo = new EndpointReference(applicationRealm)
};

foreach (var claim in claims)
{
rst.Claims.Add(claim);
}

new RequestClaim(ClaimTypes.Name);

// request token and return
RequestSecurityTokenResponse response;
var output = factory.CreateChannel().Issue(rst, out response);
return output;
}
}



Please ensure you understand the code above before attempting to integrate a 3rd party STS via FormsSignIn.aspx.cs.


With all of the above complete, you are now ready to modify the FormsSignIn.aspx.cs.  Modifying the page is simple, just add the required namespace and the required SubmitButton modification.  The code below shows the modification to the SubmitButton method.  This approach first attempts to login to the remote STS and then falls back to ADFS, enabling both authentication methods to be supported.


Add the following Namespaces to the FormsSignIn.aspx.cs

using System.Net;
using System.Net.Security;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Security;
using System.ServiceModel.Security.Tokens;
using System.IdentityModel.Protocols.WSTrust;
using System.Security.Cryptography.X509Certificates;
using System.IdentityModel.Tokens;
using System.Security.Claims;

using Gm.Adfs.Helper;



Modify the SubmitButton with the following code, modifying the OtherSTSAddress and YourSTSAddress variables.

    const string OtherSTSAddress = "https://idserv.riddify.co.uk/issue/wstrust/mixed/username";
const string YourSTSAddress = "https://mcad.riddify.co.uk/adfs/ls";

protected void SubmitButton_Click(object sender, EventArgs e)
{
try
{
try
{
var token = RemoteSignIn.SignIn(
OtherSTSAddress,
YourSTSAddress,
UsernameTextBox.Text,
PasswordTextBox.Text,
true,
X509CertificateValidationMode.None,
new RequestClaim(ClaimTypes.Name));
SignIn(token);
}
catch (FaultException fex)
{
SignIn(UsernameTextBox.Text, PasswordTextBox.Text);
}
}
catch (AuthenticationFailedException ex)
{
HandleError(ex.Message);
}
//catch (Exception ex)
//{
// Response.Write(ex.ToString());
//}
}

Please take note of the RequestClaims line of code, this can be modified to enable custom claims to be progressed through ADFS.  Below I have modified the code to show a request for twitter account from IdentityServer.

            var token = RemoteSignIn.SignIn(
OtherSTSAddress,
YourSTSAddress,
UsernameTextBox.Text,
PasswordTextBox.Text,
true,
X509CertificateValidationMode.None,
new RequestClaim(ClaimTypes.Name),
new RequestClaim("http://identityserver.thinktecture.com/claims/profileclaims/twittername"));
SignIn(token);



These claims will all need to be configured to pass through to your receiving relying party, so please do not forget!!!  This was discussed in part one but needs to be configured using a Relying Party Claim Rule.


image With the above all complete let’s modify our MVC application to obtain the Twitter name from the claim.


Modifying ASP.NET MVC Test Page.


With the above complete we can modify out ASP.NET MVC application to populate the ViewBag with both the Name and Twitter claim.

        public ActionResult Index()
{
ViewBag.Identity = Thread.CurrentPrincipal.Identity;

var claims = Thread.CurrentPrincipal.Identity as System.Security.Claims.ClaimsIdentity;
var twitter = claims.Claims.FirstOrDefault(p => p.Type == "http://identityserver.thinktecture.com/claims/profileclaims/twittername");

ViewBag.Twitter = twitter.Value;

return View();
}



Which outputs the following into the view.

@{
ViewBag.Title = "Index";
}

<h2>Index</h2>

<p>
@ViewBag.Identity.Name
</p>
<p>
@ViewBag.Twitter
</p>

<p>
@User.Identity.AuthenticationType
</p>



To screen.


image


Thank you for reading through this series.  I hope you have reached an understanding of customising ADFS for custom authentication login.


My next blog post will expose the methods used to customise identity server, replacing IUserRepository and IClaimsRepository.

18 comments:

  1. Thanks again for this post. As always awesome write up. We are implementing your exact idea using identity server. Any idea on the timeline of your next post?

    ReplyDelete
    Replies
    1. Could you please let me know when the next post will be available?

      Delete
  2. Great Post! Can't wait for the next post continuing the saga. Any idea when you'll be able to give us the writeup?

    ReplyDelete
  3. Hey I have a quick question regarding this post. How do I bypass the home realm discovery page with your example. Because Identity server is also configured as an claims application in ADFS, I see the HRD page with 2 options. Since I will be using the formsLogin.aspx page in ADFS is there anyway I can go straight to that page without the HRD page?

    ReplyDelete
    Replies
    1. You can bypass HRD with the ADFS HRD selection queryString.

      Delete
  4. Nice series of posts on Claims auth - Thanks Gary

    ReplyDelete
  5. Hi Gary, I am getting following error when i try to login using ADFS sign-in page which in turn calling identity server 2 as explained by you in this post. What would be solution?
    Error: The message with Action 'http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue' cannot be processed at the receiver, due to a ContractFilter mismatch at the EndpointDispatcher. This may be because of either a contract mismatch (mismatched Actions between sender and receiver) or a binding/security mismatch between the sender and the receiver. Check that sender and receiver have the same contract and the same binding (including security requirements, e.g. Message, Transport, None). StackTrace: at Microsoft.IdentityModel.Protocols.WSTrust.WSTrustChannel.ReadResponse(Message response) at Microsoft.IdentityModel.Protocols.WSTrust.WSTrustChannel.Issue(RequestSecurityToken rst, RequestSecurityTokenResponse& rstr) at Microsoft.IdentityModel.Protocols.WSTrust.WSTrustChannel.Issue(RequestSecurityToken rst) at FormsSignIn.SignInWithTokenFromOtherSTS(String username, String password) at FormsSignIn.SubmitButton_Click(Object sender, EventArgs e)

    ReplyDelete
  6. Are you sure you have the correct IDP URL being passed to the function.

    ReplyDelete
    Replies
    1. Hi Gary, I am able to resolve contact mismatch error by using TrustVersion.WSTrust13, but now getting following error. I have deployed ADFS2 and identity server on Windows 2008 R2 server.
      I need to resolved this quickly for my client. Plz help.

      Failed to process the Web request because the request is not valid. Cannot get protocol message from HTTP query. The following errors occurred when trying to parse incoming HTTP request:

      Microsoft.IdentityServer.Protocols.Saml.HttpSamlMessageException: MSIS7015: This request does not contain the expected protocol message or incorrect protocol parameters were found according to the HTTP SAML protocol bindings.
      at Microsoft.IdentityServer.Web.HttpSamlMessageFactory.CreateMessage(HttpContext httpContext)
      at Microsoft.IdentityServer.Web.FederationPassiveContext.EnsureCurrent(HttpContext context)

      And:
      System.IdentityModel.Tokens.SecurityTokenException: MSIS3121: SubjectConfirmationData element was missing in received token.
      at Microsoft.IdentityServer.Service.SamlProtocol.SamlProtocolService.ValidateRequestProperties(Saml2SecurityToken token)
      at Microsoft.IdentityServer.Service.SamlProtocol.SamlProtocolService.Issue(IssueRequest issueRequest)
      at Microsoft.IdentityServer.Service.SamlProtocol.SamlProtocolService.ProcessRequest(Message requestMessage)

      Delete
    2. Hi Manoj and Gary, I was wondering if and how you resolved your issue? We are getting MSIS3121 errors when sending AuthnRequests in an ADFS/IS setup. I would appreciate any input you might have, based on your experiences!

      Delete
  7. Hi Gary - this is great. One problem - how do I do the same thing using client certificates instead of username and password? I already have Identity Server configured for client certs.

    I figure I can still modify the Forms authentication handler in ADFS but the ADFS Helper classes seem to only have the one WSTrust sign in method for username and password. I don't know enough about this to know what I can do to extend it.

    ReplyDelete
    Replies
    1. Sorry but you will need to extend the existing code to accept a byte[] array and pass the cert to the identity server endpoint.

      Delete
  8. Hi Gary, Great blog! I was following your tutorial until I got to the install of Gm.Adfs.Helper. This file is compiled to .NET v4.5 but the ADFSAppPool on my install is v2.0. I am installing this onto a Win2008 R2 Server. Do I still need this helper? If so do you know what I should do? I tried changing the AppPool to v4.0 but that gave an error. As ADFS 2.0 is built on ASP.NET 4.5 I wonder why the AppPools are v2.0? Any help would be appreciated!

    ReplyDelete
    Replies
    1. Hi, if you're using ADFS 2.0 on Windows 2008 you will need to recompile the code to WIF 3.5.

      Delete
    2. Hi
      Can I know how can I recompile the code to WIF 3.5. I got WIF 4.5.

      Thanks

      Arun Skaria

      Delete
  9. Hello Gary,

    I have been trying to get this to work under win 2008 R2. I am actually receiving a token back from identity server. However, when I pass the token into the signin method, I get either of the two following errors from ADFS 2.0:

    (1)

    Microsoft.IdentityModel.Tokens.EncryptedTokenDecryptionFailedException: ID4022: The key needed to decrypt the encrypted security token could not be resolved. Ensure that the SecurityTokenResolver is populated with the required key.

    (2)

    Cannot find certificate to validate message/token signature obtained from claims provider.
    Claims provider: https://identityserver.companydomain.com/services/trust

    The error seems obvious enough. Any ideas?

    ReplyDelete
    Replies
    1. I think you have not registered the claims provider trust in ADFS.

      Delete
  10. Thanks for this wonderful article. I would like to know your input on we multiple web application some build In house and some by third party. Most of the applications are built on .net and we have windows setup available here Like windows server, Active directory. We want to implement single sign if
    If my allapplications are based on .net
    If There are some applications on Java or other language.

    (What about using SAML or ADFS)


    http://stackoverflow.com/questions/30708317/single-sign-on-sso-implementation

    ReplyDelete