Why does Django REST Framework provide different Authentication mechanisms - django

Why does Django REST Framework implement a different Authentication mechanism than the built-in Django mechanism?
To wit, there are two settings classes that one can configure:
settings.AUTHENTICATION_BACKENDS which handles the Django-level authentication, and
settings.REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] which authenticates at the REST-Framework level
The problem I'm experiencing is that I have a Middleware layer which checks whether a user is logged-in or not.
When using a web client which authenticates via sessions, this works fine. However, from mobile or when running the test suite (i.e. authenticating using HTTP headers and tokens), the middleware detects the user as an AnonymousUser, but by the time we get to the REST Framework layer, the HTTP Authorization header is read, and the user is logged-in.
Why do these not both happen BEFORE the middleware? Furthermore, why doesn't REST Framework's authentication methods not rely on the Django authentication backend?

Django Rest Framework does not perform authentication in middleware by default for the same reason that Django does not perform authentication in middleware by default: middleware applies to ALL views, and is overkill when you only want to authenticate access to a small portion of your views. Also, having the ability to provide different authentication methods for different API endpoints is a very handy feature.
Rest Framework's authentication methods do not rely on the Django authentication backend because the Django's backend is optimised for the common case, and is intimitely linked to the user model. Rest Framework aims to make it easy to:
Use many different authentication methods. (You want HMAC based authentication? done! This is not possible with django auth framework)
Serve API data without ever needing a database behind it. (You have a redis database with all your data in-memory? Serve it in milliseconds without ever waiting for a round trip to DB user model.)

Thomas' answer explains the why quite well. My answer deals more with what you can do about it. I also have a middleware class that determines the current user. I need this because we have some automatic processes (cron) and I want to attribute these to a super user, and I have some model functions that don't have access to the request object but need to know who the current user is for various functions. So this created some issues as you noticed when we added an API via the REST framework. Here's my solution (yours will need to be adjusted to your use case):
from threading import local
_user = local() # thread-safe storage for current user used by middleware below
class CurrentUserMiddleware(object):
""" Defines Middleware for storing the currently active user """
def process_request(self, request):
if not request:
# Clear the current user if also clearing the request.
_user.value = 1 # One represents automatic actions done by super admin
else:
_user.value = request.user
if not _user.value or not _user.value.is_authenticated:
try:
authenticator = TokenAuthentication()
auth = authenticator.authenticate(request)
if auth:
_user.value = auth[0]
except Exception as e:
pass
def process_response(self, request, response):
_user.value = None
return response
The authenticator bit deals with seeing if the request has been authenticated by the REST Framework and getting the user.

Related

Get DRF token from python-social-auth backend

