Cliente de un servicio REST autenticado con OAuth en Java

Escrito por el , actualizado el .
java planeta-codigo programacion
Enlace permanente Comentarios

Teniendo un servicio REST securizado con OAuth2 al invocarlo deberemos realizar el flujo necesario para obtener un access token y posteriormente enviarlo al servicio REST como forma de autenticación y autorización. Usando un cliente programado en el lenguaje Java y usando la librería HttpClient podemos hacer las peticiones HTTP necesarias para la invocación del servicio.

Java

Explicaba como llamar a un servicio REST autenticado con OAuth2 en el artículo autenticación con OAuth y Keycloak en un servicio REST con JAX-RS y Spring Boot. Para ello usaba la utilidad curl para hacer las peticiones HTTP get y post necesarias tanto para obtener el access token usando el flujo client_credentials como para una vez obtenido el access token llamar al servicio REST. En una aplicación usaremos un lenguaje de programación para llamar al servicio.

En este ejemplo mostraré como llamarlo usando un cliente programado en lenguaje Java que hará las mismas peticiones get y post pero usando la librería OkHttp en vez de curl.

Primero añadiremos como dependencia del proyecto la librería HttpComponents. Como en las diferentes llamadas el intercambio de datos se realiza mediante el formato JSON añadiremos otro par dependencias para procesar los datos en este formato, en este caso usando la API de JSON-P y una implementación.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
plugins {
	id 'application'
}

repositories {
	mavenCentral()
}

dependencies {
	implementation platform('org.springframework.boot:spring-boot-dependencies:2.7.3')

	implementation('org.springframework.boot:spring-boot-starter')
	implementation('org.springframework.boot:spring-boot-starter-web')
	implementation('org.springframework.boot:spring-boot-starter-security')
	implementation('org.springframework.security:spring-security-oauth2-resource-server:5.7.3')
	implementation('org.springframework.security:spring-security-oauth2-jose:5.7.3')

	implementation("com.fasterxml.jackson.core:jackson-databind")

	implementation("com.squareup.okhttp3:okhttp:4.10.0")

	implementation "javax.json:javax.json-api:1.1"
	implementation "javax.json.bind:javax.json.bind-api:1.0"
	implementation "javax.json.bind:javax.json.bind-api:1.0"
	runtimeOnly "org.eclipse:yasson:1.0"
	runtimeOnly "org.glassfish:javax.json:1.1"
}

application {
    mainClass = 'io.github.picodotdev.blogbitix.springbootjaxrsoauth.server.Main'
	sourceCompatibility = 11
	targetCompatibility = 11
}
build.gradle

Este sencillo cliente realiza varias peticiones get y post. Una para obtener la configuración de los endpoints, el que nos interesa es el de obtener un access token, otra petición para obtener el access token y finalmente con el access token invocar al servicio mediante otra petición.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package io.github.picodotdev.blogbitix.springbootjaxrsoauth.client;

import javax.json.Json;
import javax.json.JsonObject;

import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class Main {

    public static void main(String[] args) throws Exception {
        OkHttpClient baseClient = new OkHttpClient.Builder()
                .build();

        //curl -i http://localhost:9080/realms/my-realm/.well-known/openid-configuration
        System.out.println("Getting configuration...");
        Request configurationRequest = new Request.Builder().url("http://localhost:9080/realms/my-realm/.well-known/openid-configuration").get().build();
        Response configurationResponse = baseClient.newCall(configurationRequest).execute();

        JsonObject configurationObject = Json.createReader(configurationResponse.body().charStream()).readObject();
        String tokenEndpoint = configurationObject.getString("token_endpoint");

        AccessTokenRepository accessTokenRepository = new AccessTokenRepository(tokenEndpoint, baseClient, "spring-boot-client", "Bg1r6mOYsFraDw7u8VCgmGl4JtK8vShX");

        OkHttpClient client = baseClient.newBuilder()
                .authenticator(new DefaultAuthenticator(accessTokenRepository))
                .build();

        System.out.println("Getting an access token...");
        accessTokenRepository.requestAccessToken();

        //curl -ik -H "Authorization: Bearer aaaaa.bbbbb.ccccc" http://localhost:9080/message?string=Hola
        while(true) {
            System.out.println("Calling OAuth secured service...");
            Request serviceRequest = new Request.Builder()
                    .url(HttpUrl.parse("http://localhost:8080/message").newBuilder().addQueryParameter("string", "Hola").build())
                    .header("Authorization", "Bearer " + accessTokenRepository.getAccessToken())
                    .get()
                    .build();
            Response serviceResponse = client.newCall(serviceRequest).execute();

            try (serviceResponse) {
                JsonObject serviceObject = Json.createReader(serviceResponse.body().charStream()).readObject();
                System.out.printf("Result: %s%n", serviceObject.toString());
            }

            Thread.sleep(5000);
        }
    }
}
Main.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package io.github.picodotdev.blogbitix.springbootjaxrsoauth.client;

