V predošlom dieli sme si ukázali, ako je možné rozbehať Keycloak v jednoduchej podobe „databázy pre používateľov a kontá“.
Teraz si ukážme, ako je možné zobrať backendovú aplikáciu s REST API, a ochrániť ju tak, aby bola dostupné len po prihlásení používateľa cez Keycloak.
Plán práce bude:
-
Pripravíme si REST API cez Spring Boot.
-
Zabezpečíme ho cez knižnicu Spring Security, ktorá sa postará o autorizáciu.
-
Dodáme integráciu s Keycloakom pomocou štandardu OAuth 2.0 / OpenID Connect 1.0.
-
Ukážeme si, ako je možné zobrať token JWT a použiť ho v REST API na informáciu o prihlásení.
REST API a Spring Boot
Pripravme si aplikáciu v Spring Boote. Najlepšie na to použiť Spring Initializr, či už cez portál https://start.spring.io alebo cez IntelliJ IDEA.
Dôležité je naklikať dva komponenty:
- Spring Web
-
pre REST API
- OAuth 2 Resource Server
-
pre integráciu s Keycloakom.
Náš backend v Spring Boote bude figurovať v role Resource Servera, čo je v terminológii OAuth 2.0 súčiastka, ktorá obsahuje používateľské dáta a zverejňuje ich autentifikovaným používateľom |
Kontrolér pre REST API
Pripravme si metódu pre REST API, ktorá zverejní stav účtu:
@GetMapping("/accounts/{accountId}/balance")
public BigDecimal getBalance(@PathVariable String accountId) {
return BigDecimal.TEN;
}
Dodajme nad triedu BankApplication
anotáciu RestController
:
@SpringBootApplication
@RestController
public class BankApplication {
///...
}
Zmeňme tiež port pre HTTP na 8888 — nechceme kolidovať s portom 8080 pre Keycloak.
server.port=8888
Spusťme teraz celý backend!
HTTP požiadavky na backend
Na požiadavky budeme používať HTTP klienta zabudovaného v IntelliJ IDEA.
Založme súbor src/test/resources/requests.http
s jediným riadkom:
http://localhost:8888/accounts/1
Súbor je ekvivalentom príkazu pre curl -X GET --location "http://localhost:8888/accounts/1" |
Ak tento súbor spustíme, uvidíme odpoveď, ktorá vyžaduje autorizáciu:
HTTP/1.1 401 (1)
WWW-Authenticate: Basic realm="Realm" (2)
...
1 | Stavový kód 401 znamená, že sa vyžaduje autorizácia. |
2 | Podľa tejto hlavičky zistíme, že sa postupuje podľa filozofie HTTP Basic. |
Toto všetko sa deje vďaka modulu Spring Security, ktorý sa automaticky objavil v projekte ako závislosť na knižnici spring-boot-starter-oauth2-resource-server .
Toto zatiaľ nemá nič spoločné s OAuth 2.0/OIDC — je to štandardné správanie Spring Security v Spring Boot-e.
|
Integrácia s Keycloakom
Metadáta pre OpenID Connect 1.0
Keycloak ako autorizačný server nad protokolom Open ID Connect zverejňuje metadáta o svojej konfigurácii vo formáte JSON. Takto vieme zistiť napríklad:
-
adresy URL pre jednotlivé endpointy podľa príslušného flowu,
-
informácie o šifrovacích metódach pre tokeny JWT,
-
flowy OAuth 2.0, ktoré tento server podporuje,
Formát sa riadi normou OpenID Connect Discovery 1.0.
Keycloak zverejňuje informácie na dohodnutej adrese, ktorá pre realm Pokojne ju navštívme z prehliadača! |
Spring Boot a modul OAuth 2.0 Resource Server dokáže z tejto adresy zistiť všetko podstatné pre integráciu!
Integrácia medzi Spring Boot a Keycloak
Do application.properties
v Spring Boote stačí dodať jedinú vlastnosť: cestu, z ktorej sa dajú odvodiť metadáta pre OIDC 1.0.
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080/realms/master
Príponu /.well-known/openid-configuration. si Spring Boot domyslí.
|
Po reštarte aplikácie môžeme zopakovať požiadavku HTTP. Uvidíme stavový kód 401, hoci s inou hlavičkou.
GET http://localhost:8888/accounts/1/balance
HTTP/1.1 401
WWW-Authenticate: Bearer (1)
1 | Vyžaduje sa hlavička Bearer , teda sa očakáva token JWT. |
ROPC flow a REST API
Na to, aby REST API fungovalo, potrebujeme získať token JWT. Najjednoduchší spôsob je využiť flow ROPC.
-
Z Keycloaku vymeníme login a heslo za JWT token
-
JWT token priložíme ku požiadavke na springové REST API.
Ak používame IntelliJ IDEA, tak dvojicu požiadaviek vieme prepojiť cez premenné a vyhodnocovače odpovedí.
### Get JWT Token
POST http://localhost:8080/realms/master/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=password&client_id=megabank&scope=openid&username=harald&password=Yei8eejaiJeith
> {%
client.global.set("jwt", response.body.access_token)
%}
### Retrieve bank balance
http://localhost:8888/accounts/1/balance
Authorization: Bearer {{jwt}}
Odpoveďou bude stav na účte.
Ak by sme používali
|
Pozor na ROPC flow
ROPC flow je síce extrémne jednoduchý, ale:
-
Každá aplikácia si musí implementovať svoju vlastnú prihlasovaciu stránku. Teraz síce získavame JWT token cez REST API, ale v reálnom nasadení (napr. cez SPA) musíme odniekiaľ zasielať login a heslo.
-
ROPC vyžaduje extrémnu dôveru v klienta: musíme si byť istí, že login a heslo odovzdané do prihlasovacej stránky v prehliadači či mobilnej appke neunikne nikam bokom, alebo sa neodcudzí po ceste.
-
Klient sa musí bezpečne starať o náš login a heslo a musí ho ukladať na citlivé miesto.
ROPC flow sa už neodporúča používať a v OAuth 2.1 zrejme ani nebude použiteľný. Ak je to možné, systém treba migrovať na lepší flow. |
Sprístupnenie prihlasovacích údajov v REST API
Knižnica Spring OAuth 2 Resource Server sa priamo integruje s možnosťami Spring Security.
Ak chceme zistiť, aký používateľ prišiel do REST API, použime anotáciu AuthenticationPrincipal
nad parametrom typu org.springframework.security.oauth2.jwt.Jwt
.
Pozor, v Spring Boote existuje viacero tried Jwt .
Treba vybrať tú správnu!
|
@GetMapping("/accounts/{accountId}/balance")
public BigDecimal getBalance(@PathVariable String accountId, @AuthenticationPrincipal Jwt jwt) (1)
{
String userId = (String) jwt.getClaims().getOrDefault("sub", ""); (2)
//...
}
1 | Informácie o prihlásenom používateľovi vo formáte JWT. |
2 | Objekt Jwt obsahuje všetky claims, teda údaje o používateľovi v podobe mapy.
Claim sub obsahuje identifikátor UUID používateľa z Keycloaku. |
Mapovanie mena používateľa
Štandardne sa názov principálu berie z claimu sub
.
To však môžeme prispôsobiť — napríklad na claim preferred_username
.
Bean JwtAuthenticationConverter konvertuje token Jwt na objekt Authentication v Spring Security.
|
@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
var authenticationConverter = new JwtAuthenticationConverter();
authenticationConverter.setPrincipalClaimName("preferred_username"); (1)
return authenticationConverter;
}
1 | Názov sa prevezme z claimu preferred_username .
Ten obsahuje napríklad nášho harald -a. |
public BigDecimal getBalance(
@PathVariable String accountId,
@CurrentSecurityContext(expression = "authentication.name") (1)
String userName)) { (2)
}
1 | Z objektu Security Context v Spring Security vieme pomocou výrazu získať priamo názov principála, teda meno používateľa. |
Repozitár
Zdrojové kódy pre celý repozitár sú na GitHube, v repozitári novotnyr/bank-restapi-oidc .
|