Skip to content

Commit 9d59d0d

Browse files
committed
Initial commit
0 parents  commit 9d59d0d

File tree

13 files changed

+533
-0
lines changed

13 files changed

+533
-0
lines changed

.github/workflows/unit_tests.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
on: [push, pull_request]
2+
3+
jobs:
4+
pypi:
5+
runs-on: ubuntu-latest
6+
7+
strategy:
8+
matrix:
9+
python-versions: [ 3.6, 3.7, 3.8, 3.9 ]
10+
django-versions: [ 2.2.0, 3.0.0, 3.1.0 ]
11+
12+
steps:
13+
- name: Checkout repo
14+
uses: actions/checkout@v2
15+
16+
- name: Set up Python ${{ matrix.python-versions }}
17+
uses: actions/setup-python@v2
18+
with:
19+
python-version: ${{ matrix.python-versions }}
20+
21+
- name: Install dependencies
22+
run: |
23+
python -m pip install --upgrade pip
24+
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
25+
pip install Django~=${{ matrix.django-versions }}
26+
27+
- name: Run tests
28+
env:
29+
DJANGO_SETTINGS_MODULE: tests.settings
30+
run: |
31+
python django-admin.py test

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.idea/
2+
venv/
3+
build/
4+
dist/
5+
*.egg-info
6+
*.egg
7+
*.pyc

