Need help reconfiguring LDAP on python web app - python

I've inherited a custom built web app built on python that currently utilizes our older LDAP instance that we are retiring, and I need to change it to use our MSAD LDAP schema for authorization instead. Anonymous bind is disabled in the MSAD, so I need to add a bind user/pass to connect to the MSAD LDAP instance, but don't know where/how. Below is the excerpt from the custom config.ini file and the authorization.py script
<config.ini>
ldap_server = ldap://ldap3.internaldomain.local
bind_organisation = cn=Company,ou=People
domain_components = dc=internaldomain,dc=local
allowed_groups =
allowed_users =
bind_timeout = 10.0
<authorization.py>
from flask import request, Response, render_template
from ldap_logging import LDAPLoggingClassWrapper
from global_config import config
import ldap
def check_auth(username, password):
try:
ldap_client = LDAPLoggingClassWrapper(ldap.initialize(config.get("LDAP", "ldap_server")))
ldap_client.set_option(ldap.OPT_NETWORK_TIMEOUT, config.getfloat("LDAP", "bind_timeout"), log_id=username)
basedn = "uid=%s,%s,%s" % (username, config.get("LDAP", "bind_organisation"), config.get("LDAP", "domain_components"))
ldap_client.simple_bind_s(basedn,password, log_id=username)
ldap_client.unbind_s(log_id=username)
return True
except (ldap.INVALID_CREDENTIALS, ldap.NO_SUCH_OBJECT) as e:
return False
def authenticate():
"""Sends a 401 response that enables basic auth"""
return Response(
'Authentication failed, you must sign in using your domain credentials to use this app.\n'
'Please contact ITS if you feel your details were correct and that this message is in error.', 401,
{'WWW-Authenticate': 'Basic realm="Please login with your domain credentials"'})
def requires_auth(f):
#wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
try:
if not auth or not check_auth(auth.username, auth.password):
return authenticate()
except Exception as e:
return render_template('server-down.html',
message = e
)
return f(*args, **kwargs)
return decorated

Related

Django - How to add access token to Client.post in Django test?

So I have some code below. Every endpoint has an authentication process, which is below as well. I want to be able to attach an access token, which is in cls.user to the Client.post so that I can test all the endpoints and ensure they are authenticating properly as well. How can I do this? So ideally I'd be attaching <bearer> <access token> to request.Meta['HTTP_AUTHORIZATION']
test.py
import json
from cheers.models import *
from warrant import Cognito
from django.urls import reverse
from django.test import TestCase
from rest_framework import status
from cheers.models import GoalCategory, Post
from dummy_factory.Factories import UserFactory, GoalFactory
class PostTest(TestCase):
#classmethod
# Generates Test DB data to persist throughout all tests
def setUpTestData(cls) -> None:
cls.goal_category = 'health'
GoalCategory.objects.create(category=cls.goal_category, emoji_url='url')
cls.user = UserFactory()
cls.goal = GoalFactory()
user_obj = User.objects.get(pk=cls.user.phone_number)
goal_obj = Goal.objects.get(pk=cls.goal.uuid)
Post.objects.create(creator_id=user_obj, goal_id=goal_obj, body='Some text')
cls.user = Cognito(<Some login credentials>)
cls.user.authenticate(password=<password>)
def test_create(self):
response = self.client.post(reverse('post'),
data=json.dumps({'creator_id': str(self.user.uuid),
'goal_id': str(self.goal.uuid),
'body': 'Some text #Test'}),
content_type='application/json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
Test authenticator function
def cognito_authenticator(view_func):
def wrapped_view(request, *args, **kwargs):
# Check the cognito token from the request.
token = request.META['HTTP_AUTHORIZATION'].split(' ')[1]
try:
jwt.decode_cognito_jwt(token)
except Exception:
# Fail if invalid
return Response("Invalid JWT", status=status.HTTP_401_UNAUTHORIZED) # Or HttpResponseForbidden()
else:
# Proceed with the view if valid
return view_func(request, *args, **kwargs)
return wrapped_view
You can set the header like this:
token = 'sometoken'
response = self.client.post(
reverse('post'),
data=json.dumps({
'creator_id': str(self.user.uuid),
'goal_id': str(self.goal.uuid),
'body': 'Some text #Test'
}),
content_type='application/json',
**{'HTTP_AUTHORIZATION': f'Bearer {token}'}
)
And then access the header using:
request.META['HTTP_AUTHORIZATION']

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: Basic Auth for one view (avoid middleware)

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

How to mock HTTP authentication in Flask for testing?

I'm just put HTTP authentication for my Flask application and my test is broken. How do I mock request.authentication to make the test pass again?
Here's my code.
server_tests.py
def test_index(self):
res = self.app.get('/')
self.assertTrue('<form' in res.data)
self.assertTrue('action="/upload"' in res.data)
self.assertEquals(200, res.status_code)
server.py
def check_auth(username, password):
"""This function is called to check if a username /
password combination is valid.
"""
return username == 'fusiontv' and password == 'fusiontv'
def authenticate():
"""Sends a 401 response that enables basic auth"""
return Response(
'Could not verify your access level for that URL.\n'
'You have to login with proper credentials', 401,
{'WWW-Authenticate': 'Basic realm="Login Required"'})
def requires_auth(f):
#wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
return authenticate()
return f(*args, **kwargs)
return decorated
#app.route("/")
#requires_auth
def index():
return render_template('index.html')
Referring to How do I mock dependencies of the views module of my Flask application in flask-testing?, you can mock it via the import chain.
Assuming server_tests imports application imports server, you probably want something like:
server_tests.py
def setUp(self):
application.server.request.authorization = MagicMock(return_value=True)

Password Protect one webpage in Flask app

I am running a Flask web app and using Apache basic authentication(with .htaccess and .htpasswd files) to password protect it. I want to password protect only one webpage in the app. When I password protect the html file for the webpage there is no effect and the webpage is still not password protected. Could this be because it is my python file that is calling the html file using render_template? I'm not sure how to fix this issue.
You need to restrict access to your endpoint. This snippet should get you started down the right path.
from functools import wraps
from flask import request, Response
def check_auth(username, password):
"""This function is called to check if a username /
password combination is valid.
"""
return username == 'admin' and password == 'secret'
def authenticate():
"""Sends a 401 response that enables basic auth"""
return Response(
'Could not verify your access level for that URL.\n'
'You have to login with proper credentials', 401,
{'WWW-Authenticate': 'Basic realm="Login Required"'})
def requires_auth(f):
#wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
return authenticate()
return f(*args, **kwargs)
return decorated
With this, you could decorate any endpoint you want to restrict with #requires_auth.
#app.route('/secret-page')
#requires_auth
def secret_page():
return render_template('secret_page.html')

Categories

Resources