OAuth, OpenID Connect, Keycloak a Spring Boot II. – Spring Boot ochránená tokenmi JWT

2023/03/25

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.

spring initializr
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:

BankApplication.java
@GetMapping("/accounts/{accountId}/balance")
public BigDecimal getBalance(@PathVariable String accountId) {
    return BigDecimal.TEN;
}

Dodajme nad triedu BankApplication anotáciu RestController:

BankApplication.java
@SpringBootApplication
@RestController
public class BankApplication {
    ///...
}

Zmeňme tiež port pre HTTP na 8888 — nechceme kolidovať s portom 8080 pre Keycloak.

application.properties
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:

src/test/resources/requests.http
http://localhost:8888/accounts/1

Súbor je ekvivalentom príkazu pre curl:

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 master je dostupná na http://localhost:8080/realms/master/.well-known/openid-configuration.

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.

application.properties
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 curl, vyzeralo by to nasledovne:

curl 'http://localhost:8888/accounts/1/balance' \
>     -H "Authorization: Bearer eyJh...WI-Q"

Pozor na ROPC flow

ROPC flow je síce extrémne jednoduchý, ale:

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

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

  3. 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.
>> Home