Skip to content

JWT Token

txgz999 edited this page Aug 7, 2019 · 16 revisions

We previously discussed the resource owner password credentials grant flow, where the authorization server and the resource server share the same machine. In that case, the bearer token can be generated by the authorization server using the machine key and can be consumed by the resource server. If these two servers belong to different company or owner, then sharing machine key becomes impossible. In that situation, we can use another kind of bearer token, called JWT (Json Web Token). In this approach, the authorization server and the resource server can share a secret string, the generating and consuming the token is based on that secret string. Multiple resource servers can each share a different secret with the authorization server. Such secret can be created when a resource server registers itself with the authorization server.

The consuming of the JWT token can be handled by the Microsoft.Owin.Security.Jwt package, but Microsoft does not provide a way to generate JWT token, we need to rely on third-party tools or implement it by ourselves.

Authorization Server

  • add reference to System.IdentityModel.dll
  • install the following packages:
    • Microsoft.Owin.Host.SystemWeb
    • Microsoft.Owin.Security.OAuth
    • System.IdentityModel.Tokens.Jwt -Version 4.0.0
  • add registered client/audience list:
public class AppConfiguration {
    public static string TokenIssuer => "JWTAuthServer";

    public static Audience[] Audiences = new Audience[] {
        new Audience {
            TokenAudienceId = "099153c2625149bc8ecb3e85e03f0022",
            TokenAudienceSecret = "IxrAjDoa2FqElO7IhrSrUJELhUckePEPVpaePlS_Xaw",
        }
    };
}

public class Audience {
    public string TokenAudienceId { get; set; }
    public string TokenAudienceSecret { get; set; }
}
  • add the provider that handles the event for requesting access token:
public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider {
    public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) {
        string clientId = string.Empty;
        string clientSecret = string.Empty;
        string symmetricKeyAsBase64 = string.Empty;

        if (!context.TryGetBasicCredentials(out clientId, out clientSecret)) {
            context.TryGetFormCredentials(out clientId, out clientSecret);
        }

        if (context.ClientId == null) {
            context.SetError("invalid_clientId", "client_Id is not set");
            return Task.FromResult<object>(null);
        }

        var audience = AppConfiguration.Audiences.FirstOrDefault(a => a.TokenAudienceId == context.ClientId);
        if (audience == null) {
            context.SetError("invalid_clientId", string.Format("Invalid client_id '{0}'", context.ClientId));
            return Task.FromResult<object>(null);
        }

        context.Validated();
        return Task.FromResult<object>(null);
    }

    public override Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context) {
        if (context.UserName != "test" || context.Password != "test") {
            context.SetError("invalid_grant", "The user name or password is incorrect.");
            return Task.FromResult<object>(null);
        }

        var identity = new ClaimsIdentity(context.Options.AuthenticationType);
        identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
        identity.AddClaim(new Claim("sub", context.UserName));
        identity.AddClaim(new Claim("role", "user"));

        var props = new AuthenticationProperties(new Dictionary<string, string> {
            { "audience", (context.ClientId == null) ? string.Empty : context.ClientId }
        });

        var ticket = new AuthenticationTicket(identity, props);
        context.Validated(ticket);
        return Task.FromResult<object>(null);
    }
}
  • add the class that generate JWT token:
public class JWTFormat : ISecureDataFormat<AuthenticationTicket> {
    private readonly string _issuer = string.Empty;

    public JWTFormat(string issuer) { _issuer = issuer; }

    public string SignatureAlgorithm {
        get { return "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256"; }
    }

    public string DigestAlgorithm {
        get { return "http://www.w3.org/2001/04/xmlenc#sha256"; }
    }

