1

I have an instance running SxC 9.0 update 1.

I was working on an API which had to be consumed by a 3rd party application.

Can you provide some input for using token bear mechanism to protect my API.

I know the approach will be different from the latest versions of sitecore(eg, 9.1).

Any help is appreciated.

Thanks in advance.

  • I'm not sure how the mechanism you use relates to Sitecore specifically for a 9.0.1 instance? Sitecore at that point is nothing more than a .net web application that you can protect following general .net guidelines. Also unsure why you are building a web service that directly runs on top of Sitecore (unless it's accessing Sitecore content). If it's for another purpose you might want to look at separating that out to another web app, so Sitecore doesn't become a bottleneck. – Dylan Young Apr 17 at 21:50
  • Hi @DylanYoung, my custom web API will be consumed by another external application. Can you suggest me the best approach to secure my Web API ? – Jijo Viswanathan Apr 18 at 9:22
  • Sitecore 9 update 1 instance doesn't have identity server module. But, I got it installed along with sitecore commerce. Any of you have experience in using the token bearer authentication used in commerce engine to be used for authenticating external clients ? – Jijo Viswanathan Apr 18 at 11:00
  • Do you want to use sitecore or 3rd party membership for authentification? I have custom implementation of jwt-token authorization for sitecore membership. – x3mxray Apr 19 at 15:40
  • @x3mxray, any of them will work for me. Could you share some details about your custom implementation? – Jijo Viswanathan Apr 19 at 15:44
6

You can implement custom DelegatingHandler base on Sitecore.Services behavior:

using System.Linq;
using System.Net.Http;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Sitecore.Services.Core.Security;
using Sitecore.Services.Infrastructure.Sitecore.Security;
using Sitecore.Services.Infrastructure.Web.Http.Security;

namespace Foundation.Authorization.Pipelines.Handlers
{
    public class JwtTokenDelegatingHandler : DelegatingHandler
    {
        private readonly ITokenProvider _tokenProvider;
        private readonly IUserService _userService;

        public JwtTokenDelegatingHandler()
            : this(new ConfiguredOrNullTokenProvider(new SigningTokenProvider()), new UserService())
        {
        }

        protected JwtTokenDelegatingHandler(ITokenProvider tokenProvider, IUserService userService)
        {
            _tokenProvider = tokenProvider;
            _userService = userService;
        }

        protected JwtTokenDelegatingHandler(HttpMessageHandler innerHandler)
            : this(innerHandler, new ConfiguredOrNullTokenProvider(new SigningTokenProvider()), new UserService())
        {
        }

        protected JwtTokenDelegatingHandler(HttpMessageHandler innerHandler, ITokenProvider tokenProvider, IUserService userService)
            : base(innerHandler)
        {
            _tokenProvider = tokenProvider;
            _userService = userService;
        }

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            JwtTokenDelegatingHandler delegatingHandler = this;
            delegatingHandler.AttemptLoginWithToken(request);
            return await base.SendAsync(request, cancellationToken);
        }

        private void AttemptLoginWithToken(HttpRequestMessage request)
        {
            if (!string.IsNullOrEmpty(request.Headers.Authorization?.Parameter))
            {
                try
                {
                    var validationResult = _tokenProvider.ValidateToken(request.Headers.Authorization.Parameter);
                    if (validationResult.IsValid)
                    {
                        var claim = validationResult.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name);

                        if (claim != null && _userService.UserExists(claim.Value))
                        {
                            _userService.SwitchToUser(claim.Value, true);
                            return; 
                        }
                    }
                }
                catch
                {
                    // token expired error
                }

            }

            request.Properties.Add("invalidtoken", true);
        }
    }
}

And create custom AuthorizeAttribute:

  public class JwtAuthorizeAttribute : AuthorizeAttribute
    {
        protected override bool IsAuthorized(HttpActionContext actionContext)
        {
            return IsValidToken(actionContext) && base.IsAuthorized(actionContext);
        }
        public override void OnAuthorization(HttpActionContext actionContext)
        {
            if(SkipAuthorization(actionContext))
                return;

            if (!IsAuthorized(actionContext))
            {
                actionContext.Response = new HttpResponseMessage(HttpStatusCode.Forbidden);
            }
        }

        private bool IsValidToken(HttpActionContext actionContext)
        {
            var valid = true;
            if (actionContext.Request.Properties.ContainsKey("invalidtoken"))
            {
                valid = ! (bool) actionContext.Request.Properties["invalidtoken"];
            }
            return valid;
        }

        private static bool SkipAuthorization(HttpActionContext actionContext)
        {
            return actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any()
                   || actionContext.ControllerContext.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any();
        }

    }

