Un voyage au cœur du code source de DRF : Pourquoi je ne créais que des authentificateurs personnalisés et la redécouverte des authentificateurs intégrés

1. J'aime DRF, mais je ne faisais pas confiance à ses authentificateurs intégrés.



Django et DRF (Django Rest Framework) sont des compagnons essentiels dans ma vie de développeur. Pourtant, en matière d'authentification, j'ai toujours eu mes propres préférences. Je privilégie généralement les méthodes JWT, API Key ou OAuth2. Elles sont devenues le standard dans les environnements applicatifs modernes.

Étrangement, en parcourant la documentation officielle de DRF, je constatais que les classes d'authentification mises en avant, telles que Basic, Session, Token, Remote, semblaient un peu « dépassées » par rapport aux approches modernes que j'utilisais. Je me suis souvent demandé : « Pourquoi un framework aussi performant que DRF intégrerait-il ce genre de choses ? » C'est pourquoi j'ai toujours préféré hériter de BaseAuthentication pour créer mes propres authentificateurs personnalisés.

2. Pourquoi rouvrir le code source des authentificateurs intégrés ?

En développant mes propres authentificateurs, une pensée m'est venue :

« Mon authentificateur s'intègre-t-il vraiment parfaitement à la philosophie de Django et DRF ? »

Je me suis inquiété de ne pas saisir la « naturalité » du schéma d'authentification conçu par DRF, au-delà de la simple affectation de l'utilisateur à request.user. C'est pourquoi, à partir d'aujourd'hui, je vais explorer en détail les quatre authentificateurs intégrés de DRF sur plusieurs articles :

  • BasicAuthentication (Authentification HTTP de base)
  • SessionAuthentication (Utilisation des sessions Django)
  • TokenAuthentication (Authentification par simple jeton)
  • RemoteUserAuthentication (Intégration d'utilisateurs distants)

3. Découvrir la 'philosophie' derrière ce qui semblait 'gênant'



Honnêtement, utiliser BasicAuthentication tel quel dans un service moderne est délicat. Il semble vulnérable sur le plan de la sécurité, et la structure qui envoie l'identifiant et le mot de passe à chaque fois est source d'inquiétude. Cependant, en examinant son code source, on découvre une conception très sophistiquée sur la manière de communiquer avec le client en cas d'échec d'authentification.

Par exemple, la différence entre renvoyer simplement un 403 Forbidden et provoquer un 401 Unauthorized conforme aux standards via authenticate_header. Ces détails sont précisément ce qui rend une classe « digne de Django ».

4. L'objectif de cette série : rendre vos personnalisations 'naturelles'

À la fin de cette série, mon objectif n'est pas une simple acquisition de connaissances :

  1. Inspiration : Comprendre la raison d'être de la structure des authentificateurs intégrés et leur philosophie de conception.
  2. Transposition : Appliquer cette philosophie à mes authentificateurs personnalisés JWT ou OAuth2 que j'utilise fréquemment.
  3. Harmonie : Faire en sorte que mes authentificateurs personnalisés fonctionnent de manière si naturelle au sein du système d'authentification de DRF qu'ils semblent avoir toujours été là.

C'est un premier pas pour devenir un développeur qui ne se contente pas d'implémenter des fonctionnalités, mais qui peut intégrer la philosophie du framework dans son code. La série d'analyse du code source des authentificateurs DRF commence maintenant.

개발자가 오래된 자물쇠를 해부하고 있는 이미지

5. Plongée dans le code source : BasicAuthentication

Assez de préambule. Ouvrons le cœur de BasicAuthentication.


En ouvrant rest_framework/authentication.py de DRF, vous découvrirez la réalité de cette classe. Elle est étonnamment concise, mais elle contient des règles solides.

class BasicAuthentication(BaseAuthentication):
    """
    HTTP Basic authentication against username/password.
    """
    www_authenticate_realm = 'api'

    def authenticate(self, request):
        auth = get_authorization_header(request).split()

        if not auth or auth[0].lower() != b'basic':
            return None

        if len(auth) == 1:
            msg = _('Invalid basic header. No credentials provided.')
            raise exceptions.AuthenticationFailed(msg)
        elif len(auth) > 2:
            msg = _('Invalid basic header. Credentials string should not contain spaces.')
            raise exceptions.AuthenticationFailed(msg)

        try:
            try:
                auth_decoded = base64.b64decode(auth[1]).decode('utf-8')
            except UnicodeDecodeError:
                auth_decoded = base64.b64decode(auth[1]).decode('latin-1')

            userid, password = auth_decoded.split(':', 1)
        except (TypeError, ValueError, UnicodeDecodeError, binascii.Error):
            msg = _('Invalid basic header. Credentials not correctly base64 encoded.')
            raise exceptions.AuthenticationFailed(msg)

        return self.authenticate_credentials(userid, password, request)

    def authenticate_credentials(self, userid, password, request=None):
        credentials = {
            get_user_model().USERNAME_FIELD: userid,
            'password': password
        }
        user = authenticate(request=request, **credentials)

        if user is None:
            raise exceptions.AuthenticationFailed(_('Invalid username/password.'))

        if not user.is_active:
            raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))

        return (user, None)

    def authenticate_header(self, request):
        return 'Basic realm="%s"' % self.www_authenticate_realm

Pourquoi le Header ?

Nous pensons souvent à request.POST ou request.data pour envoyer des données. Cependant, les informations d'authentification sont généralement placées dans l'en-tête (Header). Pourquoi ?

