Click here to Skip to main content
15,881,812 members
Articles / Security

Thinktecture Identity Server - Configuration, Customization

Rate me:
Please Sign up or sign in to vote.
4.91/5 (22 votes)
19 Nov 2013CPOL6 min read 162.3K   1.9K   48   35
Installing, extending Identity Server and implementing session token caching

Identity Server

Introduction

In my recent project, we wanted to use Thinktecture Identity Server for our authentication/authorization needs and while doing that exercise, I have come across many issues related to configuration and many findings. I just wanted to share my findings so that it will be useful to fellow developers.

The above diagram shows what I wanted to achieve and I think this is what a typical web application looks like.

What I Wanted to Achieve

  1. Install & Configure Identity Server
  2. Extend Identity Server to use our own data store to check the user credentials and get user claims
  3. Implement SecurityToken Caching
  4. Pass the token to our REST API Services

There are many articles you should definitely go through before reading this article. I have given all references at the bottom of the article. I will be discussing only the main points and solutions for each.

Identity Server Installation

I was able to install the identity server without any issues.There are many useful resources available and you should not have any problem. I had minor problems when using the self signed certificates as they are not trusted when used from other machines. So I wanted to go with OpenSSL so that I can set up a real world certificate authority and issue certificates as I wanted.

The other thing I wanted to achieve was to get the identity server check the user credentials against our own database rather than its own data store.

Certificates

Identity Server needs at least one SSL certificate for running as it needs to be hosted on HTTPS. It needs 2 more certificates for signing the security tokens and encryption but you can use the same certificate for all 3 requirements. So one certificate should be OK for now.

REM Create CA root certificate
openssl req -x509 -nodes -days 3650 -subj "/C=US/L=Redmond/O=XYZ/OU=Technology/CN=XYZ Inc" 
-newkey rsa:2048 -keyout xyzCA.key -out xyzCA.crt -config openssl.conf
openssl pkcs12 -export -out xyzCA.pfx -inkey xyzCA.key -in xyzCA.crt

Install the Root certificate on all the 3 servers (STS Server, UI Server, API Server). If they are all one server, then just install once.

@echo off
rem set server="sts.xyz.com"
set /p server="Enter Server Name: " %=%

REM Create SSL certificate for IIS, which trusts the root certificate
openssl req -nodes -days 3650 
-subj "/C=US/L=Redmond/O=XYZ/OU=Technology/CN=%server%" 
-newkey rsa:2048 -keyout %server%.key -out %server%.csr -config openssl.conf
openssl x509 -req -days 3650 -in %server%.csr -CA xyzCA.crt 
-CAkey xyzCA.key -CAcreateserial -out %server%.crt 
openssl pkcs12 -export -out %server%.pfx -inkey %server%.key 
-in %server%.crt -name "Server Certificate - %server%"
pause

Once you have the certificates, follow the Identity Server setup video available at http://vimeo.com/51088126 and it should be pretty straight forward.

Here are some screen shots of the configuration you need to create:

Identity Server -Configuration

Identity Server -Configuration

Identity Server -Configuration

Extending Identity Server

We wanted to use our own database to store user details like claims, etc. as it's integrated with the existing application.

You can follow this link which gives step by step instructions.

Handling Complex Claims

Usually Claims are stored as simple key/value pair and both are of type "string" to keep it simple and reduce dependencies. But we wanted to store some extra information (like an object) along with Claim. I started with just serializing the complex object into a JSON string and storing that value as Claim value and I was able to deserialize it at the receiving end using JSON.NET. Even though this works, I found a good article where I found a more elegant approach. You can read about it here.

UI Web Application - Configuration

At the UI end, we did not want to redirect the users to the identity server site (which is what you normally see). Instead, we want to have a login screen of our own just like a regular forms authentication site and just use the STS in the background to check user credentials and get the claims associated.

The web.config entries are as follows:

XML
<system.identitymodel>
    <identityconfiguration savebootstrapcontext="true">
      <audienceuris>
        
        <add value="http://www.xyz.com/yourapp">
      </audienceuris>
      <securitytokenhandlers>
        <add type="System.IdentityModel.Tokens.JwtSecurityTokenHandler, 
        System.IdentityModel.Tokens.Jwt, Version=2.0.0.0, Culture=neutral, 
        PublicKeyToken=31bf3856ad364e35">
        <securitytokenhandlerconfiguration>
          <issuertokenresolver type="System.IdentityModel.Tokens.X509CertificateStoreTokenResolver, 
          System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
          <certificatevalidation certificatevalidationmode="PeerOrChainTrust" 
          trustedstorelocation="LocalMachine" revocationmode="NoCheck">      
          
          <issuernameregistry type="System.IdentityModel.Tokens.ValidatingIssuerNameRegistry, 
          System.IdentityModel.Tokens.ValidatingIssuerNameRegistry, 
          Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
            <authority name="http://identityserver.v2.xyz.com/trust">
              <keys>
                <add thumbprint="C2B2219F3CAC53658E796C0402360D90AEFA08FC">
              </keys>
              <validissuers>
                <add name="http://identityserver.v2.xyz.com/trust">
              </validissuers>
            </authority>
          </issuernameregistry>
        </securitytokenhandlerconfiguration>
      </securitytokenhandlers>
      <claimsauthorizationmanager 
      type="IdentityServer.Demo.Common.Security.CustomAuthorizationManager, 
      IdentityServer.Demo.Common">
    </identityconfiguration>
  </system.identitymodel>
  <system.identitymodel.services>
    <federationconfiguration>
      <cookiehandler mode="Default" requiressl="false">
      <wsfederation realm="http://www.xyz.com/yourapp" 
      issuer="https://sts.xyz.com/issue/wsfed" 
      passiveredirectenabled="false" requirehttps="true">
    </federationconfiguration>
</system.identitymodel.services>

We need to use the ws-trust end point to achieve what we want and here is the code for both Login/Logoff methods (AccountController.cs):

C#
//
// POST: /Account/Login

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginModel model, string returnUrl)
{
    //if (ModelState.IsValid && WebSecurity.Login
    (model.UserName, model.Password, persistCookie: model.RememberMe))
    //{
    //    return RedirectToLocal(returnUrl);
    //}
    if (ModelState.IsValid)
    {
        var cp = GetClaimsFromIdentityServer(model.UserName, model.Password);
        if (cp != null)
        {
            //All set so now create a SessionSecurityToken
            var token = new SessionSecurityToken(cp) { 
                                        IsReferenceMode = true  //this is 
                                        //important.this is how you say create 
                                        //the token in reference mode meaning 
                                        //your session cookie will contain only a 
                                        //referenceid(which is very small) and 
                                        //all claims will be stored on the server
                                        };
            FederatedAuthentication.WSFederationAuthenticationModule.
            SetPrincipalAndWriteSessionToken(token, true);

            return RedirectToLocal(returnUrl);
        }
    }
    // If we got this far, something failed, redisplay form
    ModelState.AddModelError
    ("", "The user name or password provided is incorrect.");
    return View(model);
}

private ClaimsPrincipal GetClaimsFromIdentityServer(string username, string password)
{
    const string WS_TRUST_END_POINT = "https://{0}/issue/wstrust/mixed/username";
    var sts =ConfigurationManager.AppSettings["IdentityServer"];
    var factory = new System.ServiceModel.Security.WSTrustChannelFactory
    (new UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential),
                                 string.Format(WS_TRUST_END_POINT, sts));
    factory.TrustVersion = TrustVersion.WSTrust13;
    factory.Credentials.UserName.UserName = username;
    factory.Credentials.UserName.Password = password;

    var rst = new System.IdentityModel.Protocols.WSTrust.RequestSecurityToken
    {
        RequestType = RequestTypes.Issue,
        KeyType = KeyTypes.Bearer,
        TokenType = TokenTypes.JsonWebToken,  //yes we need only 
        //Json Web Tokens as they are more compact then default SAML tokens.
        //It matters as we have to send the token to your 
        //API Services with each API request
        AppliesTo = new EndpointReference
        ("http://www.xyz.com/yourapp")  //this is the RP 
        		//you created in Identity Server Admin UI

    };
    var st = factory.CreateChannel().Issue(rst);
    var token = st as GenericXmlSecurityToken;
    var handlers = FederatedAuthentication.FederationConfiguration.
    IdentityConfiguration.SecurityTokenHandlers;
    var jwtToken = handlers.ReadToken(new XmlTextReader
    (new StringReader(token.TokenXml.OuterXml))) as JwtSecurityToken;
    var identity = handlers.ValidateToken(jwtToken).First();
    var principal = new ClaimsPrincipal(identity);
    return principal;
}
//
// POST: /Account/LogOff

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
    //WebSecurity.Logout();

    System.Web.HttpContext.Current.Session.Clear();
    FormsAuthentication.SignOut();
    try
    {
        FederatedAuthentication.SessionAuthenticationModule.SignOut();
        FederatedAuthentication.SessionAuthenticationModule.DeleteSessionTokenCookie();
        FederatedAuthentication.WSFederationAuthenticationModule.SignOut(false);
    }
    catch (Exception)
    {
        //we can ignore
    }

    return RedirectToAction("Index", "Home");
}

Send the Token to API Services

I have used RestSharp to call our API Server and it works pretty well and lot simpler than HttpClient. Sample call looks like below:

C#
public ActionResult Index()
{
    ViewBag.Message = "Called the API 
    along with user claims and got the response below";
    //Calling the Web Api hosted on a remote server.
    //TokenAuthenticator will take care of adding the 
    //token to the HTTP Header before making the request to the server.
    var restClient = new RestClient(ConfigurationManager.AppSettings
    ["ApiServer"]) { Authenticator = new TokenAuthenticator() };
    string url = "api/Values";
    var request = new RestRequest(url, Method.GET);
    var data = restClient.Execute<dynamic>(request);
    ViewBag.Data = JValue.Parse(data.Content);
    var email = ViewBag.Data.Email; 
    return View();
}

The source code for the TokenAuthenticator.cs is given below:

C#
public class TokenAuthenticator : IAuthenticator
{
    public void Authenticate(IRestClient client, IRestRequest request)
    {
        var token = ClaimsPrincipal.Current.GetTokenString();
        if (!string.IsNullOrEmpty(token))
        {
            var header = new AuthenticationHeaderValue("Bearer", token);
            request.AddHeader("Authorization", header.ToString());
        }
    }
}

API Services - Configuration

Web.Config entries is as shown below:

XML
<system.identitymodel>
    <identityconfiguration savebootstrapcontext="true">
      <audienceuris>
        
		
        <add value="http://www.xyz.com/yourapp">
        <add value="http://www.xyz.com/yourapp/api">
      </audienceuris>
      <securitytokenhandlers>
        <add type="System.IdentityModel.Tokens.JwtSecurityTokenHandler, 
        System.IdentityModel.Tokens.Jwt, Version=2.0.0.0, Culture=neutral, 
        PublicKeyToken=31bf3856ad364e35">
        <securitytokenhandlerconfiguration>
          <issuertokenresolver type="System.IdentityModel.
          Tokens.X509CertificateStoreTokenResolver, System.IdentityModel, 
          Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
          <certificatevalidation certificatevalidationmode="PeerOrChainTrust" 
          trustedstorelocation="LocalMachine" revocationmode="NoCheck">

          <issuernameregistry type="System.IdentityModel.Tokens.
          ValidatingIssuerNameRegistry, System.IdentityModel.Tokens.ValidatingIssuerNameRegistry, 
          Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
            <authority name="http://identityserver.v2.xyz.com/trust">
              <keys>
                <add thumbprint="C2B2219F3CAC53658E796C0402360D90AEFA08FC">
              </keys>
              <validissuers>
                <add name="http://identityserver.v2.xyz.com/trust">
              </validissuers>
            </authority>
          </issuernameregistry>
        </securitytokenhandlerconfiguration>
      </securitytokenhandlers>
      <claimsauthorizationmanager 
      type="IdentityServer.Demo.Common.Security.CustomAuthorizationManager, 
      IdentityServer.Demo.Common">
    </identityconfiguration>
  </system.identitymodel>
  <system.identitymodel.services>
    <federationconfiguration>
      <cookiehandler mode="Default" requiressl="false">
      <wsfederation realm="http://www.xyz.com/yourapp" 
      issuer="https://sts.xyz.com/issue/wsfed" 
      passiveredirectenabled="false" requirehttps="true">
    </federationconfiguration>
</system.identitymodel.services>

In Global.asax (Application_Start), we need to add a messageHandler so that each HTTP call is intercepted before getting processed. This handler will check the HTTP Authorization header and decrypts it and populates the current user principal with all the claims.

C#
protected void Application_Start()
{
    //Add a handler to interrupt the http call and read the 
    //http authorization header and populate the current user principal
    GlobalConfiguration.Configuration.MessageHandlers.Add(new TokenValidationHandler());

    AreaRegistration.RegisterAllAreas();

    WebApiConfig.Register(GlobalConfiguration.Configuration);
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);            
}

How to Check for Claims

I would prefer to use the ClaimsAuthorizeAttribute available in Thinktecture.IdentityModal (available on Nuget). Then it will be as simple as:

C#
[ClaimsAuthorize(IdentityServer.Demo.Common.Security.Claims.ClaimTypes.Manager)]
public string Get(int id)
{
    return "value";
}

