Een reis door de DRF-broncode: Waarom ik alleen aangepaste authenticatie bouwde en de 'ingebouwde authenticatie' herontdekte

1. Ik houd van DRF, maar vertrouwde de ingebouwde authenticatie niet.



Django en DRF (Django Rest Framework) zijn mijn constante metgezellen in mijn ontwikkelaarsleven. Maar als het op authenticatie aankomt, ben ik altijd koppig geweest. Ik geef de voorkeur aan JWT, API Key of OAuth2-methoden. Dit is immers de standaard in moderne app-omgevingen.

Het vreemde is dat als ik de officiële DRF-documentatie erbij pak, de hoofdrol wordt ingenomen door enigszins 'ouderwetse' authenticatieklassen zoals Basic, Session, Token, Remote, in plaats van de moderne methoden die ik gebruik. Ik vroeg me vaak af: "Waarom zou het capabele DRF deze per se inbouwen?" Daarom erfde ik altijd van BaseAuthentication om mijn eigen aangepaste authenticatie te bouwen.

2. Waarom de broncode van de 'ingebouwde authenticatie' opnieuw openen?

Terwijl ik mijn aangepaste authenticatie bouwde, kwam ik tot een besef:

"Is de authenticatie die ik heb gebouwd wel perfect geïntegreerd met de filosofie van Django en DRF?"

Ik begon me af te vragen of ik niet het 'natuurlijke' van het door DRF ontworpen authenticatieschema miste, verder dan alleen het plaatsen van de gebruiker in request.user. Daarom ga ik vanaf vandaag, in meerdere posts, de vier ingebouwde DRF-authenticaties tot in detail analyseren.

  • BasicAuthentication (HTTP Basic-authenticatie)
  • SessionAuthentication (Gebruikmakend van Django-sessies)
  • TokenAuthentication (Eenvoudige token-authenticatie)
  • RemoteUserAuthentication (Integratie met externe authenticatie)

3. Het vinden van de 'filosofie' achter de 'onhandigheid'



Eerlijk gezegd is het ongemakkelijk om BasicAuthentication in moderne services te gebruiken. Het lijkt kwetsbaar voor beveiliging, en de structuur waarbij elke keer een gebruikersnaam en wachtwoord worden meegestuurd, voelt onveilig. Maar als je de broncode ontleedt, zie je een zeer verfijnd ontwerp over 'hoe te communiceren met de client bij authenticatiefouten'.

Denk bijvoorbeeld aan het verschil tussen simpelweg een 403 Forbidden terugsturen en het via authenticate_header sturen van een 401 Unauthorized, wat de standaard is. Zulke details waren de kern van het creëren van een "Django-achtige" klasse.

4. Het doel van deze serie: Aangepaste authenticatie 'natuurlijk' maken

Aan het einde van deze serie wil ik niet alleen kennis opdoen.

  1. Inspiratie: Ik leer waarom de ingebouwde authenticatiemethoden zo zijn opgebouwd en wat hun ontwerpbeginselen zijn.
  2. Porting: Ik pas die beginselen toe op mijn favoriete aangepaste JWT- of OAuth2-authenticatie.
  3. Harmonie: Zo laat ik mijn aangepaste authenticatie binnen het DRF-authenticatiesysteem werken alsof het er altijd al was, volledig natuurlijk.

Dit is de eerste stap om niet alleen een ontwikkelaar te zijn die functionaliteit implementeert, maar ook een ontwikkelaar die de filosofie van een framework in code kan verwerken. De DRF Authenticatie Broncode Analyse Serie begint nu.

Ontwikkelaar die een oud slot ontleedt

5. De broncode bekijken: BasicAuthentication

De inleiding was lang genoeg. Laten we nu de kern van BasicAuthentication openen.


Als je rest_framework/authentication.py van DRF opent, kom je de essentie van deze klasse tegen. Het is verrassend beknopt, maar bevat solide regels.

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

Waarom specifiek de header?

We denken meestal aan request.POST of request.data bij het versturen van gegevens. Maar authenticatie-informatie hoort standaard in de header. Waarom?

Authenticatie moet flexibel kunnen omgaan met alle HTTP-methoden (GET, POST, PUT, DELETE, enz.). Een GET-verzoek heeft helemaal geen body, en DELETE heeft vaak ook een lege body. Als authenticatie-informatie in de body zou moeten, zouden we een bizarre architectuur moeten ontwerpen waarbij we zelfs voor opvragingen gedwongen zouden zijn POST te gebruiken. Authenticatie in de header plaatsen is als de afspraak: "Welke deur je ook opent, houd je identiteitskaart in de hand."


De koude realiteit: Beveiligingskwetsbaarheden

Het grootste nadeel van deze methode is Base64. Dit is geen encryptie, maar simpelweg 'encoding'. Iedereen kan binnen een seconde je gebruikersnaam en wachtwoord achterhalen via een decodeerwebsite.

Conclusie: Absoluut verboden te gebruiken in een HTTP-omgeving. Alleen in een veilige tunnel zoals HTTPS, en dan nog in zeer beperkte mate.


