Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,12 @@ class Realtime(client: Client) : Service(client), CoroutineScope {
val encodedProject = java.net.URLEncoder.encode(client.config["project"].toString(), "UTF-8")
val queryParams = "project=$encodedProject"
val url = "${client.endpointRealtime}/realtime?$queryParams"
val request = Request.Builder().url(url).build()
val requestBuilder = Request.Builder().url(url)
val jwt = client.config["jwt"]
if (!jwt.isNullOrEmpty()) {
requestBuilder.addHeader("x-{{ spec.title | caseLower }}-jwt", jwt)
}
val request = requestBuilder.build()

val generation = socketGeneration.incrementAndGet()
socket = client.http.newWebSocket(request, {{ spec.title | caseUcfirst }}WebSocketListener(generation))
Expand Down
6 changes: 6 additions & 0 deletions templates/apple/Sources/Services/Realtime.swift.twig
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,15 @@ open class Realtime : Service {
return
}

var headers = HTTPHeaders()
if let jwt = client.config["jwt"], !jwt.isEmpty {
headers.add(name: "x-{{ spec.title | caseLower }}-jwt", value: jwt)
}

socketClient = WebSocketClient(
url,
tlsEnabled: !client.selfSigned,
headers: headers,
delegate: self
)

Expand Down
9 changes: 9 additions & 0 deletions templates/flutter/lib/src/realtime_browser.dart.twig
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ class RealtimeBrowser extends RealtimeBase with RealtimeMixin {

Future<WebSocketChannel> _getWebSocket(Uri uri) async {
await (client as ClientBrowser).init();

final jwt = client.config['jwt'];
if (jwt != null && jwt.isNotEmpty) {
uri = uri.replace(queryParameters: {
...uri.queryParameters,
'jwt': jwt,
});
}

return HtmlWebSocketChannel.connect(uri);
}

Expand Down
5 changes: 5 additions & 0 deletions templates/flutter/lib/src/realtime_io.dart.twig
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ class RealtimeIO extends RealtimeBase with RealtimeMixin {
final cookies = await (client as ClientIO).cookieJar.loadForRequest(uri);
headers = {HttpHeaders.cookieHeader: CookieManager.getCookies(cookies)};

final jwt = client.config['jwt'];
if (jwt != null && jwt.isNotEmpty) {
headers['x-{{ spec.title | caseLower }}-jwt'] = jwt;
}

final websok = IOWebSocketChannel((client as ClientIO).selfSigned
? await _connectForSelfSignedCert(uri, headers)
: await WebSocket.connect(uri.toString(), headers: headers));
Expand Down
8 changes: 7 additions & 1 deletion templates/react-native/src/client.ts.twig
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,11 @@ class Client {
const channels = new URLSearchParams();
channels.set('project', this.config.project);

const jwt = this.config.jwt;
if (jwt && Platform.OS === 'web') {
channels.set('jwt', jwt);
}

const url = this.config.endpointRealtime + '/realtime?' + channels.toString();

if (
Expand All @@ -347,7 +352,8 @@ class Client {
// @ts-ignore
this.realtime.socket = new WebSocket(url, undefined, {
headers: {
Origin: `{{ spec.title | caseLower }}-${Platform.OS}://${this.config.platform}`
Origin: `{{ spec.title | caseLower }}-${Platform.OS}://${this.config.platform}`,
...(jwt && Platform.OS !== 'web' ? { 'x-{{ spec.title | caseLower }}-jwt': jwt } : {})
}
});
this.realtime.socket.addEventListener('message', this.realtime.onMessage);
Expand Down
13 changes: 10 additions & 3 deletions templates/unity/Assets/Runtime/Realtime.cs.twig
Original file line number Diff line number Diff line change
Expand Up @@ -523,17 +523,24 @@ namespace {{ spec.title | caseUcfirst }}
{
var uri = PrepareUri();
Debug.Log($"[Realtime] Connecting to URI: {uri}");


var headers = new Dictionary<string, string>();
var jwt = _client.Config.GetValueOrDefault("jwt");
if (!string.IsNullOrEmpty(jwt))
{
headers["x-{{ spec.title | caseLower }}-jwt"] = jwt;
}

if (_webSocket == null || _webSocket.State == WebSocketState.Closed)
{
_webSocket = new WebSocket(uri);
_webSocket = new WebSocket(uri, headers);
_lastUrl = uri;
SetupWebSocketEvents();
}
else if (_lastUrl != uri && _webSocket.State != WebSocketState.Closed)
{
await CloseConnection();
_webSocket = new WebSocket(uri);
_webSocket = new WebSocket(uri, headers);
_lastUrl = uri;
SetupWebSocketEvents();
}
Expand Down
6 changes: 5 additions & 1 deletion templates/web/src/client.ts.twig
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,11 @@ class Client {

const encodedProject = encodeURIComponent((this.config.project as string) ?? '');
// URL carries only the project; channels/queries are sent via subscribe message.
const queryParams = 'project=' + encodedProject;
let queryParams = 'project=' + encodedProject;

if (this.config.jwt) {
queryParams += '&jwt=' + encodeURIComponent(this.config.jwt as string);
}

const url = this.config.endpointRealtime + '/realtime?' + queryParams;

Expand Down
14 changes: 11 additions & 3 deletions templates/web/src/services/realtime.ts.twig
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,14 @@ export class Realtime {
}

// URL carries only the project; channels/queries are sent via the subscribe message.
const queryParams = `project=${projectId}`;
let queryParams = `project=${projectId}`;

const jwt = this.client.config.jwt;
{% if language.name != 'ReactNative' %}
if (jwt) {
queryParams += `&jwt=${encodeURIComponent(jwt)}`;
}
Comment on lines +213 to +217

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 security JWT exposed in WebSocket URL and server access logs

For all non-ReactNative (browser) targets the JWT is appended to the WebSocket upgrade URL as ?jwt=<token>. Because the HTTP Upgrade request is a standard HTTP GET, the full URL — including the JWT — will appear in server-side access logs, reverse-proxy logs, and load-balancer logs. Anyone with read access to those logs can extract a valid bearer token.

The same limitation applies to the browser and flutter-browser templates. If log-based token exposure is a concern, consider rotating short-lived JWTs and ensuring log-scrubbing is in place on the server side.

{% endif %}

const endpoint =
this.client.config.endpointRealtime !== ''
Expand Down Expand Up @@ -243,10 +250,11 @@ export class Realtime {
{% if language.name == 'ReactNative' %}
const WebSocketCtor: any = WebSocket;
const socket = (this.socket = Platform.OS === 'web'
? new WebSocketCtor(url)
? new WebSocketCtor(jwt ? `${url}&jwt=${encodeURIComponent(jwt)}` : url)
: new WebSocketCtor(url, undefined, {
headers: {
Origin: `{{ spec.title | caseLower }}-${Platform.OS}://${this.client.config.platform}`
Origin: `{{ spec.title | caseLower }}-${Platform.OS}://${this.client.config.platform}`,
...(jwt ? { 'x-{{ spec.title | caseLower }}-jwt': jwt } : {})
}
}));
Comment on lines 250 to 259

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stale JWT after rotation in new Realtime class

The new Realtime class (this file) reads this.client.config.jwt once inside createSocketLocked() and embeds it in the URL (web) or header (native) only at socket-creation time. Unlike the legacy client.ts.twig path — which caches this.realtime.url and compares it on every createSocket() call so that a JWT change causes a reconnect on the next subscribe() — this class has no URL-change detection. If the caller invokes client.setJwt(newToken) while an active subscription exists, the WebSocket continues using the old JWT until the connection naturally drops and reconnects. On native platforms the new JWT won't appear even after reconnect unless createSocketLocked() is re-entered, which requires all subscriptions to be torn down and re-created.

Consider either: (a) storing the JWT used at connection time and forcing a reconnect in createSocket() when it changes, or (b) documenting that callers must disconnect() and resubscribe after a JWT rotation.

{% else %}
Expand Down
Loading