LICENSE

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
MIT License
2+
3+
Copyright 2021 Brendan Dalpe
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6+
7+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8+
9+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# django-vouch-proxy-auth
2+
Django Middleware enabling the use of the Vouch Proxy cookie for single sign-on.
3+
4+
This package subclasses Django's `RemoteUserMiddleware` and `RemoteUserBackend`.
5+
6+
## How it Works
7+
8+
1. The middleware checks for the presence of the Vouch Proxy cookie.
9+
2. If the cookie exists, it attempts to load a previous validation from Django cache.
10+
3. If the validation result is not in the Cache, send the contents of the `VouchCookie` cookie to the Vouch Proxy `/validate` endpoint.
11+
4. If the validation is successful, decode and decompress the cookie and extract the username from the JWT payload.
12+
5. Save the username in cache with a short expiration and use the SHA256 sum of the `VouchCookie` as the key. (i.e. `VouchCookie_` + `sha256sum(VouchCookie)`)
13+
14+
## Installation and Usage
15+
16+
`pip install django-vouch-proxy-auth` or add `django-vouch-proxy-auth` to your requirements file.
17+
18+
To enable the middleware, add `django_vouch_proxy_auth.middleware.VouchProxyMiddleware` after Django's `AuthenticationMiddleware`.
19+
20+
```python
21+
MIDDLEWARE = [
22+
'django.contrib.auth.middleware.AuthenticationMiddleware',
23+
...
24+
'django_vouch_proxy_auth.middleware.VouchProxyMiddleware'
25+
]
26+
```
27+
28+
This middleware is also dependent on the `VouchProxyUserBackend` Authentication Backend. Add anywhere in your `AUTHENTICATION_BACKENDS`.
29+
30+
```python
31+
AUTHENTICATION_BACKENDS = (
32+
'django_vouch_proxy_auth.backends.VouchProxyUserBackend'
33+
)
34+
```
35+
36+
Finally, you MUST tell the middleware where the `/validate` endpoint is. Add the `VOUCH_PROXY_VALIDATE_ENDPOINT` to your Django `settings.py` file.
37+
38+
```python
39+
VOUCH_PROXY_VALIDATE_ENDPOINT = 'https://login.avacado.lol/validate'
40+
```
41+
42+
## Settings
43+
### `VOUCH_PROXY_VALIDATE_ENDPOINT`
44+
Location of the Vouch Proxy validation endpoint. You MUST provide this value, or the Middleware will raise an `ImproperlyConfigured` exception.
45+
46+
### `VOUCH_PROXY_COOKIE_NAME`
47+
Default: `VouchCookie`
48+
49+
Change this setting if you are using a custom Vouch Proxy cookie name.
50+
51+
### `VOUCH_PROXY_CACHE_TIMEOUT`
52+
Default: `300` (seconds)
53+
54+
This middleware will cache the username if a successful response from the `/validate` query is returned. To reduce the load on Vouch Proxy, the middleware will only validate the cookie every 300 seconds (5 minutes) by default.
55+
56+
Set this value to a positive integer if you want to change the cache timeout.
57+
58+
Set this to `0` if you want Django to query the Vouch Proxy `/validate` endpoint on every request.
59+
60+
### `VOUCH_PROXY_CACHE_PREFIX`
61+
Default: defaults to the configured value for `VOUCH_PROXY_COOKIE_NAME` plus underscore (i.e. `VouchCookie_`)
62+
63+
Set this value if you want to change the prefix for the CacheKey.
64+
65+
### `VOUCH_PROXY_CACHE_BACKEND`
66+
Default: `default`
67+
68+
Set this value if you want to store cached results in a different cache.
69+
70+
### `VOUCH_PROXY_DISABLED_PATHS`
71+
Default: `[]`
72+
73+
Set this value (as an array) to full paths that you want to disable the middleware.
74+
75+
For example, if you have other middleware that causes conflict:
76+
```python
77+
VOUCH_PROXY_DISABLED_PATHS = ['/oidc/authenticate/', '/oidc/callback/']
78+
```
79+
80+
### `VOUCH_PROXY_CREATE_UNKNOWN_USER`
81+
Default: `True`
82+
83+
Set this to False if you do not want the middleware to automatically create a user entry on first login. You must use the
84+
85+
### `VOUCH_PROXY_FORCE_LOGOUT_IF_NO_COOKIE`
86+
Default: `False`
87+
88+
Set this to `True` if you want Django to logout the user if the Vouch Cookie is not present.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__version__ = "0.1.1"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from django.contrib.auth.backends import RemoteUserBackend
2+
from django.conf import settings
3+
4+
5+
class VouchProxyUserBackend(RemoteUserBackend):
6+
def __init__(self, *args, **kwargs):
7+
self.create_unknown_user = getattr(settings, 'VOUCH_PROXY_CREATE_UNKNOWN_USER', True)
8+
9+
super(VouchProxyUserBackend).__init__(*args, **kwargs)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from django.contrib import auth
2+
from django.contrib.auth.middleware import RemoteUserMiddleware
3+
from django.core.exceptions import ImproperlyConfigured
4+
from django.conf import settings
5+
from django.core.cache import caches
6+
import gzip
7+
import base64
8+
import jwt
9+
import requests
10+
import hashlib
11+
from requests import HTTPError
12+
13+
14+
class VouchProxyMiddleware(RemoteUserMiddleware):
15+
def __init__(self, *args, **kwargs):
16+
self.cookie_name = getattr(settings, 'VOUCH_PROXY_COOKIE_NAME', 'VouchCookie')
17+
self.cache_prefix = format(getattr(settings, 'VOUCH_PROXY_CACHE_PREFIX', '{}_'.format(self.cookie_name)))
18+
self.expiry_time = getattr(settings, 'VOUCH_PROXY_CACHE_TIMEOUT', 300)
19+
self.cache = caches[getattr(settings, 'VOUCH_PROXY_CACHE_BACKEND', 'default')]
20+
self.force_logout_if_no_cookie = getattr(settings, 'VOUCH_PROXY_FORCE_LOGOUT_IF_NO_COOKIE', False)
21+
22+
super(VouchProxyMiddleware).__init__(*args, **kwargs)
23+
24+
def process_request(self, request):
25+
if request.path in getattr(settings, 'VOUCH_PROXY_DISABLED_PATHS', []):
26+
return
27+
28+
if not hasattr(request, 'user'):
29+
raise ImproperlyConfigured(
30+
"The Django Vouch Proxy auth middleware requires the"
31+
" authentication middleware to be installed. Edit your"
32+
" MIDDLEWARE setting to insert"
33+
" 'django.contrib.auth.middleware.AuthenticationMiddleware'"
34+
" before the VouchProxyMiddleware class.")
35+
if not hasattr(settings, 'VOUCH_PROXY_VALIDATE_ENDPOINT'):
36+
raise ImproperlyConfigured(
37+
"You must provide a valid URL in VOUCH_PROXY_VALIDATE_ENDPOINT"
38+
" for the Vouch Proxy validation endpoint in your Django settings.")
39+
try:
40+
cookie = request.COOKIES[self.cookie_name]
41+
42+
cache_key = '{}{}'.format(self.cache_prefix, hashlib.sha256(cookie.encode('ascii')).hexdigest())
43+
username = self.cache.get(cache_key)
44+
if not username:
45+
validate = requests.get(settings.VOUCH_PROXY_VALIDATE_ENDPOINT, cookies={self.cookie_name: cookie})
46+
validate.raise_for_status()
47+
48+
# Vouch cookie is URL-safe Base64 encoded Gzipped data
49+
decompressed = gzip.decompress(base64.urlsafe_b64decode(cookie))
50+
payload = jwt.decode(decompressed, options={'verify_signature': False})
51+
username = payload['username']
52+
self.cache.set(cache_key, username, self.expiry_time)
53+
except (KeyError, HTTPError):
54+
# If specified header doesn't exist then remove any existing
55+
# authenticated remote-user, or return (leaving request.user set to
56+
# AnonymousUser by the AuthenticationMiddleware).
57+
if self.force_logout_if_no_cookie and request.user.is_authenticated:
58+
self._remove_invalid_user(request)
59+
return
60+
61+
# If the user is already authenticated and that user is the user we are
62+
# getting passed in the headers, then the correct user is already
63+
# persisted in the session and we don't need to continue.
64+
if request.user.is_authenticated:
65+
if request.user.get_username() == self.clean_username(username, request):
66+
return
67+
else:
68+
# An authenticated user is associated with the request, but
69+
# it does not match the authorized user in the header.
70+
self._remove_invalid_user(request)
71+
72+
# We are seeing this user for the first time in this session, attempt
73+
# to authenticate the user.
74+
user = auth.authenticate(request, remote_user=username)
75+
if user:
76+
# User is valid. Set request.user and persist user in the session
77+
# by logging the user in.
78+
request.user = user
79+
auth.login(request, user)

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
PyJWT>=2.0.0
2+
requests>=2.0.0

