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.
@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);
}
}
// 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"); }
}
@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;
}
}
// 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) { ... }
}
@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) { ... }
}
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.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")
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");
}
@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"));
}
}
@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()));
}
}
@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).