Freigeben über


How do we use custom HTTP MessageHandler for validating multiple token issuers?

Before we get in to the solution of how we can handle multiple token issuer, I’m going to bring up some of the fundamentals of how HTTP request/Response is handler to make a lot of sense out of the proposed solution.

Whenever a HTTP request is made, the call goes through the HTTP message handler and returns the HTTP response.HttpMessageHandler abstract class is the base class for message Handler.

clip_image001

I sniped the above picture form here which clearly explains how Message Handlers can be hooked inside the pipeline. As we notice, we can have multiple such custom message handler which we can chain it together. The first handler receives an HTTP request, does some processing, and gives the request to the next handler. At some point, the response is created and goes back up the chain. This pattern is called a delegating handler.

Our solution to go handle multiple token issuers is entirely based on HTTP MessageHandlers. We inherit DelegateHandler class and access the received token, we then will be able to go validate it based on the different issuers.

This technique is also followed basically to hook in to the request response and compress the message Smile, well that’s outside the discussion that we are having here.

Going back straight to the how we implement the message Handler especially with the introduction of OWIN middleware , it would be better for me to first summarize the steps before I include the code ( obviously without code this would be incomplete Smile )

  • Install the required Owin nugets (we also need Microsoft.Owin.Security.Jwt for Jwt token validation )
  • Override SendAsunc() method, here is where our complete code of validation will reside.
  • Switch the validation process based on the different issuers ( In the sample I have include token that is issued by ACS and AAD )
  • Set the claim principal post validation.
  • That’s it, we are good to go have a look at the code.
  • One more thing we should remember is to register the custom Token handler to the webAPIconfig, this come be done simply by adding the following piece of code in the webAPIConfig
 
    1: config.MessageHandlers.Add(new TokenValidationHandler());

coming to the TokenVaidationHandler class, this is how it goes;

    1: internal class TokenValidationHandler : DelegatingHandler
    2:   {
    3:       //
    4:       // SendAsync checks that incoming requests have a valid access token, and sets the current user identity using that access token.
    5:       //
    6:       protected async override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    7:       {
    8:  
    9:           try
   10:           {
   11:               String headerValue = HttpContext.Current.Request.Headers["Authorization"];
   12:               if (headerValue != null)
   13:               {
   14:                   JwtSecurityTokenHandler jwt_tokenhandler = new JwtSecurityTokenHandler();
   15:                   string[] tokenarray;
   16:                   string token;
   17:                   if (headerValue.Contains("Bearer"))
   18:                   {
   19:                       tokenarray = headerValue.Substring("Bearer ".Length).Split(new char[] { '=' }, 2);
   20:                       token = tokenarray[0];
   21:  
   22:                   }
   23:                   else
   24:                   {
   25:  
   26:                       tokenarray = headerValue.Substring("WRAP ".Length).Split(new char[] { '=' }, 2);
   27:                       token = tokenarray[1].Substring(1, tokenarray[1].Length - 2);
   28:  
   29:  
   30:                   }
   31:  
   32:                   string issuer = "";
   33:                   TokenValidationParameters parms = new System.IdentityModel.Tokens.TokenValidationParameters();
   34:                   string aadInstance = "<aadInstance for login,this should have the domain information as well> ";
   35:                   string tenant = "<mydomain>.onmicrosoft.com";
   36:                   string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
   37:                   string metadataAddress = "";
   38:  
   39:  
   40:                   if (token.Contains("Issuer"))
   41:                   {
   42:                       // Check that it starts with 'WRAP'
   43:                       if (!headerValue.StartsWith("WRAP "))
   44:                       {
   45:                           return await Task.Factory.StartNew(() =>
   46:                           {
   47:                               return new HttpResponseMessage(HttpStatusCode.Unauthorized)
   48:                               {
   49:                                   Content = new StringContent("Invalid token")
   50:                               };
   51:                           });
   52:                       }
   53:  
   54:                       string[] nameValuePair = headerValue.Substring("WRAP ".Length).Split(new char[] { '=' }, 2);
   55:  
   56:                       if (nameValuePair.Length != 2 ||
   57:                           nameValuePair[0] != "access_token" ||
   58:                           !nameValuePair[1].StartsWith("\"") ||
   59:                           !nameValuePair[1].EndsWith("\""))
   60:                       {
   61:                           throw new ApplicationException("unauthorized");
   62:                       }
   63:  
   64:                   }
   65:                   else
   66:                   {
   67:  
   68:                       JwtSecurityToken secToken = jwt_tokenhandler.ReadToken(token) as JwtSecurityToken;
   69:                       if (secToken != null)
   70:                       {
   71:                           issuer = secToken.Issuer;
   72:                       }
   73:                       if (issuer == "https://<my-acs>.accesscontrol.windows.net/")
   74:                       {
   75:  
   76:  
   77:                           metadataAddress = "https://<my-acs>.accesscontrol.windows.net/FederationMetadata/2007-06/FederationMetadata.xml";
   78:                           List<X509SecurityToken> signingTokens;
   79:                           GetTenantInformation(metadataAddress, out issuer, out signingTokens);
   80:  
   81:                           parms.ValidIssuer = issuer;
   82:                           parms.CertificateValidator = System.IdentityModel.Selectors.X509CertificateValidator.None;
   83:                           parms.IssuerSigningTokens = signingTokens;
   84:                           parms.ValidAudience = "<valid ACS on-boarded  Audience>";
   85:  
   86:  
   87:  
   88:                       }
   89:                       else
   90:                       {
   91:                           string stsDiscoveryEndpoint = string.Format("{0}/.well-known/openid-configuration", authority);
   92:                           // Get tenant information that's used to validate incoming jwt tokens
   93:                           ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint);
   94:                           OpenIdConnectConfiguration config = await configManager.GetConfigurationAsync();
   95:                           List<SecurityToken> signingTokens = config.SigningTokens.ToList();
   96:  
   97:                           parms.ValidIssuer = "https://sts.windows.net/<respective guid>/";
   98:                           parms.CertificateValidator = System.IdentityModel.Selectors.X509CertificateValidator.None;
   99:                           parms.IssuerSigningTokens = signingTokens;
  100:                           parms.ValidAudience = "<valid AAD on-boarded Audience>";
  101:                       }
  102:  
  103:                       SecurityToken validatedToken = new JwtSecurityToken();
  104:  
  105:                       var principal = jwt_tokenhandler.ValidateToken(token, parms, out validatedToken);
  106:                       // Set the ClaimsPrincipal on HttpContext.Current if the app is running in web hosted environment.
  107:                       if (HttpContext.Current != null)
  108:                       {
  109:                           HttpContext.Current.User = principal;
  110:                       }
  111:                       System.Threading.Thread.CurrentPrincipal = principal;
  112:                   }
  113:  
  114:                   
  115:               }
  116:               return await base.SendAsync(request, cancellationToken);
  117:  
  118:           }
  119:           catch (Exception ex)
  120:           {
  121:               HttpResponseMessage response = BuildResponseErrorMessage(HttpStatusCode.Unauthorized);
  122:               return response;
  123:  
  124:           }
  125:  
  126:  
  127:       }

