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