Websocket, Angular 2 and JSON Web token Authentication
Solution 1
I settled on the following protocol:
1. Client logs into the site and receives an authentication token (JSON Web Token)
GET /auth
{
user: 'maggie',
pwd: 'secret'
}
// response
{ token: '4ad42f...' }
2. Authenticated client requests a websocket connection ticket
GET /ws_ticket
Authorization: Bearer 4ad42f...
// response: single-use ticket (will only pass validation once)
{ ticket: 'd76a55...', expires: 1475406042 }
3. Client opens the websocket, sending the ticket in query param
var socket = new WebSocket('wss://example.com/channel/?ticket=d76a55...');
4. Websocket server (PHP) then validates the ticket before accepting the handshake
/**
* Receives the URL used to connect to websocket. Return true to admit user,
* false to reject the connection
*/
function acceptConnection($url){
$params = parse_str(parse_url($url, PHP_URL_QUERY));
return validateTicket($params['ticket']);
}
/** Returns true if ticket is valid, never-used, and not expired. */
function validateTicket($ticket){/*...*/}
Solution 2
Use djangorestframework-jwt to generated your JWTs, and the following Django-Channels 2 middleware.
The token can be set via the djangorestframework-jwt http APIs, and it will also be sent for WebSocket connections if JWT_AUTH_COOKIE
is defined.
settings.py
JWT_AUTH = {
'JWT_AUTH_COOKIE': 'JWT', # the cookie will also be sent on WebSocket connections
}
routing.py:
from channels.routing import ProtocolTypeRouter, URLRouter
from django.urls import path
from json_token_auth import JsonTokenAuthMiddlewareStack
from yourapp.consumers import SocketCostumer
application = ProtocolTypeRouter({
"websocket": JsonTokenAuthMiddlewareStack(
URLRouter([
path("socket/", SocketCostumer),
]),
),
})
json_token_auth.py
from http import cookies
from channels.auth import AuthMiddlewareStack
from django.contrib.auth.models import AnonymousUser
from django.db import close_old_connections
from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication
class JsonWebTokenAuthenticationFromScope(BaseJSONWebTokenAuthentication):
"""
Extracts the JWT from a channel scope (instead of an http request)
"""
def get_jwt_value(self, scope):
try:
cookie = next(x for x in scope['headers'] if x[0].decode('utf-8') == 'cookie')[1].decode('utf-8')
return cookies.SimpleCookie(cookie)['JWT'].value
except:
return None
class JsonTokenAuthMiddleware(BaseJSONWebTokenAuthentication):
"""
Token authorization middleware for Django Channels 2
"""
def __init__(self, inner):
self.inner = inner
def __call__(self, scope):
try:
# Close old database connections to prevent usage of timed out connections
close_old_connections()
user, jwt_value = JsonWebTokenAuthenticationFromScope().authenticate(scope)
scope['user'] = user
except:
scope['user'] = AnonymousUser()
return self.inner(scope)
def JsonTokenAuthMiddlewareStack(inner):
return JsonTokenAuthMiddleware(AuthMiddlewareStack(inner))
Related videos on Youtube
BeetleJuice
Updated on July 09, 2022Comments
-
BeetleJuice almost 2 years
My Angular 2 app (coded in typescript) has a simple authentication scheme:
- User logs in:
- Server returns JSON Web Token (JWT)
abc123...
- On every API call, the app sends the JWT in the
Authorization
header - Server validates the JWT and grants access
Now I'd like to add websockets. I'm wondering how to authenticate the user there. Since I don't control which headers are sent to the websocket server (WS), I cannot send the JWT.
My idea so far (not yet implemented):
- Client opens websocket:
let sock = new WebSocket('wss://example.com/channel/');
- WS server accepts the handshake without any authentication check. Standard HTTP headers are available at this stage.
- Client listens to the
open
event on the socket. Once the socket is open:- client sends a message with
type='auth'
payload='JWT_VALUE'
- client sends a message with
- WS server expects 1st message on a socket to be of type
auth
. Once that is received, server reads the payload, validatesJWT_VALUE
and sets anisAuthenticated
flag- If validation fails, server disconnects the socket
- If a client without
isAuthenticated
sends any other type of message, server disconnects the socket
2 problems: server resources can be taken up by clients who connect but never send the JWT, and a cleaner solution would block the handshake if the client is not authenticated.
Other ideas:
- Client could send JWT in the path:
new WebSocket('wss://example.com/channel/<JWT>/')
- pro: this info is available during the handshake
- con: the path doesn't seem to be the "appropriate" place for a JWT. Specifically because intermediate proxies and access logs will save the path; When designing the HTTP API I already made the decision not to include the JWT in the url
- Server could read the client's IP + UserAgent and match against a DB record that was created by the HTTP server when the JWT was issued. Server will then guess who is connecting
- pro: this info may be available during the handshake (not sure about IP)
- con: it seems horribly insecure to "guess" that a client should be associated with a JWT when the client never presented it in the first place. It would mean for instance that someone who spoofs the victim's UA and uses the same network (proxy, public wifi, university intranet...) will be able to impersonate the victim.
How do you authenticate clients on websockets? Assume the user already logged in via HTTP and that the Angular 2 app has a JWT token.
-
Roman Kolesnikov over 7 yearsI actually implemented your first idea - to send send JWT token in the first message after handshake. It is not a very clean solution, but it works.
-
BillyBBone about 7 yearsThank you for sharing your solution. I am looking at implementing something similar; i.e. passing authentication information through the query string arguments. Why did you choose to create a one-time token (ticket) in #2, instead of passing the JWT directly? It looks like your WSS URL is secure, so passing the JWT could be a simpler option, no?
-
BeetleJuice about 7 years@BillyBBone As I mentioned in my OP, I had already decided when designing the HTTP API to keep the JWT out of URLs. Specifically, I don't want these tokens visible in server logs (even my own logs)
-
BeetleJuice almost 7 yearsThat's interesting; I hadn't thought of that, and I like the simplicity but I have a few concerns. Wouldn't web traffic logs store this information? Also this requires the user to enter her username and password to open a websocket (not good experience since websockets are opened in the background) or the application to cache the username and password in local storage (which I don't want to do)
-
Shekhar Sahu about 4 years@BeetleJuice how did you create the ticket?
-
BeetleJuice about 4 years@ShekharSahu if I recall, it was just a random string. Any UUID string would have worked for my purposes
-
Marko Kohtala over 3 yearsIt would be interesting what size of a password you can use. Just use the token as password if you can verify the password yourself on the server. Clients place the password in Authorization: Basic header and it would not show up in web traffic logs.
-
oort over 3 yearsForgive me if this is a stupid question - but does this mean that once the handshake has been established, you're sure that the connection between client and server is secure? And should the connection drop, this process will be repeated?