Access token query string after negotiate is called
Closed this issue · 9 comments
How to pass accessToken as a query string in redirect url when negotiate is called?
I create a Hubconnection using HubConnectionBuilder and providing accessToken via accessTokenProvider in httpConnectionOptions, but when negotiate is called I need that access token to be provided as a query string how to achieve that?
Commented part of the code fixes issue for me
Redirection URL is sent by the server and is opaque for the client. The client does not have any knowledge about this URL, cannot reason about its structure or change its contents. I am looking at how the TypeScript SignalR client handles this and I am not seeing any code that would append user's query parameters to the redirect URL. I am curious about your requirement and how it works with other clients.
They are using this [https://www.npmjs.com/package/@microsoft/signalr/v/7.0.10] 3rd party library, and they expect that token would be sent with the query string, I'm not sure if its fail on their part or I'm not understanding something
Built-in JWT authentication
On the server, bearer token authentication is configured using the JWT Bearer middleware:
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using SignalRAuthenticationSample.Data;
using SignalRAuthenticationSample.Hubs;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using SignalRAuthenticationSample;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.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 the Authority to the expected value for
// the authentication provider. This ensures the token
// is appropriately validated.
options.Authority = "Authority URL"; // TODO: Update URL
// 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.
// Sending the access token in the query string is required when using WebSockets or ServerSentEvents
// due to a limitation in Browser APIs. We restrict it to only calls to the
// SignalR hub in this code.
// See https://docs.microsoft.com/aspnet/core/signalr/security#access-token-logging
// for more information about security considerations when using
// the query string to transmit the access token.
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;
}
};
});
builder.Services.AddRazorPages();
builder.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!
builder.Services.AddSingleton<IUserIdProvider, NameUserIdProvider>();
// Change to use email as the user identifier for SignalR
// builder.Services.AddSingleton<IUserIdProvider, EmailBasedUserIdProvider>();
// WARNING: use *either* the NameUserIdProvider *or* the
// EmailBasedUserIdProvider, but do not use both.
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.MapHub<ChatHub>("/chatHub");
app.Run();`
```
They are using this [https://www.npmjs.com/package/@microsoft/signalr/v/7.0.10]
I am curious how the access token is sent using this library. The code pointers I provided above link to this library and I didn't see the library handling query string parameters in any special way, so I must be missing something.
I think one thing to make it work could be to use webSockets and skip negotiation. In this case you could append access token to the query string.
Thanks for quick response, I'll try to solve it somehow
The browser webSocket API does not allow to send headers. Because of that the client you linked to moves access_token provided by the access token factory to the query string:
I wonder if this is related.
If this is not an Azure scenario, you could skip negotiation by setting HttpConnectionOptions.skipNegotiation
to true
. If you do this you have to use WebSockets but then you should be able to provide the access token in the URL passed when creating the connection.
Seems like just passing accessToken to the URL and not skipping the negotiation process solves the issue, connection is open and I receive events, but then I'm confused a little bit how it will work when the accessToken is expired?
Seems like just passing accessToken to the URL and not skipping the negotiation process solves the issue
Oh, I thought you opened the issue because it didn't work...
but then I'm confused a little bit how it will work when the accessToken is expired?
It kinda works until you hit edges. If you are using webSockets, which is most likely because it is the default and most reliable transport, you are only authorized when establishing the connection. Once the connection is established it will continue to work even if the token expires. The problems start when your connection gets disconnected - the client will try to reconnect but, if the token has expired, reconnection will fail. If this happens you need to get a new access token and start the connection from scratch.
If, for whatever reason you end up using long polling then if the token expires the connection will start failing unexpectedly, it won't be able to reconnect and then it will be closed.
At the time I was not able to make it work, you can close the issue, thank you for responses! And apparently when token expires I'm not receiving any events anymore even though the connection is open, so its still a question mark when to refresh the token and how to update the signalR connection with the new token