-
Notifications
You must be signed in to change notification settings - Fork 446
Jwt #888
Comments
Can you pass it in the queryString? |
I'd like to know, too :) |
Yes, you can pass the JWT token in the query string and use the authentication handler events to get the token. Will try to put together a sample. |
Why would you pass the token in the querystring? This is unsecure. Auth tokens should only be passed in the header, the querystring is logged everywhere on the network. |
@damienbod because thats' the only way to get it over the wire with websockets from javascript. Sending it over the websocket itself isn't using http, it's custom. This problem isn't unique to SignalR see https://stackoverflow.com/questions/31680641/passing-jwt-to-node-js-websocket-in-authorization-header-on-initial-connection as an example. |
Perhaps we need to attempt a sample of sending the token as part of a handshake over the websocket after connection, assuming the ASP.NET Core security bits are factored such that would even be possible right now (it might not be). This has been an issue forever. |
I don't think we should, that would be adding authentication to our protocol. The ASP.NET authentication stack is very much tied to HTTP. The authorization stack isn't. |
I meant a sample at the application level. |
In previous release (SignalR 2 & Core 1.1), we used to make like this: ng Client const channelConfig = new ChannelConfig();
channelConfig.url = this.appConfig.config.signalRConfig.url;
channelConfig.hubName = this.appConfig.config.signalRConfig.hubName;
this.channelService.init(channelConfig);
this.channelService.start({user: this._adalService.userInfo.username, token: this._adalService.userInfo.token}); Middleware public static void UseSignalRQSToken(this IApplicationBuilder app)
{
app.Use(async (context, next) =>
{
var x = context.Request.Path.Value;
if (x.StartsWith("/signalr/"))
{
StringValues token;
if (context.Request.Query.TryGetValue("token", out token))
{
context.Request.Headers.Add("Authorization", new string[] { "bearer " + token });
}
}
await next();
});
} Hub [Authorize]
[HubName("SignalRHub")]
public class SignalRHub : Hub
{
} Is it possible to do the same with SignalR Core & core 2.0? |
any update on this? It would be great to be able to add JWT auth to connections. I suppose i could do this manually somehow but it would be great to get some built in support. |
Actually it works just like @Zigzag95 said. TS-Client (ng)
Server middleware exactly like @Zigzag95 's. Just be sure to add app.UseSignalRQSToken() before app.UseAuthentication(); in startup.cs |
@Zigzag95 @MoxxNull Your sending your access_token in the querystring for everyone to copy paste. Your application is now unsecure. @davidfowl @DamianEdwards You should not support this, a different way of securing SignalR should be considered, maybe one time tokens, but not access_tokens in the query string |
@willthiswork89 what built in support are you expecting? There's nothing unique here really. There's nothing in our stack today that issues JWT tokens, so assuming you're using something like identity server to do that, the only question is about passing the token via the query string and then using the JWT Bearer authentication handler read the token from the query string instead of a header. |
@damienbod there's nothing to not support. It won't be blocked.
Copy and paste where? The main difference between the header and querystring is that some systems log query strings. If the access token expires and is revokable it's not as big a deal as you make it out to be, or am I missing something? |
The debate over HTTPS URLs (including query strings) is long and on-going. Yes, it's not ideal to send sensitive data in the URL even when over HTTPS. But the fact remains that when using the browser WebSocket APIs there is no other way. You only have 3 options:
A usable sample of the last would be interesting in my mind, but I'm not expecting it to be trivial. |
Right! Option 3 is messy only because it requires a protocol change. It's possible we could do this as part of negotiate but at that point this isn't http auth anymore, it's custom so you get none of the reuse of the rest of the stack. |
Why couldn't it be done in a given Hub? |
I just need the token for OnConnect(). The token is used to add the client in the appropriate group or reject it. |
You can't really get anything in OnConnectedAsync that hasn't been sent with the original web socket request. You need to invoke a method passing in the token and then you can store that on the connection object server side. @DamianEdwards it can be with an explicit method call all in user code. What code reads the JWT and makes sense for it? That code needs to be extracted from the authN handler and turned into a user. |
After giving this some thought it might be possible to do this:
services.AddAuthentication()
.AddJwtBearer(o =>
{
o.Events.OnMessageReceived = context =>
{
context.Token = (string)context.HttpContext.Items["token"];
return Task.CompletedTask;
};
}); |
@davidfowl yeah I meant completely in the application, client and server. Client establishes connection then calls |
@davidfowl When the access_token is sent in the querystring, it is public to anyone sniffing, monitoring, logging etc. and as you said most systems log the querystring, which are also public access again. This means that anyone, or bot using this token can access any APIs, data which use this access_token, if it's valid, not just the SignalR data. |
To be clear, it's not in Plaintext in transit if you use HTTPS. It will appear in server logs and any systems the app has configured to capture URLs for monitoring, but it can't be sniffed by parties in between the browser and server. |
I'm liking where @davidfowl is headed as it seems to be the closest to giving us what we need |
For us using a querystring over SSL is fine. Client side with TypeScript in an Angular app:
|
@ChristianWeyer you don't have to transfer the token to a header to make it work, you can do this: services.AddAuthentication()
.AddJwtBearer(o =>
{
o.Events.OnMessageReceived = context =>
{
context.Token = context.HttpContext.Request.Query[_queryStringName];
return Task.CompletedTask;
};
}); |
We are already using the IdentityServer middleware for active token introspection. |
@ChristianWeyer Asuming you are using
|
@DamianEdwards, @davidfowl in RFC6455 there one interesting point:
I've found an example how to add custom header to handshake: https://blog.heckel.xyz/2014/10/30/http-basic-auth-for-websocket-connections-with-undertow/ I'm not an expert like You guys, but maybe JWT token could be added as a header to handshake and then on serwer-side #888 (comment) could be used. Adding one additional header to handshake won't be that bad, if it's not there or server won't be configured to handle that header then nothing would change. The only thing I'm not sure is whether custom headers for handshake can be set in a web browser 😕 |
No, it cannot. |
Working solutionStartup public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.ClientId = Configuration["Authentication:AzureAd:ClientId"];
options.Authority = Configuration["Authentication:AzureAd:AADInstance"] + Configuration["Authentication:AzureAd:TenantId"];
})
.AddJwtBearer(options =>
{
options.Authority = Configuration["Authentication:AzureAd:AADInstance"] + Configuration["Authentication:AzureAd:TenantId"];
options.Audience = Configuration["Authentication:AzureAd:Audience"];
options.Events = new JwtBearerEvents();
options.Events.OnMessageReceived = context =>
{
StringValues token;
if (context.Request.Path.Value.StartsWith("/signalr") && context.Request.Query.TryGetValue("token", out token))
{
context.Token = token;
}
return Task.CompletedTask;
};
});
services.AddSignalR(options => options.JsonSerializerSettings = new Newtonsoft.Json.JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() });
services.AddSingleton(typeof(HubLifetimeManager<NotificationHub>), typeof(NotificationHubLifetimeManager<NotificationHub>));
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseAuthentication();
app.UseSignalR(routes =>
{
routes.MapHub<NotificationHub>("signalr");
});
app.UseMvc();
} Hub [Authorize(AuthenticationSchemes = " Bearer")]
public class NotificationHub: Hub
{
public async Task Send(string message)
{
await Clients.All.InvokeAsync("Send", message);
}
public override async Task OnConnectedAsync()
{
...
}
public override async Task OnDisconnectedAsync(Exception exception)
{
...
}
}
HubLifetimeManager public class NotificationHubLifetimeManager<THub> : DefaultHubLifetimeManager<THub>
where THub : Hub
{
} Controller [Authorize(AuthenticationSchemes = "OpenIdConnect, Bearer")]
public class TestController : Controller
{
public TestController(HubLifetimeManager<NotificationHub> hubManager)
{
_hubManager = hubManager;
}
[HttpGet("testSig")]
public async Task Send(string message)
{
await _hubManager.InvokeAllAsync("Send", new string[] { "Hello" });
}
} Angular service import { Injectable } from '@angular/core';
// these will be added once ng cli will release 1.5
// import { HubConnection, TransportType } from '@aspnet/signalr-client';
// import { IHubConnectionOptions } from '@aspnet/signalr-client/dist/src/IHubConnectionOptions';
declare var signalR: any;
@Injectable()
export class SignalRService {
// public connection: HubConnection;
public connection: any;
public init(url: string, token: string) {
// const options: IHubConnectionOptions = {
// transport: TransportType.WebSockets
// };
const options: any = {
transport: 0
};
this.connection = new signalR.HubConnection(url + '?token=' + token, options);
this.connection.start();
}
} .angular-cli.json {
...
"scripts": [
"../node_modules/@aspnet/signalr-client/dist/browser/signalr-clientES5-1.0.0-alpha1-final.min.js",
"../node_modules/@aspnet/signalr-client/dist/browser/signalr-msgpackprotocol-1.0.0-alpha1-final.min.js"
]
...
}
|
@Zigzag95 could You please post link to issue in angular cli that You mentioned in Your code?
|
@Misiu Up to 1.4, ng cli does not support support ES2015 because of uglifyjs. |
@Zigzag95 thanks for link 😄 |
Did someone manage to do it with calling custom method after handshake? Can this be done and then used |
I merged a change (0bafb30 18a6549) today that adds an API that allows to pass a JWT token when starting a connection. When using the C# client the JWT token will be passed in the header. When using the TS client the token will be passed in the header when sending an HTTP request with XmlHttpRequest (i.e. for negotiate, long polling - send/poll request, server sent events - send request) or in the query string if the underlying API does not allow setting headers (webSockets, eventSource used for the server-to-client channel in the ServerSentEvents transport) as the |
@moozzyk The latest npm version is |
You can get the latest dev build via |
I was trying to add Authorization to a SignalR Hub within a project using IdentityServer4 auth, and came across this issue when looking for a solution. Originally, I thought @Falco20019's comment above would be the answer. As it turns out, my Hub was still throwing 401s upon attempting to connect via WebSockets and SSE connections, even though I could see Eventually I came across this comment over at the IdentityServer4 repo, where it was discovered that even though Startup.cs...
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = WebOptions.Sts.Authority;
options.ApiName = WebOptions.Sts.ApiName;
options.RequireHttpsMetadata = false;
options.TokenRetriever = BearerTokenRetriever.FromHeaderAndQueryString;
});
... Custom TokenRetriever// https://github.com/IdentityServer/IdentityServer4/issues/2349
public class BearerTokenRetriever
{
static Func<HttpRequest, string> AuthHeaderTokenRetriever { get; set; }
static Func<HttpRequest, string> QueryStringTokenRetriever { get; set; }
static BearerTokenRetriever()
{
AuthHeaderTokenRetriever = TokenRetrieval.FromAuthorizationHeader();
QueryStringTokenRetriever = TokenRetrieval.FromQueryString();
}
public static string FromHeaderAndQueryString(HttpRequest request)
{
var token = AuthHeaderTokenRetriever(request);
if (string.IsNullOrEmpty(token))
{
token = QueryStringTokenRetriever(request);
}
return token;
}
} Note that the custom TokenRetriever above is a slightly trimmed down version of the one described in the IdentityServer4 repo, where we look for the access_token in the query string if it cannot be found in the Authorization header - it works for my particular use-cases. Hope this is of use to anyone else using IdentityServer4. |
Any sample for how to pass a Auth token while connecting.
The text was updated successfully, but these errors were encountered: