Django's Custom Authentication Middleware & Authentication Backend - django

I'm writing a custom Authentication middleware that check the incoming requests for the "Authorization" key in the header, which contains a token.
I'm using this token to check with a third-party (Microsoft Graph) for the validity of the user. MS Graph will respond with an object like below
# the response object
{
'#odata.context': 'https://graph.microsoft.com/v1.0/$metadata#users/$entity',
'businessPhones': ['xxx'],
'displayName': 'xxx',
'givenName': 'xxx',
'id': 'xxx',
'jobTitle': None,
'mail': 'xxx',
'mobilePhone': None,
'officeLocation': None,
'preferredLanguage': 'xxx',
'surname': 'xxx',
'userPrincipalName': 'xxx'
}
EDIT: Adding custom middleware code here:
class AuthenticationMiddleware(MiddlewareMixin):
if not request.user.is_authenticated:
if "Authorization" in request.headers:
# Make a request to MS Graph with the given token
# to get user details and append to request
token = request.headers["Authorization"]
elif "accessToken" in request.GET:
token = request.GET["accessToken"]
else:
token = None
if token:
url = "https://graph.microsoft.com/v1.0/me/"
payload = {}
headers = {"Authorization": "Bearer {}".format(token)}
response = requests.request("GET", url, headers=headers, data=payload)
if response.ok:
request.custom_user = response.json()
else:
request.custom_user = AnonymousUser
else:
request.custom_user = AnonymousUser
Now I want to design this to work just like Django's default authentication backend with proper group and permission. How can I work on a LazyObject to be able to check for user's group membership and permission?
UPDATE
It looks like there's also a custom backend authentication that works like this.
Is it doing the same thing as I'm doing with the middleware?
from django.contrib.auth.backends import BaseBackend
class MyBackend(BaseBackend):
def authenticate(self, request, token=None):
# Check the token and return a user.
...

you should custom an Middleware like the below, and add it to middlewares in settings
class SimpleMiddleware:
def __init__(self, get_response):
self.get_response = get_response
# One-time configuration and initialization.
def __call__(self, request):
# Code to be executed for each request before
# the view (and later middleware) are called.
response = self.get_response(request)
# todo: do something you want in response
return response
see also:https://docs.djangoproject.com/en/3.1/topics/http/middleware/
EDIT:
Is it doing the same thing as I'm doing with the middleware?
no, it's not.
the most different is that
The backend is used for connecting with database, and the middleware is aim to process the request. You can find more examples code in django.middleware package.
and if you want to custome how to save the infomation to database eg: customer the authenticate method, you should customer a backend for the work. otherwise you may custome an middleware to process all of the requests. Because it is easy to customize middleware.

Related

CSRF Origin check failed Django/Vue.JS

I am currently making a web app that uses Vue.JS for the frontend part and Django for the backend. I'm using django-rest-framework to communicate between the client and the server.
I would like to specify that everything works fine when using Postman for testing my requests
When trying to login or register (those are only the two features I have implemented yet) The server sends back the following response:
"CSRF Failed: Origin checking failed - http://localhost:8080 does not match any trusted origins." Status: 403
The csrftoken cookie is there and I made sure it's sent
Here are some details on my setup
Requests are sent using Axios. In main.js I overrode the following axios.defaults
axios.defaults.baseURL = 'http://localhost:8000';
axios.defaults.xsrfCookieName = 'csrftoken';
axios.defaults.xsrfHeaderName = 'X-CSRFToken';
axios.defaults.withCredentials = true;
And the login request is sent in this method in a Pinia user store:
async login(email, password)
{
try
{
let response = await axios.post('authentication/login/', {email: email, password:
password});
this.setUserData(response.data.id, response.data.email);
}
catch(error)
{
console.log(error);
}
},
Server side:
My User view is written the following way:
def UserAuthenticationView(APIView):
serializer_class = UserSerializer
def get(self, request):
#retrieve currently logged-in user
pass
def post(self, request):
#register new user in database
pass
#api_view(['POST])
def login(request):
# decode request sent as json
body = json.loads(request.body)
# make sure all fields are present
try:
email = body['email']
password = body['password']
except KeyError:
return Response("Email or password are missing", status=400)
# validateEmail() and validatePassword() are custom functions that check the format of both
# fields
if (not validateEmail(email) or not validatePassword(password)):
return Response("Email or password are missing", status=400)
user = authenticate(email=email, password=password)
# check if authentication worked
if user is None:
return Response("Failed to authenticate user", status=400)
else:
# log user on session and return it
login(request, user)
return Response(UserAuthenticationView.serializer_class(user).data)
Inside settings.py I added the following line
CSRF_TRUSTED_ORIGINS = ["http://localhost:8080"]
But because my login view needs to access the body of the request I get an internal server error. From what I understand, the CSRF Middleware reads the body of the request first and I should use csrf_exempt() to prevent this behaviour
So in urls.py I tried the following with no success:
urlpatterns = [
path('', views.UserAuthenticationView.as_view()),
path('login/', csrf_exempt(views.UserAuthenticationView.login)),
What I don't understand either is that APIView is supposed to automatically csrf_exempt all views inside it but my register view, which corresponds to the post() method of UserAuthenticationView doesn't work for the same reason.

How to write custom authentication backend for one endpoint only (/metrics) in Django?

I have a custom middleware in Django to force all the requests to go through a login authentication (with few exceptions like api/token).
This project allows users to authenticate either via a JWT token or a login in
/admin/login and all unauthenticated users are redirected to /admin/login. for authentication.
We deployed the project in Kubernetes and we want Prometheus to scrape /metrics endpoint but we don't want it to be exposed to unauthenticated users. Prometheus allows for authentication with username and password. The thing is that when a request is sent to /metrics, because of the middleware, the request is redirected to /admin/login.
So I believe I need to write a custom authentication backend specifically designed for the metrics endpoint and place it before the other authentication methods.
The request always goes through the middleware first so it will always be redirected to /admin/login and then will go through the authentication backend.
What is the right way of doing this?
middleware.py
class LoginRequiredMiddleware(MiddlewareMixin):
def __init__(self, get_response):
self.get_response = get_response
def process_request(self, request):
assert hasattr(request, 'user')
path = request.path_info.lstrip('/')
if path == '' or path == '/':
return self.get_response(request)
url_is_exempt = any(url.match(path) for url in EXEMPT_URLS)
if request.user.is_authenticated or url_is_exempt:
# If the user is authenticated OR the URL is in the exempt list
# go to the requested page
return self.get_response(request)
else:
# Trying to access any page as a non authenticated user
return redirect(f"{settings.LOGIN_URL}?next=/{path}")
backends.py
class MetricsAuthBackend(BaseBackend):
def authenticate(self, request, username=None, password=None):
if '/metrics' in request.path:
if username == "username":
#need to fix this to use the hash of the password
pwd_valid = check_password(password, "password")
if pwd_valid:
user = User.objects.get(username=username)
return user
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None

Django: Authentication credentials not povided error as access and refresh tokens are set as HttpOnly cookies without Authorization header

I have set SimpleJWT access and refresh tokens as HttpOnly cookies. Therefore, I thought that I don't need to use the 'Authorization' header anymore, so I removed it. Now, when I'm making requests it's showing:
{"detail": "Authentication credentials were not provided."}
Here, I think I need to set the Authorization header to access token in the view. So far I have written this code but it is not working. I want it to set the Authorization header to the request and proceed:
def list(self, request):
access = request.COOKIES.get('access')
request.META['Authorization'] = f"Bearer {access}"
print(request.META.get('Authorization'))
serializer = self.serializer_class(instance = self.request.user)
return Response(serializer.data)
How do I send the Authorization header with the request if it's not coming from the client?
Define your custom JWT authentication view like this:
from rest_framework_simplejwt.authentication import JWTAuthentication
from django.conf import settings
class CustomAuthentication(JWTAuthentication):
def authenticate(self, request):
header = self.get_header(request)
if header is None:
raw_token = request.COOKIES.get(settings.SIMPLE_JWT['AUTH_COOKIE']) or None
else:
raw_token = self.get_raw_token(header)
if raw_token is None:
return None
validated_token = self.get_validated_token(raw_token)
return self.get_user(validated_token), validated_token
Here give path of custom JWT Token:
settings.py:
SIMPLE_JWT = {
........
'AUTH_COOKIE': 'access_token',
}
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'authentication.authenticate.CustomAuthentication',
),
}

request.POST returns old values after updating it in custom middleware - django 1.11.9

I am using django 1.11.9
I want to add client_id and client_secret to the django POST request.
Here is how my middleware.py file looks like:
class LoginMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
# One-time configuration and initialization.
def __call__(self, request):
# auth_header = get_authorization_header(request)
# Code to be executed for each request before
# the view (and later middleware) are called.
#Add Django authentication app client data to the request
request.POST = request.POST.copy()
request.POST['client_id'] = '12345678'
request.POST['client_secret'] = '12345678'
response = self.get_response(request)
# Code to be executed for each request/response after
# the view is called.
return response
Middleware is being successfully processed when I check it with a debugger. Thought when a view is called the 'client_id' and 'client_secret' fields are missing in the request.
After some experimenting i figure out that request is not getting updated and when it is called in a different view, it returns old values.
I am later using request in rest_framework_social_oauth2. And this is the point when 'client_id' and 'client_secret' disappear.
class ConvertTokenView(CsrfExemptMixin, OAuthLibMixin, APIView):
"""
Implements an endpoint to convert a provider token to an access token
The endpoint is used in the following flows:
* Authorization code
* Client credentials
"""
server_class = SocialTokenServer
validator_class = oauth2_settings.OAUTH2_VALIDATOR_CLASS
oauthlib_backend_class = KeepRequestCore
permission_classes = (permissions.AllowAny,)
def post(self, request, *args, **kwargs):
import pdb ; pdb.set_trace()
# Use the rest framework `.data` to fake the post body of the django request.
request._request.POST = request._request.POST.copy()
for key, value in request.data.items():
request._request.POST[key] = value
url, headers, body, status = self.create_token_response(request._request)
response = Response(data=json.loads(body), status=status)
for k, v in headers.items():
response[k] = v
return response
I need to add client_id and client_secret to the request body, so it can be later used by rest_framework_social_oauth2.
What could be the problem? How to properly update the request?
As you're working with request and processing a request, you have to implement process_request method, so the result will be something like:
class LoginMiddleware(object):
def process_request(self, request):
request.session['client_id'] = '12345678'
and then in your view:
def your_view(request):
client_id = request.session['client_id']

Test CSRF Verification with Django Rest Framework

I'm using Django Rest Framework 3 and would like to test the CSRF verification.
First, I initialize the DRF APIClient:
client = APIClient(enforce_csrf_checks=True)
Then I set a password on a user so I can login and get a session:
superuser.set_password('1234')
superuser.save()
client.login(email=superuser.email, password='1234')
Now we need a CSRF token. For that I simply create a request and retrieve the token from the cookies.
response = client.request()
csrftoken = client.cookies['csrftoken'].value
When inspecting the code, this seems to work, I get back a valid looking CSRF token. I then do the POST request, passing in the csrfmiddlewartoken parameter:
data = {'name': 'My fancy test report', 'csrfmiddlewaretoken': csrftoken}
response = client.post(API_BASE + '/reports', data=data, format='json')
assert response.status_code == status.HTTP_201_CREATED, response.content
The problem is, this fails:
tests/api/test_api.py:156: in test_csrf_success
assert response.status_code == status.HTTP_201_CREATED, response.content
E AssertionError: {"detail":"CSRF Failed: CSRF token missing or incorrect."}
E assert 403 == 201
E + where 403 = <rest_framework.response.Response object at 0x7f7bd6453bd0>.status_code
E + and 201 = status.HTTP_201_CREATED
What's the correct way to test CSRF verification with DRF?
EDIT
So, after researching this a bit, I discovered the following:
Django will not necessarily set a CSRF token in the header, unless it is rendering a template that explicitly has the csrf_token template tag included. This means that you need to request a page that renders a form with a csrf token, or you need to create a token-requesting view that is decorated with ensure_csrf_cookie.
Because the csrf token is unique per session, it is possible to create a generic token-setting view that looks something like the following:
from django.views.decorators.csrf import ensure_csrf_cookie
#ensure_csrf_cookie
def token_security(request):
return HttpResponse() # json or whatever
Then, any time you wish to POST to a CSRF protected endpoint and there is no CSRF token in the cookies, issue a GET against this view and it should set the cookie, which can then be used to POST.
Original answer below:
The following works in my tests (I am using factories to create User objects, but you could create them manually):
class TestLoginApi(APITestCase):
def setUp(self):
self.client = APIClient(enforce_csrf_checks=True)
self.path = reverse("registration:login")
self.user = UserFactory()
def tearDown(self):
self.client.logout()
def _get_token(self, url, data):
resp = self.client.get(url)
data['csrfmiddlewaretoken'] = resp.cookies['csrftoken'].value
return data
def test_login(self):
data = {'username': self.user.username,
'password': PASSWORD}
data = self._get_token(self.path, data)
# This should log us in.
# The client should re-use its cookies, but if we're using the
# `requests` library or something, we'd have to re-use cookies manually.
resp = self.client.post(self.path, data=data)
self.assertEqual(resp.status_code, 200)
etc.
If this is all done dynamically, you must also be sure that your view sets a cookie on the GET, because according to the Django docs (see the Warning), it will not be set automatically if you are not POSTing back from a template that had the {% csrf_token %} set.
If you need to set it, that looks something like this (in your DRF views.py):
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import ensure_csrf_cookie
#method_decorator(ensure_csrf_cookie)
def get(self, request, *args, **kwargs):
return SomeJson...
Finally, for my Django Rest Framework views, I had to make sure that the POSTs were csrf protected as well (but this does not look like a problem you are having):
from django.views.decorators.csrf import csrf_protect
#method_decorator(csrf_protect)
def post(self, request, *args, **kwargs):
return SomeJson...