L'authentification doit être flexible et compatible avec toutes les méthodes HTTP (GET, POST, PUT, DELETE, etc.). Une requête GET n'a pas de corps, et une requête DELETE est souvent vide. Si nous devions placer les informations d'authentification dans le corps, nous serions contraints d'utiliser POST même pour une simple consultation, ce qui mènerait à une conception étrange. Placer l'authentification dans l'en-tête est comme une promesse : « Peu importe la porte que vous ouvrez, ayez toujours votre carte d'identité en main. »


La dure réalité : une faille de sécurité

Le principal inconvénient de cette méthode est l'utilisation de Base64. Il s'agit d'un simple « encodage », et non d'un chiffrement. N'importe qui peut décoder votre identifiant et votre mot de passe en une seconde via un site de décodage.

Conclusion : À ne jamais utiliser dans un environnement HTTP. Elle ne doit être employée que dans un tunnel sécurisé HTTPS, et ce, pour des usages très limités.


Première inspiration : la sécurité rentable pour la communication M2M

Malgré tout, cet ancien authentificateur présente un intérêt particulier : la communication de serveur à serveur. Lorsque la mise en place d'un système HMAC ou d'API Key complexes est trop coûteuse en ressources, et que l'envoi de données brutes est risqué, nous pouvons légèrement adapter cette structure.

  • Chiffrement personnalisé : Au lieu de Base64, envoyez un contenu binaire chiffré avec une clé symétrique connue des deux serveurs uniquement.
  • Implémentation simplifiée : Sans handshake complexe, une communication rapide et sécurisée est possible via le format Basic <Encrypted_Data>. C'est un canal efficace pour échanger des « mots de passe que nous seuls connaissons ».

Deuxième inspiration : le rouage manquant authenticate_header()

La méthode la plus remarquable est authenticate_header(). Nulle part dans le code source elle n'est appelée directement. Pourtant, elle est la clé essentielle qui génère le '401 Unauthorized' au sein du gestionnaire d'exceptions de DRF.

Lors de la création d'authentificateurs personnalisés, nous oublions souvent d'inclure cette méthode. En effet, son absence n'a aucun impact sur l'acte d'authentification lui-même. Cependant, cette petite différence peut rendre le code de la vue beaucoup plus désordonné, comme nous allons le comparer.

Cas 1. Quand authenticate_header est omis (approche manuelle)

Sans cette méthode, DRF considère qu'il ne peut pas « fournir de guide sur la manière de s'authentifier » et renvoie un 403 Forbidden au lieu d'un 401 en cas d'échec d'authentification. Pour éviter cela, un « travail fastidieux » est nécessaire dans la vue, comme suit :

# ❌ En l'absence d'authenticate_header
class MyPostAPIView(APIView):
    # Si IsAuthenticated est utilisé, un 403 est toujours renvoyé. Pour un 401, nous l'autorisons d'abord.
    permission_classes = [AllowAny] 

    def post(self, request):
        # Il faut vérifier manuellement l'authentification dans la vue et renvoyer un 401.
        if not request.user or not request.user.is_authenticated:
            return Response(
                {"detail": "로그인이 필요합니다."}, 
                status=status.HTTP_401_UNAUTHORIZED
            )

        # ... Le reste de la logique métier ...

Cas 2. Quand authenticate_header est implémenté (implémentation de la philosophie DRF)

En revanche, si une seule ligne d'instruction (authenticate_header) est ajoutée à l'authentificateur personnalisé, le vaste schéma d'authentification de DRF le reconnaît et assemble automatiquement la réponse 401.

# ✅ En présence d'authenticate_header
class MyPostAPIView(APIView):
    # DRF renverra automatiquement un 401, permettant un contrôle déclaratif des permissions.
    permission_classes = [IsAuthenticated] 

    def post(self, request):
        # La vue se concentre uniquement sur la 'logique métier'.
        # La gestion de l'échec d'authentification est déjà traitée en amont (par la méthode authenticate_header de l'authentificateur).
        ... 

C'est la véritable esthétique « à la Django ». Au lieu de disperser du code fragmenté dans la vue, cela donne le sentiment d'insérer parfaitement notre code entre les rouages conçus par le framework. Avec cette seule méthode, nous pouvons clairement séparer les rôles d'« authentification » et d'« autorisation », et maintenir la vue beaucoup plus légère.


Le véritable attrait de DRF : la concession silencieuse (l'esthétique de None)

Revenons une dernière fois à cette partie du code source.

if not auth or auth[0].lower() != b'basic':
    return None

Au lieu de générer une erreur lorsque le format ne correspond pas, la méthode renvoie None et se retire discrètement. Ce traitement apparemment anodin confère à DRF sa flexibilité. Grâce à cela, nous pouvons inclure plusieurs authentificateurs dans la liste AUTHENTICATION_CLASSES dans un ordre précis. Ne ressentez-vous pas cette attention des développeurs qui disent : « Ah, ce n'est pas ma méthode ? Alors, vérifie l'authentificateur suivant ! » ?


Conclusion

BasicAuthentication peut sembler archaïque, mais sa structure sous-tend les systèmes d'authentification modernes. En particulier, sa gestion des exceptions et son support de l'authentification multiple sont des atouts que nous devons absolument intégrer lors de la création d'authentificateurs personnalisés.

Nous avons franchi le premier obstacle. Dans le prochain article, nous explorerons SessionAuthentication, un système familier mais souvent mal compris.

« Ce que vous ignoriez sur l'authentification par session », restez à l'écoute !