import java.io.IOException;
import javax.json.Json;
import javax.json.JsonObject;

import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

public class AccessTokenRepository {

    private String tokenEndpoint;
    private OkHttpClient client;
    private String clientId;
    private String clientSecret;

    private String accessToken;
    private String refreshToken;

    public AccessTokenRepository(String tokenEndpoint, OkHttpClient client, String clientId, String clientSecret) {
        this.tokenEndpoint = tokenEndpoint;
        this.client = client;
        this.clientId = clientId;
        this.clientSecret = clientSecret;
    }

    public String getAccessToken() {
        return accessToken;
    }

    public void requestAccessToken() throws IOException {
        System.out.println("Requesting access token");

        RequestBody data = new FormBody.Builder()
                .add("grant_type", "client_credentials")
                .add("client_id", clientId)
                .add("client_secret", clientSecret)
                .build();
        Request tokenRequest = new Request.Builder().url(tokenEndpoint).post(data).build();
        Response tokenResponse = client.newCall(tokenRequest).execute();

        try (tokenResponse) {
            JsonObject tokenObject = Json.createReader(tokenResponse.body().charStream()).readObject();
            accessToken = tokenObject.getString("access_token");
            refreshToken = tokenObject.getString("refresh_token", null);
            Integer expiresIn = tokenObject.getInt("expires_in");
            Integer refreshExpires = tokenObject.getInt("refresh_expires_in", -1);

            System.out.printf("Access token: %s%n", accessToken);
            System.out.printf("Expires in: %d%n", expiresIn);
            System.out.printf("Refresh expires in: %d%n", refreshExpires);
            System.out.printf("Refresh token: %s%n", refreshToken);
        }
    }

    ...
}
AccessTokenRepository.java

Iniciado Keycloak con Docker, configurado el realm y creado un cliente junto con un rol e iniciado el servicio REST podemos ejecutar el cliente que invoque al servicio. El resultado de las trazas que obtendremos en la terminal será el siguiente.