setup.cfg

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
[metadata]
2+
name = django-vouch-proxy-auth
3+
version = attr: django_vouch_proxy_auth.__version__
4+
author = Brendan Dalpe
5+
author_email = bdalpe@gmail.com
6+
description = Django Middleware to enable SSO using Vouch Proxy
7+
license = MIT
8+
keywords =
9+
sso
10+
django
11+
vouch
12+
url = https://github.com/bdalpe/django-vouch-proxy-auth
13+
long_description = file: README.md
14+
long_description_content_type = text/markdown
15+
classifiers =
16+
Development Status :: 5 - Production/Stable
17+
Intended Audience :: Developers
18+
Natural Language :: English
19+
License :: OSI Approved :: MIT License
20+
Programming Language :: Python
21+
Programming Language :: Python :: 3
22+
Programming Language :: Python :: 3 :: Only
23+
Programming Language :: Python :: 3.6
24+
Programming Language :: Python :: 3.7
25+
Programming Language :: Python :: 3.8
26+
Programming Language :: Python :: 3.9
27+
Topic :: Utilities
28+
Framework :: Django
29+
Framework :: Django :: 2.2
30+
Framework :: Django :: 3.0
31+
Framework :: Django :: 3.1
32+
33+
[options]
34+
zip_safe = false
35+
include_package_data = true
36+
python_requires = >= 3.6
37+
packages = find:
38+
install_requires =
39+
PyJWT >= 2.0.0
40+
requests >= 2.0.0
41+
42+
[options.packages.find]
43+
exclude =
44+
tests
45+
tests.*

setup.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env python3
2+
3+
from setuptools import setup
4+
5+
setup()

0 commit comments

Comments
 (0)