BT

Accueil InfoQ Articles Jakarta Security Et REST Dans Le Cloud Partie 4 : Combinaison De JWT Avec OAuth 2

Jakarta Security Et REST Dans Le Cloud Partie 4 : Combinaison De JWT Avec OAuth 2

Favoris

Points Clés

  • La sécurité en Java
  • L’API Jakarta Security
  • OAuth2 et JWT
  • Cloud
  • PaaS

Oauth2 est sans aucun doute l'un des protocoles de sécurité les plus connus aujourd'hui. L'un de ses avantages est la non-exposition d'informations sensibles, telles que l'utilisateur et le mot de passe, de manière cohérente, comme le fait le mécanisme BASIC. Cependant, il y a une augmentation de sa complexité, surtout quand on parle d'échange de jetons, qui n'ont pas beaucoup d'utilité puisqu'ils ne contiennent aucune information. Cependant, nous pouvons leur faire assumer une petite responsabilité, comme le transport d'informations en toute sécurité. Cet article expliquera comment intégrer OAuth2 avec JWT.

Dans la troisième partie de cette série, nous avons parlé du mécanisme OAuth2 et des coûts et avantages liés à la complexité et à la possibilité de ne pas surexposer les données de connexion et de mot de passe. L'une des caractéristiques remarquables de ce mécanisme se trouve dans la communication de sécurité du jeton. Jusque-là, il n'a qu'une seule utilité : la référence. Ce pointeur fonctionne comme un lien. Cependant, il ne dispose pas des informations proprement dites. Il fonctionne comme un mécanisme OAuth2. Nous envoyons un jeton, qui à son tour est vérifié pour l'existence dans la base de données afin que les informations d'authentification de l'utilisateur et les informations d'identification respectives soient recherchées. Autrement dit, l'état entier de l'utilisateur est dans la base.

Cette approche peut entraîner des problèmes de performances, car nous devons vérifier l'authenticité d'un tel jeton, impliquant de rechercher les informations dans la base de données à chaque interaction. Autrement dit, si un service doit s'intégrer à quatre autres dans le même mécanisme d'authentification et d'autorisation, il devra rechercher le même état de l'utilisateur quatre fois.

Une nouvelle stratégie serait que nous puissions utiliser le jeton de manière complète au lieu d'être simplement un pointeur. Cela signifie que si nous pouvions stocker des informations utilisateur avec le symbole lui-même, nous aurions un gain de performances. Ceci est possible grâce aux JSON Web Token, une norme industrielle ouverte pour représenter une communication sécurisée entre les parties. Avec JWT, nous avons la possibilité de signer et / ou de chiffrer les informations que nous voulons; dans ce cas, ce serait l'utilisateur et les rôles à utiliser. L'objectif de l'article n'est pas de parler de JWT, mais de son intégration avec OAuth2. Si le lecteur veut en savoir plus sur le sujet, il existe un manuel très sympa sur JWT.

Commençons par nous familiariser avec la conception de la partie 3, pour ne pas avoir à partir de zéro. La première étape consistera à ajouter JWT aux dépendances. Cette bibliothèque sera responsable de la lecture et de l'écriture de jetons JWT.

<dependency>
  <groupId>com.auth0</groupId>
  <artifactId>java-jwt</artifactId>
  <version>3.10.3</version>
</dependency>

Une fois les dépendances définies, l'étape suivante consistera à modifier le code lui-même. Dans les entités, AccessToken sera modifié, car nous changerons l'ID afin qu'il puisse recevoir le JWT sous forme de chaîne, en plus de stocker le secret. Ce secret sera unique et généré aléatoirement pour chaque entité et sera chargé de confirmer la signature et de vérifier l'authenticité du JWT. Il est à noter que la signature est chargée de vérifier l'authenticité du JWT et de confirmer qu'il n'a pas été modifié, c'est-à-dire que la signature ne signifie pas chiffrement. Pour plus d'informations sur JWT et la cryptographie, nous pouvons accéder au lien concernant JWE.