I am trying to integrate Social Authentication in my DRF backend. I decided to go with python-social-auth. If I serve my social login through Django (and an HTML view), I can see my login in successful. I also figured that I can redirect after successful social authentication as outlined here.
Until now, my frontend (a Nuxt app) was using DRF tokens. Even though:
I can create valid tokens for social accounts.
My frontend can redirect the users to -> complete authorization with OAuth sites e.g. Google, Twitter -> return back to the frontend.
Is it possible for me to somehow manage to redirect a successfully authenticated user to the frontend with the associated and valid DRF token as a query parameter?
You can, it's not trivial, tho. It's possible because the mechanism to retrieve URLs (success or errors ones) is delegated to strategies setting() method, which in the end invokes get_setting() on final implementations. This method you can override to add your custom logic.
These steps should get you on the road:
Define a custom strategy
from social_django.strategy import DjangoStrategy
class CustomStrategy(DjangoStrategy):
def get_setting(self, name):
if name == 'LOGIN_REDIRECT_URL':
token = get_drf_token()
return f'/?toke={token}'
else:
return super().get_setting(name)
Point your SOCIAL_AUTH_STRATEGY settings to this new strategy (import path syntax)
Besides using a custom strategy, which I believe is more the library's style, there is another explicit solution:
Since python-social-auth allows redirection after successful authentication, open another endpoint that will a. generate a token b. redirect to the frontend. So I defined follow api_view:
#api_view(http_method_names=['get'])
def user_token(request):
if request.user.is_authenticated:
token, e = Token.objects.get_or_create(user=request.user)
return redirect('<front-end>/?token='+token.key)
else:
return redirect(`<front-end>/login/?error=true')
Now a potential authentication flow is like this:
User visits front-end -> Click 'login using Github et.al.' -> Login at the third party (say, Github) -> Back to backend /success/ -> redirect to front-end url (based on the above view) with the token -> Handle the query parameter on the frontend (which is pretty trivial in my case where I am using Nuxt).
If we mount user_token at path /success/token/, then once the user visits: http:<backend>:/login/github/?next=/success/token/, every right step on users part will take him/her to the front-end with the query param token set as the right token.
UPDATE: This will only work if DRF has session authentication active, otherwise request.user.is_authenticated can never be true.

When do django AUTHENTICATION_CLASSES run with respect to Middleware

I see there is a django.contrib.auth.middleware.AuthenticationMiddleware but reading the documentation I don't think it determines when auth is actually auth is run. Does auth happen before or after middleware? Can that ordering be changed?
The authentication process and AuthenticationMiddleware unfortunately have little to do with each other. Authentication as a process is the practice of verifying credentials. In order to not have to do this for each request, one can create a "login session", which associates the browser with an authentication process that happened in the past. For the end user this is transparent as a "login process". This results in a "logged in user" and AuthenticationMiddleware puts this logged in user on the request object as request.user or if no login process has occurred, then the AnonymousUser.
In Django, authentication is done by whatever calls django.contrib.auth.authenticate and the login process by django.contrib.auth.login. In a vanilla installation this is done by django.contrib.auth.views.LoginView, which isn't hooked up to any url, except the admin login.
Authentication backends are not middleware. They are sources of truth for authentication data that is queried by the authenticate function. It's list is executed in order defined by the settings and the first one that returns True wins.
I think in the past the intention was for the authentication middleware to do more, but as it stands, a better name would be CurrentUserMiddleware.
No fixed place
You seem to think there is one spot where authentication backends are called on each request. This isn't the case, but depending on the authentication method, it can be. For example, Django Rest Framework's token authentication sends a token header on each request, which is authenticated each time in the view.
In theory, one can create a middleware that authenticates and sets the authenticated user each time, especially with token based auth, as they send credentials with each request. It's just unpractical, because it becomes harder and less explicit to exclude views from the process.

Using Django Session Framework with a Token instead of a Cookie?

I have a DRF API with protected endpoints that returns filtered data depending on what a user has permission to access.
I have a separate Django OAuth2 provider which contains the user models and the values necessary to determine what the user has permission to access.
The user should be able to authenticate via a login endpoint on the DRF API. The API in turn gets a token from the Oauth2 provider on behalf of the user, and makes a few calls to get a list of resources the user is allowed to access.
Ideally the DRF API would then generate a token and return it to the user. Whenever the user makes a subsequent request (after login) using the token, the API would be able to filter results via the values returned by calls to the Oauth provider.
The question is how to store this information. This feels similar to storing data in an anonymous user session, but using a request header instead of a cookie. I've considered rolling a customized version of django.contrib.sessions.middleware.SessionMiddleware, but I'd prefer to use an established method instead of writing custom code, as this seems like it should not be a unique problem.
To reiterate: Is it possible to create an anonymous user session, store information it it, and retrieve the session via a request header instead of a cookie?
Here is the original SessionMiddleware.process_request provided by Django. Lets take a quick look at it.
def process_request(self, request):
session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None)
request.session = self.SessionStore(session_key)
We can clearly see that it explicitly gets the session identifier form the cookies using the SESSION_COOKIE_NAME property defined in the settings. Hence, we absolutely must create our own subclass of this SessionMiddleware and define our own process_request behaviour.
Irrespective of whether the incoming token is authenticated or not, we need to retrieve the token value from the header, and use that to initiate our session engine. Here's how it might look:
from django.contrib.sessions.middleware import SessionMiddleware
from django.conf import settings
class CustomSessionMiddleware(SessionMiddleware):
def process_request(self, request):
session_key = request.META.get("HTTP_%s" % settings.SESSION_KEY_NAME, None)
request.session = self.SessionStore(session_key)
Make sure you set the SESSION_KEY_NAME property in your django settings file to the name of the header key in which this token will be sent. Then, replace django's original SessionMiddleware with the path to your custom session middleware and your requests.session should start giving you data based on the input token.
Note: You may also need to modify the process_response behaviour since you may not need to send back Set-Cookie headers.

Django: CSRF check only when certain conditions apply

I have a simple REST API that needs to be accessible both from a web app as well as from a remote service.
The remote service is authenticated via a custom HTTP header containing an API key.
How can I protect that API such that requests from a web browser are CSRF protected, but the CSRF check is not done when authenticated via API key? Or, in general, how can I enable CSRF protection for some requests on a specific view, but not others?
Currently, I have a decorator that checks the request for an API key and authenticates the API user roughly this way:
# Regular auth
if request.user.is_authenticated():
# DO CSRF verification, then continue calling the view
elif 'HTTP_X_API_KEY' in request.META:
api_key = request.META['HTTP_X_API_KEY']
user = authenticate(username=settings.API_USER_NAME, password=api_key)
login(request, user)
# If user is authenticated and autzorized, continue calling the view
# but WITHOUT invoking CSRF protection
The problem as stated is, that I only want CSRF protection for regular users, but not for the API user.
Okay, so after a bit more of tinkering around, the solution was to disable the CSRF middleware and enable csrf_protect for all cases where CSRF protection is required.
This works in the special case of the API, as every call is being decorated anyway, so the risk of some view being forgotten is negligible.
What does not work however, is the other way around, using csrf_exempt. The problem there is that the decorator only sets a csrf_exempt property on the view, which, if you have multiple decorators, may be masked again, even if you use functools.wraps.
Also, since csrf_exempt puts a property on the view, it cannot be dynamically enabled or disabled based on the request's content - it's really a very static thing to do.

Are there authentication examples with Django and Tastypie?

Are there basic authentication examples with Django and Tastypie?. I'm a little bit confused about how the authentication in Django works, specially with Tastypie.I wanna know how the authentication works with api keys and how to authenticate a user with the built-in User model which Django has. Any suggestion or code are really appreciated.
Thanks.
Just to answer your questions regarding authentication:
How the authentication in Django works?
Django authentication required SessionMiddleware to work. Once a session has been loaded, the Django authentication backend reads a special cookie _auth_user (IIRC) which contains currently logged in user's ID. If you have access to the django shell, you can manipulate it and make yourself logged in as any user! Once the backend notices there is a _auth_user key, it then adds a lazy User object to the request (so it delays the User.objects.get(...) until it is really needed). If there is no such key in the session dict, the user is claimed to be anonymous and an instance of AnonymousUser is added to the request object instead.
How does the authentication work in Tastypie?
Before your resource view is executed, a Resource.is_authenticated(request) method is called, which in turn calls the is_authenticated(request) method of the authentication backend of your the Resource of your choice. If the method returns False, the authentication is claimed to be failed and returns with Unauthorized error. If the method returns a HttpResponse, the response is returned instead. If the method returns True, the request is claimed to have been authenticated.
How does User model authentication work in Tastypie?
The User model authentication can be performed using SessionAuthentication backend provided by the Tastypie itself. What it does is creating a session for the current request so that the authentication middleware can then automatically insert relevant user model to the request. Notice that for this method to work, your API client has to support storing cookies and resending them in future requests.
You might find this useful. It allows you to authenticate the user based on the Django session cookie.
https://github.com/amezcua/TastyPie-DjangoCookie-Auth/blob/master/DjangoCookieAuth.py
I am using this in my application and it works!