OAuth, OpenID Connect, Keycloak a Spring Boot VI. – Flow pre Client Credentials

2023/04/06

Ukážeme si ako použiť aplikáciu v Spring Boote ako OAuth klientku, ktorá sa autorizuje ako servisná aplikácia.

OAuth podporuje aj situácie, kde sa prístup k údajom udeľuje aplikáciám, ktoré nemajú používateľa. Obvykle ide o servisné aplikácie, démonov, monitoringy a podobne. V týchto aplikáciách neexistuje používateľ, ktorý by schvaľoval prístup k zdrojom — sama aplikácia sa považuje za vlastníka zdrojov.

Grant (flow) typu client_credentials slúži pre aplikácie, ktoré figurujú v dvojrole klienta (OAuth Client) i vlastníka zdrojov (OAuth Resource Owner).

Flow Client Credentials

Vo flowe figurujú len tri súčiastky:

  • Authorization Server: napríklad Keycloak

  • Client: je ním „servisná“ aplikácia.

  • Resource Server: napríklad REST API

Klient je vždy dôverný (confidential), čiže má obvykle uzavretý kód, a dokáže starostlivo ochrániť tajomstvo Client Secret, ktoré sa považuje za citlivý údaj medzi ním a autorizačným serverom.
  1. Klient sa prezentuje svojim identifikátorom (Client ID) a klientskym tajomstvom (Client Secret).

  2. Autorizačný server vráti token.

  3. Klient sa pomocou tokenu autorizuje voči Resource Serveru.

Klient kontaktuje priamo endpoint pre token. Kroky s endpointom pre autorizáciu sa v tomto flowe ignorujú.
flow

Registrácia klienta

Klienta zaregistrujeme v Keycloaku (autorizačnom serveri).

  1. Vytvoríme nového klienta s identifikátorom techbank.

  2. Zapneme autentifikáciu klienta (Client Authentication), pretože ide o dôverného klienta (confidential client).

  3. Vypneme všetky flowy a ponecháme len Service accounts roles, čo zodpovedá grantu Client Credentials z OAuth 2.0.

keycloak registration

Skúška správnosti

Ak je klient registrovanný, môžeme overiť flow.

Ukážka dopytu:

curl -X POST --location "http://localhost:8080/realms/master/protocol/openid-connect/token" \ (1)
    -H "Content-Type: application/x-www-form-urlencoded" \ (2)
    -d "grant_type=client_credentials" \ (3)
    --basic --user techbank:E5wzwZqbFiH1Gwh2qou8O332yzJ5mj5g (4)
1 Požiadavku typu POST posielame na endpoint Keycloaku, ktorý vydá token.
2 Telo požiadavky sú formulárové údaje.
3 Používame flow typu Client Credentials.
4 Klient sa autentifikuje dvojicou Client ID a Client Secret, ktorá zodpovedá zaregistrovaným údajom v autorizačnom serveri Keycloak

Odpoveďou bude prístupový token (access token). Ak používame OpenID Connect, token bude vo formáte JWT.

{
  "access_token": "eyJh...",
  "expires_in": 60,
  "refresh_expires_in": 0,
  "token_type": "Bearer",
  "not-before-policy": 0,
  "scope": "profile email"
}

Token následne vieme priložiť ku volaniu resource servera, teda nejakého REST API.

http://localhost:9999/accounts/1/balance
Authorization: Bearer {{jwt}}

Pre Keycloak bude vyzerať JWT token nasledovne:

{
  "exp": 1680819148,
  "iat": 1680819088,
  "jti": "9f28ee20-7631-43f3-b3d8-5ca6d5c5222a",
  "iss": "http://localhost:8080/realms/master",
  "aud": "account",
  "sub": "2ee74099-6f39-42e0-a3c4-f73b5fe03874", (1)
  "typ": "Bearer",
  "azp": "techbank",
  "acr": "1",
  "allowed-origins": [
    "http://localhost:9991"
  ],
  "realm_access": {
    "roles": [
      "default-roles-master",
      "offline_access",
      "uma_authorization"
    ]
  },
  "resource_access": {
    "account": {
      "roles": [
        "manage-account",
        "manage-account-links",
        "view-profile"
      ]
    }
  },
  "scope": "profile email",
  "email_verified": false,
  "clientId": "techbank",
  "clientHost": "127.0.0.1",
  "preferred_username": "service-account-techbank",
  "clientAddress": "127.0.0.1"
}
1 Reprezentuje identifikátor technického používateľa. V Keycloaku vznikne používateľ service-account-techbank, ktorému je možné nastaviť roly a ďalšie oprávnenia.

Klient v Spring Boote

Klienta, ktorý sa bude pripájať k resource serveru postavíme na základe triedy WebClient, teda reaktívnom HTTP klientovi.

Použijeme závislosti:

  • Spring OAuth 2 Client: spring-boot-starter-oauth2-client

  • Spring WebFlux: spring-boot-starter-webflux s podporou pre reaktívne aplikácie

Konfigurácia aplikácie

