Django: Basic Auth for one view (avoid middleware) - python

I need to provide http-basic-auth to one view.
I want to avoid modifying the middleware settings.
Background: This is a view which gets filled in by a remote application.

When you do a basic auth request, you're really adding credentials into the Authorization header. Before transit, these credentials are base64-encoded, so you need to decode them on receipt.
The following code snippet presumes that there's only one valid username and password:
import base64
def my_view(request):
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
token_type, _, credentials = auth_header.partition(' ')
expected = base64.b64encode(b'username:password').decode()
if token_type != 'Basic' or credentials != expected:
return HttpResponse(status=401)
# Your authenticated code here:
...
If you wish to compare to the username and password of a User model, try the following instead:
def my_view(request):
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
token_type, _, credentials = auth_header.partition(' ')
username, password = base64.b64decode(credentials).split(':')
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
return HttpResponse(status=401)
password_valid = user.check_password(password)
if token_type != 'Basic' or not password_valid:
return HttpResponse(status=401)
# Your authenticated code here:
...
Please note that this latter version is not extremely secure. At first glance, I can see that it is vulnerable to timing attacks, for example.

You can try a custom decorator (as seems to be the recommended way here and here) instead of adding new middleware:
my_app/decorators.py:
import base64
from django.http import HttpResponse
from django.contrib.auth import authenticate
from django.conf import settings
def basicauth(view):
def wrap(request, *args, **kwargs):
if 'HTTP_AUTHORIZATION' in request.META:
auth = request.META['HTTP_AUTHORIZATION'].split()
if len(auth) == 2:
if auth[0].lower() == "basic":
uname, passwd = base64.b64decode(auth[1]).decode(
"utf8"
).split(':', 1)
user = authenticate(username=uname, password=passwd)
if user is not None and user.is_active:
request.user = user
return view(request, *args, **kwargs)
response = HttpResponse()
response.status_code = 401
response['WWW-Authenticate'] = 'Basic realm="{}"'.format(
settings.BASIC_AUTH_REALM
)
return response
return wrap
Then use this to decorate your view:
from my_app.decorators import basicauth
#basicauth
def my_view(request):
...

This library could be used: https://github.com/hirokiky/django-basicauth
Basic auth utilities for Django.
The docs show how to use it:
Applying decorator to CBVs
To apply #basic_auth_requried decorator to Class Based Views, use
django.utils.decorators.method_decorator.
Source: https://github.com/hirokiky/django-basicauth#applying-decorator-to-cbvs

For those that already use django-rest-framework (DRF):
DRF has a BasicAuthentication class which, more-or-less, does what is described in the other answers (see source).
This class can also be used in "normal" Django views.
For example:
from rest_framework.authentication import BasicAuthentication
def my_view(request):
# use django-rest-framework's basic authentication to get user
user = None
user_auth_tuple = BasicAuthentication().authenticate(request)
if user_auth_tuple is not None:
user, _ = user_auth_tuple

Related

Django to return a view with TokenAuthentication for WebView