1
2
3
4
5
6
7
8
9
Getting configuration...
Getting an access token...
Requesting access token
Access token: eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJQY1ZFRTN0TEl2bGhhVDFpVE16aWV2ZVN5SnJTMXlZTERNY3YzMTUwd0FrIn0.eyJleHAiOjE2NjMzNDkyNTcsImlhdCI6MTY2MzM0OTE5NywianRpIjoiNDA3YjI1NDEtOGVmMC00MzcyLWExZTAtYzE4OGM3ZTgyMzhmIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDgwL3JlYWxtcy9teS1yZWFsbSIsImF1ZCI6InNwcmluZy1ib290LWNsaWVudCIsInN1YiI6IjYzOTI1NWJlLTU0NGUtNGQ2Zi1iNDhjLWE2NjQ1YmY2OGYyYyIsInR5cCI6IkJlYXJlciIsImF6cCI6InNwcmluZy1ib290LWNsaWVudCIsInNlc3Npb25fc3RhdGUiOiIwZDg5ODM1Zi1lZTE3LTQwNWItYjRlYy01NDdhNWFhYTM2N2IiLCJhY3IiOiIxIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm1lc3NhZ2UtcXVlcnkiXX0sInJlc291cmNlX2FjY2VzcyI6eyJzcHJpbmctYm9vdC1jbGllbnQiOnsicm9sZXMiOlsibWVzc2FnZS1xdWVyeS0yIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiMGQ4OTgzNWYtZWUxNy00MDViLWI0ZWMtNTQ3YTVhYWEzNjdiIiwiY2xpZW50SWQiOiJzcHJpbmctYm9vdC1jbGllbnQiLCJjbGllbnRIb3N0IjoiMTcyLjIzLjAuMSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoic2VydmljZS1hY2NvdW50LXNwcmluZy1ib290LWNsaWVudCIsImNsaWVudEFkZHJlc3MiOiIxNzIuMjMuMC4xIn0.4YhPuYuPTqDD-1I2k4CHed14qw3jzWveACqMYzVX5aulzYuS1-Fe2QLOchX8G-g5JqJ_Ggwah9P7NISWfJTaTD_vmq8yCyoqtVPGSy41x0ohWhXId-Gqk_LX9uAVGq_YCQB3EqOwX5cBZcGNLsERY74xs019susIk4ZzvpUWDWYIp-ZmZig2klVVLI8jDG_UMWBsiRCVECE0gRxLTTyEIFPHY-EoUHGM3Wm-p_S6s-vUOPNqtyoq2r2cs0Y5KZ_JUW7vc45B6DvfyDwRcs8MkxPW4LVf24Vxw1Qj0Ygg6HAUvg_aXY_KLY_3ie8gmmgT0t4Xcl34YbguNYwu7IQBqQ
Expires in: 60
Refresh expires in: 1800
Refresh token: eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJiNzk4MGU2NC02ZjdhLTQxNTctYmFmNi1lN2FmYmM0OThmNzEifQ.eyJleHAiOjE2NjMzNTA5OTcsImlhdCI6MTY2MzM0OTE5NywianRpIjoiY2UzYzBiZDUtOWY4Ni00MzU4LWJiNzUtMGIxOGJhYjU1MDNmIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDgwL3JlYWxtcy9teS1yZWFsbSIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6OTA4MC9yZWFsbXMvbXktcmVhbG0iLCJzdWIiOiI2MzkyNTViZS01NDRlLTRkNmYtYjQ4Yy1hNjY0NWJmNjhmMmMiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoic3ByaW5nLWJvb3QtY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6IjBkODk4MzVmLWVlMTctNDA1Yi1iNGVjLTU0N2E1YWFhMzY3YiIsInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsInNpZCI6IjBkODk4MzVmLWVlMTctNDA1Yi1iNGVjLTU0N2E1YWFhMzY3YiJ9.81WSVPtrwiNnbLPgnyPjUP075n_wYAQGkKQYCD7s39A
Calling OAuth secured service...
Result: {"message":"Hola","date":"2022-09-16T17:26:37.540+00:00"}
System.out

En las trazas vemos el endpoint para obtener access token, el access token obtenido, refresh token y tiempos de expiración de los mismos, finalmente los datos devueltos por el servicio. Como se observa los access token para el cliente son una cadena opaca bastante larga de caracteres, está cifrada firmada digitalmente y contiene información como el rol y tiempos de expiración. Enviado el access token al servicio REST Spring Security vcalida la firma digital del token, descifrará la información, validará su tiempo de expiración y se comprobará si tiene el rol necesario para acceder al endpoint del servicio REST u validaciones sobre otros claims. Notar que con la información incluida en el token y el hecho de que está firmado digitalmente no es necesario que el servicio REST se comunique con el servidor de autorización de OAuth que ha emitido el access token para hacer la validación.

Dos buenos libros sobre OAuth que he leído son los siguientes uno explica detalladamente el protocolo OAuth incluidas las varias formas de obtener un token además del mostrado en este artículo usando las credenciales del cliente. El otro libro es sobre el proveedor de identidad Keycloak que implementa autenticación con OpenID Connect.

Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub.


Comparte el artículo: