Diving into DRF Source Code: Why I Only Built Custom Authenticators and My Rediscovery of 'Built-in Authenticators'
1. I Love DRF, But I Didn't Trust Its Built-in Authenticators
Django and DRF (Django Rest Framework) have been constant companions throughout my development career. However, when it came to authentication, I always had a stubborn streak. I primarily favored JWT, API Key, or OAuth2 methods, as these are the standards in modern application environments.
Oddly enough, when I consulted the official DRF documentation, I found that authentication classes like Basic, Session, Token, and Remote—which seemed a bit 'old-fashioned'—took center stage, rather than the modern approaches I preferred. I often wondered, "Why would a capable framework like DRF bother to include these?" Consequently, I always created my own custom authenticators by inheriting from BaseAuthentication.
2. Why Revisit the 'Built-in Authenticators' Source Code?
While building custom authenticators, a thought suddenly struck me:
"Do my custom authenticators truly align with the philosophy of Django and DRF?"
I began to wonder if I was missing the 'naturalness' of DRF's designed authentication schema, going beyond simply populating request.user. This led me to embark on a multi-part series, starting today, to thoroughly dissect DRF's four built-in authenticators:
- BasicAuthentication (HTTP Basic Authentication)
- SessionAuthentication (Leveraging Django Sessions)
- TokenAuthentication (Simple Token Authentication)
- RemoteUserAuthentication (External Authentication Integration)
3. Uncovering the 'Philosophy' Behind the 'Awkwardness'
Frankly, using BasicAuthentication directly in modern services feels awkward. It appears vulnerable to security risks, and the structure of sending username and password with every request feels inherently insecure. However, a deeper look into its source code reveals a meticulously crafted design concerning 'how to communicate with the client upon authentication failure.'
For instance, there's a significant difference between simply returning a 403 Forbidden and prompting a standard 401 Unauthorized response via authenticate_header. These subtle details are precisely what make a class "Django-esque."
4. The Goal of This Series: Making Custom Implementations 'Seamless'
What I aim to gain from this series is not merely knowledge.
- Inspiration: To understand the design philosophy behind the structure of these built-in authenticators.
- Porting: To apply that philosophy to the JWT or OAuth2 custom authenticators I frequently use.
- Harmony: To ensure that my custom authenticators integrate so seamlessly into DRF's authentication system that they feel like they've always belonged there.
This is the first step towards becoming a developer who can not only implement features but also embed the framework's philosophy into their code. The DRF Authenticator Source Code Analysis series begins now.

5. Diving into the Source Code: BasicAuthentication
My apologies for the lengthy introduction. Let's now open up the heart of BasicAuthentication.
Opening DRF's rest_framework/authentication.py reveals the true nature of this class. It's surprisingly concise, yet it encapsulates robust rules.
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
Why the Header, Specifically?
We typically think of request.POST or request.data when sending data. However, for authentication information, it's standard practice to include it in the header. Why is this the case?
Authentication must flexibly respond to all HTTP methods (GET, POST, PUT, DELETE, etc.). GET requests often have no body, and DELETE requests frequently have an empty body. If authentication information had to be placed in the body, we would be forced into an awkward design where we'd have to use POST even for simple queries. Placing authentication in the header is like an unspoken agreement: "No matter which door you open, always have your ID ready."
The Stark Reality: Security Vulnerabilities
The biggest drawback of this method is Base64. It's merely 'encoding,' not encryption. Anyone can use a decoding site to uncover your username and password in seconds.
Conclusion: Never use this in an HTTP environment. It should only be used within a secure HTTPS tunnel, and even then, only for very limited purposes.
First Inspiration: Cost-Effective Security for Machine-to-Machine (M2M) Communication
Despite its age, there are moments when this authenticator becomes appealing. Specifically, when servers communicate with each other. When building HMAC or complex API Key systems feels like an overkill in terms of resources, yet sending raw data is too insecure, we can 'slightly' twist this structure.
- Custom Encryption: Instead of Base64, transmit binary content encrypted with a symmetric key known only to both servers.
- Simple Implementation: This allows for fast and secure communication without complex handshakes, using just the
Basic <Encrypted_Data>format. It becomes an efficient channel for exchanging "our secret code."
Second Inspiration: The Missing Cog authenticate_header()
The most noteworthy method is authenticate_header(). Nowhere in the source code will you find it being directly called. Yet, it serves as the crucial key for generating a '401 Unauthorized' response within DRF's exception handler.
When creating custom authenticators, we often overlook including this method. Indeed, omitting it has no direct impact on the act of authentication itself. However, it's worth comparing how this subtle difference can clutter the code at the View level.
Case 1. When authenticate_header is Omitted (Manual Approach)
Without this method, DRF determines it "cannot provide guidance on how to authenticate" and, upon authentication failure, returns a 403 Forbidden instead of a 401. To avoid this, the View requires the following 'manual effort':
# ❌ When authenticate_header is missing
class MyPostAPIView(APIView):
# Using IsAuthenticated would always result in 403, so we temporarily open it for 401.
permission_classes = [AllowAny]
def post(self, request):
# Authentication status must be manually checked within the view, and a 401 must be returned manually.
if not request.user or not request.user.is_authenticated:
return Response(
{"detail": "Login required."},
status=status.HTTP_401_UNAUTHORIZED
)
# ... actual logic starts ...
Case 2. When authenticate_header is Implemented (Embodying DRF's Philosophy)
Conversely, by simply adding a single line of guidance (authenticate_header) to your custom authenticator, DRF's comprehensive authentication schema recognizes it and automatically constructs the 401 response.
# ✅ When authenticate_header is present
class MyPostAPIView(APIView):
# Now DRF will automatically return 401, allowing for declarative permission control.
permission_classes = [IsAuthenticated]
def post(self, request):
# The view focuses solely on 'business logic'.
# Authentication failure handling is already taken care of at a higher level (the authenticator's authenticate_header method).
...
This is the very essence of "Django-esque" elegance. Instead of scattering fragmented code throughout your views, it feels like perfectly slotting your code into the framework's designed cogs. With just this one method, we can clearly separate the roles of 'authentication' and 'authorization' and keep our views significantly lighter.
DRF's True Charm: Graceful Concession (The Elegance of None)
Finally, let's revisit this part of the source code:
if not auth or auth[0].lower() != b'basic':
return None
Instead of throwing an error when the format doesn't match, it silently steps aside by returning None. This seemingly minor detail is what gives DRF its flexibility. Thanks to this, we can list multiple authenticators in the AUTHENTICATION_CLASSES list, in order. Can't you feel the thoughtful touch of developers saying, "Oh, this isn't my method? Then check the next authenticator!"?
Conclusion
While BasicAuthentication might appear antiquated, its underlying structure forms the bedrock of modern authentication systems. Its approach to exception handling and support for multiple authenticators, in particular, are invaluable assets we must adopt when building custom authenticators.
We've now conquered the first hurdle. In the next post, we'll delve into SessionAuthentication, a familiar yet often enigmatic component.
Get ready for "Things You Didn't Know About Session Authentication Even While Using It!" Stay tuned!
There are no comments.