I am trying to create a flutter app which will use webview to display authenticated data from my Django App.
Steps Involved:
Flutter app sends authentication request
Django validates the user credentials (user id & Password) and returns authtoken
Flutter then sends a request via a webview to a url (which requires login).
I would like to login the user in webapp using this token and return the webview.
If the url does not require authentcation, it works like a charm.
When the url requires authentication, I am redirected to the login page and I want users to bypass that using token authentication which is already aquired in Step 1
here is my Django view.
class QuizTake(FormView):
permission_classes = (IsAuthenticated,)
form_class = QuestionForm
template_name = 'question.html'
result_template_name = 'result.html'
single_complete_template_name = 'single_complete.html'
login_template_name='login.html'
def dispatch(self, request, *args, **kwargs):
self.quiz = get_object_or_404(Quiz, url=self.kwargs['quiz_name'])
print(self.kwargs['quiz_name'])
"""
Authenticate if the request has token authentication
"""
if self.quiz.draft and not request.user.has_perm('quiz.change_quiz'):
raise PermissionDenied
try:
self.logged_in_user = self.request.user.is_authenticated()
except TypeError:
self.logged_in_user = self.request.user.is_authenticated
if self.logged_in_user:
self.sitting = Sitting.objects.user_sitting(request.user,
self.quiz)
else:
self.sitting = self.anon_load_sitting()
if self.sitting is False:
print("sitting false")
if self.logged_in_user:
return render(request, self.single_complete_template_name)
else:
redirecturl = "/login/?next=/quiz/"+self.kwargs['quiz_name']+"/take/"
return redirect(redirecturl)
return super(QuizTake, self).dispatch(request, *args, **kwargs)
Flutter Code
class _QuizLauncherState extends State<QuizLauncher> {
final String url, authtoken;
final int userId;
String quizUrl;
_QuizLauncherState(this.url, this.authtoken,this.userId);
void initState() {
quizUrl = 'https://test.mysite.com/quiz/$url/take';
print(quizUrl);
//for reference https://test.mysite.com/quiz/56df5d90-7f67-45ff-8fe1-7c07728ba9ab/take/
super.initState();
}
Completer<WebViewController> _controller = Completer<WebViewController>();
final Set<String> _favorites = Set<String>();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// This drop down menu demonstrates that Flutter widgets can be shown over the web view.
actions: <Widget>[
NavigationControls(_controller.future),
Menu(_controller.future, () => _favorites),
],
),
body: WebView(
javascriptMode: JavascriptMode.unrestricted,
onWebViewCreated: (WebViewController webViewController) {
Map<String, String> headers = {"Authorization": "Bearer " + authtoken};
webViewController.loadUrl(quizUrl, headers: headers);
},
),
);
}
}
Is this possible at all? If there are any alternate ways, please tell me. Basically, I am trying to access a url via webview which requires authentication, using authtoken. Please help.
You can use custom authentication classes like this, say if you are using Authorization header:
from rest_framework.authentication import BaseAuthentication
class MyCustomAuth(BaseAuthentication):
def authenticate(self, request):
auth_method, token = request.META['HTTP_AUTHORIZATION'].split(' ', 1)
# Get your user via the token here
if you_got_your_user:
return user, None
return None # or raise AuthFailedException
class QuizTake(FormView):
authentication_classes = (MyCustomAuth, )
This still depends on how your token identifies the user though. For example if you are using JWT, there are existing authentication classes already that handles this for you.
EDIT:
Looked at knox documentation from here. If you used knox, then you should probably use their own TokenAuthentication class. Can you try with below code:
from knox.auth import TokenAuthentication
class QuizTake(FormView):
authentication_classes = (TokenAuthentication, )
You can use authentication from rest framework lib like as below code.
import base64
import binascii
from django.contrib.auth import authenticate, get_user_model
from django.middleware.csrf import CsrfViewMiddleware
from django.utils.translation import gettext_lazy as _
from rest_framework import HTTP_HEADER_ENCODING, exceptions
def get_authorization_header(request):
auth = request.META.get('HTTP_AUTHORIZATION', b'')
if isinstance(auth, str):
auth = auth.encode(HTTP_HEADER_ENCODING)
return auth
class BaseAuthentication:
raise NotImplementedError(".authenticate() must be overridden.")
def authenticate_header(self, request):
pass
class SessionAuthentication(BaseAuthentication):
user = getattr(request._request, 'user', None)
if not user or not user.is_active:
return None
self.enforce_csrf(request)
return (user, None)
def enforce_csrf(self, request):
def dummy_get_response(request):
return None
check = CSRFCheck(dummy_get_response)
check.process_request(request)
reason = check.process_view(request, None, (), {})
if reason:
raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)
class TokenAuthentication(BaseAuthentication):
keyword = 'Token'
model = None
def get_model(self):
if self.model is not None:
return self.model
from rest_framework.authtoken.models import Token
return Token
Or go through the below link for better understanding
[Toke Authorization]

