Roly (roles) umožňujú v Keycloaku pridávať používateľom ľubovoľné „nálepky“, z ktorých je potom možné odvodzovať rozličné druhy oprávnení (permissions).
Rolou je admin
, guest
, teller
, čokoľvek.
Ukážeme si:
-
Ako možno pridať rolu používateľovi v rámci clienta.
-
Ako v REST API (Spring Boot) vytiahnuť rolu.
-
Ako v REST API určiť autorizáciu: teda prístup k niektorému volaniu len pre vybrané roly.
Pridávanie roly
Vytváranie roly pre klienta megabank
Prihlásme sa do administrátorskej konzoly Keycloaku ako administrátorky (obvykle konto admin
).
Rolu môžeme vytvoriť buď pre celý realm alebo len pre konkrétneho klienta.
Skúsme vytvoriť pre našu megabanku z predošlých dielov rolu withdrawer
pre človeka, ktorý si dokáže vybrať peniaze.
V bloku Clients na karte Roles vytvorme novú rolu tlačidlom Create Role.
Použime názov roly (withdrawer
) a prípadný popis.
Rola je tým vytvorená a môžeme ju pridať používateľovi!
Pridanie roly používateľovi
V bočnej lište v sekcii Users
kliknime na používateľa harald
a pridajme mu rolu.
Na karte Role Mapping priraďme rolu tlačidlom Assign Role.
Uvidíme zoznam rolí — ale keďže ide o zoznam rolí celého realmu a nie klienta, musíme rolu vyhľadať cez textové pole — ale predtým ešte prepnime vo filtri možnosť na Filter by clients.
Vyberme rolu a priraďme ju používateľovi — a to je všetko!
Rola a tokeny JWT
Rola klienta sa automaticky objaví v tokene JWT. Skúsme spustiť klasické prihlásenie cez flow ROPC:
curl -X POST --location "http://localhost:8080/realms/master/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password" \
-d "client_id=megabank" \
-d "scope=openid" \
-d "username=harald" \
-d "password=Yei8eejaiJeith"
Po dekódovaní tokenu JWT uvidíme claim resource_access
:
{
"megabank": {
"roles": ["withdrawer"]
},
"account": {
"roles": ["manage-account", "manage-account-links", "view-profile"]
}
}
Roly a Spring Security
Roly z tokenu JWT môžeme namapovať na roly v Spring Security.
Do aplikačného kontextu v Springu dodáme objekt typu Converter<Jwt, Collection<GrantedAuthority>> , ktorý zmení token JWT na zoznam autorít (rolí) v Spring Security.
|
public class KeycloakClaimRoleAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
public static final String RESOURCE_ACCESS_CLAIM = "resource_access";
public static final String ROLES_CLAIM = "roles";
private final String oAuth2ClientId;
public KeycloakClaimRoleAuthoritiesConverter(String oAuth2ClientId) {
this.oAuth2ClientId = oAuth2ClientId;
}
@SuppressWarnings("unchecked")
@Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
Map<String, Object> resourceAccessValues = jwt.getClaimAsMap(RESOURCE_ACCESS_CLAIM);
if (resourceAccessValues == null) {
return Collections.emptyList();
}
Object clientRolesObject = resourceAccessValues.get(this.oAuth2ClientId);
if (!(clientRolesObject instanceof Map<?, ?>)) {
return Collections.emptyList();
}
Map<String, ?> clientRoles = (Map<String, ?>) clientRolesObject;
Object rolesObject = clientRoles.get(ROLES_CLAIM);
if (!(rolesObject instanceof Collection<?>)) {
return Collections.emptyList();
}
Collection<Object> rolesList = (Collection<Object>) rolesObject;
List<GrantedAuthority> authorities = new ArrayList<>();
for (Object roleObject : rolesList) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(roleObject.toString());
authorities.add(authority);
}
return authorities;
}
}
Dodajme potom bean v podobne metódy:
@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
var authenticationConverter = new JwtAuthenticationConverter();
authenticationConverter.setJwtGrantedAuthoritiesConverter(new KeycloakClaimRoleAuthoritiesConverter("megabank")); (1)
authenticationConverter.setPrincipalClaimName("preferred_username"); /(2)
return authenticationConverter;
}
1 | Autority v Spring Security budeme prevádzať pomocou nášho konvertéra. |
2 | Ako bonus vieme nastaviť meno principala z claimu, ktorý obsahuje meno používateľa namiesto jeho identifikátora UUID. |
Roly a autorizácie
Toto všetko môžeme skombinovať aj do overovania rolí pri volaní metód.
-
Zapneme podporu pre autorizáciu volania metód.
-
Dodáme nad metódu overovanie role.
Podpora pre autorizáciu volania metód
Od Spring Security 6.0 používame anotáciu @EnableMethodSecurity
.
@EnableMethodSecurity
public class BankApplication {
//...
}
V starších Spring Security bola štandardom anotácia @EnableGlobalMethodSecurity .
|
Teraz dodáme anotáciu @PreAuthorize
, ktorá sa spustí iba ak má používateľ rolu withdrawer
nastavenú v Keycloaku.
V opačnom prípade získame stavový kód 503 (Forbidden).
@PostMapping("/accounts/{accountId}/withdrawals")
@PreAuthorize("hasAuthority('withdrawer')")
public BigDecimal withdrawTenCrowns(@PathVariable String accountId,
@CurrentSecurityContext(expression = "authentication.name") String userName) {
logger.info("Withdrawing 10 SKK: account: {}, user {}", accountId, userName);
return BigDecimal.ZERO;
}
Roly v rámci realmu
Používateľovi môžeme priradiť aj globálnu rolu platnú pre celý realm, teda pre všetkých klientov.
Rolu potom pridáme v administrátorskej konzole v sekcii Realm Roles.
Tlačidlom Create Role vytvoríme rolu takým istým spôsobom ako v prípade roly pre konkrétneho klienta.
Vytvorme teda rolu creditor
Rolu priradíme používateľovi podobne ako v prípade roly klienta, ba dokonca ju automaticky uvidíme v zozname rolí a nemusíme ju vyhľadávať.
Roly realmu a JWT
Roly realmu sa objavia v tokene JWT na inom mieste, budú schované v claime realm_access
v podobe zanoreného JSONu.
{
"roles": [
"default-roles-master",
"offline_access",
"creditor",
"uma_authorization"
]
}
Vidíme rolu creditor
a niekoľko implicitných systémových rolí.
Aj tieto roly vieme namapovať na roly / autority v Spring Security. Konverter bude vyzerať trochu inak:
package com.github.novotnyr.bank;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Extracts Keycloak roles to authorities.
* <p>
* Source:
* <pre>
* "realm_access : { "roles":["visitor"] }
* </pre>
* </p>
*/
public class KeycloakAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
public static final String REALM_ACCESS_CLAIM = "realm_access";
public static final String ROLES_CLAIM = "roles";
@SuppressWarnings("unchecked")
@Override
public Collection<GrantedAuthority> convert(Jwt source) {
if (!source.hasClaim(REALM_ACCESS_CLAIM)) {
return Collections.emptyList();
}
Object claim = source.getClaim(REALM_ACCESS_CLAIM);
if (!(claim instanceof Map)) {
return Collections.emptyList();
}
Map<String, Object> realmAccess = (Map<String, Object>) claim;
if (!realmAccess.containsKey(ROLES_CLAIM)) {
return Collections.emptyList();
}
Object rolesClaimObject = realmAccess.get(ROLES_CLAIM);
if (!(rolesClaimObject instanceof Collection<?> roleObjects)) {
return Collections.emptyList();
}
List<GrantedAuthority> authorities = new ArrayList<>();
for (Object roleObject : roleObjects) {
String role = roleObject.toString();
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role);
authorities.add(simpleGrantedAuthority);
}
return authorities;
}
}
Samozrejme, musíme sa rozhodnúť, ktorý konverter použijeme ako bean — či ten, ktorý vyťahuje roly z realmu alebo klienta.
Na domácu úlohu si môžeme implementovať kompozitný konverter, ktorý zlúči roly realmu s rolami _klienta. |
Všetko ostatné sa zachová: automaticky máme k dispozícii roly dostupné v @Secured
či @PreAuthorize
.
Repozitár
Zdrojové kódy pre celý repozitár sú na GitHube, v repozitári novotnyr/bank-restapi-oidc .
|