Skip to content
This repository has been archived by the owner on Dec 18, 2018. It is now read-only.

Jwt #888

Closed
aminebizid opened this issue Sep 15, 2017 · 39 comments
Closed

Jwt #888

aminebizid opened this issue Sep 15, 2017 · 39 comments
Milestone

Comments

@aminebizid
Copy link

Any sample for how to pass a Auth token while connecting.

@moozzyk
Copy link
Contributor

moozzyk commented Sep 15, 2017

Can you pass it in the queryString?

@ghost
Copy link

ghost commented Sep 15, 2017

I'd like to know, too :)

@davidfowl
Copy link
Member

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.

@damienbod
Copy link

damienbod commented Sep 16, 2017

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.

@davidfowl
Copy link
Member

davidfowl commented Sep 16, 2017

@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.

@DamianEdwards
Copy link
Member

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.

@davidfowl
Copy link
Member

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.

@DamianEdwards
Copy link
Member

I meant a sample at the application level.

@aminebizid
Copy link
Author

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?

@willthiswork89
Copy link

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.

@ghost
Copy link

ghost commented Sep 19, 2017

Actually it works just like @Zigzag95 said.

TS-Client (ng)

let url = hubUrl + "?token=" + access_token;           
this.httpCon = new HttpConnection(url, { transport: TransportType.WebSockets }); 
this.hubConnection = new HubConnection(this.httpCon);        
this.hubConnection.start().then((v) => { console.log("Connection Init complete!"); })
		.catch(err => console.error(err)));

Server middleware exactly like @Zigzag95 's. Just be sure to add app.UseSignalRQSToken() before app.UseAuthentication(); in startup.cs

@damienbod
Copy link

@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

@davidfowl
Copy link
Member

@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.

@davidfowl
Copy link
Member

@damienbod there's nothing to not support. It won't be blocked.

@Zigzag95 @MoxxNull Your sending your access_token in the querystring for everyone to copy paste. Your application is now unsecure.

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?

@DamianEdwards
Copy link
Member

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:

  • Use cookies
  • Send tokens in query string
  • Send tokens over the WebSocket itself after onconnect

A usable sample of the last would be interesting in my mind, but I'm not expecting it to be trivial.

@davidfowl
Copy link
Member

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.

@DamianEdwards
Copy link
Member

Why couldn't it be done in a given Hub?

@aminebizid
Copy link
Author

I just need the token for OnConnect(). The token is used to add the client in the appropriate group or reject it.

@davidfowl
Copy link
Member

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.

@davidfowl
Copy link
Member

After giving this some thought it might be possible to do this:

  • Connect via websockets
  • Send a message to the server with the token and stash it in httpContext.Items["token"]
  • Handle the MessageReceived event on the JwtBearer authentication handler to retrieve to the token from the httpContext.Items["token"]
services.AddAuthentication()
    .AddJwtBearer(o =>
    {
        o.Events.OnMessageReceived = context =>
        {
            context.Token = (string)context.HttpContext.Items["token"];
            return Task.CompletedTask;
        };
    });

@DamianEdwards
Copy link
Member

@davidfowl yeah I meant completely in the application, client and server. Client establishes connection then calls hub.Authenticate(token) basically, server validates token and stores it in its connection state and carries on. Any calls to other methods before Authenticate would explode with "Unauthorized". Wondering if that could be handled in a HubManager<MyHub>.

@damienbod
Copy link

damienbod commented Sep 20, 2017

@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.
The public network, gateways, etc also log this, monitor this. By adding the auth token to the querystring, you are giving potentially lots of different users the rights to your APIs and SignalR data.
The access_token per default in most systems lives for 1 hour.

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.

@DamianEdwards
Copy link
Member

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.

@willthiswork89
Copy link

I'm liking where @davidfowl is headed as it seems to be the closest to giving us what we need

@ChristianWeyer
Copy link

For us using a querystring over SSL is fine.

Client side with TypeScript in an Angular app:
this._hubConnection = new HubConnection(this._config.SignalRBaseUrl + 'ordersHub' + '?authorization=' + this._securityService.accessToken);
Server with ASP.NET Core 2.0 MVC Web API:

public Task Invoke(HttpContext context)
       {
           var authorizationQueryStringValue = context.Request.Query[_queryStringName];

           if (!string.IsNullOrWhiteSpace(authorizationQueryStringValue) && 
               !context.Request.Headers.ContainsKey(_authorizationHeaderName))
           {
               context.Request.Headers.Append(_authorizationHeaderName, "Bearer " + authorizationQueryStringValue);
           }

           return this._next(context);
       }

@davidfowl
Copy link
Member

davidfowl commented Sep 21, 2017

@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;
        };
    });

@ChristianWeyer
Copy link

We are already using the IdentityServer middleware for active token introspection.
When using your approach we obviously get an exception with ' Scheme already exists: Bearer'

@Falco20019
Copy link

Falco20019 commented Sep 21, 2017

@ChristianWeyer Asuming you are using IdentityServer4.AccessTokenValidation, just use the OnMessageReceived event of that options. In case of AddIdentityServerAuthentication the JwtBearerOptions and IdentityServerAuthenticationOptions both have the Events property. And if it's null, just do

JwtBearerEvents = new JwtBearerEvents
{
    OnMessageReceived = ...
}

@muratg muratg added this to the Discussions milestone Sep 21, 2017
@Misiu
Copy link

Misiu commented Sep 22, 2017

@DamianEdwards, @davidfowl in RFC6455 there one interesting point:

The request MAY include any other header fields, for example,
cookies [RFC6265] and/or authentication-related header fields
such as the |Authorization| header field [RFC2616], which are
processed according to documents that define them.

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 😕

@davidfowl
Copy link
Member

The only thing I'm not sure is whether custom headers for handshake can be set in a web browser

No, it cannot.

@aminebizid
Copy link
Author

Working solution

Startup

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"
        ]
...
}

@Misiu
Copy link

Misiu commented Sep 23, 2017

@Zigzag95 could You please post link to issue in angular cli that You mentioned in Your code?

these will be added once ng cli will release 1.5

@aminebizid
Copy link
Author

aminebizid commented Sep 26, 2017

@Misiu Up to 1.4, ng cli does not support support ES2015 because of uglifyjs.

https://github.com/angular/angular-cli/commit/dea04b1

@Misiu
Copy link

Misiu commented Sep 26, 2017

@Zigzag95 thanks for link 😄

@MaklaCof
Copy link

Did someone manage to do it with calling custom method after handshake? Can this be done and then used [Authorize] attribute on Hub methods?

@moozzyk
Copy link
Contributor

moozzyk commented Nov 22, 2017

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 signalRTokenHeader queryString parameter. When using the TS client it is required to copy the token to the MessageReceivedContext.Token as shown in the sample.

@dimamarksman
Copy link

@moozzyk The latest npm version is 1.0.0-alpha2-final and it does not include your changes.
How can I get it using npm?

@BrennanConroy
Copy link
Member

You can get the latest dev build via npm install @aspnet/signalr-client --registry https://dotnet.myget.org/f/aspnetcore-ci-dev/npm/

@spallister
Copy link

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 OnMessageReceived being called, with context.Token being set to the access token extracted from the query string.

Eventually I came across this comment over at the IdentityServer4 repo, where it was discovered that even though OnMessageReceived is called, authentication had already failed in HandleAuthenticateAsync within IdentityServer4.AccessTokenValidation. As described in the comment, the solution is to replace the TokenRetriever used for AccessTokenValidation. The following solution ended up working for me:

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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests