Servidor OAuth, gateway y servicio REST utilizando tokens JWT con Spring

Escrito por el .
java planeta-codigo programacion software spring
Comentarios

Spring
Java

Hace unos días encontré un articulo del blog técnido de los desarrolladores de Idealista. En él comentaban que tenían una API para realizar simulaciones hipotecarias usando Spring como framework, Spring Security OAuth como forma de autenticación y autorización y JWT como forma de codificar el token que otorga el servidor OAuth y contiene la información necesaria para que el servidor de recursos permita o no el acceso al recurso que aloja.

Ya había oído mencionar JWT pero este artículo me ha permitido conocer su utilidad, y no es poca. Como se menciona en el artículo JWT tiene la ventaja de que que no es necesario persistirlo en una base de datos y contiene toda la información que el servidor de recursos necesita para realizar la autorización ya que es capaz de cargar con información arbitraria que el servicio desee en el momento de la emisión, la autenticación y comprobación de que ha sido emitido por el servidor OAuth la realiza sabiendo que el token está firmado.

Los tokens son una serie de caracteres aparentemente sin sentido al estar hasheados y firmados con una clave compartida entre servidor OAuth y el servidor de recurso o para mayor seguridad mediante clave privada en el servidor OAuth y su clave pública asociada en el servidor de recursos, con la firma el servidor de recursos el capaz de comprobar la autenticidad del token sin necesidad de comunicarse con él. Los tokens de OAuth son más cortos, los tokens JWT con más largos ya que contienen información adicional. Se componen de tres partes separadas por un punto, una cabecera con el algoritmo hash utilizado y tipo de token, un documento JSON con datos y una firma de verificación.

El hecho de que los tokens JWT no sea necesario persistirlos en base de datos elimina la necesidad de tener su infraestructura, como desventaja es que no es tan fácil de revocar el acceso a un token JWT y por ello se les concede un tiempo de expiración corto. En el articulo se analizaba su infraestructura y hay varios elementos configurables de diferentes formas, son:

  • El servidor OAuth que proporciona los tokens, realiza la autenticación y proporciona las autorizaciones.
  • El servidor del recurso al que se le envía el token, en base a las autorizaciones otorgadas por el servidor OAuth al token y las autorizaciones necesarias para acceder al recurso concedo o no acceso al recurso.
  • En el caso de múltiples servicios con múltiples recursos es conveniente un gateway para que sea el punto de entrada de todos los servicios, de esta forma los clientes solo necesitarán conocer el gateway en vez de los múltiples servicios individuales. El gateway se encarga de hacer de proxy en base a información en la petición como ruta, host, parámetros, cabeceras, … de redirigir la petición al servicio encargado de atenderla y devolver la respuesta. Un ejemplo de gateway es Zuul como ya he mostrado en el artículo Proxy para microservicios con Spring Cloud Netflix y Zuul.

Puede haber más elementos en la infraestructura y quizá sea el caso de un sistema real como sería un servidor de descubrimiento con Eureka o un servidor de configuración con Spring Cloud Config, en la serie de artículos sobre Spring Cloud los muestro. Para este ejemplo obvio estos otros servidores y me centro en los más relacionados con el artículo. Aunque lógicamente son diferentes servicios se puede crear uno que proporcione varios de ellos al mismo tiempo, por ejemplo, un servicio que haga al mismo tiempo de servidor de OAuth y de gateway que es una de las posibles cambios que dejan al final en el artículo de Idealista.

Spring ha creado su propio proyecto de gateway para sustituir a Zuul, Spring Cloud Gateway y será el que use en este artículo. Soporta Spring Boot 2, Spring Framework 5, coincidencia por cualquier parámetro de la petición, filtros y transformaciones o predicados, el patrón circuit breaker, limitación de peticiones y reescritura de rutas.

Los servicios los mantengo separados ya que al combinarlos pueden surgir problemas de integración al usar diferentes versiones de librerías de Spring aún cuando todos los proyectos son de Spring. Por ejemplo, Spring Cloud Gateway utiliza Spring WebFlux que puede ser diferente del lo que utilice Spring Security OAuth y la integración puede no estar exenta de problemas.

OAuth JWT

Servidor OAuth

Empezando por el servidor OAuth y las dependencias que necesita, son spring-security-oauth2 y para generar tokens JWT spring-security-jwt, el resto son dependencias necesarias de Spring Boot

 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
