Injection De Token Oauth2 Dans Des Tests JUnit

Lorsque l’on développe des tests API, il arrive fréquemment de devoir s’authentifier pour effectuer l’appel. Réécrire le code permettant de récupérer un token valide est non seulement fastidieux, source d’erreur, mais également une mauvaise pratique en termes de maintenabilité. Nous allons voir comment injecter un token valide directement en paramètre de test JUnit de manière à pouvoir l’utiliser simplement là où cela est nécessaire.

Introduction

Une solution couramment mise en place pour répondre à cette problématique est de créer une méthode utilitaire permettant de récupérer le token suivant le flow de connexion supporté par l’application. Cette méthode sera probablement définie dans une classe utils ou common, noyée au milieu de nombreuses autres méthodes. Cette classe étant elle-même sûrement dans un package util totalement générique. Les problématiques sont très bien expliqué dans cet article. Je vous propose donc de voir comment mettre en œuvre le mécanisme d’extension JUnit pour réaliser proprement une injection de token dans chaque test, le nécessitant.

JUnit Extension

Les extensions JUnit est une fonctionnalité apparue avec JUnit 5 permettant de modifier le comportant de JUnit lors de l’exécution des tests. La documentation est très bien faite, je vous invite à la lire. Une extension s’utilise via une annotation spéciale @ExtendWith sur une classe de test, un champ de classe, un paramètre de constructeur, dans une méthode de tests ou sur les méthodes annotées @BeforeAll, @AfterAll, @BeforeEach, and @AfterEach.

Exemple pour une classe :

1
2
@ExtendWith(MyExtension.class)
class MyTestClass {}

Ou un test en particulier

1
2
3
4
5
6
class MyTestClass {

  @Test
  @ExtendWith(MyExtension.class)
  void myTest() {}
}

BearerParameterExtension

Il existe plusieurs types d’extensions proposées par JUnit. Nous allons utiliser une extension implémentant l’interface ParameterResolver. Cette interface définie une extension permettant d’injecter une valeur lorsque qu’un test demande un paramètre spécifique dans la signature de sa méthode. Dans notre cas, notre test va demander un token en paramètre.

1
2
3
void MyTest(Token token){

}

Un objet Token est passé ici à la place d’un simple String afin de permettre à notre extension de différencier notre token de n’importe quel autre paramètre de type String.

Voici maintenant le code de l’extension :

 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
import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharset;

import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;

import com.fasterxml.jackson.databind.ObjectMapper;

public class BearerParameterResolver implements ParameterResolver {

  @Overrride
  public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
    return parameterContext.getParameter().getType().equals(Token.class)
  }

  @Override
  public Object resolverParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
    return parameterContext.findAnnotation(ClientCredential.class).map(annotation -> {
      String realm = annotation.realm();
      String clientId = annotation.clientId();
      String clientSecret = annotation.clientSecret();

      try {
        String token = authenticate(realm, clientId, clientSecret);
        return new Token(token);
      } catch (IOException | InterruptedException e) {
        throw new ParameterResolutionException("Failed to authenticate with Keycloak", e);
      }
    })
  }

  private String authenticate(String realm, String cleintId, String clientSecret) throws IOException, InterruptionException {
    String tokenUrl = Env.KEYCLOAK_URL + "/realms/" + realm + "/protocol/openid-connect/token";
    String body = "grant_type=client_credentials"
      + "&client_id=" + URLEncoder.encode(clientId, StandardCharset.UTF_8)
      + "&client_secret=" + URLEncoder.encode(clientSecret, StandardCharset.UTF_8);

    HttpRequest request = HttpRequest.newBuilder()
      .uri(URI.create(tokenUrl))
      .header("Content-Type", "application/x-www-form-urlencoded")
      .POST(HttpRequest.BodyPublishers.ofString(body))
      .build();

    HttpClient client = HttpClient.newHttpClient();
    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

    ObjectMapper mapper = new ObjectMapper();
    TokenResponse tokenResponse = objectMapper.readValue(response.body(), TokenResponse.class);
    return tokenResponse.access_token;
  }
}

Plusieurs points à préciser ici :

  • Notre extension utilise une annotation à appliquer sur le paramètre du test pour configurer l’injection.
  • L’authorization provider utilisé ici est Keycloak. J’utilise donc le concept de realm pour reconstruire dynamiquement l’URL du token provider dans la méthode authenticate. Une implémentation plus générique demanderait l’URL du token endpoint mais je préfère garder l’annotation la plus concise possible.
  • La méthode authenticate va faire l’authentification et récupérer le token. C’est l’équivalent de notre méthode utilitaire dont je parlais en début d’article.

Utilisation de l’extension

En reprenant l’exemple du test ci-dessus, nous pouvons utiliser l’extension de la manière suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import static io.restassured.RestAssured.*;

@ExtendWith(BearerParameterResolver.class)
class MyTestClass {

  @Test
  
  void myTest(@ClientCredential(clientId = "myClientId", clientSecret = "mySecret", realm = "myRealm") Token token) {
    given()
      .body("...")
      .contentType("application/json")
      .auth().preemptive().oauth2(token.getValue())
    .when()
      .post("...")
    .then()
      .statusCode(200)
  }
}

Nous pouvons donc maintenant simplement injecter des tokens valides dans nos tests. Ici, nous l’utilisons via restAssured pour effectuer une requête POST sur notre API en étant authentifié.

Point à améliorer

Le code n’est pas parfait. Voici quelques points à améliorer selon moi :

  • Prendre en compte, plusieurs flow de connection via différentes annotations (@ClientCredentials, @DirectGrant, @AuthorizationCode…)
  • Remplacer la classe Env par un chargement des variables via fichiers de configuration par environnement
  • Dans le test, trouver un autre moyen que de passer en dur le secret à l’annotation

Conclusion

Le système d’extension de JUnit permet d’injecter facilement des objets en paramètre des tests. L’injection de dépendance permet de découpler la logique de préparation des données nécessaires à l’exécution du test de celle purement fonctionnelle. Cela apporte une plus grande lisibilité et une maintenance facilitée. D’autres extensions existent avec JUnit pour, par exemple agir sur le cycle de vie des tests et permettre de démarrer automatiquement des conteneurs docker, importer des données dans une base… .

Annexe

Pour référence voici les classes Token et `TokenResponse utiliser dans le code ci-dessus :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Token {

  private String value;

  public Token(String token) {
    this.value = value;
  }

  public String getValue() {
    return value;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class TokenResponse {
  @JsonProperty("access_token")
  public String accessToken;
  @JsonProperty("expires_in")
  public int expiresIn;
  @JsonProperty("refresh_expires_in")
  public String refreshExpiresIn;
  @JsonProperty("token_type")
  public String tokenType;
  @JsonProperty("not-before-policy")
  public String not_before_policy;
  public String scope;
}
0%