Eerste inspiratie: Kosteneffectieve beveiliging voor machine-naar-machine (M2M) communicatie

Toch zijn er momenten dat deze oude authenticatiemethode aantrekkelijk is. Namelijk wanneer server en server met elkaar communiceren. Als het opbouwen van HMAC of complexe API Key-systemen te veel middelen kost, maar het versturen van ruwe data onveilig is, kunnen we deze structuur 'lichtjes' aanpassen.

  • Aangepaste encryptie: In plaats van Base64 versturen we versleutelde binaire inhoud met een symmetrische sleutel die alleen beide servers kennen.
  • Eenvoudige implementatie: Zonder complexe handshakes is snelle en veilige communicatie mogelijk met alleen het Basic <Encrypted_Data>-formaat. Het wordt een efficiënt kanaal om "geheime codes die alleen wij kennen" uit te wisselen.

Tweede inspiratie: Het verloren tandwiel authenticate_header()

De methode die het meest de aandacht verdient, is authenticate_header(). Nergens in de broncode wordt deze direct aangeroepen. Toch is deze de cruciale sleutel voor het genereren van '401 Unauthorized' binnen de DRF-uitzonderingshandler.

Bij het bouwen van aangepaste authenticatie vergeten we vaak deze methode toe te voegen. Het weglaten ervan heeft namelijk geen directe invloed op de authenticatie zelf. Maar het is de moeite waard om te vergelijken hoeveel rommeliger de code in de View wordt door dit kleine verschil.

Geval 1. Wanneer authenticate_header wordt weggelaten (handmatige methode)

Zonder deze methode zal DRF oordelen dat het "geen leidraad kan geven over hoe te authenticeren" en zal het bij een authenticatiefout een 403 Forbidden retourneren in plaats van een 401. Om dit te voorkomen, is de volgende 'handmatige arbeid' in de View nodig:

# ❌ Als authenticate_header ontbreekt
class MyPostAPIView(APIView):
    # Als IsAuthenticated wordt gebruikt, wordt er altijd 403 geretourneerd, dus staat het voor 401 open.
    permission_classes = [AllowAny] 

    def post(self, request):
        # Binnen de View moet handmatig worden gecontroleerd of de gebruiker is geauthenticeerd en 401 handmatig worden geretourneerd.
        if not request.user or not request.user.is_authenticated:
            return Response(
                {"detail": "Inloggen is vereist."}, 
                status=status.HTTP_401_UNAUTHORIZED
            )

        # ... start van de daadwerkelijke logica ...

Geval 2. Wanneer authenticate_header is geïmplementeerd (implementatie van de DRF-filosofie)

Aan de andere kant, als je slechts één regel instructie (authenticate_header) in je aangepaste authenticatie opneemt, zal het grote authenticatieschema van DRF dit herkennen en automatisch een 401-antwoord samenstellen.

# ✅ Als authenticate_header aanwezig is
class MyPostAPIView(APIView):
    # Nu zal DRF automatisch 401 retourneren, waardoor we declaratief rechten kunnen beheren.
    permission_classes = [IsAuthenticated] 

    def post(self, request):
        # De View concentreert zich uitsluitend op de 'bedrijfslogica'.
        # De afhandeling van authenticatiefouten is al afgehandeld op een hoger niveau (de authenticate_header-methode van de authenticatie).
        ... 

Dit is de ware "Django-achtige" esthetiek. In plaats van gefragmenteerde code door de View te verspreiden, voelt het alsof je je code perfect in de tandwielen van het door het framework ontworpen systeem plaatst. Met deze ene methode kunnen we de rollen van 'authenticatie' en 'autorisatie' duidelijk scheiden en de View aanzienlijk lichter houden.


De ware charme van DRF: Stille concessie (de esthetiek van None)

Laten we tot slot dit deel van de broncode nog eens bekijken.

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

Wanneer het formaat niet klopt, wordt er geen foutmelding gegenereerd, maar keert de methode stilzwijgend terug met None. Deze kleine handeling creëert de flexibiliteit van DRF. Hierdoor kunnen we meerdere authenticatiemethoden in de AUTHENTICATION_CLASSES-lijst op volgorde plaatsen. Voel je niet de sporen van ontwikkelaars die zeggen: "Oh, dit is mijn methode niet? Controleer dan de volgende authenticatiemethode!"?


Conclusie

BasicAuthentication mag dan ouderwets lijken, de structuur ervan vormt de basis van moderne authenticatiesystemen. Met name de manier waarop uitzonderingen worden afgehandeld en meervoudige authenticatie wordt ondersteund, zijn activa die we absoluut moeten opnemen bij het bouwen van aangepaste authenticatie.

Zo, de eerste horde is genomen. In de volgende post zullen we SessionAuthentication ontleden, wat ons bekend is maar nog steeds raadselachtig kan zijn.

"Feiten die je niet wist, zelfs als je sessie-authenticatie gebruikte", kijk ernaar uit!