Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Cookies and sessions

HTTP does not remember earlier requests: each one arrives "from scratch". To recognise a user again, servers use cookies — small name=value pairs that the server sends in the Set-Cookie header and the browser returns with every following request. A session is the user's data kept on the server, while the cookie holds only its id. Basic authentication from the roles lesson is fine for the API; for the blog pages we will build sign-in through a form and a session.

Cookies

// src/main/java/com/example/blog/Main.java
config.routes.post("/theme/{name}", ctx -> {
    ctx.cookie("theme", ctx.pathParam("name"), 60 * 60 * 24 * 365);
    ctx.result("Saved");
});
config.routes.get("/theme", ctx -> {
    String theme = ctx.cookie("theme");
    ctx.result("Theme: " + (theme == null ? "light" : theme));
});
config.routes.delete("/theme", ctx -> {
    ctx.removeCookie("theme");
    ctx.status(204);
});

The third argument of ctx.cookie is the lifetime in seconds (a year here). ctx.cookie("theme") reads the cookie from the request, and ctx.removeCookie asks the browser to delete it.

curl -i -X POST http://localhost:8080/theme/dark
curl -b "theme=dark" http://localhost:8080/theme
HTTP/1.1 200 OK
Content-Type: text/plain
Set-Cookie: theme=dark; Path=/; Expires=Mon, 4 Oct 2027 11:37:37 GMT; Max-Age=31536000

Saved
Theme: dark

The -b option sends a cookie the way a browser would. The response to DELETE /theme contains Set-Cookie: theme=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT — a date in the past, so the browser deletes the cookie.

For several values there is ctx.cookieStore(): set("visits", 3) and get("visits"). It keeps them in one cookie, javalin-cookie-store, as JSON in Base64, for example eyJ2aXNpdHMiOjF9 — that is {"visits":1}. The user can read and change any cookie, so never keep roles or permissions in them.

Sign-in with a session

The sign-in form is the template src/main/resources/templates/login.mustache with the username and password fields and an {{#error}} block, like the post form. The controller:

// src/main/java/com/example/blog/LoginController.java
public class LoginController {
 
    public void form(Context ctx) {
        ctx.render("templates/login.mustache");
    }
 
    public void login(Context ctx) {
        String username = ctx.formParam("username");
        Role role = Auth.login(username, ctx.formParam("password"));
        if (role == null) {
            ctx.status(HttpStatus.UNAUTHORIZED);
            ctx.render("templates/login.mustache", Map.of("error", "Wrong login or password"));
            return;
        }
        ctx.sessionAttribute("user", username);
        ctx.sessionAttribute("role", role);
        ctx.redirect("/blog", HttpStatus.SEE_OTHER);
    }
 
    public void logout(Context ctx) {
        ctx.req().getSession().invalidate();
        ctx.redirect("/blog", HttpStatus.SEE_OTHER);
    }
}

ctx.sessionAttribute("role", role) puts a value into the session; Jetty creates the session and sets the JSESSIONID cookie. invalidate() removes the whole session. Add a sign-out button — a form with method="post" and action="/logout" — to posts.mustache.

The role from the session

In Auth, the password check moves to a public login method, and roleOf looks into the session first. A guest on /blog... pages is sent to the sign-in form instead of getting 401:

// src/main/java/com/example/blog/Auth.java
public static Role login(String username, String password) {
    User user = username == null ? null : USERS.get(username);
    if (user == null || !user.password().equals(password)) {
        return null;
    }
    return user.role();
}
 
private static Role roleOf(Context ctx) {
    Role fromSession = ctx.sessionAttribute("role");
    if (fromSession != null) {
        return fromSession;
    }
    BasicAuthCredentials credentials = ctx.basicAuthCredentials();
    return credentials == null ? null : login(credentials.getUsername(), credentials.getPassword());
}
 
// in check(), right after Role role = roleOf(ctx);
if (role == null && ctx.path().startsWith("/blog")) {
    ctx.redirect("/login", HttpStatus.SEE_OTHER);
    return;
}

A redirect in beforeMatched cancels the main handler. Now give the routes their roles:

// src/main/java/com/example/blog/Main.java
var login = new LoginController();
 
// in the apiBuilder block
get("/blog/new", posts::newForm, Role.USER, Role.ADMIN);
post("/blog", posts::submit, Role.USER, Role.ADMIN);
post("/uploads", uploads::upload, Role.USER, Role.ADMIN);
get("/login", login::form, Role.ANYONE);
post("/login", login::login, Role.ANYONE);
post("/logout", login::logout, Role.ANYONE);

Trying it out

The -c option saves received cookies to a file, and -b sends them back. The responses show the status and the main headers:

curl -i http://localhost:8080/blog/new
curl -i -c cookies.txt http://localhost:8080/login -d "username=oleg" -d "password=changeme"
curl -i -b cookies.txt http://localhost:8080/blog/new
HTTP/1.1 303 See Other
Location: /login

HTTP/1.1 303 See Other
Set-Cookie: JSESSIONID=node01ve8gobd2790y9b59hr9x58lj0.node0; Path=/; HttpOnly; SameSite=Lax
Location: /blog

HTTP/1.1 200 OK
Content-Type: text/html

The guest was sent to sign in, and after signing in the form opened. An HttpOnly cookie is not available to JavaScript on the page. A wrong password returns 401 and the form with the text "Wrong login or password", and POST /logout removes the session — after that /blog/new redirects to /login again.

Common mistake

Jetty keeps sessions in memory. After a server restart the old JSESSIONID cookie points to a session that no longer exists, and the signed-in user is a guest again:

curl -i -b cookies.txt http://localhost:8080/blog/new
HTTP/1.1 303 See Other
Location: /login

This is fine during development. If the server restarts often or there are several servers, sessions are stored in a database or Redis, and the cookie holds only the id.

Thanks for reading!

Donate to the site — we work hard for you.