Skip to main content

Client JWT authentication

To securely authenticate incoming real-time client connections, Centrifugo can use a JSON Web Token (JWT) issued by your application backend. This process allows Centrifugo to identify the user's ID in your application securely. Additionally, your application can include extra information within the JWT claims, which Centrifugo can then utilize. This chapter will explain how such connection token may be created and used.

tip

If you prefer not to use JWTs, consider the proxy feature. It enables the proxying of connection requests from Centrifugo to your application's backend endpoint for authentication.

tip

Using JWT for authentication can be beneficial in scenarios involving massive reconnects. As the authentication information is encoded in the token, this can significantly reduce the load on your application's session backend. For more details, refer to our blog post.

Upon connection, the client should supply a connection JWT containing several predefined credential claims. Below is a diagram illustrating this:

For more information about handling connection tokens on the client side, see the client SDK specification.

Currently, Centrifugo supports HMAC, RSA, and ECDSA JWT algorithms - specifically HS256, HS384, HS512, RSA256, RSA384, RSA512, EC256, EC384, and EC512.

Here, we will demonstrate example snippets using the Javascript Centrifugo client for the client-side and the PyJWT Python library to generate a connection token on the backend side.

To add an HMAC secret key to Centrifugo, insert token_hmac_secret_key into the configuration file:

config.json
{
...
"token_hmac_secret_key": "<YOUR-SECRET-STRING-HERE>"
}

To add RSA public key (must be PEM encoded string) add token_rsa_public_key option, ex:

config.json
{
...
"token_rsa_public_key": "-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZ..."
}

To add ECDSA public key (must be PEM encoded string) add token_ecdsa_public_key option, ex:

config.json
{
...
"token_ecdsa_public_key": "-----BEGIN PUBLIC KEY-----\nxyz23adf..."
}

Connection JWT Claims

For connection JWT, Centrifugo uses some standard claims defined in RFC 7519, as well as custom Centrifugo-specific claims.

sub

This standard JWT claim must contain the ID of the current application user (as a string).

If a user is not authenticated in the application but you wish to allow them to connect to Centrifugo, an empty string can be used as the user ID in the sub claim. This facilitates anonymous access. In such cases, you might need to enable the corresponding channel namespace options that allow protocol features for anonymous users.

exp

This claim specifies the UNIX timestamp (in seconds) when the token will expire. It is a standard JWT claim - all JWT libraries across different programming languages provide an API to set it.

If the exp claim is not included, Centrifugo will not expire the connection. When included, a special algorithm will identify connections with an exp in the past and initiate the connection refresh mechanism. The refresh mechanism allows a connection to be extended. If the refresh fails, Centrifugo will eventually close the client connection, which will not be accepted again until new valid and current credentials are provided in the connection token.

The connection expiration mechanism can be utilized in scenarios where you do not want users to remain subscribed to channels after being banned or deactivated in the application. It also serves to protect users from token leakage by setting a reasonably short expiration time.

Choose the exp value judiciously; too short a value can lead to frequent application hits with refresh requests, whereas too long a value can result in delayed user connection deactivation. It's a matter of balance.

Further details on connection expiration can be found below.

iat

This represents the UNIX time when the token was issued (in seconds). Refer to the definition in RFC 7519. This claim is optional but can be advantageous in conjunction with Centrifugo PRO's token revocation features.

jti

This is a unique identifier for the token. Refer to the definition in RFC 7519. This claim is optional but can be beneficial in conjunction with Centrifugo PRO's token revocation features.

aud

By default, Centrifugo does not check JWT audience (rfc7519 aud claim).

But you can force this check by setting token_audience string option:

config.json
{
"token_audience": "centrifugo"
}
caution

Setting token_audience will also affect subscription tokens (used for channel token authorization). If you need to separate connection token configuration and subscription token configuration check out separate subscription token config feature.

iss

By default, Centrifugo does not check JWT issuer (rfc7519 iss claim).

But you can force this check by setting token_issuer string option:

config.json
{
"token_issuer": "my_app"
}
caution

Setting token_issuer will also affect subscription tokens (used for channel token authorization). If you need to separate connection token configuration and subscription token configuration check out separate subscription token config feature.

info

This optional claim provides additional information about the client's connection for Centrifugo. This information will be included:

  • in online presence data
  • join/leave events
  • and into client-side channel publications

b64info

For those utilizing the binary Protobuf protocol and requiring the info to be custom bytes, this field should be used.

It contains a base64 encoded representation of your bytes. Centrifugo will decode the base64 back into bytes upon receipt and incorporate the result into the various places described above.