For extracting the tenant issuer information form the signing in token we can use the below code which parses the token against the MetadataAddress (for accessing the certificate) and sets the issuer and the signingTokens.

    1: static void GetTenantInformation(string metadataAddress, out string issuer, out List<X509SecurityToken> signingTokens)
    2:  
    3:       {
    4:  
    5:           signingTokens = new List<X509SecurityToken>();
    6:  
    7:           issuer = null;
    8:  
    9: MetadataSerializer serializer = new MetadataSerializer()
   10:  
   11:           {
   12:  
   13: //CertificateValidationMode = X509CertificateValidationMode.None
   14:  
   15:           };
   16:  
   17: MetadataBase metadata = serializer.ReadMetadata(XmlReader.Create(metadataAddress));
   18:  
   19: EntityDescriptor entityDescriptor = (EntityDescriptor)metadata;
   20:  
   21: // get the issuer name 
   22:  
   23: if (!string.IsNullOrWhiteSpace(entityDescriptor.EntityId.Id))
   24:  
   25:           {
   26:  
   27:               issuer = entityDescriptor.EntityId.Id;
   28:  
   29:           }
   30:  
   31: // get the signing certs 
   32:  
   33:           signingTokens = ReadSigningCertsFromMetadata(entityDescriptor);
   34:  
   35:       }
   36:  

In order for us to read the signing Certs from the entityDesciptor from the above code we use the below method

    1: static List<X509SecurityToken> ReadSigningCertsFromMetadata(EntityDescriptor entityDescriptor)
    2:  
    3:       {
    4:  
    5: List<X509SecurityToken> stsSigningTokens = new List<X509SecurityToken>();
    6:  
    7: SecurityTokenServiceDescriptor stsd = entityDescriptor.RoleDescriptors.OfType<SecurityTokenServiceDescriptor>().First();
    8:  
    9: if (stsd != null && stsd.Keys != null)
   10:  
   11:           {
   12:  
   13: IEnumerable<X509RawDataKeyIdentifierClause> x509DataClauses = stsd.Keys.Where(key => key.KeyInfo != null && (key.Use == KeyType.Signing || key.Use == KeyType.Unspecified)).
   14:  
   15:                                                            Select(key => key.KeyInfo.OfType<X509RawDataKeyIdentifierClause>().First());
   16:  
   17:               stsSigningTokens.AddRange(x509DataClauses.Select(clause => new X509SecurityToken(new X509Certificate2(clause.GetX509RawData()))));
   18:  
   19:           }
   20:  
   21: else
   22:  
   23:           {
   24:  
   25: throw new InvalidOperationException("There is no RoleDescriptor of type SecurityTokenServiceType in the metadata");
   26:  
   27:           }
   28:  
   29: return stsSigningTokens;
   30:  
   31:       }
   32:  

The below code is basically to Build the response Message based on the HTTP statusCode.

    1: private HttpResponseMessage BuildResponseErrorMessage(HttpStatusCode statusCode)
    2:  
    3:       {
    4:  
    5: HttpResponseMessage response = new HttpResponseMessage(statusCode);
    6:  
    7: //
    8:  
    9: // The Scheme should be "Bearer", authorization_uri should point to the tenant url and resource_id should point to the audience.
   10:  
   11: //
   12:  
   13: AuthenticationHeaderValue authenticateHeader = new AuthenticationHeaderValue("Bearer", "unathorized");
   14:  
   15:           response.Headers.WwwAuthenticate.Add(authenticateHeader);
   16:  
   17: return response;
   18:  
   19:       }
   20:  
   21:   }

Connecting the above Token Validator code we should be able to handle multiple token issuers in one Message handler. This might be of some use to those who are trying to handle multiple issuers Smile. Please note that this is not a production ready code and the sole purpose of this is to depict how we can hook in Message handler for token validation. This would need further testing based on the varied scenario before its been used.

Happy Coding!

^Ganesh Shankaran