Serverless SignalR authentication with JWT in Azure Function - signalr

I am using a SignalRConnectionInfo input binding in an Azure function. In this binding I need to provide the userId of the current principal, however by default the Azure function only supports the use of pre-defined headers from App Service Authentication, as outlined here, and in the below example:
[FunctionName("negotiate")]
public static SignalRConnectionInfo Negotiate(
[HttpTrigger(AuthorizationLevel.Anonymous)]HttpRequest req,
[SignalRConnectionInfo(HubName = "foo", UserId = "{headers.x-ms-client-principal-id}")] SignalRConnectionInfo connectionInfo)
{
return connectionInfo;
}
As I am not using App Service authentication, and do not wish to, this is not suitable for my needs.
What I do currently have is a JWT which the user provides to an API we host in another App Service in order to authenticate the request and identify themselves. How can I amend the UserId property of the SignalRConnectionInfo binding to retrieve the UserId from the claims within that JWT?

AFAIK there isn't a way to extract information from a JWT using binding expressions.
Instead, you will have to use Runtime Binding to first extract the information from the JWT and then use it in the binding to get the SignalR Connection Information.
Here's a functional example where the JWT is retrieved from the Authorization header, validated, then applied to the SignalRConnectionInfo attribute.
[FunctionName("Negotiate")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "negotiate")] HttpRequest req,
IBinder binder)
{
if (req.Headers.ContainsKey("Authorization"))
{
var principal = TryGetPrincipal(req.Headers["Authorization"].ToString());
if (principal != null)
{
var connectionInfo = await binder.BindAsync<SignalRConnectionInfo>(new SignalRConnectionInfoAttribute
{
HubName = _hubName,
UserId = principal.FindFirst(ClaimTypes.NameIdentifier).Value
});
return new OkObjectResult(connectionInfo);
}
}
return new UnauthorizedResult();
}
public ClaimsPrincipal TryGetPrincipal(string token) {
// implementation varies based on authorization type...
}

Related

ASP.NET SignalR client-server authentication using (Azure active directory b2c) Json web token validation

