Skip to content

WebSocket security

Configuration cheatsheet for security.ws. If you followed Secure endpoints, you already know the essentials — this page lists the fields and the Origin rules.

Pattern-based rules are declared on security.rules via rules.ws(…). Both HTTP and WebSocket guards are installed as soon as the plugin is registered; declare rules on security.rules or set http.fallback / ws.fallback to control access (default fallback is deny). The WS guard runs as wsBeforeUpgrade. Enforcement runs once, at the handshake — not per message.

WsSecurityConfig

Field / method Type Default Effect
authentication AuthenticationStrategy? null Upgrade-time authentication; null → anonymous.
allowedOrigins Collection<String>? null Exact origins allowed; checked before authentication.
fallback Rule? null When no WS rule matches (null = deny).

See Rules DSL for rules.ws(…), built-in rules, and matching semantics. Same as HTTP: first match wins, deny-by-default, route roles override the table. WS rules match on path only.

Example

import io.github.mzlnk.javalin.security.authentication.Identity
import io.github.mzlnk.javalin.security.authorization.Rules
import io.github.mzlnk.javalin.security.identity
import io.github.mzlnk.javalin.security.security
import io.javalin.Javalin

Javalin.create { config ->
    config.security { security ->
        security.rules.ws("/ws/public/*", Rules.allow())
        security.rules.ws("/ws/chat", Rules.authenticated())
        security.rules.ws("/ws/admin", Rules.hasRole(Role.ADMIN))
        security.ws.authentication = myStrategy
        security.ws.allowedOrigins = listOf("https://app.example.com")
        security.ws.fallback = Rules.deny()
    }
    config.routes.ws("/ws/chat") { ws ->
        // Behind Rules.authenticated() — identity() is safe
        ws.onConnect { ctx -> ctx.send("welcome ${ctx.identity<Identity>().name}") }
    }
}
import io.github.mzlnk.javalin.security.JavalinSecurityPlugin;
import io.github.mzlnk.javalin.security.authentication.Identity;
import io.github.mzlnk.javalin.security.authorization.Rules;
import io.javalin.Javalin;
import java.util.List;

import static io.github.mzlnk.javalin.security.SecurityExtensions.identity;

Javalin.create(config -> {
    config.registerPlugin(new JavalinSecurityPlugin(security -> {
        security.rules.ws("/ws/public/*", Rules.allow());
        security.rules.ws("/ws/chat", Rules.authenticated());
        security.rules.ws("/ws/admin", Rules.hasRole(Role.ADMIN));
        security.ws.authentication = myStrategy;
        security.ws.allowedOrigins = List.of("https://app.example.com");
        security.ws.fallback = Rules.deny();
    }));
    config.routes.ws("/ws/chat", ws ->
        // Behind Rules.authenticated() — identity() is safe
        ws.onConnect(ctx -> ctx.send("welcome " + identity(ctx, Identity.class).getName())));
});

Origin allow-listing

When set, missing or unlisted Origin values are rejected before authentication. An empty collection (or blank entry) fails at startup — leave allowedOrigins unset to disable the check. Origins must be exact strings (scheme + host + optional port).

Upgrade-time only

Authentication runs during wsBeforeUpgrade. Denied upgrades never reach onConnect. Read identity with WsContext.authentication() / identity<T>() / identityOrNull<T>(). Mid-session expiry is not re-checked — enforce it in your protocol if needed.

JWT in the browser

Browsers cannot set Authorization on the WebSocket handshake. Carry the JWT in an HttpOnly, Secure cookie and configure the JWT strategy to read it from there (see JwtConfig in the API reference). Always set allowedOrigins when using cookies — otherwise the socket is open to cross-site WebSocket hijacking. Native clients can still use Authorization: Bearer ….