JWT(JSON Web Tokens) as your authenticator in Sitecore

Hello all πŸ™‚
I would like to share with you guys how to use JWT and take it a step further in your Sitecore application. JWT has been part of Sitecore since the 8.2 release, so it’s nothing new but it’s still very cool.

JWT – JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties

There are some great posts about using JWT in Sitecore
Token-based authorization with Sitecore Services Client
Sitecore WebAPI: ServicesApiController and JWT Token Security

So what is so special about it? Well, it contains encrypted user data. So we could use it as a session in a none session environment(where the token will act as a “client cookie”). But in this case, we will use it as an identifier and authenticator. Which will be sent back and forth between rest api(in our case) and client(mobile app).

In Sitecore, the JWT is used for a special user – sitecore\ServicesAPI.
Let us take it a step further, why not use it for all users.

Another thing is that Sitecore only stores the user in the token. Again why stop there? We could put whatever we want. In this case, we will add first name and last name together with the user.

Ok, so how do we do this? First, we look at how Sitecore implemented JWT. Then we grab the juicy parts πŸ™‚

In order to use the JWT implementation in Sitecore, we need the following packages:
Sitecore.Services.Infrastructure.Noreferences
Sitecore.Services.Infrastructure.Sitecore.Noreferences
Sitecore.Services.Core.Noreferences

Next thing is to look into the Login action in the Sitecore.Services.Infrastructure.Sitecore.Mvc.ServicesAuthenticationController, it’s the one who returns the jwt token:
https://host/sitecore/api/scc/auth/login

[HttpPost]
[RequireHttps]
public ActionResult Login(string domain, string username, string password)
{
  try
  {
	this._userService.Login(domain, username, password);
	string token = this._tokenProvider.GenerateToken((IEnumerable<Claim>) new Claim[1]
	{
	  new Claim("User", Context.User.Name)
	});
	if (token == null)
	  return (ActionResult) new HttpStatusCodeResult(HttpStatusCode.OK);
	this.Response.StatusCode = 200;
	return (ActionResult) new JsonResult()
	{
	  Data = (object) new{ token = token }
	};
  }
  catch (ArgumentException ex)
  {
	return (ActionResult) new HttpStatusCodeResult(HttpStatusCode.BadRequest);
  }
  catch (AuthenticationException ex)
  {
	return (ActionResult) new HttpStatusCodeResult(HttpStatusCode.Forbidden);
  }
}

Notice the UserService that makes the login, we will probably use that.
And look at our crown jewel, _tokenProvider.GenerateToken – That is the token generator. Here they also add the “User attribute” as a “Claim”. Great stuff πŸ™‚
Ok so we have what we need, now we can do our own Login action.

This is a “typical” Habitat project, that means we will use Sitecore.Services.Infrastructure.Sitecore.DependencyInjection. So what services/providers need to be injected into our controller?
Let’s start with the IUserService(it’s injected into the Sitecore.Services.Infrastructure.Sitecore.Mvc.ServicesAuthenticationController):

public ServicesAuthenticationController(IUserService userService)
{
  this._userService = userService;
}

Now if we check furher, we will find the UserService being instantiated in Sitecore.Services.Infrastructure.Sitecore.DependencyInjection.ComponentServicesConfigurator. Jackpot!

public void Configure(IServiceCollection serviceCollection)
{
  serviceCollection.AddScoped<IUserService, UserService>().AddScoped<IExceptionLogger, SitecoreExceptionLogger>().AddScoped<SitecoreExceptionLogger, SitecoreExceptionLogger>().AddScoped<ILogger, SitecoreLogger>().AddScoped<IHandlerProvider, HandlerProvider>().AddScoped<IEntityValidator, EntityValidator>();
  ...
}

Wonderful, that means it will be instantiated per request.
Scoped is a single instance for the duration of the scoped request, which means per HTTP request in ASP.NET

Great stuff, so in our controller we just need to put IUserService in the constructor:

private readonly IUserService _userService;  

public CustomJwtAuthenticatorController(IUserService userService)
{
  _userService = userService;
}

Ok let’s move on, we should also try to inject the _tokenProvider. How did Sitecore do? Let’s look into Sitecore.Services.Infrastructure.Sitecore.Mvc.ServicesAuthenticationController:

public ServicesAuthenticationController()
      : this((IUserService) new UserService(), (ITokenProvider) new ConfiguredOrNullTokenProvider((ITokenProvider) new SigningTokenProvider()))
    {
    }

Unfortunately no injection here…
* Notice the β€œnewing” of the UserService, it should not be necessary since it already is instantiated per request