I have two application...... one is JavaScript signalR client and the other one is asp.net web application used as signalR server to broadcast the updates to the client. And I was trying to use azure active directory b2c service to offer authentication and authorization for user through client application to access the resources in the server. So, that only the authenticated user of JavaScript client can initiate signalR connection with the asp.net web application hosting signalR server after the token validation.
As, signalR uses web-sockets we cannot supply the token in the HTTP connection request header. It seems that I should use query string to supply authentication token in the signalR connection request.
After receiving that token in the asp.net server application I need to validate that token and allow the JavaScript client application to have a signalR connection.
I want to implement exactly the same thing in this blog post https://kwilson.io/blog/authorize-your-azure-ad-users-with-signalr/ but using azure active directory b2c.
It seems like others might also have same problem using ASP.NET SignalR Client and server architecture.
Actually, with lots of efforts I was able to solve this issue by customizing the AuthorizeModule of signalR hubs. Actually I override AuthorizeHubConnection() and AuthorizeHubMethodInvocation() using AuthorizeAttribute inheritance in CustomAuthorization class.
First of all I added the GlobalHost.HubPipeline.AddModule(module) in app.Map("/signalr", map =>{ .... } in startup Configuration. You can see it in the following startup.cs.
using Microsoft.Owin;
using Microsoft.Owin.Cors;
using Owin;
using Microsoft.AspNet.SignalR;
using TestCarSurveillance.RealTimeCommunication.AuthorizationConfiguration;
using Microsoft.AspNet.SignalR.Hubs;
[assembly: OwinStartup(typeof(TestCarSurveillance.RealTimeCommunication.Startup))]
namespace TestCarSurveillance.RealTimeCommunication
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
//After adding Authorization module in GlobalHost.HubPipeline.AddModule(module)
//program was unable to create the log file so I have added it.
log4net.Config.XmlConfigurator.Configure();
// Branch the pipeline here for requests that start with "/signalr"
//app.UseWelcomePage("/");
app.Map("/signalr", map =>
{
// Setup the CORS middleware to run before SignalR.
// By default this will allow all origins. You can
// configure the set of origins and/or http verbs by
// providing a cors options with a different policy.
map.UseCors(CorsOptions.AllowAll);
var hubConfiguration = new HubConfiguration
{
EnableDetailedErrors = true,
// You can enable JSONP by uncommenting line below.
// JSONP requests are insecure but some older browsers (and some
// versions of IE) require JSONP to work cross domain
EnableJSONP = true
};
// Require authentication for all hubs
var authorizer = new CustomAuthorization();
var module = new AuthorizeModule(authorizer, authorizer);
GlobalHost.HubPipeline.AddModule(module);
map.RunSignalR(hubConfiguration);
});
}
}
}
This Authorize module calls CustomAuthorize.cs class in each signalR hub OnConnected(), OnDisconnected(), OnReconnected() and hub methods that the client can call.
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
using Microsoft.AspNet.SignalR.Owin;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin.Security.Jwt;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.Linq;
using System.Security.Claims;
namespace TestCarSurveillance.RealTimeCommunication.AuthorizationConfiguration
{
public class CustomAuthorization : AuthorizeAttribute
{
// These values are pulled from web.config for b2c authorization
public static string aadInstance = ConfigurationManager.AppSettings["ida:AadInstance"];
public static string tenant = ConfigurationManager.AppSettings["ida:Tenant"];
public static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
public static string signUpInPolicy = ConfigurationManager.AppSettings["ida:SignUpInPolicyId"];
static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
//This method is called multiple times before the connection with signalR is established.
public override bool AuthorizeHubConnection(HubDescriptor hubDescriptor, IRequest request)
{
var metadataEndpoint = string.Format(aadInstance, tenant, signUpInPolicy);
// Extract JWT token from query string.
var userJwtToken = request.QueryString.Get("Authorization");
if (string.IsNullOrEmpty(userJwtToken))
{
return false;
}
// Validate JWT token.
//var tokenValidationParameters = new TokenValidationParameters { ValidAudience = ClientId };
//Contains a set of parameters that are used by a SecurityTokenHandler when validating a SecurityToken.
TokenValidationParameters tvps = new TokenValidationParameters
{
// Accept only those tokens where the audience of the token is equal to the client ID of this app
// This is where you specify that your API only accepts tokens from its own clients
// here the valid audience is supplied to check against the token's audience
ValidAudience = clientId,
ValidateIssuer = false,
// It is the authentication scheme used for token validation
AuthenticationType = signUpInPolicy,
//SaveSigninToken = true,
//I’ve configured the “NameClaimType” of the “TokenValidationParameters” to use the claim named “objectidentifer” (“oid”)
//This will facilitate reading the unique user id for the authenticated user inside the controllers, all we need to call
//now inside the controller is: “User.Identity.Name” instead of querying the claims collection each time
//Gets or sets a String that defines the NameClaimType.
NameClaimType = "http://schemas.microsoft.com/identity/claims/objectidentifier"
};
try
{
var jwtFormat = new JwtFormat(tvps, new OpenIdConnectCachingSecurityTokenProvider(metadataEndpoint));
var authenticationTicket = jwtFormat.Unprotect(userJwtToken);
if(authenticationTicket != null && authenticationTicket.Identity !=null && authenticationTicket.Identity.IsAuthenticated)
{
var email = authenticationTicket.Identity.FindFirst(p => p.Type == "emails").Value;
// It is done to call the async method from sync method
//the ArgumentException will be caught as you’d expect, because .GetAwaiter().GetResult() unrolls the first exception the same way await does.
//This approach follows the principle of least surprise and is easier to understand.
// set the authenticated user principal into environment so that it can be used in the future
request.Environment["server.User"] = new ClaimsPrincipal(authenticationTicket.Identity);
return true;
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
log.Error(ex);
//throw ex;
}
return false;
}
public override bool AuthorizeHubMethodInvocation(IHubIncomingInvokerContext hubIncomingInvokerContext, bool appliesToMethod)
{
var connectionId = hubIncomingInvokerContext.Hub.Context.ConnectionId;
//Check the authenticated user principal from environment
var environment = hubIncomingInvokerContext.Hub.Context.Request.Environment;
//ClaimsPrincipal supports multiple claims based identities
var principal = environment["server.User"] as ClaimsPrincipal;
if(principal != null && principal.Identity != null && principal.Identity.IsAuthenticated)
{
// create a new HubCallerContext instance with the principal generated from token
// and replace the current context so that in hubs we can retrieve current user identity
hubIncomingInvokerContext.Hub.Context = new HubCallerContext(new ServerRequest(environment), connectionId);
return true;
}
return false;
}
}
}
After we receive the token from the query string we need to setup TokenValidationParameters use it in metadataEndpoint for token validation. The token validation is done in before having the hub connection so, that only the authorized user can have a connection and if the connection is not successful it returns 401 response. It is implemented in OpenIdConnectCachingSecurityTokenProvider.cs class. This class is being used by having following line of code in AuthorizeHubConnection() method.
var jwtFormat = new JwtFormat(tvps, new OpenIdConnectCachingSecurityTokenProvider(metadataEndpoint));
var authenticationTicket = jwtFormat.Unprotect(userJwtToken);
As, the last part of this authorization configuration I have inherited IIssureSecurityKeyProvider in OpenIdConnectCachingSecurityTokenProvider.cs class. The complete implementation of it can be seen in the following code.
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin.Security.Jwt;
//using System.IdentityModel.Tokens;
namespace TestCarSurveillance.RealTimeCommunication.AuthorizationConfiguration
{
//IIssuerSecurityKeyProvider Interface Provides security Key information to the implementing class.
// This class is necessary because the OAuthBearer Middleware does not leverage
// the OpenID Connect metadata endpoint exposed by the STS by default.
internal class OpenIdConnectCachingSecurityTokenProvider : IIssuerSecurityKeyProvider
{
//Manages the retrieval of Configuration data.
public ConfigurationManager<OpenIdConnectConfiguration> _configManager;
private string _issuer;
private IEnumerable<SecurityKey> _keys;
//this class will be responsible for communicating with the “Metadata Discovery Endpoint” and issue HTTP requests to get the signing keys
//that our API will use to validate signatures from our IdP, those keys exists in the jwks_uri which can read from the discovery endpoint
private readonly string _metadataEndpoint;
//Represents a lock that is used to manage access to a resource, allowing multiple threads for reading or exclusive access for writing.
private readonly ReaderWriterLockSlim _synclock = new ReaderWriterLockSlim();
public OpenIdConnectCachingSecurityTokenProvider(string metadataEndpoint)
{
_metadataEndpoint = metadataEndpoint;
//_configManager = new ConfigurationManager<OpenIdConnectConfiguration>(metadataEndpoint, new OpenIdConnectConfigurationRetriever());
_configManager = new ConfigurationManager<OpenIdConnectConfiguration>(metadataEndpoint, new OpenIdConnectConfigurationRetriever());
//_configManager = new ConfigurationManager<OpenIdConnectConfiguration>(metadataEndpoint);
RetrieveMetadata();
}
/// <summary>
/// Gets the issuer the credentials are for.
/// </summary>
/// <value>
/// The issuer the credentials are for.
/// </value>
public string Issuer
{
get
{
RetrieveMetadata();
_synclock.EnterReadLock();
try
{
return _issuer;
}
finally
{
_synclock.ExitReadLock();
}
}
}
/// <summary>
/// Gets all known security keys.
/// </summary>
/// <value>
/// All known security keys.
/// </value>
public IEnumerable<SecurityKey> SecurityKeys
{
get
{
RetrieveMetadata();
_synclock.EnterReadLock();
try
{
return _keys;
}
finally
{
_synclock.ExitReadLock();
}
}
}
private void RetrieveMetadata()
{
_synclock.EnterWriteLock();
try
{
//Task represents an asynchronous operation.
//Task.Run Method Queues the specified work to run on the ThreadPool and returns a task or Task<TResult> handle for that work.
OpenIdConnectConfiguration config = Task.Run(_configManager.GetConfigurationAsync).Result;
_issuer = config.Issuer;
_keys = config.SigningKeys;
}
finally
{
_synclock.ExitWriteLock();
}
}
}
}
After implementing this we do not need to have [Authorize] attribute in any hub method and this middle-ware will handle the request authorization and only authorized user will have a signalR connection and only authorized user can invoke the hub method.
At last I would like to mention that for this client server architecture to work we need to have separate b2c tenant client application and b2c tenant server application and b2c tenant client application should have API access to the b2c tenant server application. Azure b2c application should be configured as in this example https://learn.microsoft.com/en-us/aspnet/core/security/authentication/azure-ad-b2c-webapi?view=aspnetcore-2.1
Although, it is for .net core but it is also valid for asp.net and only difference is that b2c configuration should be at web.config

How to re-validate token for multi-tenant ASP.NET Identity?

I have implemented a custom OAuthAuthorizationServerProvider to add a domain constraint for the account login. Everything was good. However, I met a problem that, once the user get the token, they can use it for whatever system they want. For example:
They request the TokenEndpointPath with proper username and password (assume it is the admin account of Tenant 1): http://localhost:40721/api/v1/account/auth and receive the Bearer Token.
Now they use it to access: http://localhost:40720/api/v1/info/admin, which is of Tenant 0. The request is considered Authorized.
I tried changing the CreateProperties method but it did not help:
public static AuthenticationProperties CreateProperties(string userName)
{
var tenant = DependencyUtils.Resolve<IdentityTenant>();
IDictionary<string, string> data = new Dictionary<string, string>
{
{ "userName", userName },
{ "tenantId", tenant.Tenant.Id.ToString() },
};
return new AuthenticationProperties(data);
}
I also tried overriding ValidateAuthorizeRequest, but it is never called in my debug.
Do I need to implement a check anywhere else, so the Token is only valid for a domain/correct tenant?
(NOTE: a tenant may have multiple domains, so it's great if I can manually perform an account check against correct tenant rather than sticking to a domain. However, it's a plus if I could do that, or else, simply limit the token to the domain is ok)
Not a direct answer to my question (since it's not inside ASP.NET Identity workflow), but the simplest fix I applied was to use ActionFilterAttribute instead.
public class DomainValidationFilter : ActionFilterAttribute
{
public override Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
{
// Other Code...
// Validate if the logged in user is from correct tenant
var principal = actionContext.ControllerContext.RequestContext.Principal;
if (principal != null && principal.Identity != null && principal.Identity.IsAuthenticated)
{
var userId = int.Parse(principal.Identity.GetUserId());
// Validate against the tenant Id of your own storage, and use this code to invalidate the request if it is trying to exploit:
actionContext.Response = actionContext.Request.CreateResponse(System.Net.HttpStatusCode.Unauthorized, "Invalid Token");
}
return base.OnActionExecutingAsync(actionContext, cancellationToken);
}
}
Then applies the Filter to all actions by registering it in either FilterConfig or WebApiConfig:
config.Filters.Add(new DomainValidationFilter());

When calling AcquireTokenByRefreshToken on the AuthenticationContext instance with Microsoft.IdentityModel.Clients.ActiveDirectory?

I am developing a multi-tenant application registered on my Azure AD that consumes Office 365 apis, Graph API etc.
I followed this Microsoft sample to build my work which uses ADAL .NET library and OpenIdConnect: Microsoft.IdentityModel.Clients.ActiveDirectory, Version=2.19.0.0
In ADAL.NET, we use an AuthenticationContext instance with a custom inherited class for the TokenCache (see code the sample code here).
For each request to the authorized resources, depending on the API, we invoke one of these methods (see code below) to get the auth_token that will be put in the request Bearer parameter. Is it the correct way to do it?
We never make use of the method AcquireTokenByRefreshTokenAsync, does it mean that our application never uses the refresh_token? Does it mean that our user will have to relog after one hour? Should we implement a kind of refreshing procedure with AcquireTokenByRefreshTokenAsync in the catch statement? Can it be made without prompting anything to the end-user?
REMARK: I posted a question regarding OpenIdConnect authentication ticket lifetime. To me these two questions are unrelated but they may be.
string signInUserId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
string userObjectId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
string tenantId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
public async Task<string> AcquireOutlook365TokenAsync()
{
AuthenticationContext authContext = new AuthenticationContext(string.Format("{0}/{1}", SettingsHelper.AuthorizationUri, tenantId), new ADALTokenCache(signInUserId));
try
{
var result = await authContext.AcquireTokenSilentAsync(#"https://outlook.office365.com/",
new ClientCredential(SettingsHelper.ClientId, SettingsHelper.AppKey),
new UserIdentifier(userObjectId, UserIdentifierType.UniqueId));
return result.AccessToken;
}
catch (AdalException exception)
{
//handle token acquisition failure
if (exception.ErrorCode == AdalError.FailedToAcquireTokenSilently)
{
authContext.TokenCache.Clear();
}
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Unauthorized));
}
}
public async Task<string> AcquireAzureGraphTokenAsync()
{
AuthenticationContext authContext = new AuthenticationContext(string.Format("{0}/{1}", SettingsHelper.AuthorizationUri, tenantId), new ADALTokenCache(signInUserId));
try
{
var result = await authContext.AcquireTokenSilentAsync(#"https://graph.windows.net/",
new ClientCredential(SettingsHelper.ClientId, SettingsHelper.AppKey),
new UserIdentifier(userObjectId, UserIdentifierType.UniqueId));
return result.AccessToken;
}
catch (AdalException exception)
{
//Same as other method
}
}
ADAL uses the stored refresh tokens automatically and transparently, you aren't required to perform any explicit action. AcquireTOkenByRefreshToken is in the ADAL surface for legacy reasons, and has been removed from version 3.x. More background at http://www.cloudidentity.com/blog/2015/08/13/adal-3-didnt-return-refresh-tokens-for-5-months-and-nobody-noticed/

