Safeguard
Application Security•

ASVS 5.0 V4.4.4: Issue WebSocket Tokens Over Logged In HTTPS

Get the WebSocket token from an already authenticated HTTPS session, never from the socket itself. A short walk through the exchange and how to test it.

Safeguard Security Team
3 min read

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.

  1. The browser, with its normal session cookie and CSRF protection, calls POST /api/socket-token.
  2. The server checks the session, creates a short lived token bound to that session, and returns it.
  3. 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-token without 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

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.