DEV Community

Cover image for Deep Dive into PandApache3: Implementation d'authentification et de la securité
Mary 🇪🇺 🇷🇴 🇫🇷
Mary 🇪🇺 🇷🇴 🇫🇷

Posted on • Edited on

Deep Dive into PandApache3: Implementation d'authentification et de la securité

Dans les précédents articles, nous avons d'abord vu comment notre serveur web démarre, puis comment il commence à gérer les connexions et à générer les réponses. Maintenant, il est temps d’ajouter à notre serveur des fonctionnalités liées à la sécurité ! En effet, il se peut que nous voulions restreindre l’accès à une partie du site web à certains utilisateurs seulement. (Nous verrons dans le prochain article que l’authentification est une brique essentielle à d'autres fonctionnalités !)

Precedents articles:


La configuration des répertoires

Directory

Comme nous l'avons vu précédemment, notre serveur web a un répertoire racine, www, qui contient notre site web avec, par exemple, notre fichier index.html.

Imaginons que mon serveur contienne certains documents, ou certaines pages, que je ne souhaite pas rendre publiques ou en tout cas pas accessibles à tout le monde. Comment organiser cela ?

L’approche la plus simple et courante est de ranger tous mes documents dans un répertoire spécial, puis de mettre un mot de passe pour protéger ce répertoire. Cependant, nous pouvons avoir plusieurs répertoires dans un serveur web pour organiser nos ressources. Nous ne pouvons donc pas nous baser uniquement sur les répertoires physiques présents sur notre disque. Nous allons donc configurer des répertoires un peu spéciaux pour notre serveur web !

Commençons simplement par notre répertoire racine. Sur PandApache, il se trouve à /etc/PandApache3/www/ et il doit par défaut être accessible à tout le monde. Voici à quoi ressemble la définition de ce répertoire dans le fichier de configuration :

<Directory /etc/PandApache3/www/>
    Require all granted
</Directory>
Enter fullscreen mode Exit fullscreen mode

C’est pour le moment plutôt simple, on définit que c’est un répertoire, son emplacement et les autorisations associées. Ici, le Require all granted indique que tout le monde peut accéder à notre répertoire.

Si nous voulons définir un autre répertoire avec d'autres permissions, sécurisé cette fois, nous pouvons modifier notre configuration comme ceci :

<Directory /etc/PandApache3/www/secure>
    AuthType Basic
    AuthName "Authentification"
    AuthUserFile /etc/PandApache3/htpasswd.txt
    Require valid-user
</Directory>
Enter fullscreen mode Exit fullscreen mode

Ici, nous définissons un second répertoire, toujours dans www, mais cette fois avec un dossier en plus : « secure ».

L’instruction Require all granted a été remplacée par Require valid-user, ce qui indique que nous voulons un utilisateur authentifié. Cette authentification va être de type Basic et nos informations d’authentification seront dans le fichier htpasswd.txt.

Quand un utilisateur va tenter de se connecter à l’URL http://pandapache3/secure/fichier.html, une fenêtre d’authentification apparaîtra pour lui permettre d’entrer un nom d’utilisateur et un mot de passe valide pour accéder à la ressource.

Entre nous

Les attributs qui définissent les conditions d’accès aux répertoires s'appellent des directives, c’est ce terme que nous allons utiliser à partir de maintenant.


Chargement des paramètres des répertoires

loading

"Talk is cheap. Show me the code" disait Linus Torvalds. Et c’est vrai. Nous avons vu le fichier de configuration, voyons maintenant comment il est interprété dans le code. Nous allons commencer par voir comment est fait le chargement de la configuration. Une fois que nous aurons une bonne compréhension des objets et de leurs attributs, nous verrons comment ils sont utilisés ensuite dans le code.

D’abord, à quoi ressemble un objet Directory ? Eh bien, sans surprise, à ce que l'on trouve dans le fichier de configuration :