@Entity
@JsonbVisibility(FieldPropertyVisibilityStrategy.class)
public  class AccessToken {
  
  static final String PREFIX =  "access_token:";
  @Id
  private String id;
  
  @JsonbProperty
  private String user;
  
  @JsonbProperty
  private String token;
  
  @JsonbProperty
  private String jwtSecret;
  
  //...
}

Pour faciliter la lecture et la manipulation du JWT, la classe UserJWT sera créée. Cette classe générera le JWT au format texte. Il est intéressant de noter qu'il existe un contrôle à la création dans la méthode de fabrique UserJWT. Cette vérification se fait par la signature, expliquée précédemment, et également par la date d'expiration.

import  com.auth0.jwt.JWT;
import  com.auth0.jwt.JWTVerifier;
import  com.auth0.jwt.algorithms.Algorithm;
import  com.auth0.jwt.exceptions.JWTVerificationException;
import  com.auth0.jwt.interfaces.Claim;
import  com.auth0.jwt.interfaces.DecodedJWT;
import  sh.platform.sample.security.User;

import  java.time.Duration;
import  java.time.LocalDateTime;
import  java.time.ZoneOffset;
import  java.util.ArrayList;
import  java.util.Collections;
import  java.util.Date;
import  java.util.Optional;
import  java.util.Set;
import  java.util.logging.Level;
import  java.util.logging.Logger;
import  java.util.stream.Collectors;
  
class UserJWT {

  private static final Logger LOGGER =  Logger.getLogger(UserJWT.class.getName());
  private static final String ISSUER =  "jakarta";
  private static final String ROLES = "roles";
  
  private final String user;
  private final Set<String> roles;

  UserJWT(String user, Set<String>  roles) {
    this.user = user;
    this.roles = roles;
  }
  
  public String getUser() {
    return user;
  }

  public Set<String> getRoles() {
    if (roles == null) {
      return Collections.emptySet();
    }
    return roles;
  }

  static String createToken(User user, Token  token, Duration duration) {
    final LocalDateTime expire =  LocalDateTime.now(ZoneOffset.UTC).plusMinutes(duration.toMinutes());
    Algorithm algorithm =  Algorithm.HMAC256(token.get());
      return JWT.create()
                .withJWTId(user.getName())
                .withIssuer(ISSUER)
                .withExpiresAt(Date.from(expire.atZone(ZoneOffset.UTC).toInstant()))
                .withClaim(ROLES, new  ArrayList<>(user.getRoles()))
                .sign(algorithm);
  }

  static Optional<UserJWT> parse(String  jwtText, Token token) {
    Algorithm algorithm =  Algorithm.HMAC256(token.get());
    try {
      JWTVerifier verifier =  JWT.require(algorithm).withIssuer(ISSUER).build();
      final DecodedJWT jwt =  verifier.verify(jwtText);
      final Claim roles =  jwt.getClaim(ROLES);
      return Optional.of(new  UserJWT(jwt.getId(),
        roles.asList(String.class).stream().collect(Collectors.toUnmodifiableSet())));
    } catch (JWTVerificationException exp)  {
      LOGGER.log(Level.WARNING,  "There is an error to load the JWT token", exp);
      return Optional.empty();
    }
  }
}

