Skip to content

Secure endpoints

Secure HTTP routes and WebSocket upgrades from a single plugin. Pattern-based rules live on security.rules — HTTP verbs and ws(...) entries share one table, while authentication strategies are configured separately on security.http and security.ws.

This walkthrough uses Basic Auth as a concrete example. Any extension (or a custom strategy) plugs in the same way.

Authentication is pluggable

javalin-security provides the plugin and the rule table, not a login mechanism. Use a built-in strategy like Basic Auth, API Key, Opaque Token, Session, JWT, or create a custom authenticator that fit your needs.

Install javalin-security plus the Basic Auth extension before you begin.

1. Create a UserLookup

Define roles, an Identity, a small in-memory user store, and a UserLookup that Basic Auth will call for each request:

import io.github.mzlnk.javalin.security.authentication.Identity
import io.github.mzlnk.javalin.security.basicauth.BasicUserDetails
import io.github.mzlnk.javalin.security.basicauth.UserLookup
import io.javalin.security.RouteRole

enum class Role : RouteRole { USER, ADMIN }

data class User(override val name: String) : Identity

val users = mapOf(
    "alice" to BasicUserDetails(User("alice"), "secret", setOf(Role.USER)),
    "admin" to BasicUserDetails(User("admin"), "secret", setOf(Role.ADMIN)),
)

val userLookup = UserLookup { users[it] }
import io.github.mzlnk.javalin.security.authentication.Identity;
import io.github.mzlnk.javalin.security.basicauth.BasicUserDetails;
import io.github.mzlnk.javalin.security.basicauth.UserLookup;
import io.javalin.security.RouteRole;
import java.util.Map;
import java.util.Set;

enum Role implements RouteRole { USER, ADMIN }

record User(String name) implements Identity {
    @Override public String getName() { return name; }
}

Map<String, BasicUserDetails> users = Map.of(
    "alice", new BasicUserDetails(new User("alice"), "secret", Set.of(Role.USER)),
    "admin", new BasicUserDetails(new User("admin"), "secret", Set.of(Role.ADMIN)));

UserLookup userLookup = users::get;

2. Configure authentication

Open config.security { … } (Kotlin) or new JavalinSecurityPlugin(…) (Java), set fields on security.http, and assign a strategy. 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).

config.security { security ->
    security.http.authentication = basicAuth { basic ->
        basic.userLookup = userLookup
        // Demo only — use a real PasswordEncoder in production.
    }
    security.http.fallback = Rules.deny()
}
config.registerPlugin(new JavalinSecurityPlugin(security -> {
    security.http.authentication = BasicAuthSecurity.basicAuth(basic -> {
        basic.userLookup = userLookup;
        // Demo only — use a real PasswordEncoder in production.
    });
    security.http.fallback = Rules.deny();
}));

For WebSocket upgrades, set fields on security.ws the same way (typically with JWT) — see WebSocket security.

3. Declare who is allowed

Add rules on security.rules. HTTP entries match path + method (a GET rule also governs HEAD). WebSocket entries match on path only. Entries are evaluated in order — first match wins. Anything that does not match a rule falls through to http.fallback or ws.fallback (deny-by-default when unset).

Verb-by-verb

security.rules.get("/api/v1/*", Rules.allow())                  // public reads
security.rules.post("/api/v1/*", Rules.authenticated())         // any logged-in user
security.rules.delete("/api/v1/*", Rules.hasRole(Role.ADMIN))   // admins only
security.rules.get("/api/v1/*", Rules.allow());                 // public reads
security.rules.post("/api/v1/*", Rules.authenticated());        // any logged-in user
security.rules.delete("/api/v1/*", Rules.hasRole(Role.ADMIN));  // admins only

Grouped with apiBuilder

When paths share a prefix, nest rules under apiBuilder — analogous to Javalin's route apiBuilder. HTTP and WebSocket rules can live in the same group:

security.rules.apiBuilder {
    path("/api/v1") {
        get("/*", Rules.allow())                                // public reads
        post("/*", Rules.authenticated())                       // any logged-in user
        delete("/*", Rules.hasRole(Role.ADMIN))                 // admins only
    }
    ws("/ws/chat", Rules.authenticated())   // path-only WebSocket rule, enforced by the WS guard
}
security.rules.apiBuilder(() -> {
    path("/api/v1", () -> {
        get("/*", Rules.allow());                               // public reads
        post("/*", Rules.authenticated());                      // any logged-in user
        delete("/*", Rules.hasRole(Role.ADMIN));                // admins only
    });
    ws("/ws/chat", Rules.authenticated());  // path-only WebSocket rule, enforced by the WS guard
});

4. Wrap it together and run

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

enum class Role : RouteRole { USER, ADMIN }

data class User(override val name: String) : Identity

fun main() {
    val users = mapOf(
        "alice" to BasicUserDetails(User("alice"), "secret", setOf(Role.USER)),
        "admin" to BasicUserDetails(User("admin"), "secret", setOf(Role.ADMIN)),
    )
    val userLookup = UserLookup { users[it] }

    Javalin.create { config ->
        config.security { security ->
            security.rules.get("/api/v1/*", Rules.allow())                  // public reads
            security.rules.post("/api/v1/*", Rules.authenticated())         // any logged-in user
            security.rules.delete("/api/v1/*", Rules.hasRole(Role.ADMIN))   // admins only

            security.http.authentication = basicAuth { basic ->
                basic.userLookup = userLookup
                // Demo only — use a real PasswordEncoder in production.
            }
            security.http.fallback = Rules.deny()
        }

        config.routes.get("/api/v1/resource") { it.result("ok") }
        config.routes.post("/api/v1/resource") { it.result("created") }
        config.routes.delete("/api/v1/resource") { it.result("deleted") }
        config.routes.get("/api/v1/me") { it.result(it.identity<User>().name) }
    }.start(7070)
}
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.github.mzlnk.javalin.security.basicauth.*;
import io.javalin.Javalin;
import io.javalin.security.RouteRole;
import java.util.Map;
import java.util.Set;

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

enum Role implements RouteRole { USER, ADMIN }

record User(String name) implements Identity {
    @Override public String getName() { return name; }
}

void main() {
    Map<String, BasicUserDetails> users = Map.of(
        "alice", new BasicUserDetails(new User("alice"), "secret", Set.of(Role.USER)),
        "admin", new BasicUserDetails(new User("admin"), "secret", Set.of(Role.ADMIN)));
    UserLookup userLookup = users::get;

    Javalin.create(config -> {
        config.registerPlugin(new JavalinSecurityPlugin(security -> {
            security.rules.get("/api/v1/*", Rules.allow());                 // public reads
            security.rules.post("/api/v1/*", Rules.authenticated());        // any logged-in user
            security.rules.delete("/api/v1/*", Rules.hasRole(Role.ADMIN));  // admins only

            security.http.authentication = BasicAuthSecurity.basicAuth(basic -> {
                basic.userLookup = userLookup;
                // Demo only — use a real PasswordEncoder in production.
            });
            security.http.fallback = Rules.deny();
        }));

        config.routes.get("/api/v1/resource", ctx -> ctx.result("ok"));
        config.routes.post("/api/v1/resource", ctx -> ctx.result("created"));
        config.routes.delete("/api/v1/resource", ctx -> ctx.result("deleted"));
        config.routes.get("/api/v1/me", ctx ->
            ctx.result(identity(ctx, User.class).getName()));
    }).start(7070);
}

5. Try it

curl -i localhost:7070/api/v1/resource                           # 200 — public GET
curl -i -X POST localhost:7070/api/v1/resource                   # 401 — no credentials
curl -i -X POST localhost:7070/api/v1/resource -u alice:secret   # 200
curl -i -X DELETE localhost:7070/api/v1/resource -u alice:secret # 403 — not ADMIN
curl -i -X DELETE localhost:7070/api/v1/resource -u admin:secret # 200

Anonymous denials return 401. Authenticated-but-forbidden calls return 403.

Next steps