    public string Protect(AuthenticationTicket data) {
        if (data == null) throw new ArgumentNullException("data");
 
        string audienceId = data.Properties.Dictionary.ContainsKey("audience") ? data.Properties.Dictionary["audience"] : null;
        if (string.IsNullOrWhiteSpace(audienceId)) throw new InvalidOperationException("AuthenticationTicket.Properties does not include audience");

        var audience = AppConfiguration.Audiences.FirstOrDefault(a => a.TokenAudienceId == audienceId);

        var keyByteArray = TextEncodings.Base64Url.Decode(audience.TokenAudienceSecret);
        var signingKey = new SigningCredentials(new InMemorySymmetricSecurityKey(keyByteArray),
                                                SignatureAlgorithm, DigestAlgorithm);

        var issued = data.Properties.IssuedUtc;
        var expires = data.Properties.ExpiresUtc;
        var token = new JwtSecurityToken(_issuer, audienceId, data.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime, signingKey);
        var handler = new JwtSecurityTokenHandler();
        var jwt = handler.WriteToken(token);
        return jwt;
    }

    public AuthenticationTicket Unprotect(string tokenString) {
        throw new NotImplementedException();
    }
}
  • add Startup.cs
public class Startup {
    public void Configuration(IAppBuilder app) {
        ConfigureOAuth(app);
    }
    
    public void ConfigureOAuth(IAppBuilder app) {
        var oAuthServerOptions = new OAuthAuthorizationServerOptions() {
            AllowInsecureHttp = true,
            TokenEndpointPath = new PathString("/token"),
            AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
            Provider = new SimpleAuthorizationServerProvider(),
            AccessTokenFormat = new JWTFormat(AppConfiguration.TokenIssuer),
        };
        app.UseOAuthAuthorizationServer(oAuthServerOptions); //Token Generation
    }
}

What makes the access token to be in the JWT format is the set to the property AccessTokenFormat, according to https://github.com/aspnet/AspNetKatana/blob/dev/src/Microsoft.Owin.Security.OAuth/OAuthAuthorizationServerOptions.cs:

AccessTokenFormat: The data format used to protect the information contained in the access token. If not provided by the application the default data protection provider depends on the host server. The SystemWeb host on IIS will use ASP.NET machine key data protection, and HttpListener and other self-hosted servers will use DPAPI data protection. If a different access token provider or format is assigned, a compatible instance must be assigned to the OAuthBearerAuthenticationOptions.AccessTokenProvider or OAuthBearerAuthenticationOptions.AccessTokenFormat property of the resource server.

Resource Server

  • install the following packages:
    • Microsoft.Owin.Host.SystemWeb
    • Microsoft.Owin.Security.Jwt
    • Microsoft.AspNet.WebApi
    • Microsoft.AspNet.WebApi.Owin
  • store client id and secret:
public class AppConfiguration {
    public static string TokenIssuer => "JWTAuthServer";

    public static Audience Audience = new Audience {
        TokenAudienceId = "099153c2625149bc8ecb3e85e03f0022",
        TokenAudienceSecret = "IxrAjDoa2FqElO7IhrSrUJELhUckePEPVpaePlS_Xaw",
    };
}

public class Audience {
    public string TokenAudienceId { get; set; }
    public string TokenAudienceSecret { get; set; }
}
  • create Startup.cs:
public class Startup {
    public void Configuration(IAppBuilder app) {
        ConfigureJWTConsumption(app);
        HttpConfiguration config = new HttpConfiguration();
        WebApiConfig.Register(config);
        app.UseWebApi(config);
    }

    private void ConfigureJWTConsumption(IAppBuilder app) {
        var issuer = AppConfiguration.TokenIssuer;
        string audienceId = AppConfiguration.Audience.TokenAudienceId;
        byte[] audienceSecret = TextEncodings.Base64Url.Decode(AppConfiguration.Audience.TokenAudienceSecret);

        app.UseJwtBearerAuthentication(new JwtBearerAuthenticationOptions {
            AuthenticationMode = AuthenticationMode.Active,
            AllowedAudiences = new[] { audienceId },                      
            IssuerSecurityKeyProviders = new IIssuerSecurityKeyProvider[] {
                new SymmetricKeyIssuerSecurityKeyProvider(issuer, audienceSecret)
            }
        });
    }
}
  • create WebApiConfig.cs and TestController.cs same as before