Une fois la modélisation modifiée, l'étape suivante consiste à changer le service OAuth2. Le point intéressant est qu'en plus du TTL existant, le JWT vérifie également l'expiration des données. Autrement dit, il y a une double vérification de la cohérence des données.

  import  jakarta.nosql.mapping.keyvalue.KeyValueTemplate;
  import  org.eclipse.microprofile.config.inject.ConfigProperty;
  import  sh.platform.sample.security.SecurityService;
  import  sh.platform.sample.security.User;
  import  sh.platform.sample.security.UserNotAuthorizedException;

  import  javax.enterprise.context.ApplicationScoped;
  import  javax.inject.Inject;
  import  javax.validation.ConstraintViolation;
  import  javax.validation.ConstraintViolationException;
  import  javax.validation.Validator;
  import  java.time.Duration;
  import  java.util.Arrays;
  import  java.util.Set;

  @ApplicationScoped
  class  Oauth2Service {
  
   static final int EXPIRE_IN = 3600;

   static final Duration EXPIRES =  Duration.ofSeconds(EXPIRE_IN);

   @Inject
   private SecurityService securityService;

   @Inject
   @ConfigProperty(name =  "keyvalue")
   private KeyValueTemplate template;
   
   @Inject
   private Validator validator;

   public Oauth2Response token(Oauth2Request  request) {
     final  Set<ConstraintViolation<Oauth2Request>> violations =  validator.validate(request, Oauth2Request
      .GenerateToken.class);
     if (!violations.isEmpty()) {
       throw new  ConstraintViolationException(violations);
     }
   
     final User user =  securityService.findBy(request.getUsername(), request.getPassword());
     final UserToken userToken =  template.get(request.getUsername(), UserToken.class)
       .orElse(new  UserToken(user.getName()));
	   
     final Token token = Token.generate();
 
     final String jwt =  UserJWT.createToken(user, token, EXPIRES);
     AccessToken accessToken = new  AccessToken(jwt, token, user.getName());
     RefreshToken refreshToken = new  RefreshToken(userToken, jwt, user.getName());

     template.put(refreshToken, EXPIRES);
     template.put(Arrays.asList(userToken,  accessToken));
  
     final Oauth2Response response =  Oauth2Response.of(accessToken, refreshToken, EXPIRE_IN);
     return response;
  }
  
  public Oauth2Response  refreshToken(Oauth2Request request) {
    final  Set<ConstraintViolation<Oauth2Request>> violations =  validator.validate(request, Oauth2Request
      .RefreshToken.class);
    if (!violations.isEmpty()) {
      throw new  ConstraintViolationException(violations);
    }
  
    RefreshToken refreshToken =  template.get(RefreshToken.PREFIX + request.getRefreshToken(),  RefreshToken.class)
      .orElseThrow(() -> new  UserNotAuthorizedException("Invalid Token"));
	  
    final UserToken userToken =  template.get(refreshToken.getUser(), UserToken.class)
     .orElse(new  UserToken(refreshToken.getUser()));
	 
    final User user =  securityService.findBy(refreshToken.getUser());
    final Token token = Token.generate();
    final String jwt =  UserJWT.createToken(user, token, EXPIRES);
    AccessToken accessToken = new  AccessToken(jwt, token, refreshToken.getUser());
    refreshToken.update(accessToken,  userToken, template);
    template.put(accessToken, EXPIRES);
    final Oauth2Response response =  Oauth2Response.of(accessToken, refreshToken, EXPIRE_IN);
    return response;
  }
}

