0

I'm using python and urllib module to POST my credentials to a generic django view for loging in, that's all. I tried some stuff based on the standard basic authentification pattern, modifying the authenticate header.... but all the same, It keeps throwing me a 403 forbidden code. I searched for an answer that address this matter, but as to me.... I couldn't find any.

CODE

from django.contrib.auth import views
urlpatterns = [
    path('accounts/login/', views.LoginView.as_view(template_name='accounts/login.html'), name='login'),
]

settings.py

"""
Django settings for tests project.

Generated by 'django-admin startproject' using Django 2.2.17.

For more information on this file, see
https://docs.djangoproject.com/en/2.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.2/ref/settings/
"""

import os

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'ukqp@#s^bo)wcl)qr#fc1%+-1rm(f1-6(8trin3rl)6=dbwt@9'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
LOGIN_REDIRECT_URL = '/catalog/'
ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'catalog.apps.CatalogConfig',
    'accounts.apps.AccountsConfig',
]

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',
]

ROOT_URLCONF = 'tests.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
                'catalog.context_processors.muelte',
            ],
        },
    },
]

STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static'),
]

WSGI_APPLICATION = 'tests.wsgi.application'


# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}


# Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/

STATIC_URL = '/static/

' My Attempt

import urllib.parse, urllib.request
base = 'http://localhost:8000/'
login_path = 'accounts/login/'
blurl = urllib.parse.urljoin(base, login_path)
headers = {
    'username': 'l0new0lf',
    'password': 'ColdSOul',
}
rq = urllib.request.Request(blurl, method='post', headers=headers)
print(urllib.request.urlopen(rq))

Traceback:

Traceback (most recent call last):
  File "/home/l0new0lf/Desktop/algo.py", line 11, in <module>
    print(urllib.request.urlopen(rq))
  File "/usr/lib/python3.8/urllib/request.py", line 222, in urlopen
    return opener.open(url, data, timeout)
  File "/usr/lib/python3.8/urllib/request.py", line 531, in open
    response = meth(req, response)
  File "/usr/lib/python3.8/urllib/request.py", line 640, in http_response
    response = self.parent.error(
  File "/usr/lib/python3.8/urllib/request.py", line 569, in error
    return self._call_chain(*args)
  File "/usr/lib/python3.8/urllib/request.py", line 502, in _call_chain
    result = func(*args)
  File "/usr/lib/python3.8/urllib/request.py", line 649, in http_error_default
    raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 403: Forbidden

####EDIT 1####

I noted i was missing csrf token, added it to POST headers, but still the same error. Though this approach doesnt work, because AFAIK each csrf token is generated for each request independently.

import urllib.parse, urllib.request
from bs4 import BeautifulSoup
base = 'http://localhost:8000/'
login_path = 'accounts/login/'
blurl = urllib.parse.urljoin(base, login_path)
headers = {
    
    'username': 'l0new0lf',
    'password': 'ColdSOul',
}
get_rq = urllib.request.urlopen(blurl)
soup = BeautifulSoup(get_rq.read(), features='lxml')
form = soup.select_one('form')
csrf = form.select_one('form input[name^="csrf"]')
headers.update(((csrf['name'], csrf['value']),))
rq = urllib.request.Request(blurl, method='post', headers=headers)
print(urllib.request.urlopen(rq))

PD: I don't come across this problem only when authenticating, but also in POSTing any data to any view. Also, i do accept answers based on requests module too, if more convenient.

Many thanks in advance.

L0neW0lf
  • 33
  • 7

2 Answers2

0

By Default Django Views has CSRF token checking

Case 1. Check this answer here for making your api testing code to work

Case 2. If you want to remove this feature use the function csrf_exempt

    urlpatterns = [
    path('accounts/login/', csrf_exempt(views.LoginView.as_view(template_name='accounts/login.html'), name='login')),  ]  

Case 3. Try testing the same in your login page form as documented here

If you are still facing 403 issue double check your login credentials

Renjith Thankachan
  • 4,178
  • 1
  • 30
  • 47
  • Please check my EDIT #1, i make two requests, a GET to fetch the csrf_token value then add it to the POST request. – L0neW0lf Mar 12 '21 at 06:07
  • 1. Have you tried https://docs.djangoproject.com/en/3.1/topics/auth/default/#django.contrib.auth.authenticate for checking whether your login credentials are okey? try running the function in your django management shell 2. Try to run the code i mentioned in Case 1 and check the api response – Renjith Thankachan Mar 13 '21 at 02:19
0

I inquired a little on what csrf is, and how to communicate with webserver that make use of this security measure (what decent webservers doesn't have this nowadays?), and thanks to @Renjith Thankachan answer which poured me the light, AFAIK there are only two ways of doing it.

The conventional and 1st Way:

i realized that one needs add "csrfmiddlewaretoken" and its respective value with the format key=value to the POST body, also as a header except that this time its typed as just "csrftoken", otherwise it won't work.

Through Headers and 2nd Way: This is for cases when you opt for performing XMLHtppRequest, Django states in the docs that one may specify a header with "X-CSRFToken" as its name, so you can get rid of the hassle of filling up the POST body with the csrf token. Just as it states below.

AJAX

While the above method can be used for AJAX POST requests, it has some inconveniences: you have to remember to pass the CSRF token in as POST data with every POST request. For this reason, there is an alternative method: on each XMLHttpRequest, set a custom X-CSRFToken header (as specified by the CSRF_HEADER_NAME setting) to the value of the CSRF token. This is often easier because many JavaScript frameworks provide hooks that allow headers to be set on every request.

REPRESENTED IN CODE:

import urllib.parse, urllib.request
from bs4 import BeautifulSoup
import requests

base = 'http://localhost:8000/'
login_path = 'accounts/login/'
blurl = urllib.parse.urljoin(base, login_path)
get = urllib.request.urlopen(blurl)
form = {
    'username': 'l0new0lf',
    'password': 'DonTMind',
    #PASS CSRF TOKEN THROUGH POST BODY, THIS IS THE USUAL APPROACH
    'csrfmiddlewaretoken' : get.headers['set-cookie'].split('=', maxsplit=1)[1].split(';', maxsplit=1)[0],
}

csrfcookie = get.headers['set-cookie']
data = bytearray()
first = True

for item in reversed(form.items()):
    if not first:
        data += b'&'
    else:
        first = False
    data += item[0].encode('ascii') + b'=' + item[1].encode('ascii')
data = bytes(data)

rq = urllib.request.Request(blurl, data=data, method='post')
rq.add_header('Cookie' , csrfcookie)
rq.add_header('Content-Type', 'application/x-www-form-urlencoded')
rq.add_header('Content-Length', str(len(data)))
#PASS CSRF TOKEN THROGH AN HTTP HEADER, this is better since we don't need to be careful about passing it in to the body each time we make a post request.
#rq.add_header('X-CSRFToken', csrfcookie)

try:
    urllib.request.urlopen(rq)
except Exception as e:
    print(e.status != 403)

Uncomment and comment as necessary to test the 2nd solution.

L0neW0lf
  • 33
  • 7