Django passthrough API request to hide credentials

I need to make a request to b.com/rest/foo to get json data for my application. I want to do it this way to protect the credentials rather than expose them on every page.
I found Consume an API in Django REST, server side, and serve it ,client side, in Angular and the corresponding answer at https://stackoverflow.com/a/65672890/2193381 to be a great starting point.
I created a local url to replicate the data that the external server will return and then tried the following
import requests
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required
from django.views.decorators.cache import never_cache
#never_cache
#login_required
def fakedata(request, item):
return JsonResponse({'item': item})
def getdata(request, item):
url = f"http://localhost:8080/rest/{item}"
username = os.getenv('SITE_USERNAME', None)
password = os.getenv('SITE_PASSWORD', None)
userpass = dict(username=username, password=password)
data = requests.get(
url,
auth=requests.auth.HTTPBasicAuth(**userpass),
)
if data is not None and data.status_code == 200:
try:
return JsonResponse(data.json(), safe=False)
except ValueError:
print("!JSON")
return JsonResponse({})
print("!data")
return JsonResponse({})
urlpatterns = [
path('rest/<str:item>', fakedata),
path('foo/<str:item>', getdata),
]
When I test it with
python manage.py runserver 8080
and call http://localhost:8080/foo/a, I am getting back the html of the login page and not the data from http://localhost:8080/rest/a that I am expecting.
What changes do I need to make to get it to work?
I just went through Django documentation and found this useful and working.
you can first authenticate the user by using the authenticate() method then login with request and call your fakedata() function with passing the request:
from django.contrib.auth import authenticate, login
user = authenticate(request, **userpass)
if user is not None:
login(request, user)
data = fakedata(request, item)
else:
pass

How can I authenticate a user with a query parameter on any url?

Let's say the user lands on https://example.com/any/page?token=hhdo28h3do782.
What's the recommended way to authenticate and login a user with the query string?
I was thinking about creating some sort of catch-all view (I'd also like to know how to do this :D) that calls authenticate(). Then I would have in place a custom backend that would authenticate the user.
Is this the ideal way to achieve what I want?
Cheers!
To do this, you need to create a custom authentication backend that validates api keys.
In this example, the request is checked for a valid token automatically. You don't need to modify and of your views at all. This is because it includes custom middleware that authenticates the user.
For brevity, I'm assuming that the valid user tokens are stored in a model that is foreign keyed to the django auth.User model.
# my_project/authentication_backends.py
from django.contrib import auth
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User
from django.contrib.auth.middleware import AuthenticationMiddleware
TOKEN_QUERY_PARAM = "token"
class TokenMiddleware(AuthenticationMiddleware):
def process_request(self, request):
try:
token = request.GET[TOKEN_QUERY_PARAM]
except KeyError:
# A token isn't included in the query params
return
if request.user.is_authenticated:
# Here you can check that the authenticated user has the same `token` value
# as the one in the request. Otherwise, logout the already authenticated
# user.
if request.user.token.key == token:
return
else:
auth.logout(request)
user = auth.authenticate(request, token=token)
if user:
# The token is valid. Save the user to the request and session.
request.user = user
auth.login(request, user)
class TokenBackend(ModelBackend):
def authenticate(self, request, token=None):
if not token:
return None
try:
return User.objects.get(token__key=token)
except User.DoesNotExist:
# A user with that token does not exist
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
Now, you can add the paths to AUTHENTICATION_BACKENDS and MIDDLEWARE in your settings.py in addition to any existing backends or middleware you may already have. If you're using the defaults, it would look like this:
MIDDLEWARE = [
# ...
"django.contrib.auth.middleware.AuthenticationMiddleware",
# This is the dotted path to your backend class. For this example,
# I'm pretending that the class is in the file:
# my_project/authentication_backends.py
"my_project.authentication_backends.TokenMiddleware",
# ...
]
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"my_project.authentication_backends.TokenBackend",
]
I assume you are using the Django REST Framework and also enabled the TokenAuthentication mechanism in your project. If so, go ahead with this,
from rest_framework.authentication import TokenAuthentication
class QueryParamAuthentication(TokenAuthentication):
query_param_name = 'token'
def authenticate(self, request):
token = request.query_params.get(self.query_param_name)
if token:
return self.authenticate_credentials(token)
return None
and then, change DRF DEFAULT_AUTHENTICATION_CLASSES as
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'dotted.path.to.QueryParamAuthentication'
),
# rest of your DRF settings...
}
Update
to do this without DRF, you have to write custom model backend (which is a bit lengthy topic)
Refer: Writing an authentication backend
So, start with a way of managing your tokens. Here's a basic model:
class Token(models.Model):
code = models.CharField(max_length=255)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
expires = models.DateTimeField()
A custom authentication backend can be produced to check the validity of the tokens:
class TokenAuthenticationBackend(ModelBackend):
def authenticate(self, request, token=None):
try:
token = Token.objects.get(code=token, expires__gte=now())
except Token.DoesNotExist:
return None
else:
return token.user
If you're using class-based views, you could write a mixin that checks for the presence of the token then does your authentication logic:
class UrlTokenAuthenticationMixin:
def dispatch(self, request, *args, **kwargs):
if 'token' in request.GET:
user = authenticate(request, request.GET['token'])
if user:
login(request, user)
return super(UrlTokenAuthenticationMixin, self).dispatch(request, *args, **kwargs)
To use this on a given view, just declare your views as follows:
class MyView(UrlTokenAuthenticationMixin, TemplateView):
# view code here
For example.
An alternative way to implement this as a blanket catch-all would be to use middleware rather than a mixin:
class TokenAuthMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if 'token' in request.GET:
user = authenticate(request, request.GET['token'])
if user:
login(request, user)
return self.get_response(request)

