In part 1 of this post I described how to solve the first part of the problem: making sure the JWT token we got from ADAL JS gets sent to the server (i.e. the SignalR hub). Part 2 describes how the server extracts the token, validates it and create a principal out of it. In another post, I already described how to configure an Owin middleware pipeline that does exactly this: via UseWindowsAzureActiveDirectoryBearerAuthentication (and if you Google this extension method you'll find a lot more information).
So ideally I would tap into the same Owin middleware pipeline that regular ASP.NET requests pass through. Unfortunately, that's impossible: SignalR uses different abstractions for similar concepts like 'request' and 'caller context'. So there's some plumbing involved, especially where token validation is involved. I copied some classes from the Katana Project library for that, especially from the Microsoft.Owin.Security.ActiveDirectory
package.
The SignalR protocol can be roughly divided into two stages: connection setup and realtime communication over this connection (there's of course a lot more detail to it). You'd want to authenticate the client and validate its token on the connect, not on every subsequent call. It doesn't make sense to authenticate each realtime call since these aren't possible anyway without first connecting.
To implement authentication for SignalR hubs, the AuthorizeAttribute
is provided. It implements two interfaces: IAuthorizeHubConnection
and IAuthorizeHubMethodInvocation
, essentially implementing both SignalR protocol stages: connect and communicate.
So what does this look like? And what can we borrow from Katana to simplify and improve things? First the outline of our JwtTokenAuthorizeAttribute
(by the way: I attached a zip file with a VS2015 project containing all code at the end of this post):
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class JwtTokenAuthorizeAttribute : AuthorizeAttribute { public override bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request) { // Authorize a connection attempt from the client. We expect a token on the request. ... } public override bool AuthorizeHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod) { // Make sure the context for each method call contains our authenticated principal. No // additional authentication is performed here. ... } }
And this is how we apply it:
[JwtTokenAuthorize] public class NewEventHub : Microsoft.AspNet.SignalR.Hub { .... }
Connection authorization
All that leaves us is implementing the attribute class. First step is getting the token from the IRequest
, which is simple:
public override bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request) { // Extract JWT token from query string. var userJwtToken = request.QueryString.Get("token"); if (string.IsNullOrEmpty(userJwtToken)) { return false; } ...
You can see in the first part of this two-part series that I named the query string parameter token
but you can give it any name you like of course. If there is no token on the query string, we return false
to indicate authentication did not succeed.
The next step is where the magic happens: validating the token and extracting a ClaimsPrincipal
from the set of claims in the JWT (JSON Web Token). Validating the token means checking the token cryptographic signature. The question then becomes: what do we check against? Each issued JWT is signed by the private key part of a public/private key pair maintained by Azure AD (assuming of course we actually obtained a token from Azure AD). An application can use the corresponding public key to check the token signature. The public key is found in the tenants federation metadata document. This document is found on the following URL: https://login.windows.net/yourtenant.onmicrosoft.com/federationmetadata/2007-06/federationmetadata.xml
.
Lucky for us, a lot of code for handling the federation metadata document and validating the token is already available in the Katana project.
public class JwtTokenAuthorizeAttribute : AuthorizeAttribute { // Location of the federation metadata document for our tenant. private const string SecurityTokenServiceAddressFormat = "https://login.windows.net/{0}/federationmetadata/2007-06/federationmetadata.xml"; private static readonly string Tenant = "yourtenant.onmicrosoft.com"; private static readonly string ClientId = "12345678-ABCD-EFAB-1234-ABCDEF123456"; private static readonly string MetadataEndpoint = string.Format(CultureInfo.InvariantCulture, SecurityTokenServiceAddressFormat, Tenant); private static readonly IIssuerSecurityTokenProvider CachingSecurityTokenProvider = new WsFedCachingSecurityTokenProvider( metadataEndpoint: MetadataEndpoint, backchannelCertificateValidator: null, backchannelTimeout: TimeSpan.FromMinutes(1), backchannelHttpHandler: null); public override bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request) { // Extract JWT token from query string (which we already did). ... // Validate JWT token. var tokenValidationParameters = new TokenValidationParameters { ValidAudience = ClientId }; var jwtFormat = new JwtFormat(tokenValidationParameters, CachingSecurityTokenProvider); var authenticationTicket = jwtFormat.Unprotect(userJwtToken); ...
We start with the JwtFormat
class. This class is used to extract and validate the JWT. It's in fact a wrapper around the JwtSecurityTokenHandler
class with the added bonus of 'automatic' retrieval of SecurityToken
s from the tenants federation metadata document (in this case a X509SecurityToken
).
The tenants security tokens are retrieved through the IIssuerSecurityTokenProvider
interface. Unfortunately, this is where code reuse ends and copying begins. There exists an implementation of IIssuerSecurityTokenProvider
that is also used in the pipeline you set up when using UseWindowsAzureActiveDirectoryBearerAuthentication
: WsFedCachingSecurityTokenProvider
. This class handles communication with the federation metadata endpoint, extracts the security tokens necessary to validate the JWT signature and maintains a simple cache of this information; just what we need. However, this class is internal. And it uses a number of other internal classes.
So what I did for my project was copy all the necessary classes from the Microsoft.Owin.Security.ActiveDirectory
Katana project. In the code above, you see the WsFedCachingSecurityTokenProvider
configured with just the URL for the metadata document (and a timeout that governs communication with the metadata endpoint). Simple as that. The call to JwtFormat.Unprotect
takes care of the rest.
The next steps are some obligatory checks against the AuthenticationTicket
:
public override bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request) { // Extract and validate token. ... // Check ticket properties. if (authenticationTicket == null) { return false; } var currentUtc = DateTimeOffset.UtcNow; if (authenticationTicket.Properties.ExpiresUtc.HasValue && authenticationTicket.Properties.ExpiresUtc.Value < currentUtc) { return false; } if (!authenticationTicket.Identity.IsAuthenticated) { return false; } ...
The ticket shouldn't be null
, it should not be expired and the identity should be authenticated. The final step is to somehow store the authenticated identity so that we can use it in our SignalR hub method calls. Remember, we are still just connecting with the hub and not calling any methods on it.
public override bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request) { // Extract and validate token, check basic authentication ticket properties. ... // Create a principal from the authenticated identity. var claimsPrincipal = new ClaimsPrincipal(authenticationTicket.Identity); // Remember new principal in environment for later use in method invocations. request.Environment["server.User"] = newClaimsPrincipal; // Return true to indicate authentication succeeded. return true; }
We create a ClaimsPrincipal
from the identity and store it in the environment under the key server.User
. You may wonder where this key comes from. The core Owin spec defines a number of required environment keys and the Katana project extends this set. One of the extension keys is server.User
which should be of type IPrincipal
.
Method invocation authorization
Remember that the SignalR AuthorizeAttribute
implemented two interfaces. We have implemented IAuthorizeHubConnection
so what's left is IAuthorizeHubMethodInvocation
. This code is a lot shorter:
public override bool AuthorizeHubMethodInvocation( IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod) { HubCallerContext hubCallerContext = hubIncomingInvokerContext.Hub.Context; var environment = hubCallerContext.Request.Environment; object claimsPrincipalObject; ClaimsPrincipal claimsPrincipal; if (environment.TryGetValue("server.User", out claimsPrincipalObject) && (claimsPrincipal = claimsPrincipalObject as ClaimsPrincipal) != null && claimsPrincipal.Identities.Any(id => id.IsAuthenticated)) { var connectionId = hubCallerContext.ConnectionId; hubIncomingInvokerContext.Hub.Context = new HubCallerContext(new ServerRequest(environment), connectionId); return true; } return false; }
Here we pick the ClaimsPrincipal
from the environment where it was stored in the connection process. If we find it, we create a new HubCallerContext
using the environment containing the principal.
Calling hub methods
Well, we're finally where we want to be: actually call a SignalR hub method with a principal that originates from the JWT we sent from the client. A sample hub method may look like this:
[JwtTokenAuthorize] public class NewEventHub : Microsoft.AspNet.SignalR.Hub { public async Task CopyEvent(int eventId) { // Get current principal. var currentPrincipal = ClaimsPrincipal.Current; var currentIdentity = currentPrincipal.Identity; // Do stuff that requires authentication. return "Copy event successful"; } }
Note that we did not have to get the principal from some environment using the server.User
key. This is because SignalR internally uses the same Owin classes as the Katana project so a principal stored in the environment as server.User
is automatically translated into a principal on the current call context.
Credits
Some credits should go to Shaun Xu for this blog post. It shows where in the environment to store the authenticated principal and how to set the context so method calls have access to this principal.