How to authenticate an access token using OWIN OAuthBearerAuthentication?

What I want:
A token generator use OAuthAuthorizationServer and token consumer use OAuthBearerAuthentication (authenticate the access token).
Use OWIN pipeline to manage all stuff, token stuff and web api stuff.
What about the code:
public void Configuration(IAppBuilder app)
{
app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions
{
AuthorizeEndpointPath = "/Authorize",
AllowInsecureHttp = true,
Provider = new OAuthAuthorizationServerProvider
{
OnGrantCustomExtension = GrantCustomExtension,
OnValidateClientRedirectUri = ValidateClientRedirectUri,
OnValidateClientAuthentication = ValidateClientAuthentication,
}
});
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
Provider = new OAuthBearerAuthenticationProvider
{
//Handles applying the authentication challenge to the response message.
ApplyChallenge=MyApplyChallenge,
//Handles processing OAuth bearer token.
RequestToken=MyRequestToken,
//Handles validating the identity produced from an OAuth bearer token.
ValidateIdentity = MyValidateIdentity,
}
});
app.UseWebApi(new WebApplication3.Config.MyWebApiConfiguration());
}
What's the question:
The 3 properties of OAuthBearerAuthenticationProvider,
ApplyChallenge, RequestToken and ValidateIdentity. How to
implement the 3 methods?
In the token authetication process, What I thought is to decrypt the access token, validate the token from the client, and if the token is validated, put the identities of the token to the HttpContext.Current.User.
The OAuthBearerAuthenticationProvider's responsibility is to fulfill the
previous steps. Am I right?
As you know, UseOAuthAuthorizationServer has the job of authenticating the user. Then, UseOAuthBearerAuthentication has the job of ensuring that only authenticated users can access your application. Often, these two jobs are assigned to different web application. It looks like your application is doing both.
There are certainly some cases were you need to override the default OAuthBearerAuthenticationProvider. Maybe you do, or maybe you don't In my case, ApplicationCookie didn't quite fit the scenario. So, I'm storing a 3rd party JWT token in a cookie, rather than the header, and using it to indicate that the user is authenticated to a web application. I also needed to redirect to my own login page, rather than provide a 401.
Here's an implementation that does both:
public class CustomOAuthBearerProvider : IOAuthBearerAuthenticationProvider
{
public Task ApplyChallenge(OAuthChallengeContext context)
{
context.Response.Redirect("/Account/Login");
return Task.FromResult<object>(null);
}
public Task RequestToken(OAuthRequestTokenContext context)
{
string token = context.Request.Cookies[SessionKey];
if (!string.IsNullOrEmpty(token))
{
context.Token = token;
}
return Task.FromResult<object>(null);
}
public Task ValidateIdentity(OAuthValidateIdentityContext context)
{
return Task.FromResult<object>(null);
}
}
I didn't need to do anything special in ValidateIdentity, but I needed to satisfy the interface.
To wire this up, tell your app to use JwtBearerAuthentication with your provider:
// controllers with an [Authorize] attribute will be validated with JWT
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AllowedAudiences = audiences.ToArray(),
IssuerSecurityTokenProviders = providers.ToArray(),
Provider = new CookieOAuthBearerProvider()
}
);