Basic auth protected views in DRF

I have some API endpoints that i need to protect using HTTP Basic Authentication in Django Rest Framework. There is BasicAuthentication in DRF, but that actually authenticates against a user in Django, which is not what I'm looking for.
I found a solution using a custom permission, but ti means monkey patching the views to set the correct authenticate header.
Is there a better way?
class BasicAuthPermission(permissions.BasePermission):
def has_permission(self, request, view):
credentials = view.credentials # Will raise AttributeError on missing credentials
realm = getattr(view, 'realm', 'Protected')
auth = request.headers.get('Authorization')
with suppress(ValueError, AttributeError):
auth = b64decode(auth.split()[-1]).decode()
if auth != credentials:
# Monkey patch style
view.get_authenticate_header = lambda r: f'Basic realm="{realm}"'
raise exceptions.AuthenticationFailed('Bad credentials.')
return True
Im my view:
class ProtectedApiView(generics.GenericAPIView):
permission_classes = [BasicAuthPermission]
credentials = 'user:password'
# ...
Following Arakkal's suggestion in comment, I did this with an Authentication class instead. It does feel less hacky, but I can not set credentials on the View, like I did originally.
I realize "anonymous authentication" is a weird name, but that's because Django doesn't know anything about the user. So for all practical purposes anonymous.
from base64 import b64decode
import binascii
from rest_framework import generics, exceptions, authentication
class AnonymousBasicAuthentication(authentication.BaseAuthentication):
"""
HTTP Basic authentication against preset credentials.
"""
www_authenticate_realm = 'api'
credentials: str = None
def authenticate(self, request):
try:
auth, encoded = authentication.get_authorization_header(request).split(maxsplit=1)
except ValueError:
raise exceptions.AuthenticationFailed('Invalid basic header.')
if not auth or auth.lower() != b'basic':
raise exceptions.AuthenticationFailed('Authentication needed')
try:
credentials = b64decode(encoded).decode(authentication.HTTP_HEADER_ENCODING)
except (TypeError, UnicodeDecodeError, binascii.Error):
raise exceptions.AuthenticationFailed('Invalid basic header. Credentials not correctly base64 encoded.')
if self.credentials != credentials:
raise exceptions.AuthenticationFailed('Invalid username/password.')
def authenticate_header(self, request):
return 'Basic realm="{}"'.format(self.www_authenticate_realm)
class MyAuthentication(AnonymousBasicAuthentication):
credentials = 'user:password'
class MyProtectedView(generics.GenericAPIView):
authentication_classes = [MyAuthentication]
# ...

