Créer un Middleware d'Authentification avec Actix Web en Rust

Un tutoriel débutant qui vous guidera dans la création d'un middleware d'authentification "Bearer" avec Actix Web.

On tient les bonnes résolutions pour 2022 et on se met au Rust !

Vous trouverez ci-joint un article sur du Rust avec une implémentation d’un middleware pour le framework Actix.

Je vous souhaite une bonne lecture.

Sommaire :

  • Packager un Middleware Actix
  • Packages Rust utilisés
  • Code et patterns liés au Middleware d'Authentification Actix en Rust
  • Testing du Middleware Actix

Packager un Middleware Actix

Dans ce tutoriel nous allons implémenter un middleware sur le framework web Actix.

J’ai choisi d’en faire un package, car cela a plusieurs avantages en terme d’architecture logiciel :

  • dans des architectures micro service cela permet d’éviter la duplication de code
  • centraliser toutes les logiques liées au middleware dans un seul projet
  • réduire la masse de code à tester

Afin de vous épauler au long de ce tutoriel, voici le code source de ce MiddleWare Actix.

Petit disclaimer, pour me faciliter le développement de middleware j’ai volontairement créé les dossiers models et config au sein du dossier src. Pour une implémentation complète avec une logique micro-service, il faudrait que ces deux dossiers soient des packages à importer.

Packages Rust utilisés

Voici les principaux packages utilisés pour coder ce middleware d'authentification avec Actix Web en Rust. Vous retrouverez notamment leurs documentations sur les liens ci-dessous. Elles seront vos boussoles dans l’océan qu’est Rust. L'ensemble des packages sont disponibles dans le fichier Cargo.toml.

  • Actix-web: le Framework Web en Rust
  • Serde: la library de sérialization et désarialisation en Rust

Ce sont les deux gros packages du projet. Vous serez certainement amenés à les utiliser dans d’autres projets.

Code et patterns liés au Middleware d'Authentification Actix en Rust

Nous allons devoir créer une nouvelle structure et implémenter le trait FromRequest à notre nouvelle structure.

Un trait est un ensemble de méthodes que l'objet sur lequel il est appliqué doit implémenter. Si vous voulez aller plus loin sur ce sujet voici "Traits and You: A Deep Dive", une conférence Rust qui m'a permis d'appréhender le concept de trait.

Revenons à notre implémentation, pour cela nous pouvons nous référer à la documentation FromRequest Rust de Actix.

[derive(Debug)]
pub struct AuthorizationMiddleware;

impl FromRequest for AuthorizationMiddleware {
    type Error = Error;
    type Future = Ready<Result<AuthorizationMiddleware, Error>>;
    type Config = ();

    }

Dans la documentation, on remarque qu’à minima, nous avons besoin de re-implémenter la fonction from_request tout en respectant sa signature initiale.

from_request est une fonction que nous allons utiliser dans le router Actix. Le but d’un middleware est d’intercepter la requête et de faire des pré-traitements avant son entrée dans le service. Dans notre cas, nous voulons vérifier si notre JWT (JSON Web Token) est bon et refuser l’accès au endpoint, si ce n’est pas le cas.

La fonction from_request prend en argument un HttpRequest, le payload de la requête et va retourner une Future définie plus haut par :

type Future = Ready<Result<AuthorizationMiddleware, Error>>;

Dans le cas où le JWT est bon, la fonction retournera un AuthorizationMiddleware et dans l’autre cas une Error.

fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
        let auth = req.headers().get("Authorization");
        match auth {
            Some(_) => {
	            // Todo apply the token checking
            }
            None => err(ErrorUnauthorized("Blocked"))
        }
    }

La première étape pour notre fonction est de récupérer dans la request le header "Authorization".

En utilisant la fonction headers(), cela nous retourne une Map [Key, value] . Cette Map contient l’ensemble des en-têtes de notre requête. Grâce a l'appel de get("Authorization") nous récupérons une Option[&headerValue] qui contiendra la valeur de "Authorization" si elle s'avère être présente dans celle-ci. Si ce n'est pas le cas, cela retournera None.

Les Option font partie des types monadic. Les types modanic sont liés au langage fonctionnel et permettent la mise en place de différents patterns de code. Voici un article sur les types modanic en Scala. Le concept est relativement similaire en Rust.

Dans notre cas nous allons pouvoir réaliser du pattern matching avec une Option à deux états possibles si l'option est définie. On peut voir le pattern matching comme une sorte de condition if ou un switchcase. Si notre option contient quelque chose on va rentrer dans le cas Some(\_) ⇒. S’il y a une erreur ou si elle est vide, on rentrera dans le None ⇒.

Dans notre cas, cela permet de tester si dans les en-têtes il y a bien "Authorization".

Si c'est le cas, nous continuons la logique de notre code, mais dans le cas ou cela échoue nous pouvons retourner une erreur "HTTP Unauthorized".

Une fois que nous avons récupéré le "Authorization" dans l'en-tête, nous allons chercher à récupérer notre token et retirer l’en-tête de ce dernier. Un en-tête "Authorization" est constitué comme ceci: Authorization: <type> <credentials>. Dans notre exemple le type de notre en-tête sera Bearer. C'est pour cette raison que, par la suite, nous allons réaliser un appel à split sur Bearer afin d'extraire notre token.

let split: Vec<&str> = auth.unwrap().to_str().unwrap().split("Bearer").collect();
let token = split[1].trim();
let config: Config = Config {};
let var = config.get_config_with_key("SECRET_KEY");
let key = var.as_bytes();

Le reste du code sur cette partie est assez parlant, je fais donc une avance rapide jusqu’au match decode::<Claims>.

match decode::<Claims>(
    &token.to_string(),
    &DecodingKey::from_secret(key.as_ref()),
    &Validation::new(Algorithm::HS256),
) {
    Ok(_token) => ok(AuthorizationMiddleware),
    Err(_e) => {
        _e;
        err(ErrorUnauthorized("Invalid token"))}
}

Le match decode::<Claims> est la fonction de la librairie jsonwebtoken qui prend comme type de sortie une structure Claims définie dans le dossier models de notre projet (models/user.rs).

Point intéressant, la fonction décode et retourne un autre type de modanic propre au Rust. C'est le type Result "Ok" ou "Err". Sur ce type Result nous pouvons encore réaliser du pattern matching. Si decode retourne un "Ok" nous allons retourner AuthorizationMiddleware sinon une erreur HTTP afin de refuser l’accès au endpoint.

Testing du Middleware Actix

Une fois notre middleware codé nous allons devoir le tester pour être sûr de son bon fonctionnement. Vous pouvez déjà trouver les tests sur mon projet GitHub. Concernant le détails, cela sera pour un prochain article sur comment réaliser des tests en Rust (alors restez à l'affut! 😉).

Félicitations, votre premier Middleware d'authentification Actix est prêt!

Nous venons de voir comment créer un middleware d'authentification avec Actix Web en Rust.

Je vous propose de me rejoindre sur mon LinkedIn afin de ne pas râter mon prochain article sur les tests en rust.

En attendant, je vous remercie d’avoir lu cet article ! En espérant vous avoir apporté les premières bases du langage Rust, avec comme exemple, la notion de middleware.

Apprenez à programmer avec des exemples simples et concrets.

English / Français