Roles mapping¶
A JwtRolesMapper turns a verified JWT into the set of RouteRoles granted to the caller.
Those roles land on Authentication.roles and drive hasRole / hasAnyRole rules and
role-declaring routes in javalin-security.
The default grants NO roles
jwt.rolesMapper defaults to JwtRolesMapper.noRoles(), which always returns an empty set.
With it, every role-based check fails. Authenticated callers can only satisfy
authenticated / allow. As soon as you use roles, configure a real mapper.
Built-in mappers¶
| Mapper | Reads from | Behavior |
|---|---|---|
noRoles() |
— | Always empty (the default). |
fromClaim(name, roleOf) |
A string or list-of-strings claim. | Each value → a role via roleOf. |
fromScope(roleOf) |
The space-delimited scope claim (OAuth 2.0). |
Each scope token → a role via roleOf. |
The roleOf function maps a string name to a RouteRole?. Returning null drops that name —
unknown roles are ignored rather than causing failure.
From a claim¶
Use this when your issuer puts roles in a custom claim, for example "roles": ["ADMIN", "USER"]
or a single "role": "ADMIN".
fromClaim accepts either a String claim (a single role) or a Collection<*> claim (each
element's toString() becomes a name). An absent or unrecognized claim yields an empty set.
From the OAuth scope claim¶
Use this when your authorization server issues OAuth scopes, for example
"scope": "orders:read orders:write admin".
fromScope reads the standard scope claim, splits it on spaces, drops blanks, and maps each
token via roleOf.
Custom mapper¶
JwtRolesMapper is a functional interface. Implement it directly for anything more complex —
nested claims, Keycloak's realm_access.roles, combining multiple claims, and so on.
// Keycloak: roles live under realm_access.roles
jwt.rolesMapper = JwtRolesMapper { token ->
val realmAccess = token.claim<Map<String, Any?>>("realm_access")
@Suppress("UNCHECKED_CAST")
val names = (realmAccess?.get("roles") as? Collection<*>)?.mapNotNull { it?.toString() } ?: emptyList()
names.mapNotNull { name -> Role.entries.find { it.name == name } }.toSet()
}
// Keycloak: roles live under realm_access.roles
jwt.rolesMapper = token -> {
Map<String, Object> realmAccess = token.claim("realm_access");
if (realmAccess == null) return Set.of();
Object roles = realmAccess.get("roles");
if (!(roles instanceof Collection<?> c)) return Set.of();
return c.stream()
.map(Object::toString)
.map(name -> { try { return (RouteRole) Role.valueOf(name); }
catch (IllegalArgumentException e) { return null; } })
.filter(Objects::nonNull)
.collect(Collectors.toSet());
};