ASVS 5.0 V4.4.4 (level 2) requires that dedicated WebSocket session tokens are first obtained or validated through the previously authenticated HTTPS session, when you move an existing HTTPS session over to a WebSocket channel.
In plain terms, the user logs in the normal way. Then the page makes an authenticated HTTPS call that returns a token meant for the socket. The socket accepts only that token. The WebSocket itself is never where authentication starts.
Why it matters
A WebSocket handshake gives you very few places to run a secure login flow. There are no standard headers from the browser API, error handling is limited, and a second login mechanism means a second chance for bugs. If the socket server accepts raw credentials or issues its own sessions, you now maintain two authentication systems, and attackers go for the weaker one.
Chaining the token to an authenticated HTTPS call keeps one front door. All the rate limiting, MFA, lockout and logging you built for the normal login still apply, and the socket inherits the result.
How to implement it
Design it as a two step exchange.
- The browser, with its normal session cookie and CSRF protection, calls
POST /api/socket-token. - The server checks the session, creates a short lived token bound to that session, and returns it.
- The client opens the WebSocket and presents the token. The socket server validates it against the same store and ties the connection to the user.
Express handler for step 1:
app.post('/api/socket-token', requireSession, csrfProtection, async (req, res) => {
const token = await issueSocketToken(req.session.id, req.user.id);
res.set('Cache-Control', 'no-store');
res.json({ token, expiresIn: 30 });
});
Client code for steps 2 and 3:
const { token } = await (await fetch('/api/socket-token', { method: 'POST', credentials: 'include' })).json();
const ws = new WebSocket('wss://app.example.com/socket', ['bearer', token]);
Use the subprotocol field so the token is not in the URL, and have the server pick the bearer subprotocol after validation. On the server, validate during the upgrade, before accepting. Handle re authentication too: when the HTTPS session expires or is revoked, close its sockets.
How to test it
Check that the socket has no way in other than the token.
- Open the socket with no token, an empty token and a random token. All must be refused during the handshake.
- Call
/api/socket-tokenwithout a session cookie. It must return 401. - Call it from another origin without a CSRF token. It must be refused.
- Confirm the response carries
Cache-Control: no-store. - Use the token twice, and use it after the session logs out. Both must fail.
Review the socket server code for any path that accepts a username and password, or that creates a session on its own.
Common mistakes
- A socket server with its own login message type.
- A token endpoint that works with only a cookie and no CSRF protection, so another site can mint tokens.
- Returning the token in a cacheable response.
- Not closing live sockets when the parent session is revoked.
- Handing out the token to unauthenticated visitors "just for public data" and then reusing the same path for private data.
Quick check
Where should WebSocket authentication begin?
In the normal authenticated HTTPS session, which issues a short lived socket token. The socket only validates that token.
Sources
- OWASP Application Security Verification Standard, requirement V4.4.4 (level 2)
- RFC 6455, The WebSocket Protocol