Django using one time call function output as global value

While initializing the project, i want to call login function from projects settings for once and use the output token anywhere in project. I want to use same token whether I call again to check if token changed.
def login(creds):
r = requests.post(base_url + '/api/admin/login', json=creds, headers=headers, verify=False)
if r.status_code == 200:
token = r.json()['token']
return token
If i call function in project settings, function starts each time. I don't want to dump token to a file and read each time. Any other way? Thanks.
The best way would be to separate responsibilities for login, authentication and use of token. In practice you'd store token in session. Django provides full support for anonymous sessions, but this could limit your application in cases, when you'd like to store some data associated to that user in the local database.
What I recommend is to setup User model in the local database where you save usernames. Next you create a function for authentication:
import requests
def authentication(username, password):
creds = {'username': username, 'password': password}
headers = 'whatever'
r = requests.post(base_url + '/api/admin/login', json=creds, headers=headers, verify=False)
if r.status_code == 200:
token = r.json()['token']
# Create user in the local database if it does not exist yet
user, created = User.objects.get_or_create(username=username)
return token
else:
error = 'Something went wrong'
return error
Next you create login functionality.
from django.urls import reverse
from django.http import HttpResponseRedirect
from django.contrib.auth import login
from django.shortcuts import render
def login(request):
# Check if user is authenticated by call is_authenticated() and
# additionally check if token is stored in session
if request.user.is_authenticated and request.session.get('token', False):
return HttpResponseRedirect(reverse('user_page'))
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
auth_data = authenticate(username=username, password=password)
if auth_data is not None:
# Save token in session
request.session['user_token'] = auth_data
# Get user and login
# Because you created a user in local database,
# you can login that user in this step
user = User.objects.get(username=username)
login(request, user)
next = request.POST.get('next', '/') if request.POST.get('next') else '/'
return HttpResponseRedirect(next)
else:
# Implement some error handling here
return HttpResponseRedirect(reverse('login'))
else:
return render(request, 'login.html', {})
The next logical step would be to control this users tokens somehow. Decorators come to rescue. You create a decorator that checks token. For example something in this sense:
import functools
from django.contrib.auth import REDIRECT_FIELD_NAME
from urllib.parse import urlparse
from django.shortcuts import resolve_url
from django.contrib.auth.views import redirect_to_login
def token_required(func, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None):
#functools.wraps(func)
def decorated_function(request, *args, **kwargs):
# Check if token is saved in session
# Redirect to login page if not
if not request.session.get('token', False):
path = request.build_absolute_uri()
resolved_login_url = resolve_url(login_url or settings.LOGIN_URL)
# If the login url is the same scheme and net location then just
# use the path as the "next" url.
login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
current_scheme, current_netloc = urlparse(path)[:2]
if ((not login_scheme or login_scheme == current_scheme) and
(not login_netloc or login_netloc == current_netloc)):
path = request.get_full_path()
return redirect_to_login(
path, resolved_login_url, redirect_field_name)
return func(request, *args, **kwargs)
return decorated_function
You'd need to implement your logic here. This decorator only check if token is stored in session, nothing else. You'd then use this decorator in views as in
from django.contrib.auth import logout
#token_required
#login_required
def user_logout(request):
logout(request)
return HttpResponseRedirect('/')

Categories

Resources