Login and roles
Right now anyone can create or delete a post. We want everyone to read, logged-in users to write and only the administrator to delete. Two ideas cover this. Authentication finds out who came (checks the login and password). Authorization decides whether this person may do the action. In Javalin you give routes roles and write the check yourself in a beforeMatched handler.
Roles
A role is any enum that implements the RouteRole interface:
// src/main/java/com/example/blog/Role.java
package com.example.blog;
import io.javalin.security.RouteRole;
public enum Role implements RouteRole {
ANYONE, USER, ADMIN
}Roles are listed as the last arguments when a route is declared:
// src/main/java/com/example/blog/Main.java
path("/posts", () -> {
get(posts::getAll, Role.ANYONE);
post(posts::create, Role.USER, Role.ADMIN);
path("/{id}", () -> {
get(posts::getOne, Role.ANYONE);
delete(posts::delete, Role.ADMIN);
});
});
get("/blog", posts::page, Role.ANYONE);The check
Roles by themselves forbid nothing — they are only labels. The Auth class does the check. The user sends the login and password with the Basic scheme: in the Authorization header, which curl builds with the -u option.
// src/main/java/com/example/blog/Auth.java
public class Auth {
private record User(String password, Role role) {
}
private static final Map<String, User> USERS = Map.of(
"anna", new User("changeme", Role.ADMIN),
"oleg", new User("changeme", Role.USER)
);
public static void check(Context ctx) {
Set<RouteRole> allowed = ctx.routeRoles();
if (allowed.isEmpty() || allowed.contains(Role.ANYONE)) {
return;
}
Role role = roleOf(ctx);
if (role == null) {
ctx.header("WWW-Authenticate", "Basic");
throw new UnauthorizedResponse();
}
if (!allowed.contains(role)) {
throw new ForbiddenResponse();
}
}
private static Role roleOf(Context ctx) {
BasicAuthCredentials credentials = ctx.basicAuthCredentials(); // io.javalin.security
if (credentials == null) {
return null;
}
User user = USERS.get(credentials.getUsername());
if (user == null || !user.password().equals(credentials.getPassword())) {
return null;
}
return user.role();
}
}ctx.routeRoles() returns the roles of the matched route. No login gives 401 Unauthorized; logged in but with an unsuitable role gives 403 Forbidden. For simplicity the users live in a map; in a real project they are stored in the database, and passwords only as a hash.
Enable the check in the configuration:
// src/main/java/com/example/blog/Main.java
config.routes.beforeMatched(Auth::check);Checking with curl
curl -i -X POST http://localhost:8080/posts -d '{"title":"x","body":"xxxxxx","author":"Oleg"}'
curl -i -u oleg:changeme -X DELETE http://localhost:8080/posts/2
curl -i -u anna:changeme -X DELETE http://localhost:8080/posts/2HTTP/1.1 401 Unauthorized
Content-Type: text/plain
WWW-Authenticate: Basic
Unauthorized
HTTP/1.1 403 Forbidden
Content-Type: text/plain
Forbidden
HTTP/1.1 204 No Content
Content-Type: text/plain
Without a password you cannot create a post, Oleg cannot delete, and Anna the administrator can. GET /posts is still open to everyone. Basic authentication sends the password almost as plain text, so on a real server it is used only over HTTPS.
Common mistake
If you forget the config.routes.beforeMatched(Auth::check) line, routes with roles stay open: Javalin stores the roles but does not check them itself. A route with Role.ADMIN answers 200 to anyone, and there is no error in the log. After adding protection, always try a request without -u — it must get 401.