I have a SPA (angular 7) and an API (.Net Core) which I authenticate with Azure AD. I'm using adal-angular4 to integrate my angular application with AAD.
Everything works great, but I'm also using SignalR with the API as server and when I try to connect from my SPA I get 401 Unauthorized on the negotiate "request" and I get this back in the Response Headers:
The request contains my Bearer token in the Authorization header, and when I run the token through jwt.io, I can see that the "aud" value is the Azure AD ClientId for my SPA.
All regular request to the API contains the same token and I have no issues with those. I have [Authorize] on all my Controllers and on my Hub, but it's only the SignalR Hub that causes this issue.
My server Startup:
public Startup(IConfiguration configuration, IHostingEnvironment env)
{
Configuration = configuration;
_env = env;
}
public IConfiguration Configuration { get; }
private IHostingEnvironment _env;
public void ConfigureServices(IServiceCollection services)
{
StartupHandler.SetupDbContext(services, Configuration.GetConnectionString("DevDb"));
// Setup Authentication
services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
.AddAzureADBearer(options =>
{
Configuration.Bind("AzureAD", options);
});
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
// Add functionality to inject IOptions<T>
services.AddOptions();
// Add AzureAD object so it can be injected
services.Configure<AzureAdConfig>(Configuration.GetSection("AzureAd"));
services.AddSignalR(options =>
{
options.EnableDetailedErrors = true;
options.KeepAliveInterval = TimeSpan.FromSeconds(10);
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseDeveloperExceptionPage();
app.UseHsts();
}
app.UseCookiePolicy();
app.UseHttpsRedirection();
//app.UseCors("AllowAllOrigins");
app.UseCors(builder =>
{
builder.AllowAnyOrigin();
builder.AllowAnyMethod().AllowAnyHeader();
builder.AllowCredentials();
});
app.UseAuthentication();
app.UseSignalR(routes => routes.MapHub<MainHub>("/mainhub"));
app.UseStaticFiles(new StaticFileOptions()
{
FileProvider = new PhysicalFileProvider(Path.Combine(_env.ContentRootPath, "Files")),
RequestPath = new PathString("/Files")
});
app.UseMvc();
}
My SignalR Hub:
[Authorize]
public class MainHub : Hub
{
private readonly IEntityDbContext _ctx;
public MainHub(IEntityDbContext ctx)
{
_ctx = ctx;
_signalRService = signalRService;
}
public override Task OnConnectedAsync()
{
return base.OnConnectedAsync();
}
public override Task OnDisconnectedAsync(Exception exception)
{
return base.OnDisconnectedAsync(exception);
}
}
And this is my SignalRService on my angular client. I'm running startConnection() in the constructor of app.component.ts.
export class SignalRService {
private hubConnection: signalR.HubConnection;
constructor(private adal: AdalService) {}
startConnection(): void {
this.hubConnection = new signalR.HubConnectionBuilder()
.withUrl(AppConstants.SignalRUrl, { accessTokenFactory: () => this.adal.userInfo.token})
.build();
this.hubConnection.serverTimeoutInMilliseconds = 60000;
this.hubConnection.on('userConnected', (user) =>
{
console.log(user);
});
this.hubConnection.start()
.then(() => console.log('Connection started'))
.catch(err =>
{
console.log('Error while starting connection: ' + err);
});
}
}
I have tried this solution, but I can't get that to work either.
Edit
When I've implemented the solution from the official docs, the API stops working on regular requests as well and I get back:
I've populate the IssuerSigningKey
property in TokenValidationParameters
with new SymmetricSecurityKey(Guid.NewGuid().ToByteArray());
. Am I doing anything wrong here?
/EDIT
Why won't SignalR accept my accesstoken when the API otherwise accept it?
Sign in to the Azure portal. In the upper-left side of the page, select + Create a resource. On the Create a resource page, in the Search services and marketplace text box, enter signalr and then select SignalR Service from the list.
If your SignalR application transmits sensitive information between the client and server, use SSL for the transport.
A SignalR connection can end in any of the following ways: If the client calls the Stop method, a stop message is sent to the server, and both client and server end the SignalR connection immediately.
If you want to restrict access to it you need to authenticate users and authorize their actions. You authenticate using standard web auth methods (forms auth, cookies, Windows auth, etc.) and you can authorize in code using SignalR constructs (like the Authorize attribute you point out) or with your own code.
Just have a look at the official docs. You need a special handling for the JWT Bearer events so your authentication is working. The token needs to be forwarded to the hubs. Have a look at the part where I said
THAT PART IS MISSING
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddAuthentication(options =>
{
// Identity made Cookie authentication the default.
// However, we want JWT Bearer Auth to be the default.
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
// Configure JWT Bearer Auth to expect our security key
options.TokenValidationParameters =
new TokenValidationParameters
{
LifetimeValidator = (before, expires, token, param) =>
{
return expires > DateTime.UtcNow;
},
ValidateAudience = false,
ValidateIssuer = false,
ValidateActor = false,
ValidateLifetime = true,
IssuerSigningKey = SecurityKey
};
//THAT IS THE PART WHICH IS MISSING IN YOUR CONFIG !
// We have to hook the OnMessageReceived event in order to
// allow the JWT authentication handler to read the access
// token from the query string when a WebSocket or
// Server-Sent Events request comes in.
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/hubs/chat")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSignalR();
// Change to use Name as the user identifier for SignalR
// WARNING: This requires that the source of your JWT token
// ensures that the Name claim is unique!
// If the Name claim isn't unique, users could receive messages
// intended for a different user!
services.AddSingleton<IUserIdProvider, NameUserIdProvider>();
// Change to use email as the user identifier for SignalR
// services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();
// WARNING: use *either* the NameUserIdProvider *or* the
// EmailBasedUserIdProvider, but do not use both.
}
When validating the signature of access token , you should get the public key since Azure AD may sign token using any one of a certain set of public-private key pairs , the keys could be found at :
https://login.microsoftonline.com/{tenant}/.well-known/openid-configuration
Within the JSON response, you’ll see a property jwks_uri
which is the URI that contains the JSON Web Key Set for Azure AD. Matching the kid
claim in jwt token , you can find the key which AAD used to sign the token with asymmetric encryption algorithms, such as RSA 256 by default .
In asp.net core apis , when validating the access token which issued by Azure AD , you can use AddJwtBearer
extension and provide the correct Authority
, so that middleware will correctly get the keys from Azure AD OpenID configuration endpoint :
options.Authority = "https://login.microsoftonline.com/yourtenant.onmicrosoft.com/"
Another choice is to use AddAzureADBearer
extension from Microsoft.AspNetCore.Authentication.AzureAD.UI
library . You should also set correct authority(instance + domain)
, middleware will help validating the signature and claims based on your configuration .
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With