Serveur de ressources OAuth 2.0 avec Spring Security 5

1. Vue d'ensemble

Dans ce didacticiel, nous allons apprendre à configurer un serveur de ressources OAuth 2.0 à l'aide de Spring Security 5 .

Nous le ferons en utilisant JWT ainsi que des jetons opaques, les deux types de jetons de support pris en charge par Spring Security.

Avant de passer à l'implémentation et aux exemples de code, nous allons établir quelques informations générales.

2. Un peu de contexte

2.1. Que sont les JWT et les jetons opaques?

JWT ou JSON Web Token est un moyen de transférer des informations sensibles en toute sécurité au format JSON largement accepté. Les informations contenues peuvent concerner l'utilisateur ou le jeton lui-même, comme son expiration et son émetteur.

D'un autre côté, un jeton opaque, comme son nom l'indique, est opaque en termes d'informations qu'il porte. Le jeton est simplement un identifiant qui pointe vers les informations stockées sur le serveur d'autorisation - il est validé par introspection à l'extrémité du serveur.

2.2. Qu'est-ce qu'un serveur de ressources?

Dans le contexte d'OAuth 2.0, un serveur de ressources est une application qui protège les ressources via des jetons OAuth . Ces jetons sont émis par un serveur d'autorisation, généralement à une application cliente. Le travail du serveur de ressources consiste à valider le jeton avant de servir une ressource au client.

La validité d'un jeton est déterminée par plusieurs choses:

  • Ce jeton provient-il du serveur d'autorisation configuré?
  • Est-ce qu'il n'a pas expiré?
  • Ce serveur de ressources est-il son public cible?
  • Le jeton dispose-t-il de l'autorité requise pour accéder à la ressource demandée?

Pour visualiser, regardons un diagramme de séquence pour le flux de code d'autorisation et voyons tous les acteurs en action:

Comme nous pouvons le voir à l'étape 8, lorsque l'application cliente appelle l'API du serveur de ressources pour accéder à une ressource protégée, elle se rend d'abord au serveur d'autorisation pour valider le jeton contenu dans l'en- tête Authorization: Bearer de la demande , puis répond au client.

L'étape 9 est ce sur quoi nous nous concentrons dans ce didacticiel.

Très bien, passons maintenant à la partie code. Nous allons configurer un serveur d'autorisation à l'aide de Keycloak, un serveur de ressources validant les jetons JWT, un autre serveur de ressources validant les jetons opaques et quelques tests JUnit pour simuler les applications clientes et vérifier les réponses.

3. Serveur d'autorisation

Tout d'abord, nous allons configurer un serveur d'autorisation, ou la chose qui émet des jetons.

Pour cela, nous utiliserons Keycloak intégré dans une application Spring Boot . Keycloak est une solution open source de gestion des identités et des accès. Étant donné que nous nous concentrons sur le serveur de ressources dans ce didacticiel, nous n'y approfondirons pas.

Notre serveur Keycloak intégré a deux clients définis - fooClient et barClient - correspondant à nos deux applications de serveur de ressources.

4. Serveur de ressources - Utilisation des JWT

Notre serveur de ressources aura quatre composants principaux:

  • Modèle - la ressource à protéger
  • API - un contrôleur REST pour exposer la ressource
  • Configuration de la sécurité - une classe pour définir le contrôle d'accès pour la ressource protégée que l'API expose
  • application.yml - un fichier de configuration pour déclarer des propriétés, y compris des informations sur le serveur d'autorisation

Voyons-les un par un pour notre serveur de ressources gérant les jetons JWT, après avoir jeté un coup d'œil aux dépendances.

4.1. Dépendances de Maven

Nous aurons principalement besoin du spring-boot-starter-oauth2-resource-server , le démarreur de Spring Boot pour la prise en charge du serveur de ressources. Ce démarreur inclut Spring Security par défaut, nous n'avons donc pas besoin de l'ajouter explicitement:

 org.springframework.boot spring-boot-starter-web 2.2.6.RELEASE   org.springframework.boot spring-boot-starter-oauth2-resource-server 2.2.6.RELEASE   org.apache.commons commons-lang3 3.9 

En dehors de cela, nous avons également ajouté un support Web.

Pour nos besoins de démonstration, nous générerons des ressources de manière aléatoire au lieu de les obtenir à partir d'une base de données, avec l'aide de la bibliothèque commons-lang3 d'Apache .

4.2. Modèle

Pour rester simple, nous utiliserons Foo , un POJO, comme ressource protégée:

public class Foo { private long id; private String name; // constructor, getters and setters } 

4.3. API

Voici notre contrôleur de repos, pour rendre Foo disponible pour la manipulation:

@RestController @RequestMapping(value = "/foos") public class FooController { @GetMapping(value = "/{id}") public Foo findOne(@PathVariable Long id) { return new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4)); } @GetMapping public List findAll() { List fooList = new ArrayList(); fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4))); fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4))); fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4))); return fooList; } @ResponseStatus(HttpStatus.CREATED) @PostMapping public void create(@RequestBody Foo newFoo) { logger.info("Foo created"); } }

Comme il est évident, nous avons la possibilité de GET all Foo s, GET a Foo by id et POST a Foo .

4.4. Configuration de la sécurité

Dans cette classe de configuration, nous définissons les niveaux d'accès pour notre ressource:

@Configuration public class JWTSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests(authz -> authz .antMatchers(HttpMethod.GET, "/foos/**").hasAuthority("SCOPE_read") .antMatchers(HttpMethod.POST, "/foos").hasAuthority("SCOPE_write") .anyRequest().authenticated()) .oauth2ResourceServer(oauth2 -> oauth2.jwt()); } } 