channels

This is an optional array of strings identifying the server-side channels to which the client will be subscribed. Further details can be found in the documentation on server-side subscriptions.

tip

It's important to note that the channels claim is sometimes misinterpreted by users as a list of channel permissions. It does not serve that purpose. Instead, using this claim causes the client to be automatically subscribed to the specified channels upon connection, making it unnecessary to invoke the subscribe API from the client side. More information can be found in the server-side subscriptions documentation.

subs

This optional claim is a map of channels with options, providing a more detailed approach to server-side subscriptions compared to the channels claim, as it allows for the annotation of each channel with additional information and data through options.

tip

The term subs is shorthand for subscriptions. It should not be confused with the sub claim mentioned earlier, which is a standard JWT claim used to provide a user ID (short for subject). Despite their similar names, these claims serve distinct purposes within a connection JWT.

Example:

{
...
"subs": {
"channel1": {
"data": {"welcome": "welcome to channel1"}
},
"channel2": {
"data": {"welcome": "welcome to channel2"}
}
}
}

Subscribe options:

FieldTypeOptionalDescription
infoJSON objectyesCustom channel info
b64infostringyesCustom channel info in Base64 - to pass binary channel info
dataJSON objectyesCustom JSON data to return in subscription context inside Connect reply
b64datastringyesSame as data but in Base64 to send binary data
overrideSubscribeOptionOverrideyesAllows dynamically override some channel options defined in Centrifugo configuration on a per-connection basis (see below available fields)

SubscribeOptionOverride

Allow per-connection overrides of some channel namespace options:

FieldTypeOptionalDescription
presenceBoolValueyesOverride presence from namespace options
join_leaveBoolValueyesOverride join_leave from namespace options
force_recoveryBoolValueyesOverride force_recovery from namespace options
force_positioningBoolValueyesOverride force_positioning from namespace options
force_push_join_leaveBoolValueyesOverride force_push_join_leave from namespace options

BoolValue is an object like this:

{
"value": true/false
}

meta

meta is an additional JSON object (e.g., {"key": "value"}) that is attached to a connection. It differs from info as it is never disclosed to clients within presence and join/leave events; it is only accessible on the server side. It can be included in proxy calls from Centrifugo to the application backend (refer to the proxy_include_connection_meta option). In Centrifugo PRO, there is a connections API method that returns this metadata within the connection description object.

expire_at

Although Centrifugo typically uses the exp claim to manage connection expiration, there may be scenarios where you want to separate the token expiration check from the connection expiration time. When the expire_at claim is included in the JWT, Centrifugo uses it to determine the connection expiration time, while the JWT expiration is still verified using the exp claim.

expire_at is a UNIX timestamp indicating when the connection should expire.

  • To expire the connection at a specific future time, set it to that time.
  • To prevent connection expiration, set it to 0 (token exp claim will still be checked).

Connection expiration

As mentioned, the exp claim in a connection token is designed to expire the client connection at some point in time. Here's a detailed look at the process when Centrifugo identifies that the connection is going to expire.

First, activate the client expiration mechanism in Centrifugo by providing a connection JWT with an exp claim:

import jwt
import time

token = jwt.encode({"sub": "42", "exp": int(time.time()) + 10*60}, "secret", algorithm="HS256")

print(token)

Assuming the exp claim is set to expire in 10 minutes, the client connects to Centrifugo with this token. Centrifugo will maintain the connection for the specified duration. Once the time elapses, Centrifugo allows a grace period (default is 25 seconds) for the client to refresh its credentials with a new valid token containing an updated exp.

Upon initial connection, the client receives a ttl value in the connect response, indicating the seconds remaining before it must initiate a refresh command with new credentials. Centrifugo SDKs handle this ttl internally and automatically begin the refresh process.

SDKs provide mechanisms to hook into this process and provide a function to get new token. It's up to developer to decide how to load new token from the backend – in web browser this is usually a simple fetch request and response may look like this:

{
"token": token
}

You should provide the same connection JWT you issued when the page was initially rendered, but with an updated and valid exp. Our SDKs will then send this token to the Centrifugo server, and the connection will be extended for the period set in the new exp.

When you load new token from your app backend user authentication must be facilitated by your app's session mechanism. So you know for whom you are are going to generate an updated token.

Examples: create connection JWT

Let's look at how to generate connection HS256 JWT in Python:

Simplest token

import jwt

token = jwt.encode({"sub": "42"}, "secret").decode()

print(token)

