Security
Mateu provides two security annotations: @KeycloakSecured for authentication and @EyesOnly for role-based authorization. Both are declarative — no security filter chains or interceptor configuration required.
Authentication with @KeycloakSecured
Section titled “Authentication with @KeycloakSecured”@KeycloakSecured is a type-level annotation. Apply it to your @UI class to require Keycloak login before the application loads:
@UI("")@KeycloakSecured( url = "https://auth.example.com/auth", realm = "my-realm", clientId = "my-client")public class App { @Menu Products products; @Menu Orders orders;}| Parameter | Description |
|---|---|
url | Base URL of your Keycloak server |
realm | Keycloak realm name |
clientId | OAuth2 client ID registered in Keycloak |
jsUrl | Optional: custom URL for the Keycloak JS adapter (defaults to {url}/js/keycloak.js) |
When the application loads, unauthenticated users are redirected to the Keycloak login page. After login, the browser receives a JWT Bearer token that is sent with every subsequent request.
Authorization with @EyesOnly
Section titled “Authorization with @EyesOnly”@EyesOnly can be applied to fields, methods, and types. It hides the annotated element from any user who does not satisfy the required conditions.
public class App { @Menu Products products; // visible to all authenticated users
@Menu @EyesOnly(roles = "admin") AdminPanel admin; // only visible to users with the "admin" role
@Menu @EyesOnly(roles = {"editor", "admin"}) CmsPages pages; // visible to "editor" OR "admin"}@EyesOnly can be applied to:
@Menufields — hides the menu entry@Menumethods — hides the menu entry- Classes (types) — hides the whole page or orchestrator
How authorization works
Section titled “How authorization works”Mateu reads the JWT Bearer token from the Authorization request header. It decodes the payload and checks the claims. Signature verification is expected to happen at the API gateway before the request reaches Mateu.
@EyesOnly attribute | JWT claim checked |
|---|---|
roles | realm_access.roles (Keycloak realm roles) |
scopes | scope claim (space-separated list) |
groups | (reserved for future use) |
permissions | (reserved for future use) |
If any required condition is not met, the element is omitted from the response. The user never sees the menu entry or page.
Condition logic
Section titled “Condition logic”Multiple values within a single attribute use OR logic:
@EyesOnly(roles = {"admin", "superuser"}) // user must have "admin" OR "superuser"Multiple attributes use AND logic:
@EyesOnly(roles = "admin", scopes = "write") // must have "admin" AND "write"Securing individual fields
Section titled “Securing individual fields”@EyesOnly on a record field hides it from both the form and the listing:
public record User( String id, String name, @EyesOnly(roles = "admin") String internalNote) {}Users without the admin role see id and name but not internalNote.
Typical deployment setup
Section titled “Typical deployment setup”In a distributed system, an API gateway (Nginx, Envoy, Kong, etc.) validates the JWT signature and token expiry. Mateu trusts the token but does not re-validate the signature:
Browser → API Gateway (validates JWT signature + expiry) → Mateu backend (reads claims from Bearer token, enforces @EyesOnly)For common cases, the gateway can also inject identity headers (X-User-Id, X-User-Email) that action handlers read directly.
Reading the current user in actions
Section titled “Reading the current user in actions”Inside action handlers, read the JWT or injected headers from the HttpRequest:
@Overridepublic Object handleAction(String actionId, HttpRequest httpRequest) { String authHeader = httpRequest.getHeaderValue("Authorization"); // parse the Bearer token to extract user identity, or read injected headers: String userId = httpRequest.getHeaderValue("X-User-Id"); return null;}Service-owned authorization
Section titled “Service-owned authorization”In a microservices deployment, each service enforces @EyesOnly independently. The shell forwards the JWT to the service backend, which applies its own rules. A user who lacks a role sees the menu entry removed in the service’s response — not just hidden in the shell.
See Service-owned UI modules for how this fits into a distributed architecture.
- Service-owned UI modules — how each service enforces its own authorization
- Rules — client-side field visibility (complement to server-side
@EyesOnly) - Testing — how to test pages that depend on authorization headers