You can also check for claims in code. Check the Get method in ValuesController. I have also provided some extension methods to help in checking the claims even easier.

When Your Session Cookie Becomes Too Big

Once your application becomes complex, so are the number of claims to handle. By default, all the claims are stored as part of the session cookie and browsers like Safari impose a restriction on the size of the cookie. So one fine day, when you add few more claims to the application, you will start getting serialization errors. That's because only partial cookie will be sent back to the server and server does not know what to do with it. So the solution for this problem is to create the security token in "Reference" mode. What it means is to store the token on the server and just store a reference session id as the cookie. See the image below. The cookie size is just few bytes:

Cookie Size

C#
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginModel model, string returnUrl)
{
    //if (ModelState.IsValid && WebSecurity.Login
    //(model.UserName, model.Password, persistCookie: model.RememberMe))
    //{
    //    return RedirectToLocal(returnUrl);
    //}
    if (ModelState.IsValid)
    {
        var cp = GetClaimsFromIdentityServer(model.UserName, model.Password);
        if (cp != null)
        {
            //All set so now create a SessionSecurityToken
            var token = new SessionSecurityToken(cp) { 
                                        IsReferenceMode = true  //this is important.
                                        //this is how you say create the token in 
                                        //reference mode meaning the your session cookie 
                                        //will contain only a referenceid(which is very small) 
                                        //and all claims will be stored on the server
                                        };
            FederatedAuthentication.WSFederationAuthenticationModule.
            SetPrincipalAndWriteSessionToken(token, true);
            
            return RedirectToLocal(returnUrl);
        }
    }
    // If we got this far, something failed, redisplay form
    ModelState.AddModelError
    ("", "The user name or password provided is incorrect.");
    return View(model);
}

SessionToken Caching

The Reference mode will work as long as you are on a single server instance scenario but it will not work when you have a web farm scenario because by default the cached tokens are stored in server memory. The solution to this problem is to cache the tokens in a custom data store. Again Thinktecture.IdentityModel is our friend here. All we need to do is to implement a simple interface and couple of lines of code added to Global.asax. I have provided implementations for caching the tokens in SQL Server/MongoDB/AppFabric. So you can choose whichever you want. In Global.asax, you need to add the below lines of code in Init method:

C#
protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    WebApiConfig.Register(GlobalConfiguration.Configuration);
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
    AuthConfig.RegisterAuth();

    AntiForgeryConfig.UniqueClaimTypeIdentifier = 
    ClaimTypes.Name; //http://brockallen.com/2012/07/08/mvc-4-antiforgerytoken-and-claims/

    //Configure the SessionSecurityTokenCache Provider 
    //PassiveSessionConfiguration.ConfigureSessionCache(new SqlTokenCacheRepository());
    PassiveSessionConfiguration.ConfigureSessionCache(new MongoTokenCacheRepository());
    //PassiveSessionConfiguration.ConfigureSessionCache(new AppFabricTokenCacheRepository());
    //PassiveSessionConfiguration.ConfigureSessionCache(new SessionTokenCacheRepository());
}
public override void Init()
{
    PassiveModuleConfiguration.CacheSessionsOnServer();
    PassiveModuleConfiguration.SuppressLoginRedirectsForApiCalls();
    base.Init();
}

Common Errors

Here are some common errors you will come across while doing this exercise and solutions for the same. WIF10201: No valid key mapping found for securityToken: 'System.IdentityModel.Tokens.X509SecurityToken' and issuer: 'http://identityserver.v2.xyz.com/trust'.

Just make sure you have used the correct thumbprint for your certificate in issuerNameRegistry entry in web.config:

XML
<issuernameregistry type="System.IdentityModel.Tokens.ValidatingIssuerNameRegistry, 
                 System.IdentityModel.Tokens.ValidatingIssuerNameRegistry, Version=2.0.0.0, 
                 Culture=neutral, PublicKeyToken=31bf3856ad364e35">
<authority name="http://identityserver.v2.xyz.com/trust">
  <keys>
    <add thumbprint="C2B2219F3CAC53658E796C0402360D90AEFA08FC"> 
  </keys>
  <validissuers>
    <add name="http://identityserver.v2.xyz.com/trust">
  </validissuers>
</authority>
</issuernameregistry>	

Security Token Error

ID4243: Could not create a SecurityToken. A token was not found in the token cache and no cookie was found in the context.

I have seen this error mostly during development where I keep stopping/starting the development web server. What it is saying is that there is a session cookie found but nothing available on the server token cache corresponding to that cookie. You can just delete all the cookies for the domain and you should be good to go.

