Security & Authentication

The authentication system is based on UserDetailsService, an abstraction that lets you organize your User model however you want. All auth helpers are available via the static Auth class — no inheritance required.

Session authentication

1. Create your UserDetailsService
@UserDetailsServiceImpl
public class AppUserDetailsService implements UserDetailsService {

    @Inject
    private UserRepository userRepository;

    @Override
    public UserDetails loadByUsername(String username) {
        return userRepository.findByUsername(username);
    }

    @Override
    public UserDetails loadById(Object id) {
        return userRepository.findById(id);
    }
}
2. Implement your User model
// Extend UserDetails with custom fields
public interface AppUserDetails extends UserDetails {
    String getEmail();
    String getUUID();
}

// Implement it in your User model
@Table("users")
public class User extends Model implements AppUserDetails {
    public Object getId()       { return getLong("id"); }
    public String getUsername() { return getString("username"); }
    public String getPassword() { return getString("password"); }
    public String getRole()     { return getString("role"); }
    public String getEmail()    { return getString("email"); }
    public String getUUID()     { return getString("uuid"); }
}
3. Login and logout
@Controller
public class AuthController extends BaseController {

    @POST("/login")
    @CsrfProtect
    public String login(Request req, Response res) {
        try {
            if (Auth.login(req.queryParams("username"), req.queryParams("password"), req)) {
                res.redirect(Auth.getRedirectAfterLogin(req, "/dashboard"));
                halt();
            }
            return redirectWithFlash(req, res, "error", "Invalid credentials.", "/login");
        } catch (LoginLockedException e) {
            return redirectWithFlash(req, res, "error", e.getMessage(), "/login");
        }
    }

    @GET("/logout")
    @RequireLogin
    public String logout(Request req, Response res) {
        Auth.logout(req.session());
        res.redirect("/login");
        halt();
        return null;
    }
}
Protecting routes
@RequireLogin — redirect unauthenticated users to /login
// Method level
@GET("/dashboard")
@RequireLogin
public String dashboard(Request req, Response res) { ... }

// Class level — all routes protected
@Controller
@RequireLogin
public class DashboardController extends BaseController {
    @GET("/dashboard")
    public String index(Request req, Response res) { ... }

    @GET("/settings")
    public String settings(Request req, Response res) { ... }
}
@HasRole — restrict to a specific role
@Controller
@HasRole("ADMIN")
public class AdminController extends BaseController {

    @GET("/admin")
    public String index(Request req, Response res) { ... }

    // Method-level takes precedence over class-level
    @GET("/admin/users")
    @HasRole("SUPERADMIN")
    public String users(Request req, Response res) { ... }
}
Injecting the current user

Add a @CurrentUser parameter to receive the authenticated user directly. Resolved from the per-request cache — no extra DB call.

@GET("/profile")
@RequireLogin
public String profile(Request req, Response res, @CurrentUser AppUserDetails user) {
    return render("profile.html", Map.of("user", user, "email", user.getEmail()));
}
Auth helpers
Auth.login(username, password, req)    // Login with brute force protection
Auth.logout(req.session())             // Logout
Auth.isLogged(req)                     // Is the user authenticated?
Auth.hasRole(req, "ADMIN")             // Does the user have this role?
Auth.user(req)                         // Get authenticated user (cached per request)
Auth.hashPassword("secret")            // Hash a password
Auth.checkPassword("secret", hash)     // Verify a password
Auth.getRedirectAfterLogin(req, "/dashboard")
Brute force protection

Auth.login() automatically enforces rate limiting per IP and per username. After 5 consecutive failed attempts, the account is locked for 15 minutes. Counters reset on successful login.

try {
    if (Auth.login(username, password, req)) {
        res.redirect("/dashboard");
        halt();
    }
    return redirectWithFlash(req, res, "error", "Invalid credentials.", "/login");
} catch (LoginLockedException e) {
    // e.getMessage() → "Too many failed attempts. Try again in 847 seconds."
    return redirectWithFlash(req, res, "error", e.getMessage(), "/login");
}

API / Bearer token authentication

1. Implement TokenResolver
@TokenResolverImpl
public class AppTokenResolver implements TokenResolver {

    @Override
    public UserDetails resolve(String token) {
        ApiToken t = ApiToken.findFirst("token = ?", token);
        if (t == null || t.isExpired()) return null;
        return User.findById(t.getLong("user_id"));
    }
}
2. Protect your API routes
@ApiController — entire controller uses Bearer auth
@Controller
@ApiController
public class UserApiController extends BaseController {

    @GET("/api/users")
    @RequireLogin
    public String index(Request req, Response res, @CurrentUser UserDetails user) {
        return json(map("users", userRepository.findAll()));
    }
}
@Bearer — single route
@GET("/api/me")
@Bearer
public String me(Request req, Response res, @CurrentUser UserDetails user) {
    return json(map("user", user));
}

On failure, API routes return JSON instead of redirecting — {"error": "Unauthorized"} (401) or {"error": "Forbidden"} (403).