Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.NET Core WebSockets - Token Authorization #2881

Closed
FabioMorcillo opened this issue Feb 16, 2018 · 9 comments
Closed

.NET Core WebSockets - Token Authorization #2881

FabioMorcillo opened this issue Feb 16, 2018 · 9 comments

Comments

@FabioMorcillo
Copy link

Hi,

.NET Core WebSockets support token jwt authentication ? Have example to configure ?

Thank you,

@Tratcher
Copy link
Member

Yes, you can use bearer token auth with WebSocket requests. Are you asking for client or server code samples? On the server you configure JwtBearer normally. See
https://github.com/aspnet/Security/blob/5de25bb11cfb2bf60d05ea2be36e80d86b38d18b/samples/JwtBearerSample/Startup.cs#L47-L53
https://github.com/aspnet/Security/blob/5de25bb11cfb2bf60d05ea2be36e80d86b38d18b/samples/JwtBearerSample/Startup.cs#L64

Where would you be consuming the WebSocket? In an MVC controller? In that case you want to add the [Authorize] attribute.

@davidfowl
Copy link
Member

If you're using the browser client you cannot set headers. This means the defaults won't just work. You have to send it in the query string and get it on the server like this https://github.com/aspnet/SignalR/blob/4394b57143ad0517333ffbe88848fbb68264a614/samples/JwtSample/Startup.cs#L53-L60.

@blowdart
Copy link
Contributor

You shouldn’t ever be sending tokens in the query string, they can be logged client side, server side, and by any proxies in the way. It’s simply not secure. @davidfowl is there no way to set the authorization header for signalr requests? If not there needs to be.

@davidfowl
Copy link
Member

Unfortunately, it's nothing to do with SignalR, it's a browser limitation. Alternatively, you can do something custom with the websocket connection itself (sending the JWT with a custom websocket message), but then you won't be able to use the JWT auth handler as is.

The way it would work in that world is that you would send the JWT Token over the websocket with some custom format, parse it on the server side, then store it in HttpContext.Items. From there, you should be able to call context.AuthenticateAsync which will call the OnMessageReceived to get the token out of HttpContext.Items.

options.Events = new JwtBearerEvents
{
    OnMessageReceived = context =>
    {
        var jwtToken = context.Items["jwtToken"];

        if (!string.IsNullOrEmpty(jwtToken) && context.HttpContext.WebSockets.IsWebSocketRequest)
        {
            context.Token = jwtToken;
        }
        return Task.CompletedTask;
    }
};

There are other issues with authentication over websockets like the token expiring. Since this only happens once, you'll need some way to get a new token or disconnect the client when the token is expired.

@brockallen
Copy link

In the past I've done this by implementing a custom ajax endpoint that accepts the access token, and then in turn issues a custom cookie that's only meant for the web sockets connection. That way 1) you have more control over cookie sliding, 2) avoid access token expiration issues since you're now managing the cookie expiration/sliding, 3) then when reconnects happen the cookie is auto-resent, and 4) no manual passing of tokens and leaking into logs.

@Eilon
Copy link
Member

Eilon commented Mar 5, 2018

Closing because it looks like this discussion has concluded.

@Eilon Eilon closed this as completed Mar 5, 2018
@kedzior-io
Copy link

@davidfowl

Unfortunately, it's nothing to do with SignalR, it's a browser limitation. Alternatively, you can do something custom with the websocket connection itself (sending the JWT with a custom websocket message), but then you won't be able to use the JWT auth handler as is.

The way it would work in that world is that you would send the JWT Token over the websocket with some custom format, parse it on the server side, then store it in HttpContext.Items. From there, you should be able to call context.AuthenticateAsync which will call the OnMessageReceived to get the token out of HttpContext.Items.

options.Events = new JwtBearerEvents
{
    OnMessageReceived = context =>
    {
        var jwtToken = context.Items["jwtToken"];

        if (!string.IsNullOrEmpty(jwtToken) && context.HttpContext.WebSockets.IsWebSocketRequest)
        {
            context.Token = jwtToken;
        }
        return Task.CompletedTask;
    }
};

There are other issues with authentication over websockets like the token expiring. Since this only happens once, you'll need some way to get a new token or disconnect the client when the token is expired.

@davidfowl thanks for this, I was able to authorize by sending token from the client as:

socket = new WebSocket(connectionUrl.value, token.value);

and getting token like this:

                x.Events = new JwtBearerEvents
                {
                    OnMessageReceived = context =>
                    {
                        if (context.Request.Headers.ContainsKey("sec-websocket-protocol") && context.HttpContext.WebSockets.IsWebSocketRequest)
                        {
                            context.Token = context.Request.Headers["sec-websocket-protocol"];
                            context.Request.Headers["sec-websocket-protocol"] = "heythere";
                        }
                        return Task.CompletedTask;
                    }
                };

However, now the browser expects response and throws an error:

Error during WebSocket handshake: Sent non-empty 'Sec-WebSocket-Protocol' header but no response was received

So in my WSController I do:

WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync("heythere");

But this time I get:
WebSocket connection to 'wss://localhost:44379/api/ws' failed: Error during WebSocket handshake: Unexpected response code: 400

and at the backend I get:

System.Net.WebSockets.WebSocketException (0x80004005): The remote party closed the WebSocket connection without completing the close handshake.

Any idea?

@kedzior-io
Copy link

Never mind :-)
Running it directly with Krestrel gave more insights (rather than IIS Express).
Sent protocol has to match the response so that work like charm:

socket = new WebSocket(connectionUrl.value, ["heythere",token.value]);

@umarmohammed
Copy link

@kedzior-io Found this page when googling, and your comments were a great help.

I realise that this is all a bit hacky, but here's what I had to when using the RxJs webSocket wrapper.

In the client:

private subject = webSocket({
  url: this.url,
  protocol: ['jwt', this.auth.token]
});

On the server:

// Startup.cs
options.Events = new JwtBearerEvents
{
  OnMessageReceived = context =>
  {
    if (context.Request.Headers.ContainsKey("sec-websocket-protocol") && context.HttpContext.WebSockets.IsWebSocketRequest)
    {
          var protocols = context.Request.Headers["sec-websocket-protocol"].ToString().Split(", ");
	  context.Request.Headers["sec-websocket-protocol"] = protocols[0];
	  context.Token = protocols[1];
    }
    return Task.CompletedTask;
  }
};
// Controller
var context = ControllerContext.HttpContext;
if (context.WebSockets.IsWebSocketRequest)
{
  WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync("jwt");
  await Echo(context, webSocket);
}
else
{
  context.Response.StatusCode = 400;
}

@dotnet dotnet locked as resolved and limited conversation to collaborators Dec 4, 2019
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

8 participants