Azure SignalR Services with Bearer Authentication - azure

We are trying to scale out our calendar application, that uses SignalR to push updates to clients based on their user's OrganizationId. Previously, the SignalR stuff was hosted within the single App Server, but to make it work across multiple servers we have opted to use Azure SignalR Services.
However, when the application uses the Azure solution, autorisation breaks.
Authentication is set up in Startup.cs to look for the token in the url/query-string when dealing with Hub endpoints:
//From: Startup.cs (abridged)
public IServiceProvider ConfigureServices(IServiceCollection services)
var authenticationBuilder = services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = OAuthValidationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OAuthValidationDefaults.AuthenticationScheme;
});
authenticationBuilder
.AddOAuthValidation(options => {
options.Events.OnRetrieveToken = async context => {
// Based on https://learn.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-3.0
var accessToken = context.HttpContext.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/signalr/calendar")) {
context.Token = accessToken;
}
return;
};
})
.AddOpenIdConnectServer(options => {
options.TokenEndpointPath = "/token";
options.ProviderType = typeof(ApplicationOAuthProvider);
/*...*/
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime applicationLifetime) {
app.UseAuthentication();
}
When using the Azure SignalR Service, the OnRetrieveToken event code is simply never hit, which makes sense given that the request is no longer directed at the App Service, but instead to the url of the Azure SignalR Service.
This Hub works while SignalR is hosted on the App Server:
[Authorize(Roles = "Manager, Seller")]
public class CalendarHub : Hub<ICalendarClient> {
private IHttpContextAccessor httpContextAccessor;
public CalendarHub(IHttpContextAccessor httpContextAccessor) { this.httpContextAccessor = httpContextAccessor }
public override async Task OnConnectedAsync() {
await Groups.AddToGroupAsync(Context.ConnectionId, GetClaimValue("OrganizationId"));
await base.OnConnectedAsync();
}
private string GetClaimValue(string claimType) {
var identity = (ClaimsIdentity)httpContextAccessor?.HttpContext?.User.Identity;
var claim = identity?.FindFirst(c => c.Type == claimType);
if (claim == null)
throw new InvalidOperationException($"No claim of type {claimType} found.");
return claim.Value;
}
}
But when I switch to the Azure solution:
//From: Startup.cs (abridged)
public IServiceProvider ConfigureServices(IServiceCollection services)
services.AddSignalR().AddAzureSignalR();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime applicationLifetime) {
app.UseAzureSignalR(routes => routes.MapHub<CalendarHub>("/signalr/calendar"));
}
...connecting to the hub causes exception No claim of type OrganizationId found. because the identity is completely empty, as if no user was authenticated. This is especially strange, given that I've restricted access to users of specific roles.

Turns out the error is the same as this question where HttpContext is used to get the claim values, because that's what we do everywhere else. And this seems to work as long as it is the App Service itself handling the connection to the client.
But Azure SignalR Service supplies the claims somewhere else:
The correct way is using just Context which has the type HubCallerContext when accessed from a SignalR Hub. All the claims are available from here with no extra work.
So the method for getting the claim becomes
private string GetClaimValue(string claimType) {
var identity = Context.User.Identity;
var claim = identity.FindFirst(c => c.Type == claimType);
if (claim == null)
throw new InvalidOperationException($"No claim of type {claimType} found.");
return claim.Value;
}

Related

Can I use a basic Web App in Azure to just define a SignalR hub with AZ SignalR Service and use other apps as clients to communicate between them

I have been struggling with this for few days so I am asking whether what I am trying to do is possible.
Basically I am experimenting things with signalR in Azure. I am using the latest version of SignalR NuGet.
I have a small Web App defining a simple signalR hub I want to access via a Azure SignalR Service.
I then have a couple of apps which communicate via hub in the signalR service.
I have various issues.
Getting the hub to load on the AZ SignalR Service. It does not work all the time. Complaining about Serverless/Default settings...
2023-02-16T14:05:25.034917571Z info: Microsoft.Azure.SignalR.Connections.Client.Internal.WebSocketsTransport[1]
2023-02-16T14:05:25.034960072Z Starting transport. Transfer mode: Binary. Url: 'wss://nmg-opus-common-inventory-dev-sigr.service.signalr.net/server/?hub=myhub&cid=9e64b380-b752-4175-8fd2-215a2b62139d'.
2023-02-16T14:05:25.057819457Z info: Microsoft.Azure.SignalR.Connections.Client.Internal.WebSocketsTransport[11]
2023-02-16T14:05:25.057859557Z WebSocket closed by the server. Close status NormalClosure.
2023-02-16T14:05:25.057865857Z info: Microsoft.Azure.SignalR.ServiceConnection[24]
2023-02-16T14:05:25.057870457Z Connection to service '(Primary)https://nmg-opus-common-inventory-dev-sigr.service.signalr.net(hub=MyHub)' handshake failed, probably caused by network instability or service restart. Will retry after the back off period. Error detail: Azure SignalR Service is in serverless mode, server connection is not allowed.. Id: 9e64b380-b752-4175-8fd2-215a2b62139d
2023-02-16T14:05:25.057875657Z info: Microsoft.Azure.SignalR.Connections.Client.Internal.WebSocketsTransport[6]
2023-02-16T14:05:25.057880257Z Transport is stopping.
2
On the client side, it seems that the connection succeeds if the service is set to Serverless mode.
Once connected, my call to join a group just hang and never returns. The connection happens after I click an element (div) on a blazor page.
So I am not sure why something so simple does not work.
here is my very simple hub :
public class MyHub : Hub
{
public async Task SendMessage(string groupName, string message)
{
await Group(groupName).SendAsync("ReceiveMessage", message);
}
public async Task JoinGroup(string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
}
public async Task LeaveGroup(string groupName)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
}
public override async Task OnConnectedAsync()
{
await Clients.Client(Context.ConnectionId).SendAsync("Connected", Context.ConnectionId);
}
}
here is the classic startup method for the app defining hub
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddAzureAppConfiguration();
services.AddSignalR(e => { e.MaximumReceiveMessageSize = 10240000; });
string SignalRURLHub = Configuration.GetValue<string>("Resources:SignalR:Inventory:ConnectionString");
services.AddSignalR().AddAzureSignalR(SignalRURLHub);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAzureAppConfiguration();
app.UseRouting();
var SignalRHubName = $"/{Configuration.GetValue<string>("Resources:SignalR:HubName")}";
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<MyHub>(SignalRHubName);
});
}
}
Code from the client applications Initializing the connection and starting it.
protected HubConnection _connection;
private readonly string groupName;
protected ClientHandler(string connectionString, string hubName, string groupName, string userId = null)
{
var serviceUtils = new ServiceUtils(connectionString);
var url = GetClientUrl(serviceUtils.Endpoint, hubName);
_connection = new HubConnectionBuilder()
.WithUrl(url
, option =>
{
option.AccessTokenProvider = () =>
{
return Task.FromResult(serviceUtils.GenerateAccessToken(url, userId));
};
})
.WithAutomaticReconnect(new AlwaysRetryPolicy())
.ConfigureLogging(logging =>
{
logging.SetMinimumLevel(LogLevel.Trace);
logging.AddConsole();
})
.Build();
this.groupName = groupName;
_connection.Reconnected += JoinGroup;
_connection.On("Connected", new[] { typeof(string) }, OnInitialConnection);
}
private string GetClientUrl(string endpoint, string hubName)
{
return $"{endpoint}/client/?hub={hubName}";
}
private async Task JoinGroup(string contextId)
{
try
{
await _connection.InvokeAsync("JoinGroup", groupName); //JoinGroup is C# method name
}
catch (Exception e)
{
}
}
// Called separately after the ClientHandler object is created.
public async Task StartAsyncWithRetry(CancellationToken cancellationToken = default)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
await _connection.StartAsync()
.ContinueWith(res =>
{
_connection.InvokeAsync("JoinGroup", groupName);
});
if (_connection.State == HubConnectionState.Disconnected)
{
await Task.Delay(GetRandomDelayMilliseconds());
continue;
}
return;
}
catch (Exception e)
{
//throw;
//cancellationToken.
await Task.Delay(GetRandomDelayMilliseconds());
}
}
}
I have tried tweaking various setting in Azure, in my apps, changed my code from synch to async...
I must be missing something ... I've found lot of post but many of them were out of date. Also tried my luck with ChatGPT ...

.Net Core Web API Basic Authentication Authorize does not work on Azure

I wrote a custom auth handler for a web API in .net core 3.0 following this tutorial by Jason Watmore. The Authorize attribute works great IIS express. However, when I publish the code to Azure Web App the Authorize attribute does not fire. There is no authentication challenge and data is returned without authentication.
Azure Authentication Authorization Settings
Here is the custom BasicAuthenticationHandler
public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly IAPIRepo _apiRepo;
public BasicAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder,
ISystemClock clock,
IAPIRepo apiRepo): base(options, logger, encoder, clock)
{
_apiRepo = apiRepo;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
//throw new NotImplementedException();
if (!Request.Headers.ContainsKey("Authorization"))
return AuthenticateResult.Fail("Missing Authorization Header");
User user = null;
try
{
var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
var credentials = Encoding.UTF8.GetString(credentialBytes).Split(new[] { ':' }, 2);
var username = credentials[0];
var password = credentials[1];
user = _apiRepo.Authenticate(username, password);
}
catch
{
return AuthenticateResult.Fail("Invalid Authorization Header");
}
if (user == null)
return AuthenticateResult.Fail("Invalid Username or Password");
var claims = new[] {
new Claim(ClaimTypes.NameIdentifier, user.User_Id.ToString()),
new Claim(ClaimTypes.Name, user.UserName),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
Startup.cs
services.AddScoped<IAPIRepo, APIRepo>();
services.AddAuthentication("BasicAuthentication")
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Edit: Difference between .net core 2.2 and 3.1. Changing the run time to 3.1 fixed the issue
It looks like you are using the Startup.cs of .NET Core 3.0 instead of 3.1 like the article is using.

Activate Azure Ad authentication when you hit https://host:port/swagger on net Core 2 Api?

I make all changes on my api to use Azure Ad with this and this link features, but when the api is deployed, I need to make the user who gets the Url https://myapi.com/swagger (for example) to redirect it to azure Ad login,then know if the client have rights or not to use this api and redirect it again to my api and show the enpoints he have access.
I make some changes on startup.cs to use OpenIdConnect
//Add AddAzureAdBearer Auth options
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = OpenIdConnectDefaults.AuthenticationScheme;
//options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddOpenIdConnect(option =>
{
option.ClientId = Client;
option.Authority = $"{Instance}/{Tenant}";
option.SignedOutRedirectUri = "https://localhost:44308";
option.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
option.SaveTokens = true;
option.Events = new OpenIdConnectEvents
{
OnRemoteFailure = context =>
{
context.HandleResponse();
return Task.CompletedTask;
}
};
})
.AddCookie()
.AddAzureAdBearer(options => _configuration.Bind("Ad", options));
And I add a HomeController to redirect to swagger UI:
[Authorize]
public class HomeController : Controller
{
[HttpGet("")]
public ActionResult Index()
{
return Redirect("~/swagger");
}
}
When I launch the api, it works as spected, but when y write https://{host:port}/swagger it does not work, don't hit the authentication process and goes to https://{host:port}/swagger/index.html automatically.
How can I fix this?
I'm working with net core 2.0 and Swashbuckle for swagger.
You you need to add Swagger support to ConfigureServices(IServiceCollection services) and to Configure(IApplicationBuilder app, IHostingEnvironment env) in your application’s Startup.cs file. To do so, you need to create a SwaggerServiceExtensions class and add the necessary code to support Swagger in your app.
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Swashbuckle.AspNetCore.Swagger;
namespace JwtSwaggerDemo.Infrastructure
{
public static class SwaggerServiceExtensions
{
public static IServiceCollection AddSwaggerDocumentation(this IServiceCollection services)
{
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1.0", new Info { Title = "Main API v1.0", Version = "v1.0" });
c.AddSecurityDefinition("Bearer", new ApiKeyScheme
{
Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
Name = "Authorization",
In = "header",
Type = "apiKey"
});
});
return services;
}
public static IApplicationBuilder UseSwaggerDocumentation(this IApplicationBuilder app)
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1.0/swagger.json", "Versioned API v1.0");
c.DocExpansion("none");
});
return app;
}
}
}
Changes in Startup.cs file
Using the above class, the only thing you need to do in your Startup.cs file is the following:
namespace JwtSwaggerDemo
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
public IServiceProvider ConfigureServices(IServiceCollection services)
{
//... rest of services configuration
services.AddSwaggerDocumentation();
//...
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
//.... rest of app configuration
app.UseSwaggerDocumentation();
}
//.... rest of app configuration
}
}
}
Authorize requests in Swagger UI
Now, when you load the Swagger’s UI address (e.g: https://localhost:44321/swagger/#/), you will see an Authorize button at the top. Clicking on it leads to a modal window, which allows you to authorize your app with a JWT token, by adding Bearer in the value input field.

Azure App Service Mobile authentication not compatible with MVC/Forms

I have added a Web API service to a 'legacy' MVC project (not vNext), still using Membership and the forms authentication module.
All OK, until I decided to make the web api a mobile service using the latest SDKs for Azure App Services for Mobile.
I have so far narrowed the problem to this
//app.UseAppServiceAuthentication(new AppServiceAuthenticationOptions()
//{
// SigningKey = CloudConfigurationManager.GetSetting("authSigningKey"),
// ValidAudiences = new[] { CloudConfigurationManager.GetSetting("authAudience") },
// ValidIssuers = new[] { CloudConfigurationManager.GetSetting("authIssuer") },
// TokenHandler = GlobalConfiguration.Configuration.GetAppServiceTokenHandler()
//});
This changes the rest of the app so that MVC + forms auth doesn't work. Running out of time to research the problem.
Any clues??
The following solution is working for me:
In your Startup register call UseCookieAuthentication, UseExternalSignInCookie or UseOAuthAuthorizationServer before calling UseAppServiceAuthentication.
Second step:
Add the following class to your project:
private sealed class CustomAppServiceAuthenticationMiddleware : AppServiceAuthenticationMiddleware
{
private readonly ILogger _logger;
public CustomAppServiceAuthenticationMiddleware(OwinMiddleware next, IAppBuilder appBuilder, AppServiceAuthenticationOptions options) : base(next, appBuilder, options)
{
_logger = (ILogger)GetType().BaseType.GetField("logger", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(this);
}
protected override AuthenticationHandler<AppServiceAuthenticationOptions> CreateHandler()
{
return new AppServiceAuthenticationHandler(_logger);
}
public override Task Invoke(IOwinContext context)
{
string logLine = $"AppServiceAuthMiddleware: {context.Request.Path}";
if (context.Request.Headers.TryGetValue("Authorization", out var values))
logLine += $"; Authorization: {values.First().Split(' ').FirstOrDefault()}";
if (context.Request.Headers.TryGetValue("X-ZUMO-AUTH", out values))
logLine += $"; X-ZUMO-AUTH: {values.First()}";
_logger.WriteVerbose(logLine);
Debug.WriteLine(logLine);
if (IsZumoAuth(context))
{
return base.Invoke(context);
}
return Next.Invoke(context);
}
private bool IsZumoAuth(IOwinContext context)
{
return context.Request.Headers.ContainsKey("X-ZUMO-AUTH");
}
}
Thrid step:
Replace the app.UseAppServiceAuthentication with the following:
app.Use(typeof(CustomAppServiceAuthenticationMiddleware), app, new AppServiceAuthenticationOptions
{
SigningKey = ...,
ValidAudiences = ...,
ValidIssuers = ...,
TokenHandler = ...
});
This will the owin pipline make call the AppServiceAuthenticationMiddleware just for ZUMO-AUTH auth.
I've got a mixed web & mobile app.
With this approach the membership auth on the web app is working.
In the app, some custom oauth (refresh token based) plus the azure auth (facebook, google, ...) is working too. All that within the same asp.net application.

How do I modify WIF's ValidatingIssuerNameRegistry to support Azure AD, ACS, Facebook, LiveID and other IDPs?

I have an application that I'd like to expose to as many users as possible. To accomplish this I'm following the directions as explained here to connect my app to Azure Active Directory and a variation of these instructions to connect AAD to Azure ACS 2.0.
Azure ACS 2.0 will handle all the federated domains, and Microsoft Accounts (formerly LiveID or Passport). It will also handle Facebook, Twitter, and other OAuth services.
Azure Active directory will handle Office 365, and anyone who is syncing their corporate Active Directory to the cloud.
My home realm discovery page will issue a GET at the following URL to determine if the LiveID or AzureAD domain should be used.
https://login.microsoftonline.com/GetUserRealmExtended.srf?login=EMAIL#COMPANY.com
or
http://odc.officeapps.live.com/odc/emailhrd/getidp?hm=0&emailAddress=USER%COMPANY.com
If the user doesn't exist, I'll use Azure ACS with a federation to that company. Lacking that, the user won't be able to log in.
Now that I explained my configuration, I intend to have Windows Identity Foundation (WIF) to allow authentications from both ACS 2.0 and ADFS.
Question
How do I get WIF 4.5, and specifically the ValidatingIssuerNameRegistry to properly handle multiple trusts to multiple IDPs?
Below is code that comes with VS2013 when federating an application with Azure Active Directory. It responds to all federation requests and does other things that I don't understand. Any links or information on this class would be helpful
public class DatabaseIssuerNameRegistry : ValidatingIssuerNameRegistry
{
public static bool ContainsTenant(string tenantId)
{
using (TenantDbContext context = new TenantDbContext())
{
return context.Tenants
.Where(tenant => tenant.Id == tenantId)
.Any();
}
}
public static bool ContainsKey(string thumbprint)
{
using (TenantDbContext context = new TenantDbContext())
{
return context.IssuingAuthorityKeys
.Where(key => key.Id == thumbprint)
.Any();
}
}
public static void RefreshKeys(string metadataLocation)
{
IssuingAuthority issuingAuthority = ValidatingIssuerNameRegistry.GetIssuingAuthority(metadataLocation);
bool newKeys = false;
foreach (string thumbprint in issuingAuthority.Thumbprints)
{
if (!ContainsKey(thumbprint))
{
newKeys = true;
break;
}
}
if (newKeys)
{
using (TenantDbContext context = new TenantDbContext())
{
context.IssuingAuthorityKeys.RemoveRange(context.IssuingAuthorityKeys);
foreach (string thumbprint in issuingAuthority.Thumbprints)
{
context.IssuingAuthorityKeys.Add(new IssuingAuthorityKey { Id = thumbprint });
}
context.SaveChanges();
}
}
}
public static bool TryAddTenant(string tenantId, string signupToken)
{
if (!ContainsTenant(tenantId))
{
using (TenantDbContext context = new TenantDbContext())
{
SignupToken existingToken = context.SignupTokens.Where(token => token.Id == signupToken).FirstOrDefault();
if (existingToken != null)
{
context.SignupTokens.Remove(existingToken);
context.Tenants.Add(new Tenant { Id = tenantId });
context.SaveChanges();
return true;
}
}
}
return false;
}
public static void AddSignupToken(string signupToken, DateTimeOffset expirationTime)
{
using (TenantDbContext context = new TenantDbContext())
{
context.SignupTokens.Add(new SignupToken
{
Id = signupToken,
ExpirationDate = expirationTime
});
context.SaveChanges();
}
}
public static void CleanUpExpiredSignupTokens()
{
DateTimeOffset now = DateTimeOffset.UtcNow;
using (TenantDbContext context = new TenantDbContext())
{
IQueryable<SignupToken> tokensToRemove = context.SignupTokens.Where(token => token.ExpirationDate <= now);
if (tokensToRemove.Any())
{
context.SignupTokens.RemoveRange(tokensToRemove);
context.SaveChanges();
}
}
}
protected override bool IsThumbprintValid(string thumbprint, string issuer)
{
string issuerID = issuer.TrimEnd('/').Split('/').Last();
return ContainsTenant(issuerID) &&
ContainsKey(thumbprint);
}
}
Vittorio Bertocci does a good job of explaining the DatabaseIssuerNameRegistry in this post.
VS2013 RTM, Organizational Accounts and Publishing to Windows Azure Web Sites!
The bottom line is that DatabaseIssuerNameRegistry is just an Entity Framework based ValidatingIssuerNameRegistry that looks up the issuer name from a database using the the thumbprint of the token and verifies that it matches the configured value for the Issuer name, as opposed to using the web.config.It is more flexible and handles updating thumbprints if/when the authority changes them.

Resources