Klient pre OAuth potrebuje konfiguráciu — identifikátor klienta, jeho tajomstvo, explicitne uvedený flow a adresu, kde je k dispozícii Keycloak.

spring.security.oauth2.client.registration.keycloak.client-id=techbank
spring.security.oauth2.client.registration.keycloak.client-secret=E5wzwZqbFiH1Gwh2qou8O332yzJ5mj5g
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=client_credentials
spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8080/realms/master

Konfigurácia klienta pre HTTP

Naša servisná aplikácia bude každých pár sekúnd pingať REST API na vzdialenom serveri — teda bude posielať jednoduchú požiadavku v HTTP.

Aby dokázala bežať dlho, priamo ju spustíme ako službu bežiacu nad reaktívnym serverom Netty, o čo sa postará Spring WebFlux.

Budeme potrebovať:

  1. bean, ktorý bude udržiavať autorizovaný stav

  2. HTTP klienta WebClient

  3. funkciu, ktorá prepojí klienta s autorizačnými mechanizmami OAuth a bude automaticky posielať prihlasovacie požiadavky na Keycloak.

beans

Bean pre autorizovaný stav

Trieda ReactiveOAuth2AuthorizedClientManager berie klientov, ktorí sú nakonfigurovaní v application.properties, dokáže ich autorizovať voči autorizačnému serveru, po úspešnej autentifikácii ich vyhlásiť za autorizovaných klientov a tento zoznam ukladať (perzistovať) do vhodného úložiska.

Keďže náš klient nepobeží v rámci webovej aplikácie — požiadavky cez HTTP budú vyvolávané autonómne, použijeme implementáciu AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager.

Trieda má dve závislosti, ktoré nám Spring Boot WebFlux dodá automaticky:

  • zoznam klientov spravovaný ReactiveClientRegistrationRepository.

  • manažment autorizovaných klientov ReactiveOAuth2AuthorizedClientService, ktorý dokáže na základe identifikátora klienta a autentifikácie Authentication poskytnúť autorizovaného klienta, a zároveň ich udržiavať v príslušnom úložisku.

@Bean
ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
        ReactiveClientRegistrationRepository clientRegistrations,
        ReactiveOAuth2AuthorizedClientService authorizedClientService) {

    return new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrations, authorizedClientService);
}

WebClient

Klienta pre HTTP prepojíme s autorizáciou.

Trieda ServerOAuth2AuthorizedClientExchangeFilterFunction integruje klienta pre HTTP (WebClient) s mechanizmami OAuth.

Spolupracuje s ReactiveOAuth2AuthorizedClientManager, ktorý rieši nízkoúrovňové technikálie.

Ak tieto tri triedy prepojíme, získame klienta WebClient, ktorý vie automaticky kontaktovať Keycloak, získať token, a priložiť ho k požiadavkam smerovaným na REST API.

OAuthConfiguration.java
@Bean
public WebClient webClientSecurityCustomizer(
        ReactiveOAuth2AuthorizedClientManager authorizedClients) {(1)
    var oAuthFilter
        = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
                authorizedClients);(2)
    oAuthFilter.setDefaultClientRegistrationId("keycloak");(3)

    return WebClient.builder()
            .filter(oAuthFilter) (4)
            .build();
}
1 Ako závislosť si vyžiadame správcu autorizovaných klientov pre OAuth. Tú dostaneme v podobe beanu nakonfigurovaného v predošlom kroku.
2 Vytvoríme filter, ktorý sa postará o integráciu s OAuth.
3 Keďže filter beží autonómne, mimo požiadavky HTTP, musíme explicitne povedať, na ktorého klienta z application.properties sa táto konfigurácia vzťahuje.
4 Filter zapojíme do klienta WebClient.

Integrácie

Od tejto chvíle môžeme automaticky používať klienta WebClient.

Ak chceme napríklad periodicky posielať dopyty na server:

  1. zapneme anotáciu @EnableScheduling,

  2. vyrobíme metódu s anotáciou @Scheduled,

  3. automaticky nadrôtujeme klienta WebClient,

  4. voláme požiadavky na resource server.

@Component
public class ScheduledBalanceChecker {
    @Autowired
    private WebClient webClient; (1)

    @Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS) (3)
    public void checkBalance() {
        int accountId = 1;
        BigDecimal balance = webClient.get()
                .uri("http://localhost:9999/accounts/{accountId}/balance", accountId)
                .retrieve() (2)
                .bodyToMono(BigDecimal.class)
                .block();
        logger.info("Account {} has balance {}", accountId, balance);
    }
}
1 Necháme si automaticky nadrôtovať klienta pre HTTP požiadavky vrátane integrácie s OAuth.
2 Voláme klienta.
3 Periodicky zapneme volanie metódy.
V tomto prípade sa s každým volaním klienta získa nový token z autorizačného servera. V každej iterácii sa tak v skutočnosti vykonajú dva dopyty: jeden na autorizačný server a druhý na príslušné REST API v resource serveri.

Záver

Repozitár s kódom pre Spring Boot je k dispozícii na GitHube, v repozitáru novotnyr/bank-oidc-client-credentials.
>> Home