La dernière étape sera de changer le mécanisme. En général, les informations utilisateur ne seront pas extraites de la base de données, mais du JWT lui-même.

  import  jakarta.nosql.mapping.keyvalue.KeyValueTemplate;
  import  org.eclipse.microprofile.config.inject.ConfigProperty;
  
  import  javax.enterprise.context.ApplicationScoped;
  import  javax.inject.Inject;
  import  javax.security.enterprise.AuthenticationStatus;
  import  javax.security.enterprise.authentication.mechanism.http.HttpAuthenticationMechanism;
  import  javax.security.enterprise.authentication.mechanism.http.HttpMessageContext;
  import  javax.servlet.http.HttpServletRequest;
  import  javax.servlet.http.HttpServletResponse;
  import  java.util.Optional;
  import  java.util.regex.Matcher;
  import  java.util.regex.Pattern;
  
  @ApplicationScoped
  public  class Oauth2Authentication implements HttpAuthenticationMechanism {

  private static final Pattern  CHALLENGE_PATTERN
    = Pattern.compile("^Bearer  *([^ ]+) *$", Pattern.CASE_INSENSITIVE);
	
  @Inject
  @ConfigProperty(name = "keyvalue")
  private KeyValueTemplate template;

  @Override
  public AuthenticationStatus  validateRequest(HttpServletRequest request, HttpServletResponse response,
  HttpMessageContext httpMessageContext) {

    final String authorization =  request.getHeader("Authorization");
 
    Matcher matcher =  CHALLENGE_PATTERN.matcher(Optional.ofNullable(authorization).orElse(""));
    if (!matcher.matches()) {
      return  httpMessageContext.doNothing();
    }
    final String token = matcher.group(1);
    final Optional<AccessToken>  optional = template.get(AccessToken.PREFIX + token, AccessToken.class);

    if (!optional.isPresent()) {
      return  httpMessageContext.responseUnauthorized();
    }
    final AccessToken accessToken =  optional.get();
    final Optional<UserJWT>  optionalUserJWT = UserJWT.parse(accessToken.getToken(),  accessToken.getJwtSecretAsToken());
    if (optionalUserJWT.isPresent()) {
      final UserJWT userJWT =  optionalUserJWT.get();
      return httpMessageContext.notifyContainerAboutLogin(userJWT.getUser(),  userJWT.getRoles());
    } else {
      return  httpMessageContext.responseUnauthorized();
    }
  }
}

 

Déploiement dans le cloud

Comme il n'y a pas eu de changement dans les services ou la structure des conteneurs, la structure et la configuration d'envoi de l'application vers le cloud avec Platform.sh seront maintenues de la même manière. Là, le mécanisme a été remanié pour ne pas utiliser les informations de la base de données, réduisant ainsi le nombre de demandes. Cependant, nous avons un gros problème avec la cohérence des données, car il y a toujours la possibilité d'un changement dans la base de données qui ne sera reflété que lorsque le jeton expirera. L'utilisateur devra mettre à jour ses jetons avec le jeton d'actualisation (refresh token). Il existe une stratégie pour travailler avec cela, qui consisterait à lancer des événements pour rendre le jeton actuel inutilisable, obligeant l'utilisateur à effectuer le processus d'actualisation du jeton précocement. Le point critique est qu'il y a toujours un désavantage entre la cohérence et la disponibilité des données, et c'est à l'architecte de comprendre chaque cas et de choisir la meilleure stratégie. L'objectif principal de cette série d'articles est de savoir comment fonctionne l'API Security. Cependant, chaque ligne de code que nous écrivons est du code qui devra être maintenu, donc utiliser une solution existante peut être la meilleure option. Il existe déjà des solutions de sécurité qui méritent d'être examinées, comme Okta et Keycloak. Comme toujours, l'exemple de code est disponible sur GitHub.

 

A propos de l'auteur

Otávio Santana est un ingénieur logiciel avec une vaste expérience dans le développement open source, avec plusieurs contributions à JBoss Weld, Hibernate, Apache Commons et à d'autres projets. Axé sur le développement multilingue et les applications haute performance, Otávio a travaillé sur de grands projets dans les domaines de la finance, du gouvernement, des médias sociaux et du commerce électronique. Membre du comité exécutif du JCP et de plusieurs groupes d'experts JSR, il est également un champion Java et a reçu le JCP Outstanding Award et le Duke's Choice Award.

Evaluer cet article

Pertinence
Style

Bonjour étranger!

Vous devez créer un compte InfoQ ou cliquez sur pour déposer des commentaires. Mais il y a bien d'autres avantages à s'enregistrer.

Tirez le meilleur d'InfoQ

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

Commentaires de la Communauté

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

BT

Votre profil est-il à jour? Merci de prendre un instant pour vérifier.

Note: en cas de modification de votre adresse email, une validation sera envoyée.

Nom de votre entreprise:
Rôle dans votre entreprise:
Taille de votre entreprise:
Pays/Zone:
État/Province/Région:
Vous allez recevoir un email pour confirmer la nouvelle adresse email. Ce pop-up va se fermer de lui-même dans quelques instants.