You should also register DelegatingHandler in sitecore config (and can also setup some additional Sitecore.Services settings). Foundation.authorization.config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore>

    <settings>

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

      <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>

      <setting name="Sitecore.Services.ApiKeyParams" value="sc_apikey" />

    </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>

      <services>
        <configuration>
          <delegatingHandlers hint="list:AddDelegatingHandler">
            <patch:delete />
          </delegatingHandlers>
          <delegatingHandlers hint="list:AddDelegatingHandler">
            <delegatingHandler desc="TokenDelegatingHandler">Foundation.Authorization.Pipelines.Handlers.JwtTokenDelegatingHandler, Foundation.Authorization</delegatingHandler>
          </delegatingHandlers>
        </configuration>
      </services>

    </api>

  </sitecore>
</configuration>

Add Sitecore.Services.Token.SecurityKey in ConnectionStrings.config with generated key:

<add name="Sitecore.Services.Token.SecurityKey" connectionString="key=GHUwnYMxb75Td25yqyVdQQ8QQ8RzBG6T" />

Now you are able to use [JwtAuthorize] attribute. Example of login:

 [JwtAuthorize]
    public class AuthController : ApiController

    {
        private readonly IUserService _userService;
        private readonly ITokenProvider _tokenProvider;
        private const string Domain = "sitecore";

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

         [RequireHttps]
        [HttpPost]
        [AllowAnonymous]
        public IHttpActionResult Login([FromBody] LoginRequestModel data)
        {
            try
            {
                var login = data.Email;
                var password = data.Password;
                var username = $"{Domain}\\{login}";

                if (!_userService.UserExists(username))
                {
                    return BadRequest("User not found!");
                }


                if (!System.Web.Security.Membership.ValidateUser(username, password))
                {
                    return BadRequest("Incorrect password!");
                }

                _userService.SwitchToUser(username, true);

                if (!Context.User.IsAuthenticated)
                    return Unauthorized();

                string token = _tokenProvider.GenerateToken(new List<Claim>
            {
                new Claim(ClaimTypes.Name,  Context.User.Name)
            });

                return Ok(token); 

            }
            catch (ArgumentException argEx)
            {
                return BadRequest(argEx.Message);
            }
            catch (AuthenticationException authEx)
            {
                return Unauthorized();
            }

        }

If login is successful client will get token in response. This token should be added in the Authorization header using the Bearer schema for all client requests:

enter image description here

You can also use [JwtAuthorize] with default Role and Users parameters:

[JwtAuthorize(Roles = @"sitecore\Author")]

Also you can use advantages of Sitecore.Service. If you have several 3rd party applications that use your WebAPI you can create several Api Keys in sitecore with different access. You can inherit your controller from ServicesApiController and use [RequiredApiKey] attribute:

    [EnableCors(origins: "*", headers: "*", methods: "*")]
    [ServicesController]
    [RequiredApiKey]
    [JwtAuthorize]
    public class InteractionsApiController : ServicesApiController
    {
      ...
    }

And also don`t forget to register your API controllers in service configurator:

using Microsoft.Extensions.DependencyInjection;
using Sitecore.DependencyInjection;
using Sitecore.Services.Infrastructure.Sitecore.DependencyInjection;

namespace Demo.Api.Ioc
{
    public class RegisterDiForControllers : IServicesConfigurator
    {
        public void Configure(IServiceCollection serviceCollection)
        {
            serviceCollection.AddWebApiControllers(new[] { GetType().Assembly });
        }
    }
}

Config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore>
     <services>
       <configurator type="Demo.Api.Ioc.RegisterDiForControllers, Demo.Api" />
     </services>
 </sitecore>
</configuration>
  • I followed your post. But getting DI related issue. Seems like JwtTokenDelegatingHandler is not getting initialized when I call my constructor. Can you help me on this ? – Jijo Viswanathan Apr 21 at 3:24
  • "Message": "An error has occurred.", "ExceptionMessage": "An error occurred when trying to create a controller of type 'AuthController'. Make sure that the controller has a parameterless public constructor.", "ExceptionType": "System.InvalidOperationException", "StackTrace": " at System.Web.Http.Dispatcher.DefaultHttpControllerActivator.Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType)\r\n at System.Web.Http.Controllers.HttpControllerDescripto.. – Jijo Viswanathan Apr 21 at 3:30
  • "InnerException": { "Message": "An error has occurred.", "ExceptionMessage": "Type 'TestProject.Controllers.AuthController' does not have a default constructor", "ExceptionType": "System.ArgumentException", "StackTrace": " at System.Linq.Expressions.Expression.New(Type type)\r\n at System.Web.Http.Internal.TypeActivator.Create[TBase](Type instanceType)\r\n atSystem.Web.Http.Dispatcher.DefaultHttpControllerActivator.GetInstanceOrActivator(HttpRequestMessage request, Type controllerType, Func`1& activator)\r\n ...... – Jijo Viswanathan Apr 21 at 3:31
  • Hi, it is common exception with DI, it is not related to your question, but I added example of controller registration in my answer (see ServicesConfigurator). – x3mxray Apr 22 at 9:05
  • That is very close to what I needed. I got it working now. – Jijo Viswanathan Apr 24 at 6:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.