.netwifjwt

How to configure Microsoft JWT with symmetric key?


I'm trying to configure my ASP.NET app to accept a JSON Web Token (JWT) that is signed with a symmetric key. The STS isn't capable of using certificates for this, so we're using their symmetric key support.

On my end, I'm using Microsoft's JWT Developer Preview. Unfortunately, I've not seen any examples of how to use that with a symmetric key. After some digging around with various tools, I found the NamedKeyIssuerTokenResolver and discovered that I can configure it to use a symmetric key. For example:

<securityTokenHandlers>
  <add type="Microsoft.IdentityModel.Tokens.JWT.JWTSecurityTokenHandler,Microsoft.IdentityModel.Tokens.JWT" />
  <securityTokenHandlerConfiguration>
    <certificateValidation certificateValidationMode="PeerTrust" />
    <issuerTokenResolver
      type="Microsoft.IdentityModel.Tokens.JWT.NamedKeyIssuerTokenResolver,
        Microsoft.IdentityModel.Tokens.JWT">
      <securityKey
          symmetricKey="+zqf97FD/xyzzyplugh42ploverFeeFieFoeFooxqjE="
             name="https://localhost/TestRelyingParty" />
    </issuerTokenResolver>
  </securityTokenHandlerConfiguration>
</securityTokenHandlers>

I'm not entirely sure what I'm supposed to use for the name there. Should it be the audience Uri, perhaps the issuer Uri? In any case, I know that if I don't include a name, I get an exception when my program starts because the securityKey element requires that attribute.

Whatever the case, this still doesn't resolve the issue. After I authenticate against the STS, I get the following exception:

[SecurityTokenValidationException: JWT10310: Unable to validate signature. validationParameters.SigningTokenResolver type: 'Microsoft.IdentityModel.Tokens.JWT.NamedKeyIssuerTokenResolver', was unable to resolve key to a token.
The SecurityKeyIdentifier is: 
'SecurityKeyIdentifier
    (
    IsReadOnly = False,
    Count = 1,
    Clause[0] = Microsoft.IdentityModel.Tokens.JWT.NamedKeyIdentifierClause
    )
'. validationParameters.SigningToken was null.]
   Microsoft.IdentityModel.Tokens.JWT.JWTSecurityTokenHandler.ValidateSignature(JWTSecurityToken jwt, TokenValidationParameters validationParameters) +2111
   Microsoft.IdentityModel.Tokens.JWT.JWTSecurityTokenHandler.ValidateToken(JWTSecurityToken jwt, TokenValidationParameters validationParameters) +138
   Microsoft.IdentityModel.Tokens.JWT.JWTSecurityTokenHandler.ValidateToken(SecurityToken token) +599
   System.IdentityModel.Tokens.SecurityTokenHandlerCollection.ValidateToken(SecurityToken token) +135
   System.IdentityModel.Services.TokenReceiver.AuthenticateToken(SecurityToken token, Boolean ensureBearerToken, String endpointUri) +117
   System.IdentityModel.Services.WSFederationAuthenticationModule.SignInWithResponseMessage(HttpRequestBase request) +698
   System.IdentityModel.Services.WSFederationAuthenticationModule.OnAuthenticateRequest(Object sender, EventArgs args) +123924
   System.Web.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +80
   System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +165

Am I missing some other configuration step? Am I putting the wrong thing in the name attribute? Or is this a known bug in the JWT Developer Preview?


Solution

  • Update 2014/02/13:

    As @leastprivilege points out below, this is a whole lot easier with the RTM version of the JWT. I strongly suggest that you ignore this and go with the example he provides at http://leastprivilege.com/2013/07/16/identityserver-using-ws-federation-with-jwt-tokens-and-symmetric-signatures/.

    Note that the original answer below was for the Beta version, Microsoft.IdentityModel.Tokens.JWT. Upgrading to the release version, System.IdentityModel.Tokens.Jwt, required just a little more work. See below.

    The primary problem turns out to be that the method JWTSecurityTokenHandler.ValidateToken(token) does not fully populate the TokenValidationParameters that it passes to JWTSecurityTokenHandler.ValidateToken(token, validationParameters). In particular, it doesn't populate the SigningToken member or the ValidIssuers (or ValidIssuer).

    Interestingly, the configuration I showed in my original question actually is loaded by the token resolver, and is available at runtime, as you can see in the code below.

    I don't know how to specify the valid issuer string in the configuration file, though. I strongly suspect that there's a place to put that info, but I haven't yet figured out where it belongs.

    The solution to my problem is to create a custom security token handler that derives from JWTSecurityTokenHandler. Overriding ValidateToken(token, validationParameters) gives me the opportunity to set those parameters that I need, and then call the base class's ValidateToken method.

    public class CustomJwtSecurityTokenHandler: JWTSecurityTokenHandler
    {
        // Override ValidateSignature so that it gets the SigningToken from the configuration if it doesn't exist in
        // the validationParameters object.
        private const string KeyName = "https://localhost/TestRelyingParty";
        private const string ValidIssuerString = "https://mySTSname/trust";
        public override ClaimsPrincipal ValidateToken(JWTSecurityToken jwt, TokenValidationParameters validationParameters)
        {
            // set up valid issuers
            if ((validationParameters.ValidIssuer == null) &&
                (validationParameters.ValidIssuers == null || !validationParameters.ValidIssuers.Any()))
            {
                validationParameters.ValidIssuers = new List<string> {ValidIssuerString};
            }
            // and signing token.
            if (validationParameters.SigningToken == null)
            {
                var resolver = (NamedKeyIssuerTokenResolver)this.Configuration.IssuerTokenResolver;
                if (resolver.SecurityKeys != null)
                {
                    List<SecurityKey> skeys;
                    if (resolver.SecurityKeys.TryGetValue(KeyName, out skeys))
                    {
                        var tok = new NamedKeySecurityToken(KeyName, skeys);
                        validationParameters.SigningToken = tok;
                    }
                }
            }
            return base.ValidateToken(jwt, validationParameters);
        }
    }
    

    In my Web.config, I just had to change the security token handler:

      <securityTokenHandlers>
        <!--<add type="Microsoft.IdentityModel.Tokens.JWT.JWTSecurityTokenHandler,Microsoft.IdentityModel.Tokens.JWT" />-->
        <!-- replaces the default JWTSecurityTokenHandler -->
        <add type="TestRelyingParty.CustomJwtSecurityTokenHandler,TestRelyingParty" />
    

    Nothing like spending three or four days researching a problem that is solved with a couple dozen lines of code . . .

    Addition for new version

    In June of 2013, Microsoft officially released their JWT. They changed the namespace to System.IdentityModel.Tokens.Jwt. After upgrading to that, the solution above stopped working. To get it working, I had to add the following to my CustomJwtSecurityTokenHandler. That's in addition to the existing code.

    public override ClaimsPrincipal ValidateToken(JwtSecurityToken jwt)
    {
        var vparms = new TokenValidationParameters
            {
                AllowedAudiences = Configuration.AudienceRestriction.AllowedAudienceUris.Select(s => s.ToString())
            };
        return ValidateToken(jwt, vparms);
    }