Skip to content

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.


@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;
}
ParameterDescription
urlBase URL of your Keycloak server
realmKeycloak realm name
clientIdOAuth2 client ID registered in Keycloak
jsUrlOptional: 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.


@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:

  • @Menu fields — hides the menu entry
  • @Menu methods — hides the menu entry
  • Classes (types) — hides the whole page or orchestrator

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 attributeJWT claim checked
rolesrealm_access.roles (Keycloak realm roles)
scopesscope 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.


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"

@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.


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.


Inside action handlers, read the JWT or injected headers from the HttpRequest:

@Override
public 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;
}

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