So in order for us to inject the ITokenProvider, we need to create a ServicesConfigurator class in our project(We are going for the Habitat approach, that means we use the Sitecore.Foundation.DependencyInjection). It will instantiate the ITokenProvider at startup, we will do it as a Singleton.

namespace SandBox.Foundation.Authenticator
{
  using Microsoft.Extensions.DependencyInjection;
  using Sitecore.DependencyInjection;
  using Sitecore.Services.Infrastructure.Sitecore.Security;
  using Sitecore.Services.Infrastructure.Web.Http.Security;

  public class ServicesConfigurator : IServicesConfigurator
  {
    public void Configure(IServiceCollection serviceCollection)
    {
      serviceCollection.AddSingleton<ITokenProvider>(provider => new ConfiguredOrNullTokenProvider((ITokenProvider)new SigningTokenProvider()));
    }
  }
}

We will also need to add the proper configuration for it, here is our config patch:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <services>
      <configurator type="SandBox.Foundation.Authenticator.ServicesConfigurator, SandBox.Foundation.Authenticator" />
    </services>
  </sitecore>
</configuration>

Great, now in our controller we will have a nice clean constructor:

private readonly ITokenProvider _tokenProvider;
private readonly IUserService _userService;  

public CustomAuthenticatorController(ITokenProvider tokenProvider, IUserService userService)
{
  _tokenProvider = tokenProvider;
  _userService = userService;
}

I forgot to mention we are using/inheriting the ServicesApiController, which inherits the System.Web.Http.ApiController.

[ServicesController]
public class CustomAuthenticatorController : ServicesApiController
{
...

In order for us to use the wonderful dependency injection, we need to update the Sitecore.Foundation.DependencyInjection project. Right now the MvcControllerServicesConfigurator only supports the “MvcControllers” – System.Web.Mvc.IController.

public class MvcControllerServicesConfigurator : IServicesConfigurator
{
	public void Configure(IServiceCollection serviceCollection)
	{
		serviceCollection.AddMvcControllers("*.Feature.*");
		serviceCollection.AddClassesWithServiceAttribute("*.Feature.*");
		serviceCollection.AddClassesWithServiceAttribute("*.Foundation.*");
	}
}

In the ServiceCollectionExtensions.cs we will create new extension methods, called AddApiControllers:

public static void AddApiControllers(this IServiceCollection serviceCollection, params string[] assemblyFilters)
{
  serviceCollection.AddApiControllers(GetAssemblies(assemblyFilters));
}

public static void AddApiControllers(this IServiceCollection serviceCollection, params Assembly[] assemblies)
{
  serviceCollection.AddApiControllers(assemblies, new[] { DefaultControllerFilter });
}

public static void AddApiControllers(this IServiceCollection serviceCollection, string[] assemblyFilters, params string[] classFilters)
{
  serviceCollection.AddApiControllers(GetAssemblies(assemblyFilters), classFilters);
}

private static void AddApiControllers(this IServiceCollection serviceCollection, IEnumerable<Assembly> assemblies, string[] classFilters)
{
  var controllers = GetTypesImplementing(typeof(IHttpController), assemblies, classFilters);

  foreach (var controller in controllers)
  {
	serviceCollection.Add(controller, Lifetime.Transient);
  }
}

By doing this we are now supporting the System.Web.Http.Controllers.IHttpController

And finally our MvcControllerServicesConfigurator will now look like this:

public class MvcControllerServicesConfigurator : IServicesConfigurator
{
	public void Configure(IServiceCollection serviceCollection)
	{
		serviceCollection.AddMvcControllers("*.Feature.*");
		serviceCollection.AddApiControllers("*.Foundation.*");
		serviceCollection.AddClassesWithServiceAttribute("*.Feature.*");
		serviceCollection.AddClassesWithServiceAttribute("*.Foundation.*");
	}
}

Beautiful πŸ™‚

We also need to do some configuration updates/changes for the JWT token. Here is the config patch:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
   <settings>

      <setting name="Sitecore.Services.AllowAnonymousUser">
        <patch:attribute name="value">false</patch:attribute>
      </setting>

      <setting name="Sitecore.Services.Token.Authorization.Enabled">
        <patch:attribute name="value">true</patch:attribute>
      </setting>

      <setting name="Sitecore.Services.SecurityPolicy">
        <patch:attribute name="value">Sitecore.Services.Infrastructure.Web.Http.Security.ServicesOnPolicy, Sitecore.Services.Infrastructure</patch:attribute>
      </setting>
      
    </settings>