Note that we use the value of token_hmac_secret_key from Centrifugo config here (in this case token_hmac_secret_key value is just secret). The only two who must know the HMAC secret key is your application backend which generates JWT and Centrifugo. You should never reveal the HMAC secret key to your users.

Then you can pass this token to your client side and use it when connecting to Centrifugo:

Using centrifuge-js v3
var centrifuge = new Centrifuge("ws://localhost:8000/connection/websocket", {
token: token
});
centrifuge.connect();

See more details about working with connection tokens and handling token expiration on the client-side in the real-time SDK API spec.

Token with expiration

HS256 token that will be valid for 5 minutes:

import jwt
import time

claims = {"sub": "42", "exp": int(time.time()) + 5*60}
token = jwt.encode(claims, "secret", algorithm="HS256").decode()
print(token)

Token with additional connection info

Let's attach user name:

import jwt

claims = {"sub": "42", "info": {"name": "Alexander Emelin"}}
token = jwt.encode(claims, "secret", algorithm="HS256").decode()
print(token)

Example: connect with JWT

To connect with JWT it should be passed to Centrifugo from the client-side upon establishing real-time connection.

Our bidirectional SDKs provide options to set initial token as well as an option to set the function to load new connection token (required to handle refresh of expiring tokens). See examples in client SDK spec.

Our unidirectional transports accept JWT as part of the connect payload. The way how connect payload is passed to Centrifugo differs for each unidirectional transport.

Investigating problems with JWT

You can use jwt.io site to investigate the contents of your tokens. Also, server logs usually contain some useful information.

JSON Web Key support

Centrifugo supports JSON Web Key (JWK) spec. This means that it's possible to improve JWT security by providing an endpoint to Centrifugo from where to load JWK (by looking at kid header of JWT).

A mechanism can be enabled by providing token_jwks_public_endpoint string option to Centrifugo (HTTP address).

As soon as token_jwks_public_endpoint set all tokens will be verified using JSON Web Key Set loaded from JWKS endpoint. This makes it impossible to use non-JWK based tokens to connect and subscribe to private channels.

tip

Read a tutorial in our blog about using Centrifugo with Keycloak SSO. In that case connection tokens are verified using public key loaded from the JWKS endpoint of Keycloak.

At the moment Centrifugo caches keys loaded from an endpoint for one hour.

Centrifugo will load keys from JWKS endpoint by issuing GET HTTP request with 1 second timeout and one retry in case of failure (not configurable at the moment).

Centrifugo supports the following key types (kty) for JWKs tokens:

  • RSA
  • EC (since Centrifugo v5.1.0)
  • OKP based on Ed25519 (since Centrifugo v5.2.1)

Once enabled JWKS used for both connection and channel subscription tokens.

Dynamic JWKs endpoint

It's possible to extract variables from iss and aud JWT claims using Go regexp named groups, then use variables extracted during iss or aud matching to construct a JWKS endpoint dynamically upon token validation. In this case JWKS endpoint may be set in config as template.

To achieve this Centrifugo provides two additional options:

  • token_issuer_regex - match JWT issuer (iss claim) against this regex, extract named groups to variables, variables are then available for jwks endpoint construction.
  • token_audience_regex - match JWT audience (aud claim) against this regex, extract named groups to variables, variables are then available for jwks endpoint construction.

Let's look at the example:

{
"token_issuer_regex": "https://example.com/auth/realms/(?P<realm>[A-z]+)",
"token_jwks_public_endpoint": "https://keycloak:443/{{realm}}/protocol/openid-connect/certs",
}

To use variable in token_jwks_public_endpoint it must be wrapped in {{ }}.

When using token_issuer_regex and token_audience_regex make sure token_issuer and token_audience not used in the config - otherwise and error will be returned on Centrifugo start.

caution

Setting token_issuer_regex and token_audience_regex will also affect subscription tokens (used for channel token authorization). If you need to separate connection token configuration and subscription token configuration check out separate subscription token config feature.

Custom token user id claim

New in Centrifugo v5.4.6.

It's possible to use alternative claim in token to pass user ID: with token_user_id_claim option (string, by default "" – i.e. not used).

{
...
"token_user_id_claim": "user_id"
}

By default, Centrifugo uses sub claim of JWT to extract user ID - this is defined in JWT spec and is the recommended way to pass user ID.

Custom claim set by token_user_id_claim must follow the following regexp at this point: ^[a-zA-Z_]+$.

Setting alternative user id claim also affects subscription tokens, like any other token options. To use different config for subscription tokens Centrifugo provides separate_subscription_token_config option.