I want to enable basic access authentication in my Django project like this:
I found this post by Google, and changed my settings.py following the first answer:
MIDDLEWARE_CLASSES = (
...
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.RemoteUserMiddleware',
...
)
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.RemoteUserBackend',
)
But the authentication window doesn't come out. The project is still in debug mode and I run it by python ./manage.py runserver.
I can think of multiple ways to do this. If you want your entire django application protected by basic authentication then you can add an authentication middleware to your wsgi app. Django creates a default wsgi application in your project. Add the following middleware to this wsgi.py file:
class AuthenticationMiddleware(object):
def __init__(self, app, username, password):
self.app = app
self.username = username
self.password = password
def __unauthorized(self, start_response):
start_response('401 Unauthorized', [
('Content-type', 'text/plain'),
('WWW-Authenticate', 'Basic realm="restricted"')
])
return ['You are unauthorized and forbidden to view this resource.']
def __call__(self, environ, start_response):
authorization = environ.get('HTTP_AUTHORIZATION', None)
if not authorization:
return self.__unauthorized(start_response)
(method, authentication) = authorization.split(' ', 1)
if 'basic' != method.lower():
return self.__unauthorized(start_response)
request_username, request_password = authentication.strip().decode('base64').split(':', 1)
if self.username == request_username and self.password == request_password:
return self.app(environ, start_response)
return self.__unauthorized(start_response)
Then, instead of calling
application = get_wsgi_application()
You should use:
application = AuthenticationMiddleware(application, "myusername", "mypassword")
This will ensure that every request to your django server goes through basic authentication.
Please note that unless you're using HTTPS then basic authentication isn't secure and the user credentials will not be encrypted.
If you only want some of your views to be covered by basic authentication then you can modify the above class to be a function decorator :
def basic_auth_required(func):
#wraps(func)
def _decorator(request, *args, **kwargs):
from django.contrib.auth import authenticate, login
if request.META.has_key('HTTP_AUTHORIZATION'):
authmeth, auth = request.META['HTTP_AUTHORIZATION'].split(' ', 1)
if authmeth.lower() == 'basic':
auth = auth.strip().decode('base64')
username, password = auth.split(':', 1)
if username=='myusername' and password == 'my password':
return func(request, *args, **kwargs)
else:
return HttpResponseForbidden('<h1>Forbidden</h1>')
res = HttpResponse()
res.status_code = 401
res['WWW-Authenticate'] = 'Basic'
return res
return _decorator
Then you can decorate your views with this to activate basic authentication.
Note that the username/password are both hardcoded in the examples above. You can replace that with your own mechanism.
Hope this helps
As mentioned in the docs, the REMOTE_USER is set by the web server. Typically you will need to configure a web server like Apache or IIS to protect a site or a directory using HTTP Basic Authentication.
For debug purposes, I suggest setting a dummy user in the manage.py, say:
import os
from django.conf import settings
if settings.DEBUG:
os.environ['REMOTE_USER'] = "terry"
Related
I'm trying to build a Single Page Application with Django Rest Framework. For authentication, I'm using a login view that initiates a session and requires csrf protection on all api routes. Because there is no templating going on, the csrf_token tag is never used, so I have to manually get the token with get_token. Instead of putting it in the main index file that will be given in the home view, I want to set it on its own cookie. No this is not the CSRF Cookie that django provides, as that one has the CSRF secret, plus I mentioned I'm using sessions, so the secret is stored there. This cookie will have the token which will be used for all mutating requests.
I have tried everything to get django to accept the cookie but nothing has worked. I can login just fine the first time, because there was no previous session, but anything after that just throws an error 403. I tried using ensure_csrf_cookie but that didn't help. I tried without sessions and still nothing. I even tried rearranging the middleware order and still nothing. I even tried my own custom middleware to create the cookie but it didn't work. Here's the code that I have ended up with as of now:
views.py
#api_view(http_method_names = ["GET"])
def home(request):
"""API route for retrieving the main page of web application"""
return Response(None, status = status.HTTP_204_NO_CONTENT);
class LoginView(APIView):
"""API endpoint that allows users to login"""
def post(self, request, format = None):
"""API login handler"""
user = authenticate(username = request.data["username"], password = request.data['password']);
if user is None:
raise AuthenticationFailed;
login(request, user);
return Response(UserSerializer(user).data);
class LogoutView(APIView):
"""API endpoint that allows users to logout of application"""
def post(self, request, format = None):
logout(request);
return Response(None, status = status.HTTP_204_NO_CONTENT);
settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware'
]
# Sessions
SESSION_ENGINE = "django.contrib.sessions.backends.file"
SESSION_FILE_PATH = os.getenv("DJANGO_SESSION_FILE_PATH")
SESSION_COOKIE_AGE = int(os.getenv("SESSION_LIFETIME")) * 60
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_NAME = os.getenv("SESSION_COOKIE")
SESSION_COOKIE_SECURE = os.getenv("APP_ENV") != "local"
# CSRF
CSRF_USE_SESSIONS = True
# the following setting is for the csrf token only, not for the CSRF secret, which is the default for django
CSRF_TOKEN_CARRIER = os.getenv("XSRF_COOKIE")
CSRF_HEADER_NAME = "X-XSRF-TOKEN"
CSRF_COOKIE_SECURE = SESSION_COOKIE_SECURE
CSRF_COOKIE_AGE = SESSION_COOKIE_AGE
CSRF_TOKEN_HTTPONLY = False
REST_FRAMEWORK = {
"EXCEPTION_HANDLER":"django_app.application.exceptions.global_exception_handler",
"DEFAULT_AUTHENTICATION_CLASSES":[
"rest_framework.authentication.SessionAuthentication"
]
}
this was the custom middleware I wrote but I'm not using right now:
"""Custom CSRF Middleware for generating CSRF cookies"""
from django.conf import settings;
from django.middleware.csrf import CsrfViewMiddleware, rotate_token, get_token;
class CSRFCookieMiddleware:
"""Sets CSRF token cookie for ajax requests"""
def __init__(self, get_response):
self.get_response = get_response;
def __call__(self, request):
response = self.get_response(request);
if settings.CSRF_USE_SESSIONS:
response.set_cookie(
settings.CSRF_TOKEN_CARRIER,
get_token(request),
max_age=settings.CSRF_COOKIE_AGE,
domain=settings.CSRF_COOKIE_DOMAIN,
path=settings.CSRF_COOKIE_PATH,
secure=settings.CSRF_COOKIE_SECURE,
httponly=settings.CSRF_COOKIE_HTTPONLY,
samesite=settings.CSRF_COOKIE_SAMESITE,
);
return response;
the error 403 response:
HTTP/1.1 403 Forbidden
Date: Thu, 25 Apr 2019 17:11:28 GMT
Server: WSGIServer/0.2 CPython/3.7.0
Content-Type: application/json
Vary: Accept, Cookie
Allow: POST, OPTIONS
X-Frame-Options: SAMEORIGIN
Content-Length: 59
{
"message": "CSRF Failed: CSRF token missing or incorrect."
}
this is the http request I use in my REST client in vs code:
POST http://electro:8000/api/logout
X-XSRF-TOKEN: JFaygAm49v6wChT6CcUJaeLwq53YwzAlnEZmoE0m21cg9xLCnZGvTt6oM9MKbvV8
Cookie: electro=nzzv64gymt1aqu4whdhax1s9t91c3m58
I cannot believe how hard it is to tweak frameworks to work with single page apps when there's plenty support for static websites and APIs. So where have I gone wrong?
I finally figured out what happened. Buried deep in the django documentation, I found out that the CSRF_HEADER_NAME setting has a specific syntax/format:
# default value
CSRF_HEADER_NAME = "HTTP_X_CSRFTOKEN";
so to fix this, the docs literally say that for my case I must set the value, according to my preferences, like so:
CSRF_HEADER_NAME = "HTTP_X_XSRF_TOKEN";
So now it can accept the token at X-XSRF-TOKEN header, along with session cookie. But since I'm using sessions with csrf, I must use the custom middleware I created (see question) to set the csrf token cookie manually. This is because ensure_csrf_cookie apparently only throws you the session cookie.
Lastly, if you need to protect the login route, since I'm using SessionAuthentication, I will need the custom middleware, ensure_csrf_cookie, and csrf_protect so that I can get a starting session with csrf token and then submit those when logging in:
#api_view(http_method_names = ["GET"])
#ensure_csrf_cookie
def home(request):
"""API route for retrieving the main page of web application"""
return Response(None, status = status.HTTP_204_NO_CONTENT);
#method_decorator(csrf_protect, 'dispatch')
#method_decorator(ensure_csrf_cookie, 'dispatch')
class LoginView(APIView):
"""API endpoint that allows users to login"""
def post(self, request, format = None):
"""API login handler"""
user = authenticate(username = request.data["username"], password = request.data['password']);
if user is None:
raise AuthenticationFailed;
login(request, user);
return Response(UserSerializer(user).data);
may this help whoever is building a Single Page App backend with django
The documentation seems to be misleading regarding the replacement of hyphen to underscores.
It states:
Default: 'HTTP_X_CSRFTOKEN'
The name of the request header used for CSRF authentication.
As with other HTTP headers in request.META, the header name received from the server is normalized by converting all characters to uppercase, replacing any hyphens with underscores, and adding an 'HTTP_' prefix to the name. For example, if your client sends a 'X-XSRF-TOKEN' header, the setting should be 'HTTP_X_XSRF_TOKEN'.
I was using fetch, which lowercases all header names.
I named my client side header 'X_CSRFTOKEN', which got sent out as 'x_csrftoken'.
The request did not work until I changed the name to 'x-csrftoken'.
I found a line in Django that converts underscores to hyphens, which may be the problem.
tl;dr: To match Django's default HTTP_X_CSRFTOKEN, name your client header x-csrftoken.
After searching through the Python Flask-Login, I found ways to utilize header/api-key authentication instead of the default cookie authentication.
https://flask-login.readthedocs.io/en/latest/#disabling-session-cookie-for-apis
However, in the initial / request, Flask still responds with a cookie in the session. The above method only ensures that any new request (#login_required) is able to authenticate without the cookie (and use header or whatever method my implementation requires).
Is there a way to disable it? Or am I missing something.
Found a way. Created a 'CustomSessionInterface' and configured it.
app = Flask(__name__)
app.config.update(
DEBUG = True,
SECRET_KEY = 'secret_xxx'
)
#user_loaded_from_header.connect
def user_loaded_from_header(self, user=None):
g.login_via_header = True
class CustomSessionInterface(SecureCookieSessionInterface):
"""Disable default cookie generation."""
def should_set_cookie(self, *args, **kwargs):
return False
"""Prevent creating session from API requests."""
def save_session(self, *args, **kwargs):
if g.get('login_via_header'):
print("Custom session login via header")
return
return super(CustomSessionInterface, self).save_session(*args,
**kwargs)
app.session_interface = CustomSessionInterface()
login_manager = LoginManager()
login_manager.init_app(app)
The should_set_cookie() should return False.
I have a heroku app using django at example.herokuapp.com.
I also have a custom domain that points to this heroku app at example.com
How can I make it so that any time someone goes to example.herokuapp.com, it automatically redirects to my custom domain at example.com?
I essentially only want users to see the url example.com even if they type in example.herokuapp.com
Keep in mind that this is a django app. I could redirect every route to my custom domain, but I am wondering if there is any easier/better way to do this.
The simple solution is to just add middleware to the django app with a process_request() function that will be called everytime a route is requested.
I got the code from here: https://github.com/etianen/django-herokuapp/blob/master/herokuapp/middleware.py
Here is a file middelware.py that can be added:
from django.shortcuts import redirect
from django.core.exceptions import MiddlewareNotUsed
from django.conf import settings
SITE_DOMAIN = "example.com"
class CanonicalDomainMiddleware(object):
"""Middleware that redirects to a canonical domain."""
def __init__(self):
if settings.DEBUG or not SITE_DOMAIN:
raise MiddlewareNotUsed
def process_request(self, request):
"""If the request domain is not the canonical domain, redirect."""
hostname = request.get_host().split(":", 1)[0]
# Don't perform redirection for testing or local development.
if hostname in ("testserver", "localhost", "127.0.0.1"):
return
# Check against the site domain.
canonical_hostname = SITE_DOMAIN.split(":", 1)[0]
if hostname != canonical_hostname:
if request.is_secure():
canonical_url = "https://"
else:
canonical_url = "http://"
canonical_url += SITE_DOMAIN + request.get_full_path()
return redirect(canonical_url, permanent=True)
Lastly, be sure to add this class to the MIDDLEWARE_CLASSES list in the settings.py file.
It has been long time after #rishubk 's answer and I thought it can be better if I mention corrected CanonicalDomainMiddleware for currently Django 3.x and 4.x versions (and maybe for future ones).
Here how I changed, you can find:
https://github.com/berkaymizrak/Resume-Django-Web-App/blob/main/resume/CanonicalDomainMiddleware.py
As I see the basic changes are init function takes 2 arguments and you must use return self.get_response... if you want to do nothing instead of returning None.
from django.shortcuts import redirect
from django.core.exceptions import MiddlewareNotUsed
from django.conf import settings
class CanonicalDomainMiddleware(object):
"""Middleware that redirects to a canonical domain."""
def __init__(self, get_response):
self.get_response = get_response
if settings.DEBUG or not settings.SITE_DOMAIN:
raise MiddlewareNotUsed
def __call__(self, request):
"""If the request domain is not the canonical domain, redirect."""
hostname = request.get_host().split(":", 1)[0]
# Don't perform redirection for testing or local development.
if hostname in ("testserver", "localhost", "127.0.0.1"):
return self.get_response(request)
# Check against the site domain.
canonical_hostname = settings.SITE_DOMAIN.split(":", 1)[0]
if hostname == canonical_hostname:
return self.get_response(request)
else:
if request.is_secure():
canonical_url = "https://"
else:
canonical_url = "http://"
canonical_url += settings.SITE_DOMAIN + request.get_full_path()
return redirect(canonical_url, permanent=True)
I have my Flask app hosted in IIS in our intranet. In Flask, I'm able to get the www-authenticate header, but I need to determine the windows username. I did have Basic Authentication enabled and was able to parse out the username via that method, but I want this to be transparent to the user. In IE I have the option set to auto login to intranet sites so they're not prompted for a username and password.
I am able to get a string that can either begin with NTLM or Negotiate (depending on the setting in IIS) and a long auth string. What is a reliable way I can decode this in python/Flask?
Got it.
class RemoteUserMiddleware(object):
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
user = environ.pop('HTTP_X_PROXY_REMOTE_USER', None)
environ['REMOTE_USER'] = user
return self.app(environ, start_response)
app.wsgi_app = RemoteUserMiddleware(app.wsgi_app)
Then in the view by doing this:
username = str(request.environ.get('LOGON_USER'))
So I'm working with AppEngine (Python) and what I want to do is to provide OpenID Login setting a Default provider so the user can Log-In without problems using that provider. The thing is, I want to prompt the user a Password right after they login in order to show static content (HTML Pages); If the user doesn't enter the correct password then I want to redirect them to another page. The protection has to be server side please :) Any Ideas??
P.S. I'm seeking for a solution similar to ".htaccess/htpasswd" but for app engine.
AFAIK, GAE does not support such setup (static password after OpenID login).
The only way I see to make this work would be to serve static content via your handler:
Client makes a request for static content
Your handler is registered to handle this URL
Handler checks is user is authenticated. If not, requests a password.
When authenticated, handler reads static file and sends it back to user.
Try this out, you can mimic the .htaccess style password with Google App Engine:
def basicAuth(func):
def callf(webappRequest, *args, **kwargs):
# Parse the header to extract a user/password combo.
# We're expecting something like "Basic XZxgZRTpbjpvcGVuIHYlc4FkZQ=="
auth_header = webappRequest.request.headers.get('Authorization')
if auth_header == None:
webappRequest.response.set_status(401, message="Authorization Required")
webappRequest.response.headers['WWW-Authenticate'] = 'Basic realm="Kalydo School"'
else:
# Isolate the encoded user/passwd and decode it
auth_parts = auth_header.split(' ')
user_pass_parts = base64.b64decode(auth_parts[1]).split(':')
user_arg = user_pass_parts[0]
pass_arg = user_pass_parts[1]
if user_arg != "admin" or pass_arg != "foobar":
webappRequest.response.set_status(401, message="Authorization Required")
webappRequest.response.headers['WWW-Authenticate'] = 'Basic realm="Secure Area"'
# Rendering a 401 Error page is a good way to go...
self.response.out.write(template.render('templates/error/401.html', {}))
else:
return func(webappRequest, *args, **kwargs)
return callf
class AuthTest(webapp.RequestHandler):
#basicAuth
def get(self):
....
How-To: Dynamic WWW-Authentication (.htaccess style) on Google App Engine