    <api>
      <tokenSecurity>
        <signingProvider type="Sitecore.Services.Infrastructure.Sitecore.Security.SymetricKeySigningProvider, Sitecore.Services.Infrastructure.Sitecore">
          <param desc="connectionStringName">Sitecore.Services.Token.SecurityKey</param>
        </signingProvider>
      </tokenSecurity>
    </api>
  </sitecore>
</configuration>

Don’t forget to add the “connection string” for the SecurityKey, and don’t use the default key…

<connectionStrings>
  <!-- 
    Sitecore connection strings.
    All database connections for Sitecore are configured here.
  -->
  
  <add name="Sitecore.Services.Token.SecurityKey" connectionString="key=GHUwnYMxb75Td25yqyVdQQ8QQ8RzBG6T" />
  
  
</connectionStrings>

One last thing we need to do. The JWT token will have two new “Claim’s”, FirstName and LastName. We will get it from the “Personal” facet of the Contact(in xdb). That means we need to enable tracking for web api, so how do we do that?
Well it’s quite easy, thank’s to Martin English’s great post The easy way to enable xDB tracking for Sitecore.Services.Client and Web API. We will use the BeaconSessionRouteHandler from FXM. Let’s put it all in the Sitecore.Foundation.SitecoreExtensions.

namespace Sitecore.Foundation.SitecoreExtensions.Infrastructure.Pipelines.Initialize
{

  using Sitecore.FXM.Service.Handler;
  using Sitecore.Pipelines;
  using System.Web.Routing;

  public class EnableEntityServiceSessionStateProcessor
  {
    public void Process(PipelineArgs args)
    {
      Route route = RouteTable.Routes["EntityService"] as Route;

      if (route != null)
      {
        route.RouteHandler = new BeaconSessionRouteHandler();
      }
    }
  }
}

Here is the patch config:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  	<sitecore>
		<pipelines>
		  <initialize>
			<processor type="Sitecore.Foundation.SitecoreExtensions.Infrastructure.Pipelines.Initialize.EnableEntityServiceSessionStateProcessor, Sitecore.Foundation.SitecoreExtensions"
					   patch:after="processor[@type='Sitecore.Services.Infrastructure.Sitecore.Pipelines.ServicesWebApiInitializer, Sitecore.Services.Infrastructure.Sitecore']" />
		  </initialize>

		</pipelines>
	</sitecore>
</configuration>

Ok so we have it all prepared, it’s time to make our own Login action. The input to the Login action will be a JSON object which will contain username, password and domain.

[RequireHttps]
[HttpPost]
public HttpResponseMessage Login([FromBody] JObject jsonData)
{
  try
  {
	dynamic loginRequest = jsonData;
        
	_userService.Login(loginRequest.Domain, loginRequest.UserName, loginRequest.Password);

	if (!Context.User.IsAuthenticated)
	  return Request.CreateResponse(HttpStatusCode.Unauthorized);

	string contactIdentifier = $"{loginRequest.Domain}\\{loginRequest.UserName}";

	Tracker.Current.Session.Identify(contactIdentifier);

	Contact contact = Tracker.Current.Session.Contact;

	IContactPersonalInfo contactPersonalInfo = contact.GetFacet<IContactPersonalInfo>("Personal");
	
	string token = _tokenProvider.GenerateToken((IEnumerable<Claim>)new Claim[3]
	{
	  new Claim("User", Context.User.Name),
	  new Claim("FirstName", contactPersonalInfo.FirstName),
	  new Claim("LastName", contactPersonalInfo.Surname)
	});

	if (token == null)
	  return Request.CreateResponse(HttpStatusCode.OK);


	HttpResponseMessage responseMessage = Request.CreateResponse(HttpStatusCode.OK);
	responseMessage.Content = new PushStreamContent((stream, content, context) =>
	{
	  using (StreamWriter sw = new StreamWriter(stream, Encoding.UTF8))
	  using (JsonTextWriter jtw = new JsonTextWriter(sw))
	  {
		JsonSerializer ser = new JsonSerializer();
		ser.Serialize(jtw, new {  token });
	  }
	}, "application/json");

	return responseMessage;

  }
  catch (ArgumentException argEx)
  {
	return Request.CreateResponse(HttpStatusCode.BadRequest);
  }
  catch (AuthenticationException authEx)
  {
	return Request.CreateResponse(HttpStatusCode.Forbidden);
  }
}

Let’s do a quick run trough the code. If the login succeeds we will try to merge/identify the contact, we need it in order to get the “FirstName” and “LastName”(from the “Personal” facet of the Contact). Finally we will generate the token and also add our two new “Claim” attributes, FirstName and LastName.

We will try it in Postman:

It gives us a token πŸ™‚

Wonderful, let’s decipher the token and see what it contains. There are a number of sites that can help us with that, how about https://jwt.io/:

That’s all for now folks πŸ™‚

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s