Authentification et autorisations avec OAuth2 pour RabbitMQ

En remplacement ou en complément du backend d’authentification classique, on peut activer le backend d’authentification OAuth2. Dans la pratique, il s’agit plutôt d’authentification et d’autorisation basées sur des tokens JWT.

Configuration

L’activation du backend se fait dans la configuration de RabbitMQ. Il peut être déclaré seul ou en complément du backend interne.

# conf.d/21-auth.conf
auth_backends.1 = rabbit_auth_backend_oauth2
auth_backends.2 = rabbit_auth_backend_internal

Les tokens doivent répondre à certaines contraintes, en particulier ils doivent être signés. RabbitMQ doit être configuré de façon à pouvoir valider la signature.

Dans l’exemple ci-dessous, on a une signature HS256 (symétrique) pour laquelle on doit renseigner le secret partagé.

# advanced.config
[
  {rabbitmq_auth_backend_oauth2, [
    {key_config, [
      {default_key, <<"main-key">>},
      {signing_keys, #{
        <<"main-key">> =>
          {map, #{
              <<"alg">> => <<"HS256">>,
              <<"value">> => <<"459af6c584f776a72ba748d7944082edae6c42d3f22ada2c7270d3bae275c187">>,
              <<"kty">> => <<"MAC">>}
          }
        }
      }
    ]}
  ]}
].

Il faut aussi configurer le resource_server_id qui devra être repris dans le jeton.

# conf.d/21-auth.conf
auth_backends.1 = rabbit_auth_backend_oauth2
auth_oauth2.resource_server_id = rabbitmq

Génération du jeton JWT

Le token JWT utilisé par le client peut venir d’un serveur OAuth2, comme Keycloak, ou par un serveur applicatif. En Java, on peut utiliser simplement Nimbus.

Dans l’exemple ci-dessous, le jeton est généré pour un utilisateur nommé "user", valide pendant une heure, avec des permissions complètes pour les entités (queues et exchanges) du virtual host "/".

  private String buildToken() throws JOSEException {
    String secret = "459af6c584f776a72ba748d7944082edae6c42d3f22ada2c7270d3bae275c187";
    JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
        .audience("rabbitmq")
        .subject("user")
        .expirationTime(Date.from(Instant.now().plus(1, ChronoUnit.HOURS)))
        .claim("scope", List.of(
                "rabbitmq.write:%2F/.*",
                "rabbitmq.configure:%2F/.*",
                "rabbitmq.read:%2F/.\*"
        ))
        .build();

    SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet);
    signedJWT.sign(new MACSigner(secret));
    return signedJWT.serialize();
  }

Les permissions sont dans le scope. On y met une liste de read, write et configure, précédés du resource_server_id. Pour chaque élément de la liste, après le double point, on donne l’entité ou les entités concernées.

La spécification des entités se fait en deux parties, le virtual host et le nom de l’entité, séparées par un slash. Les noms du vhost et de l’entité doivent être encodés façon URL, d’où le %2F.

Pour les exchanges de type topic, on peut ajouter une troisième partie pour la clé de routage.

    JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
        // ...
        .claim("scope", List.of(
                "rabbitmq.write:%2F/q%2Fuser%2F" + id, // queue
                "rabbitmq.configure:%2F/q%2Fuser%2F" + id, // queue
                "rabbitmq.read:%2F/x%2Fclient%2FA/user%2F" + id,  // topic exchange
                "rabbitmq.read:%2F/q%2Fuser%2F" + id // queue
        ))
        .build();

On notera que la permission rabbitmq.read est répétée pour plusieur entités.

Utilisation du jeton JWT

Pour utiliser le jeton généré au paragraphe précédent, on le passe comme mot de passe au client RabbitMQ, sans nom d’utilisateur.

    CachingConnectionFactory connectionFactory = ...;
    connectionFactory.setPassword(token);

Si on a activé le backend oauth2 en complément au backend interne, le service d’authentification de RabbitMQ tente de valider le jeton et s’il échoue, il transmet au backend interne.