JWT authentication

AnyCable provides support for JWT-based authentication and identification.

We use the term "identification", because you can also pass a properly structured information as a part of the token to not only authentication the connection but also set up identifiers (in terms of Action Cable). This approach brings the following benefits:

  • Performance. No RPC call is required during the connection initiation, since we already have identification information. Thus, less load on the RPC server, much faster connection time (at least, 2x faster).
  • Usability. Universal way of dealing with credentials (no need to deal with cookies for web and whatever else for mobile apps).
  • Security. CSRF-safe by design. Configurable life time for tokens makes it easier to keep access under control.

Usage

See the demo of using JWT identification in a Rails app with AnyCable JS client library.

NOTE: Currently, we only support the HMAC signing algorithms.

By default, the --secret configuration parameter is used as a JWT secret key. If you want to use a custom key for JWT, you can specify it via the --jwt_secret (ANYCABLE_JWT_SECRET) parameter.

Other configuration options are:

  • (Optional) --jwt_param (ANYCABLE_ID_PARAM, default: "jid"): the name of a query string param or an HTTP header, which carries a token. The header name is prefixed with X-.
  • (Optional) --enforce_jwt (ANYCABLE_ENFORCE_JWT, default: false): whether to require all connection requests to contain a token. Connections without a token would be rejected right away. If not set, the servers fallbacks to the RPC call (if RPC is configured) or would be accepted if authentication is disabled (--noauth).

A client must provide an identification token either via a query param or via an HTTP header (if possible). For example:

import { createCable } from '@anycable/web'

let cable = createCable('ws://cable.example.com/cable?jid=[JWT_TOKEN]')

The token MUST include the ext claim with the JSON-encoded connection identifiers.

WebSocket clients may also provide tokens using sub-protocols. For that, specify the anycable-token.<token> sub-protocol in addition to the actual protocol (e.g., actioncable-v1-json):

const ws = new WebSocket(
  'wss://cable.example.com/cable',
  ['actioncable-v1-json', 'anycable-token.<TOKEN>']
);

When using AnyCable JS client, all you need is to specify the authentication strategy for the cable instance:

import { createCable } from '@anycable/web'

export default createCable(
  'ws://cable.example.com/cable',
  {
    auth: {token: "secret-value"},
    websocketAuthStrategy: 'sub-protocol'
  }
});

Generating tokens

Rails/Ruby

When using AnyCable Ruby/Rails SDK, you can generate tokens as follows:

token = AnyCable::JWT.encode({user: current_user})

# Setting TTL is also possible
token = AnyCable::JWT.encode({user: current_user}, expires_at: 10.minutes.from_now)

If you don't want to use our SDKs (why?), here is how you can generate tokens yourself:

require "jwt"
require "json"

ENCRYPTION_KEY = "some-sercret-key"

# !!! Expiration is the responsibility of the token issuer
exp = Time.now.to_i + 30

# Provides the serialized values for identifiers (`identified_by` in Action Cable)
identifiers = {user_id: 42}

# JWT payload
payload = {ext: identifiers.to_json, exp: exp}

puts JWT.encode payload, ENCRYPTION_KEY, "HS256"

JavaScript/TypeScript

You can use AnyCable server-side JS SDK to generate tokens as follows:

import { identificator } from "@anycable/serverless-js";

const jwtSecret = "very-secret";
const jwtTTL = "1h";

export const identifier = identificator(jwtSecret, jwtTTL);

// Then, somewhere in your code, generate a token and provide it to the client
const userId = authenticatedUser.id;
const token = await identifier.generateToken({ userId });

PHP

You can use the following snippet to generate tokens in PHP:

use Firebase\JWT\JWT;

$identifiers = ['user_id' => 42];
$payload = ['ext' => json_encode($identifiers), 'exp' => time() + 300];
$jwt = JWT::encode($payload, $ENCRYPTION_KEY, 'HS256');

Python

Here is an example Python code to generate AnyCable tokens:

import json
import jwt
import time

identifiers = {'user_id': 42}
payload = {'ext': json.dumps(identifiers), 'exp': int(time.time()) + 300}
jwt.encode(payload, ENCRYPTION_KEY, algorithm='HS256')

Handling expired tokens

🎥 Check out this AnyCasts episode to learn more about the expiration problem and how to solve it using anycable-client.

Whenever a server encounters a token that has expired, it rejects the connection and send the disconnect message with reason: "token_expired". It's a client responsibility to handle this situation and refresh the token.

See, for example, how anycable-client handles this.