Test From Postman

  • send a post request with the following content to the /token endpoint of authorization server:
grant_type=password&username=test&password=test&client_id=099153c2625149bc8ecb3e85e03f0022

and the response is

{
    "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1bmlxdWVfbmFtZSI6InRlc3QiLCJzdWIiOiJ0ZXN0Iiwicm9sZSI6InVzZXIiLCJpc3MiOiJKV1RBdXRoU2VydmVyIiwiYXVkIjoiMDk5MTUzYzI2MjUxNDliYzhlY2IzZTg1ZTAzZjAwMjIiLCJleHAiOjE1NjUwNjY2MTksIm5iZiI6MTU2NDk4MDIxOX0.FYnfOYvwMiIfDH-1LC9oZGnb9jEkewk84F4QkHI0uio",
    "token_type": "bearer",
    "expires_in": 86399
}
  • send the following Get request to the /api/test endpoint of the resource server, with the bearer header set to the access_token value got above, and the response is "Hello World".

Generate JWT Outside Authorization Server

Followed Denis Pashkov's article Create and Consume JWT Tokens in C#, I have created a console application that generates JWT tokens that can be consumed by the resource server defined above. In this way, we can potentially use the resource server without the need of an authorization server.

Create a console app, add reference to System.IdentityModel and System.Security, then install packages

  • System.IdentityModel.Tokens.Jwt -version 4.0.0
  • Microsoft.Owin.Security
using Microsoft.Owin.Security.DataHandler.Encoder;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens;
using System.Security.Claims;

class Program {
    static void Main(string[] args) {
        string SignatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256"; 
        string DigestAlgorithm = "http://www.w3.org/2001/04/xmlenc#sha256"; 

        var keyByteArray = TextEncodings.Base64Url.Decode(AppConfiguration.Audience.TokenAudienceSecret);
        var signingKey = new SigningCredentials(new InMemorySymmetricSecurityKey(keyByteArray),
                                                SignatureAlgorithm, DigestAlgorithm);
        var token = new JwtSecurityToken(AppConfiguration.TokenIssuer, AppConfiguration.Audience.TokenAudienceId, new List<Claim> {
            new Claim("sub", "test"),
            new Claim("role", "user")
        }, DateTime.Now, DateTime.Now.AddHours(1), signingKey);

        var handler = new JwtSecurityTokenHandler();
        var tokenString = handler.WriteToken(token);
        Console.WriteLine(tokenString);
        Console.ReadLine();
    }
}
public class AppConfiguration {
    public static string TokenIssuer => "JWTAuthServer";

    public static Audience Audience = new Audience {
        TokenAudienceId = "099153c2625149bc8ecb3e85e03f0022",
        TokenAudienceSecret = "IxrAjDoa2FqElO7IhrSrUJELhUckePEPVpaePlS_Xaw",
    };
}

public class Audience {
    public string TokenAudienceId { get; set; }
    public string TokenAudienceSecret { get; set; }
}

The reason of using the package Microsoft.Owin.Security is to decode a base64 string as in TextEncodings.Base64Url.Decode. If we prefer not to install that package, we can create own equivalent method:

public static byte[] Base64UrlDecode(string base64String) {
    base64String = base64String.Replace("-", "+").Replace("_", "/");
    var base64 = Encoding.ASCII.GetBytes(base64String);
    var padding = base64.Length * 3 % 4;
    if (padding != 0) {
        base64String = base64String.PadRight(base64String.Length + padding, '=');
    }
    return Convert.FromBase64String(base64String);
}

Then the following line of code

var keyByteArray = TextEncodings.Base64Url.Decode(AppConfiguration.Audience.TokenAudienceSecret);

can be replaced by

var keyByteArray = Base64UrlDecode(AppConfiguration.Audience.TokenAudienceSecret);

For details, see https://stackoverflow.com/questions/41685931/textencodings-base64url-decode-vs-convert-frombase64string and https://stackoverflow.com/questions/26353710/how-to-achieve-base64-url-safe-encoding-in-c

Resources

Clone this wiki locally