Security Token Error

Points of Interest

I have kept all the common code in a separate project named IdenityServer.Demo.Common and I have written comments wherever I felt necessary. I have gathered all this information by going through many blogs and MSDN documentation and I have given references to most of them below. If I miss any one, that's totally unintentional and I am glad to add the reference if you let me know. Please let me know your comments and feedback.

Nuget Packages You Need

  • Thinktecture.IdentityServer.Core (For Extending Identity Server)
  • Thinktecture.IdentityModel
  • System.IdentityModel.Tokens.Jwt
  • System.IdentityModel.Tokens.ValidatingIssuerNameRegistry
  • ServerAppFabric.Client (If you want to use AppFabric for caching)
  • RestSharp
  • Newtonsoft.Json
  • mongocsharpdriver (If you want to use MongoDB for caching)

References

History

  • Initial version - 11/13/2013

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionInstalling IdentityServer3 Pin
prasanth377-May-15 2:55
prasanth377-May-15 2:55 
AnswerRe: Installing IdentityServer3 Pin
Azeet Chebrolu7-May-15 9:54
Azeet Chebrolu7-May-15 9:54 
QuestionSessiontoken life time is different then STS token life time Pin
h pal2-Jan-15 7:01
h pal2-Jan-15 7:01 
AnswerRe: Sessiontoken life time is different then STS token life time Pin
Azeet Chebrolu5-Jan-15 10:03
Azeet Chebrolu5-Jan-15 10:03 
QuestionExcellent stuff Pin
h pal21-Dec-14 23:39
h pal21-Dec-14 23:39 
AnswerRe: Excellent stuff Pin
Azeet Chebrolu22-Dec-14 5:58
Azeet Chebrolu22-Dec-14 5:58 
QuestionExcellent Article. Really excited to explore it. Pin
Rampally Pavan26-Oct-14 10:21
Rampally Pavan26-Oct-14 10:21 
AnswerRe: Excellent Article. Really excited to explore it. Pin
Azeet Chebrolu26-Oct-14 12:33
Azeet Chebrolu26-Oct-14 12:33 
QuestionBrilliant Article Pin
lorensd18-Sep-14 6:30
lorensd18-Sep-14 6:30 
AnswerRe: Brilliant Article Pin
Azeet Chebrolu18-Sep-14 8:57
Azeet Chebrolu18-Sep-14 8:57 
GeneralRe: Brilliant Article Pin
lorensd19-Sep-14 6:58
lorensd19-Sep-14 6:58 
GeneralRe: Brilliant Article Pin
Azeet Chebrolu19-Sep-14 8:25
Azeet Chebrolu19-Sep-14 8:25 
AnswerRe: Brilliant Article Pin
lorensd22-Sep-14 6:14
lorensd22-Sep-14 6:14 
GeneralRe: Brilliant Article Pin
Azeet Chebrolu22-Sep-14 7:11
Azeet Chebrolu22-Sep-14 7:11 
GeneralRe: Brilliant Article Pin
lorensd23-Sep-14 1:21
lorensd23-Sep-14 1:21 
AnswerRe: Brilliant Article Pin
Azeet Chebrolu23-Sep-14 6:52
Azeet Chebrolu23-Sep-14 6:52 
GeneralRe: Brilliant Article Pin
lorensd23-Sep-14 4:08
lorensd23-Sep-14 4:08 
GeneralRe: Brilliant Article Pin
Azeet Chebrolu23-Sep-14 7:01
Azeet Chebrolu23-Sep-14 7:01 
GeneralRe: Brilliant Article Pin
lorensd24-Sep-14 4:05
lorensd24-Sep-14 4:05 
AnswerRe: Brilliant Article Pin
Azeet Chebrolu24-Sep-14 4:54
Azeet Chebrolu24-Sep-14 4:54 
GeneralRe: Brilliant Article Pin
lorensd25-Sep-14 1:57
lorensd25-Sep-14 1:57 
GeneralRe: Brilliant Article Pin
Azeet Chebrolu25-Sep-14 14:01
Azeet Chebrolu25-Sep-14 14:01 
GeneralRe: Brilliant Article Pin
lorensd25-Sep-14 21:54
lorensd25-Sep-14 21:54 
GeneralRe: Brilliant Article Pin
lorensd16-Oct-14 2:56
lorensd16-Oct-14 2:56 
AnswerRe: Brilliant Article Pin
Azeet Chebrolu16-Oct-14 8:14
Azeet Chebrolu16-Oct-14 8:14 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.