public class DirectoryConfig
{
    public string Path { get; set; }
    public string AuthType { get; set; }
    public string AuthName { get; set; }
    public string AuthUserFile { get; set; }
    public string Require { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Nous avons déjà vu comment le fichier de configuration était analysé pour enregistrer dans notre instance de configuration les différentes clés et valeurs. Ici, nous allons simplifier le code en omettant cette partie et en nous concentrant uniquement sur les directives.

public void ReloadConfiguration()
{
    List<string> currentSection = new List<string>();
    DirectoryConfig currentDirectory = null;
    foreach (var line in File.ReadLines(fullPath))
    {
        if (line.Trim().StartsWith("<") && line.Trim().EndsWith(">") && !line.Trim().StartsWith("</"))
        {
            Logger.LogDebug($"Starting to read new directive {line.Trim()}");
            string sectionName = line.Trim().Substring(1, line.Trim().Length - 2);
            Logger.LogInfo($"Reading section {sectionName}");

            if ((sectionName.StartsWith("Directory") || sectionName.StartsWith("Admin")) && currentDirectory == null)
            {
                currentDirectory = new DirectoryConfig
                {
                    Path = sectionName.Split(' ')[1]
                };
                Directories.Add(currentDirectory);
                currentSection.Add("Directory");
            }
            continue;
        }

        if (currentSection.Count != 0)
        {
            if (line.Trim() == "</Directory>")
            {
                currentDirectory = null;
                currentSection.Remove("Directory");
                continue;
            }
            else if (currentDirectory != null && currentSection.Last().Equals("Directory"))
            {
                getKeyValue(line);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Bien qu'un peu longue, vous allez voir que la méthode est très simple et plutôt courte à chaque tour de boucle. La première chose qui nous intéresse est de détecter la première ligne qui indique un répertoire : <Directory /etc/PandApache3/www/secure>.

Cela doit donc commencer par un < mais surtout pas par un </ et doit se terminer par un >.

Sur cette première ligne, nous pouvons récupérer notre titre de section qui sera aussi notre Path. À partir de maintenant, nous savons que nous sommes dans une section, c’est donc notre second if qui sera exécuté pour les prochaines itérations tant que nous ne sortons pas de la section.

Pour sortir de la section, il est nécessaire de rencontrer une ligne qui commence par </ et se termine par >.

Tout ce que l'on trouvera avant sera une directive que l'on pourra lire avec notre fonction getKeyValue() comme vu dans le premier article.

Entre nous

Vous vous souvenez peut-être de la fonction MapConfiguration utilisée dans getKeyValue qui permet d’assigner nos champs de configuration à des variables. C’est toujours MapConfiguration qui est utilisée pour les valeurs liées aux répertoires, mais vu que l’on peut avoir plusieurs répertoires, nous sommes obligés de les stocker dans une liste, et c’est ce dernier élément de la liste qui sera mis à jour dans MapConfiguration.

public void MapConfiguration(string key, string value)
{
    var actionMap = new Dictionary<string, Action<string>>
    {
        ["authtype"] = v => Directories.Last().AuthType = v,
        ["authname"] = v => Directories.Last().AuthName = v,
        ["authuserfile"] = v => Directories.Last().AuthUserFile = v,
        ["require"] = v => Directories.Last().Require = v
    };
    if (actionMap.TryGetValue(key.ToLower(), out var action))
    {
        action(value);
    }
}

Les nouveaux middlewares

middleware

Bien, nous avons vu que notre configuration peut être programmatiquement chargée dans PandApache3. Il est maintenant temps de l’utiliser, et c’est le moment parfait pour montrer à quel point l’architecture par middleware est intéressante et évolutive. Souvenez-vous que nous avions trois middlewares actuellement, et que chacun d’eux était utilisé pour effectuer un traitement sur notre requête.

Bon, jusqu’à maintenant, c’était surtout le middleware de routage qui faisait le travail. Mais maintenant, nous avons une nouvelle action à exécuter sur chaque requête... en fait, nous en avons deux !

Le premier middleware est bien sûr pour gérer l’authentification et voici la fonction exécutée pour ce middleware :

public async Task InvokeAsync(HttpContext context)
{
    if (context.Request.Headers.ContainsKey("Authorization"))
    {
        string authHeader = context.Request.Headers["Authorization"];
        string credentials = Encoding.UTF8.GetString(Convert.FromBase64String(authHeader.Substring("Basic ".Length).Trim()));
        string[] credentialParts = credentials.Split(':');
        if (credentialParts.Length == 2)
        {
            string username = credentialParts[0];
            string password = credentialParts[1];

            string mainDirectory = ServerConfiguration.Instance.RootDirectory;
            string filePath = Path.Combine(mainDirectory, Utils.GetFilePath(context.Request.Path));
            DirectoryConfig directoryConfig = ServerConfiguration.Instance.GetDirectory(filePath);
            if (IsValidUser(directoryConfig, username, password))
            {
                context.isAuth = true;
            }
        }
    }
    await _next(context);
}
Enter fullscreen mode Exit fullscreen mode

Ce middleware ne doit s’exécuter que si, dans l’entête, nous avons des informations d’authentification qui ont été envoyées. Si nous en avons, nous pouvons décoder ces informations en base64. Nous avons donc maintenant un nom d’utilisateur et un mot de passe. Étant donné que nous savons quelle ressource l’utilisateur a demandée (il s’agit du Path dans notre objet Request), nous pouvons facilement déterminer le répertoire qui gère cette ressource. Nous avons donc maintenant toutes les informations nécessaires pour vérifier l’authentification d’un utilisateur.

Pour cela, regardons de plus près la méthode IsValidUser :

private bool IsValidUser(DirectoryConfig directoryConfig, string username, string password)
{
    if (directoryConfig == null)
        return false;

    string authUserFile = directoryConfig.AuthUserFile;
    bool exist = FileManagerFactory.Instance().Exists(authUserFile);
    if (string.IsNullOrEmpty(authUserFile) || !exist)
    {
        Logger.LogError($"Auth User File {authUserFile} doesn't exist");
        return false;
    }

    Logger.LogInfo($"Reading from the auth user file {authUserFile}");
    foreach (string line in File.ReadAllLines(authUserFile))
    {
        string[] parts = line.Split(':');
        if (parts.Length == 2)
        {
            if (parts[0].ToLower().Equals(username.ToLower()) && parts[1].Equals(HashPassword(password)))
                return true;
        }
    }
    return false;
}
Enter fullscreen mode Exit fullscreen mode

Cette méthode va vérifier le paramètre AuthUserFile afin de lire et de comparer les informations qui s’y trouvent avec celles qui ont été envoyées par l’utilisateur. Si cela correspond, l’utilisateur est authentifié, sinon il ne l’est pas.

Le but de ce middleware est seulement d’authentifier ou non les utilisateurs, c’est sa seule responsabilité, il n’est pas fait pour refuser l’accès à une ressource.

Le statut d’authentification est gardé dans le HttpContext, qui pour l’occasion a été légèrement modifié :

public class HttpContext
{
    public Request Request { get; set; }
    public HttpResponse Response { get; set; }
    public bool isAuth { get; set; } = false;

    public HttpContext(Request request, HttpResponse response)
    {
        Request = request;
        Response = response;
    }
}
Enter fullscreen mode Exit fullscreen mode

Maintenant, c’est au tour d’un autre middleware d’autoriser l’accès ou non à une ressource. Bienvenue à DirectoryMiddleware :

public async Task InvokeAsync(HttpContext context)
{
    string mainDirectory = ServerConfiguration.Instance.RootDirectory;
    string filePath = Path.Combine(mainDirectory, Utils.GetFilePath(context.Request.Path));

    DirectoryConfig directoryConfig = ServerConfiguration.Instance.GetDirectory(filePath);

    bool authNeeded = false;

    if (directoryConfig != null && directoryConfig.Require.Equals("valid-user"))
    {
        Logger.LogDebug($"Authentication requested");
        authNeeded = true;
    }
    if (authNeeded && !context.isAuth)
    {
        context.Response = new HttpResponse(401);
        context.Response.Headers["WWW-Authenticate"] = "Basic realm=\"Authentication\"";
        Logger.LogWarning($"User not authenticated");
        return;
    }

    await _next(context);
}
Enter fullscreen mode Exit fullscreen mode

Nous savons maintenant si l’utilisateur est authentifié ou non. La prochaine étape est de savoir s’il souhaite accéder à une ressource qui nécessite une authentification.

Si la ressource appartient à un répertoire où la propriété Require a été mise à valid-user, alors une authentification est nécessaire. Nous avons le statut de l’authentification grâce au middleware précédent, nous pouvons donc autoriser la requête à passer au middleware suivant ou bien la stopper dès maintenant et générer une erreur 401.

Comme vu dans le tout premier article, notre pipeline est défini au lancement du serveur, il faut donc ajouter ces nouveaux middlewares au pipeline !

TerminalMiddleware terminalMiddleware = new TerminalMiddleware();
RoutingMiddleware routingMiddleware = new RoutingMiddleware(terminalMiddleware.InvokeAsync, fileManager);
DirectoryMiddleware directoryMiddleware = new DirectoryMiddleware(routingMiddleware.InvokeAsync);
AuthenticationMiddleware authenticationMiddleware = new AuthenticationMiddleware(directoryMiddleware.InvokeAsync);
LoggerMiddleware loggerMiddleware = new LoggerMiddleware(authenticationMiddleware.InvokeAsync);
Func<HttpContext, Task> pipeline = loggerMiddleware.InvokeAsync;
Enter fullscreen mode Exit fullscreen mode

Entre nous

Pourquoi avoir fait deux middlewares au lieu d’un seul ? Il est toujours bon de séparer les responsabilités. Actuellement, les middlewares sont courts, car nous avons un seul type d’authentification à vérifier et l’accès ne se fait que sur ce critère. Dans le futur, ces méthodes peuvent devenir plus complexes, il est donc utile et important de les séparer dès maintenant.


Limiter les méthodes HTTP

HTTP

Nous avons déjà drastiquement amélioré la sécurité avec l’authentification, mais nous pouvons aller encore plus loin assez simplement.

Si votre site web est simplement statique et destiné à la consultation, quel type de requête vous attendez-vous à recevoir ? Des requêtes de type GET, bien sûr. On pourrait donc décider que tout autre type de requête est illégitime et donc à interdire.

N’autoriser qu’un seul type de requête s’apparente au principe de sécurité de la réduction de surface. Imaginez par exemple que les méthodes HTTP de type POST contiennent une faille qui permette de contourner l’authentification. Eh bien, si votre serveur refuse de toute manière toutes les requêtes POST, vous ne craignez rien.

En plus des informations d’authentification, nos répertoires peuvent avoir la directive suivante :

<LimitVerb>
    GET
    POST
</LimitVerb>
Enter fullscreen mode Exit fullscreen mode

Cette directive permet d’indiquer quelles sont les méthodes HTTP autorisées sur notre répertoire. L’implémentation de cette sécurité se trouve au niveau du DirectoryMiddleware car c’est déjà lui qui autorise ou non l’accès à une ressource. Voici le code de vérification rajouté dans le middleware :

bool verbAccess = true;

if (directoryConfig != null)
{
    if (directoryConfig.AllowedMethods != null)
    {
        if (directoryConfig.AllowedMethods.Contains(context.Request.Verb) == false)
        {
            Logger.LogError(
                $"Verb {context.Request.Verb} not allowed for the directory {directoryConfig.Path}"
            );
            context.Response = new HttpResponse(403);
            verbAccess = false;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Parce que cet article commence déjà à être long, je vous fais grâce cette fois du code lié au chargement de la configuration, de toute façon très similaire au chargement des répertoires. Considérez juste que chaque objet DirectoryConfig dispose maintenant d’une liste avec les méthodes autorisées.

Pour pouvoir continuer, la requête doit donc soit utiliser une méthode HTTP qui est présente dans cette liste, soit cibler une ressource qui n’est pas dans un répertoire ou un répertoire où aucune méthode n’a été spécifiée (donc notre liste AllowedMethods sera null).

Si vous utilisez une méthode HTTP sur une ressource qui l’interdit, vous aurez simplement un statut 401.

Entre nous

Pourquoi utiliser un 403 et non un 401 ? Ces 2 code d’erreur sont légèrement diffèrent. Le 401 signifie Unauthorized. Vous pouvez donc vous authentifier pour ensuite avoir accès a la ressource. 403, signifie Forbidden. Le serveur, refuse donc d’exécuter votre requête et l’authentification ne changera rien. C’est le comportement attendu du serveur, seul un changement de configuration par un administrateur peut modifier cela. Voila pourquoi nous avons ici 2 codes d’erreur diffèrent


Dans cet article, nous avons approfondi la configuration de sécurité de PandApache3, en abordant les directives pour la gestion des répertoires et les limitations des méthodes HTTP. Vous avez vu comment ces mécanismes assurent une protection efficace des ressources en fonction des besoins spécifiques.

Ce premier aperçu vous fournit une base solide pour comprendre le traitement des requêtes dans PandApache3. Dans notre prochain article, nous nous pencherons sur les nouvelles fonctionnalités de la dernière version de PandApache3, qui simplifient l'administration des serveurs web dans un environnement PaaS. Restez à l'écoute pour découvrir ces innovations et leur impact sur la gestion des infrastructures web.


Merci infiniment d'avoir exploré les coulisses de PandApache3 avec moi ! Vos réflexions et votre soutien sont essentiels pour faire évoluer ce projet. 🚀
N'hésitez pas à partager vos idées et impressions dans les commentaires ci-dessous. Je suis impatient d'échanger avec vous !

Suivez mes aventures sur Twitter @pykpyky pour rester à jour sur toutes les nouvelles.

Vous pouvez également découvrir le projet complet sur GitHub et me rejoindre lors de sessions de codage en direct sur Twitch pour des sessions passionnantes et interactives. À bientôt derrière l'écran !


Top comments (0)