SignalR ISAuthenticated using Headers

My goal is:
To use custom headers with my own token to authenticate a user or machine against my signalr service.
We've been using this methodology succesfully under ASP.net WEB API to perform our own custom claims based authentication and authorization.
Our Web Api was as follows:
protected void Application_Start()
{
GlobalConfiguration.Configuration.MessageHandlers.Add(new AuthorizationHeaderHandler());
}
Then we would have a AuthorizationHandler that would overwrite the Thread.CurrentPrincipal = principal; and we would be done.
Within SignalR I have tried to implement:
1. Mark our hub using Authorize
2. Implemented custom authorize atributes
3. Tried A Custom Module. But besides returning true if the correct headers we're send I still do not get the Context.User to change to the claims based principal that we generate.
But never can we get the Context.User to show the actual user that's being used to connect to the hub.
Any suggestions are Welcome.
Main reason why we want to achieve this is because we have a couple of different user/machine types that connect to our system.
Anybody any suggestions.
Finally found the solution.
I added my own owin security middleware allowing me to handle customer header based authentication.
This could be easily expanded allowing you to combine multiple authenitication scheme's within on service.
First Create Custom Authentication Middleware:
public class AuthenticationMiddleware : OwinMiddleware
{
public AuthenticationMiddleware(OwinMiddleware next) :
base(next) { }
public override async Task Invoke(IOwinContext context)
{
var request = context.Request;
var value = request.Headers["Phocabby-MachineKey"];
var username = value;
var usernameClaim = new Claim(ClaimTypes.Name, username);
var identity = new ClaimsIdentity(new[] { usernameClaim }, "ApiKey");
var principal = new ClaimsPrincipal(identity);
principal.Identities.First().AddClaim(new Claim("CanGetApiKey", "False"));
principal.Identities.First().AddClaim(new Claim("Cabinet", "True"));
request.User = principal;
await Next.Invoke(context);
}
}
Then register it in the startup class
public void Configuration(IAppBuilder app)
{
app.Use(typeof(AuthenticationMiddleware));
app.MapSignalR();
}

Resources