plugins {
    id 'application'
    id 'org.springframework.boot' version '2.0.8.RELEASE'
}

mainClassName = 'io.github.picodotdev.blogbitix.springoauth.oauth.Main'

dependencies {
    implementation platform('org.springframework.boot:spring-boot-dependencies:2.0.8.RELEASE')
    implementation platform('org.springframework.cloud:spring-cloud-dependencies:Finchley.SR2')
    
    def excludeSpringBootStarterLogging = { exclude(group: 'org.springframework.boot', module: 'spring-boot-starter-logging') }
    compile('org.springframework.boot:spring-boot-starter', excludeSpringBootStarterLogging)
    compile('org.springframework.boot:spring-boot-starter-web', excludeSpringBootStarterLogging)
    compile('org.springframework.boot:spring-boot-starter-security', excludeSpringBootStarterLogging)
    compile('org.springframework.boot:spring-boot-starter-log4j2', excludeSpringBootStarterLogging)    
    compile('org.springframework.security.oauth:spring-security-oauth2:2.3.4.RELEASE', excludeSpringBootStarterLogging)
    compile('org.springframework.security:spring-security-jwt:1.0.10.RELEASE', excludeSpringBootStarterLogging)

    runtime('com.google.code.gson:gson:2.8.5')
    runtime('com.fasterxml.jackson.core:jackson-databind:2.9.6')
    runtime('com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.6')

    runtime('javax.xml.bind:jaxb-api:2.3.0')
    runtime('com.sun.xml.bind:jaxb-impl:2.3.0')
    runtime('org.glassfish.jaxb:jaxb-runtime:2.3.0')
    runtime('javax.activation:activation:1.1.1')
}

La clase principal de Spring Boot y que inicia la aplicación no tiene nada especial salvo la necesaria anotación @EnableAuthorizationServer para habilitar el servidor OAuth.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package io.github.picodotdev.blogbitix.springoauth.oauth;

...

@SpringBootApplication
@EnableAuthorizationServer
public class Main {

    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}

La parte importante está en la clase de configuración. La clase JwtAccessTokenConverter se encarga de codificar el token, la clase TokenStore de generarlos, DefaultTokenServices contiene referencias a ambos, los métodos heredados configure() configuran diferentes aspectos del servicio como los requisitos para acceder a los endpoint para ver el contenido de un token o los clientes OAuth que reconoce. Para cada cliente se necesita proporcionar el identificativo del cliente, su clave privada o secret, identificativo del recurso, que tipos de concesiones, grants, formas o flujos de obtener el token, que autoridades y ámbitos o scopes se le asigna al token.

 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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package io.github.picodotdev.blogbitix.springoauth.oauth;

...

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private JwtAccessTokenConverter tokenConverter;

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private DefaultTokenServices tokenServices;

    @Bean
    public JwtAccessTokenConverter tokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("1234567890");
        return converter;
    }

    @Bean
    public TokenStore tokenStore(JwtAccessTokenConverter tokenConverter) {
        return new JwtTokenStore(tokenConverter);
    }

    @Bean
    DefaultTokenServices tokenServices(TokenStore tokenStore, JwtAccessTokenConverter tokenConverter) {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(tokenStore);
        tokenServices.setTokenEnhancer(tokenConverter);
        return tokenServices;
    }

    @Bean
    public TokenStoreUserApprovalHandler userApprovalHandler(TokenStore tokenStore, ClientDetailsService clientDetailsService) {
        TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
        handler.setTokenStore(tokenStore);
        handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
        handler.setClientDetailsService(clientDetailsService);
        return handler;
    }
 
    @Bean
    public ApprovalStore approvalStore(TokenStore tokenStore) throws Exception {
        TokenApprovalStore store = new TokenApprovalStore();
        store.setTokenStore(tokenStore);
        return store;
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients().tokenKeyAccess("isAuthenticated()").checkTokenAccess("isAuthenticated()");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenServices(tokenServices);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory().withClient("client")
                .secret("{noop}1234567890")
                .resourceIds("service")
                .authorizedGrantTypes("client_credentials")
                .authorities("CLIENT")
                .scopes("read");
    }
}

El servidor OAuth de ejemplo se inicia con el comando ./gradlew oauth:run. Para obtener un token se realiza con las siguientes peticiones. Por defecto, se solicita autenticación basic pero la invocación al método allowFormAuthenticationForClients() hace que los parámetros de las credenciales se puedan indicar por parámetros.

Con el endpoint /oauth/check_token se decodifica el token. En la página de JWT hay una herramienta para decodificar el token y verificar de la firma introduciendo clave de firma en la casilla.

1
2
3
4
5
6
7
$ curl -X POST -u "client:1234567890" -d "grant_type=client_credentials" "http://localhost:8095/oauth/token"
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic2VydmljZSJdLCJzY29wZSI6WyJyZWFkIl0sImV4cCI6MTU0OTY5MjQ0MSwiYXV0aG9yaXRpZXMiOlsiQ0xJRU5UIl0sImp0aSI6IjEwMzE0NTk4LTRjZDctNDRmNi1hMmM4LTNjYjA5MGE1MjUxZSIsImNsaWVudF9pZCI6ImNsaWVudCJ9.n8Dwcd8YTms2Hl0YgTho9QdBWD1hAnOEmkcS-Wefy6c","token_type":"bearer","expires_in":43199,"scope":"read","jti":"10314598-4cd7-44f6-a2c8-3cb090a5251e"}
$ curl -X POST "http://localhost:8095/oauth/token?grant_type=client_credentials&client_id=client&client_secret=1234567890"
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic2VydmljZSJdLCJzY29wZSI6WyJyZWFkIl0sImV4cCI6MTU0OTY5MjQ1OCwiYXV0aG9yaXRpZXMiOlsiQ0xJRU5UIl0sImp0aSI6IjEzYjM1M2Q2LTQwODUtNDdiMS1hYzkyLTRiZDJhNDg3MzFhOCIsImNsaWVudF9pZCI6ImNsaWVudCJ9.CueMcwrD7pTp3pj37_BzzcUODG7PcjCacSa14-l5_Hw","token_type":"bearer","expires_in":43199,"scope":"read","jti":"13b353d6-4085-47b1-ac92-4bd2a48731a8"}

$ curl -X POST -u "client:1234567890" -d "token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic2VydmljZSJdLCJzY29wZSI6WyJyZWFkIl0sImV4cCI6MTU0OTY5MjQ1OCwiYXV0aG9yaXRpZXMiOlsiQ0xJRU5UIl0sImp0aSI6IjEzYjM1M2Q2LTQwODUtNDdiMS1hYzkyLTRiZDJhNDg3MzFhOCIsImNsaWVudF9pZCI6ImNsaWVudCJ9.CueMcwrD7pTp3pj37_BzzcUODG7PcjCacSa14-l5_Hw" http://localhost:8095/oauth/check_token
{"aud":["service"],"scope":["read"],"active":true,"exp":1549692458,"authorities":["CLIENT"],"jti":"13b353d6-4085-47b1-ac92-4bd2a48731a8","client_id":"client"}
Token JWT codificado y decodificado

Servidor Gateway

El servidor gateway en realidad no interviene en la lógica de OAuth porque la autorización se delega en cada servicio que contiene el recurso. Como se indicaba en Idealista estaría bien que el gateway librase de la responsabilidad de autorización a los servicios de los recursos para hacerlos más sencillos, creo que Spring Security en el momento del artículo no está soportado en Spring WebFlux que utiliza el gateway.

Lo único necesario par definir el gateway son las dependencias del proyecto, poco más que spring-cloud-starter-gateway, y la configuración de enrutado que matchea peticiones según el parámetro predicates, reescribe la URL hacia el servicio según el filtro RewritePath y finalmente redirige la petición a la ubicación del servicio indicada en uri. Se inicia con ./gradlew gateway:run.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
plugins {
    id 'application'
    id 'org.springframework.boot' version '2.0.8.RELEASE'
}

mainClassName = 'io.github.picodotdev.blogbitix.springoauth.gateway.Main'

dependencies {
    implementation platform('org.springframework.boot:spring-boot-dependencies:2.0.8.RELEASE')
    implementation platform('org.springframework.cloud:spring-cloud-dependencies:Finchley.SR2')
    
    def excludeSpringBootStarterLogging = { exclude(group: 'org.springframework.boot', module: 'spring-boot-starter-logging') }
    compile('org.springframework.boot:spring-boot-starter', excludeSpringBootStarterLogging)
    compile('org.springframework.boot:spring-boot-starter-log4j2', excludeSpringBootStarterLogging)
    compile('org.springframework.cloud:spring-cloud-starter-gateway', excludeSpringBootStarterLogging)

    runtime('com.google.code.gson:gson:2.8.5')
    runtime('com.fasterxml.jackson.core:jackson-databind:2.9.6')
    runtime('com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.6')
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
server.port: 8090

spring:
  cloud:
    gateway:
      routes:
      - id: path_route
        uri: http://localhost:8080/
        predicates:
        - Path=/service/
        filters:
        - RewritePath=/service/, /

Servicio, servidor de recurso

Dado que el servicio interpreta los tokens JWT y aplica reglas de seguridad necesita las mismas dependencias que utiliza el servidor OAuth.

 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
plugins {
    id 'application'
    id 'org.springframework.boot' version '2.0.8.RELEASE'
}

mainClassName = 'io.github.picodotdev.blogbitix.springoauth.service.Main'

dependencies {
    implementation platform('org.springframework.boot:spring-boot-dependencies:2.0.8.RELEASE')
    implementation platform('org.springframework.cloud:spring-cloud-dependencies:Finchley.SR2')

    def excludeSpringBootStarterLogging = { exclude(group: 'org.springframework.boot', module: 'spring-boot-starter-logging') }
    compile('org.springframework.boot:spring-boot-starter', excludeSpringBootStarterLogging)
    compile('org.springframework.boot:spring-boot-starter-web', excludeSpringBootStarterLogging)
    compile('org.springframework.boot:spring-boot-starter-log4j2', excludeSpringBootStarterLogging)
    compile('org.springframework.boot:spring-boot-starter-security', excludeSpringBootStarterLogging)
    compile('org.springframework.cloud:spring-cloud-starter-oauth2', excludeSpringBootStarterLogging)
    compile('org.springframework.security.oauth:spring-security-oauth2:2.3.4.RELEASE', excludeSpringBootStarterLogging)
    compile('org.springframework.security:spring-security-jwt:1.0.10.RELEASE', excludeSpringBootStarterLogging)
    
    runtime('com.google.code.gson:gson:2.8.5')
    runtime('com.fasterxml.jackson.core:jackson-databind:2.9.6')
    runtime('com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.6')

    runtime('javax.xml.bind:jaxb-api:2.3.0')
    runtime('com.sun.xml.bind:jaxb-impl:2.3.0')
    runtime('org.glassfish.jaxb:jaxb-runtime:2.3.0')
    runtime('javax.activation:activation:1.1.1')
}

El recurso es muy simple, solo devuelve un mensaje.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package io.github.picodotdev.blogbitix.springoauth.service;

...

@RestController
public class DefaultController {

    private Random random;

    public DefaultController() {
        this.random = new Random();
    }

    @RequestMapping("/")
    public String home(HttpServletRequest request) throws Exception {
        return String.format("Hello world (%s)", request.getRequestURL());
    }
}

El servicio comparte configuración similar al servidor de Ouath par el JwtAccessTokenConverter, TokenStore y DefaultTokenServices. En el método configure se define que el endpoint / requiere el rol CLIENT que se obtiene del token JWT enviado. Hay que utilizar la anotación @EnableResourceServer, se inicia con el comando ./gradlew service:run.

Hay que recalcar que el servicio para verificar el token y comprobar la autorización no necesita comunicarse con el servidor OAuth toda la información que necesita está en el token.

 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
package io.github.picodotdev.blogbitix.springoauth.service;

...

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    @Autowired
    private JwtAccessTokenConverter tokenConverter;

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private DefaultTokenServices tokenServices;

    @Bean
    public JwtAccessTokenConverter tokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("1234567890");
        return converter;
    }

    @Bean
    public TokenStore tokenStore(JwtAccessTokenConverter tokenConverter) {
        return new JwtTokenStore(tokenConverter);
    }

    @Bean
    DefaultTokenServices tokenServices(TokenStore tokenStore, JwtAccessTokenConverter tokenConverter) {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(tokenStore);
        tokenServices.setTokenEnhancer(tokenConverter);
        return tokenServices;
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.tokenServices(tokenServices).resourceId("service");
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/").hasAuthority("CLIENT");
    }
}

Si no se envía el token JWT se produce un error de autenticación con código de error 401 Unauthorized, si se envía un token correcto y la autoridad requerida del recurso la petición se devuelve el mensaje u el código de estado 200 OK, si se envía un token JWT con una autoridad que no corresponde con la necesaria para el recurso, en el ejemplo una autoridad DUMMY, se devuelve un código de estado 403 Forbbiden.

 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
$ curl -v http://localhost:8090/service/
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8090 (#0)
> GET /service/ HTTP/1.1
> Host: localhost:8090
> User-Agent: curl/7.63.0
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< transfer-encoding: chunked
< Cache-Control: no-store
< Pragma: no-cache
< WWW-Authenticate: Bearer realm="service", error="unauthorized", error_description="Full authentication is required to access this resource"
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< X-Frame-Options: DENY
< Content-Type: application/json;charset=UTF-8
< Date: Fri, 08 Feb 2019 18:58:03 GMT
<
* Connection #0 to host localhost left intact
{"error":"unauthorized","error_description":"Full authentication is required to access this resource"}

$ curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic2VydmljZSJdLCJzY29wZSI6WyJyZWFkIl0sImV4cCI6MTU0OTY5MjQ1OCwiYXV0aG9yaXRpZXMiOlsiQ0xJRU5UIl0sImp0aSI6IjEzYjM1M2Q2LTQwODUtNDdiMS1hYzkyLTRiZDJhNDg3MzFhOCIsImNsaWVudF9pZCI6ImNsaWVudCJ9.CueMcwrD7pTp3pj37_BzzcUODG7PcjCacSa14-l5_Hw" http://localhost:8090/service/
Hello world (http://localhost:8080/)

$ curl -X POST -u "client:1234567890" -d "token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic2VydmljZSJdLCJzY29wZSI6WyJyZWFkIl0sImV4cCI6MTU0OTY5MjQ1OCwiYXV0aG9yaXRpZXMiOlsiRFVNTVkiXSwianRpIjoiMTNiMzUzZDYtNDA4NS00N2IxLWFjOTItNGJkMmE0ODczMWE4IiwiY2xpZW50X2lkIjoiY2xpZW50In0.RaeQYdukn8Xr8S9ld5Vy2UnYboUjPyMkutNgyfVN-Bc" http://localhost:8095/oauth/check_token
{"aud":["service"],"scope":["read"],"active":true,"exp":1549692458,"authorities":["DUMMY"],"jti":"13b353d6-4085-47b1-ac92-4bd2a48731a8","client_id":"client"}
$ curl -v -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic2VydmljZSJdLCJzY29wZSI6WyJyZWFkIl0sImV4cCI6MTU0OTY5MjQ1OCwiYXV0aG9yaXRpZXMiOlsiRFVNTVkiXSwianRpIjoiMTNiMzUzZDYtNDA4NS00N2IxLWFjOTItNGJkMmE0ODczMWE4IiwiY2xpZW50X2lkIjoiY2xpZW50In0.RaeQYdukn8Xr8S9ld5Vy2UnYboUjPyMkutNgyfVN-Bc" http://localhost:8090/service/
{"error":"access_denied","error_description":"Access is denied"}
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8090 (#0)
> GET /service/ HTTP/1.1
> Host: localhost:8090
> User-Agent: curl/7.63.0
> Accept: */*
> Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsic2VydmljZSJdLCJzY29wZSI6WyJyZWFkIl0sImV4cCI6MTU0OTY5MjQ1OCwiYXV0aG9yaXRpZXMiOlsiRFVNTVkiXSwianRpIjoiMTNiMzUzZDYtNDA4NS00N2IxLWFjOTItNGJkMmE0ODczMWE4IiwiY2xpZW50X2lkIjoiY2x
pZW50In0.RaeQYdukn8Xr8S9ld5Vy2UnYboUjPyMkutNgyfVN-Bc
>
< HTTP/1.1 403 Forbidden
< transfer-encoding: chunked
< Cache-Control: no-store
< Pragma: no-cache
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< X-Frame-Options: DENY
< Content-Type: application/json;charset=UTF-8
< Date: Fri, 08 Feb 2019 19:02:14 GMT
<
* Connection #0 to host localhost left intact
{"error":"access_denied","error_description":"Access is denied"}

Los tokens JWT además de firmar se pueden cifrar, en el ejemplo se usa una conexión no segura con el protocolo HTTP usando una conexión segura HTTPS ya se proporcionaría confidencialidad para los tokens y es lo recomendado.

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradlew oauth:run, ./gradlew gateway:run, ./gradlew service:run.