Toute personne disposant d'un jeton d'accès ayant la portée de lecture peut obtenir Foo s. Afin de POSTER un nouveau Foo , leur jeton doit avoir une portée d' écriture .

De plus, nous avons ajouté un appel à jwt () en utilisant le DSL oauth2ResourceServer () pour indiquer le type de jetons pris en charge par notre serveur ici .

4.5. application.yml

Dans les propriétés de l'application, en plus du numéro de port habituel et du chemin de contexte, nous devons définir le chemin d'accès à l'URI de l'émetteur de notre serveur d'autorisation afin que le serveur de ressources puisse découvrir sa configuration de fournisseur :

server: port: 8081 servlet: context-path: /resource-server-jwt spring: security: oauth2: resourceserver: jwt: issuer-uri: //localhost:8083/auth/realms/baeldung

Le serveur de ressources utilise ces informations pour valider les jetons JWT provenant de l'application cliente, conformément à l'étape 9 de notre diagramme de séquence.

Pour que cette validation fonctionne à l'aide de la propriété issuer-uri , le serveur d'autorisation doit être opérationnel. Sinon, le serveur de ressources ne démarrerait pas.

Si nous devons le démarrer indépendamment, nous pouvons fournir la propriété jwk-set-uri à la place pour pointer vers le point de terminaison du serveur d'autorisation exposant les clés publiques:

jwk-set-uri: //localhost:8083/auth/realms/baeldung/protocol/openid-connect/certs

Et c'est tout ce dont nous avons besoin pour que notre serveur valide les jetons JWT.

4.6. Essai

Pour les tests, nous allons configurer un JUnit. Afin d'exécuter ce test, nous avons besoin du serveur d'autorisation ainsi que du serveur de ressources opérationnel.

Vérifions que nous pouvons obtenir Foo s de resource-server-jw t avec un jeton de lecture dans notre test:

@Test public void givenUserWithReadScope_whenGetFooResource_thenSuccess() { String accessToken = obtainAccessToken("read"); Response response = RestAssured.given() .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .get("//localhost:8081/resource-server-jwt/foos"); assertThat(response.as(List.class)).hasSizeGreaterThan(0); }

Dans le code ci-dessus, à la ligne 3, nous obtenons un jeton d'accès avec une portée de lecture du serveur d'autorisation, couvrant les étapes 1 à 7 de notre diagramme de séquence.

L' étape 8 est réalisée par RestAssured de get () appel. L'étape 9 est effectuée par le serveur de ressources avec les configurations que nous avons vues et est transparente pour nous en tant qu'utilisateurs.

5. Serveur de ressources - Utilisation de jetons opaques

Ensuite, voyons les mêmes composants pour notre serveur de ressources gérant les jetons opaques.

5.1. Dépendances de Maven

Pour prendre en charge les jetons opaques, nous aurons également besoin de la dépendance oauth2-oidc-sdk :

 com.nimbusds oauth2-oidc-sdk 8.19 runtime 

5.2. Model and Controller

For this one, we'll add a Bar resource:

public class Bar { private long id; private String name; // constructor, getters and setters } 

We'll also have a BarController with endpoints similar to our FooController before, to dish out Bars.

5.3. application.yml

In the application.yml here, we'll need to add an introspection-uri corresponding to our authorization server's introspection endpoint. As mentioned before, this is how an opaque token gets validated:

server: port: 8082 servlet: context-path: /resource-server-opaque spring: security: oauth2: resourceserver: opaque: introspection-uri: //localhost:8083/auth/realms/baeldung/protocol/openid-connect/token/introspect introspection-client-id: barClient introspection-client-secret: barClientSecret

5.4. Security Configuration

Keeping access levels similar to that of Foo for the Bar resource as well, this configuration class also makes a call to opaqueToken() using the oauth2ResourceServer() DSL to indicate the use of the opaque token type:

@Configuration public class OpaqueSecurityConfig extends WebSecurityConfigurerAdapter { @Value("${spring.security.oauth2.resourceserver.opaque.introspection-uri}") String introspectionUri; @Value("${spring.security.oauth2.resourceserver.opaque.introspection-client-id}") String clientId; @Value("${spring.security.oauth2.resourceserver.opaque.introspection-client-secret}") String clientSecret; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests(authz -> authz .antMatchers(HttpMethod.GET, "/bars/**").hasAuthority("SCOPE_read") .antMatchers(HttpMethod.POST, "/bars").hasAuthority("SCOPE_write") .anyRequest().authenticated()) .oauth2ResourceServer(oauth2 -> oauth2 .opaqueToken(token -> token.introspectionUri(this.introspectionUri) .introspectionClientCredentials(this.clientId, this.clientSecret))); } } 

Here we're also specifying the client credentials corresponding to the authorization server's client we'll be using. We defined these earlier in our application.yml.

5.5. Testing

We'll set up a JUnit for our opaque token-based resource server, similar to how we did it for the JWT one.

In this case, let's check if a write scoped access token can POST a Bar to resource-server-opaque:

@Test public void givenUserWithWriteScope_whenPostNewBarResource_thenCreated() { String accessToken = obtainAccessToken("read write"); Bar newBar = new Bar(Long.parseLong(randomNumeric(2)), randomAlphabetic(4)); Response response = RestAssured.given() .contentType(ContentType.JSON) .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .body(newBar) .log() .all() .post("//localhost:8082/resource-server-opaque/bars"); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED.value()); }

If we get a status of CREATED back, it means the resource server successfully validated the opaque token and created the Bar for us.

6. Conclusion

In this tutorial, we saw how to configure a Spring Security based resource server application for validating JWT as well as opaque tokens.

As we saw, with minimal setup, Spring made it possible to seamlessly validate the tokens with an issuer and send resources to the requesting party – in our case, a JUnit test.

As always, source code is available over on GitHub.