allowedflare 2023.47.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2023, Christopher Covington
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.1
2
+ Name: allowedflare
3
+ Version: 2023.47.0
4
+ Summary: Authenticate to Django with JSON Web Tokens (JWTs) signed by Cloudflare Access
5
+ Project-URL: Homepage, https://github.com/covracer/allowedflare
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: BSD License
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Topic :: Internet :: WWW/HTTP :: Session
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: cryptography
13
+ Requires-Dist: pyjwt
14
+ Requires-Dist: requests
15
+
16
+ Authenticate to Django with JSON Web Tokens (JWTs) signed by Cloudflare Access. A Django reimplementation of https://developers.cloudflare.com/cloudflare-one/identity/authorization-cookie/validating-json/#python-example
17
+
18
+ To run the demo, export these environment variables
19
+ * `ALLOWEDFLARE_ACCESS_URL` https://your-organization.cloudflareaccess.com
20
+ * `ALLOWEDFLARE_AUDIENCE` 64-character hexidecimal string
21
+ * `ALLOWEDFLARE_PRIVATE_DOMAIN` your-domain.tld
22
+
23
+ Then run
24
+ ```
25
+ docker-compose up
26
+ ```
27
+
28
+ Configure Cloudflare Tunnel public hostname demodj.your-domain.tld to http://localhost:8001 or equivalent.
29
+
30
+ ### TODO
31
+ * Better login page
32
+ * Django REST Framework (DRF) support
33
+ * Grant users view permission to all models
34
+ * (Re-) authenticating proxy for different-domain front-ends, like https://developers.cloudflare.com/cloudflare-one/identity/authorization-cookie/cors/#send-authentication-token-with-cloudflare-worker but
35
+ - Setting username so it can be logged by gunicorn
36
+ - Rewriting origin redirects
37
+ - Setting the XmlHttpRequest(?) header to avoid redirects to the sign-in page
38
+ - Will the original CF_Authorization cookie need to be copied, similar to X-Forwarded-For?
@@ -0,0 +1,23 @@
1
+ Authenticate to Django with JSON Web Tokens (JWTs) signed by Cloudflare Access. A Django reimplementation of https://developers.cloudflare.com/cloudflare-one/identity/authorization-cookie/validating-json/#python-example
2
+
3
+ To run the demo, export these environment variables
4
+ * `ALLOWEDFLARE_ACCESS_URL` https://your-organization.cloudflareaccess.com
5
+ * `ALLOWEDFLARE_AUDIENCE` 64-character hexidecimal string
6
+ * `ALLOWEDFLARE_PRIVATE_DOMAIN` your-domain.tld
7
+
8
+ Then run
9
+ ```
10
+ docker-compose up
11
+ ```
12
+
13
+ Configure Cloudflare Tunnel public hostname demodj.your-domain.tld to http://localhost:8001 or equivalent.
14
+
15
+ ### TODO
16
+ * Better login page
17
+ * Django REST Framework (DRF) support
18
+ * Grant users view permission to all models
19
+ * (Re-) authenticating proxy for different-domain front-ends, like https://developers.cloudflare.com/cloudflare-one/identity/authorization-cookie/cors/#send-authentication-token-with-cloudflare-worker but
20
+ - Setting username so it can be logged by gunicorn
21
+ - Rewriting origin redirects
22
+ - Setting the XmlHttpRequest(?) header to avoid redirects to the sign-in page
23
+ - Will the original CF_Authorization cookie need to be copied, similar to X-Forwarded-For?
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.1
2
+ Name: allowedflare
3
+ Version: 2023.47.0
4
+ Summary: Authenticate to Django with JSON Web Tokens (JWTs) signed by Cloudflare Access
5
+ Project-URL: Homepage, https://github.com/covracer/allowedflare
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: BSD License
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Topic :: Internet :: WWW/HTTP :: Session
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: cryptography
13
+ Requires-Dist: pyjwt
14
+ Requires-Dist: requests
15
+
16
+ Authenticate to Django with JSON Web Tokens (JWTs) signed by Cloudflare Access. A Django reimplementation of https://developers.cloudflare.com/cloudflare-one/identity/authorization-cookie/validating-json/#python-example
17
+
18
+ To run the demo, export these environment variables
19
+ * `ALLOWEDFLARE_ACCESS_URL` https://your-organization.cloudflareaccess.com
20
+ * `ALLOWEDFLARE_AUDIENCE` 64-character hexidecimal string
21
+ * `ALLOWEDFLARE_PRIVATE_DOMAIN` your-domain.tld
22
+
23
+ Then run
24
+ ```
25
+ docker-compose up
26
+ ```
27
+
28
+ Configure Cloudflare Tunnel public hostname demodj.your-domain.tld to http://localhost:8001 or equivalent.
29
+
30
+ ### TODO
31
+ * Better login page
32
+ * Django REST Framework (DRF) support
33
+ * Grant users view permission to all models
34
+ * (Re-) authenticating proxy for different-domain front-ends, like https://developers.cloudflare.com/cloudflare-one/identity/authorization-cookie/cors/#send-authentication-token-with-cloudflare-worker but
35
+ - Setting username so it can be logged by gunicorn
36
+ - Rewriting origin redirects
37
+ - Setting the XmlHttpRequest(?) header to avoid redirects to the sign-in page
38
+ - Will the original CF_Authorization cookie need to be copied, similar to X-Forwarded-For?
@@ -0,0 +1,9 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ allowedflare.egg-info/PKG-INFO
5
+ allowedflare.egg-info/SOURCES.txt
6
+ allowedflare.egg-info/dependency_links.txt
7
+ allowedflare.egg-info/requires.txt
8
+ allowedflare.egg-info/top_level.txt
9
+ django_allowedflare/__init__.py
@@ -0,0 +1,3 @@
1
+ cryptography
2
+ pyjwt
3
+ requests
@@ -0,0 +1 @@
1
+ django_allowedflare
@@ -0,0 +1,92 @@
1
+ import logging
2
+ from datetime import datetime, timedelta, timezone as datetime_timezone
3
+ from typing import Any
4
+
5
+ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
6
+ from django.contrib.auth.backends import ModelBackend
7
+ from django.contrib.auth.models import User
8
+ from django.http import HttpRequest
9
+ from django.utils import timezone as django_utils_timezone
10
+ from django.conf import settings
11
+ from jwt.algorithms import RSAAlgorithm
12
+ from jwt import decode, InvalidSignatureError
13
+ import requests
14
+
15
+
16
+ cache_updated = datetime.fromtimestamp(0, tz=datetime_timezone.utc)
17
+ cached_keys: list[RSAPublicKey] = []
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def clean_username(username: str) -> str:
22
+ return username.removesuffix(f'@{settings.ALLOWEDFLARE_PRIVATE_DOMAIN}')
23
+
24
+
25
+ def fetch_or_reuse_keys() -> list[RSAPublicKey]:
26
+ global cache_updated, cached_keys
27
+ now = django_utils_timezone.now()
28
+ # As of June 2023, signing keys are documented as rotated every 6 weeks
29
+ if cache_updated + timedelta(days=1) < now:
30
+ response = requests.get(f'{settings.ALLOWEDFLARE_ACCESS_URL}/cdn-cgi/access/certs').json()
31
+ if response.get('keys'):
32
+ decoded_keys = [RSAAlgorithm.from_jwk(key) for key in response['keys']]
33
+ cached_keys = [key for key in decoded_keys if isinstance(key, RSAPublicKey)]
34
+ cache_updated = now
35
+ return cached_keys
36
+
37
+
38
+ def decode_token(cf_authorization: str) -> dict:
39
+ for key in fetch_or_reuse_keys():
40
+ try:
41
+ return decode(
42
+ cf_authorization,
43
+ key=key,
44
+ audience=settings.ALLOWEDFLARE_AUDIENCE,
45
+ algorithms=['RS256'],
46
+ )
47
+ except InvalidSignatureError:
48
+ pass
49
+ return {}
50
+
51
+
52
+ def defaults_for_user(token: dict) -> dict:
53
+ return getattr(settings, 'ALLOWEDFLARE_DEFAULTS_FOR_USER', lambda token: {'is_staff': True})(
54
+ token
55
+ )
56
+
57
+
58
+ class Allowedflare(ModelBackend):
59
+ def authenticate(
60
+ self,
61
+ request: HttpRequest | None,
62
+ username: str | None = None,
63
+ password: str | None = None,
64
+ **kwargs: Any,
65
+ ) -> User | None:
66
+ if not request or 'CF_Authorization' not in request.COOKIES:
67
+ logger.warning('CF_Authorization cookie missing')
68
+ return None
69
+ token = decode_token(request.COOKIES['CF_Authorization'])
70
+ if not token:
71
+ logger.warning(f'Invalid CF_Authorization cookie {request.COOKIES["CF_Authorization"]}')
72
+ return None
73
+ if 'email' not in token:
74
+ logger.warning(f'Valid token missing email {token}')
75
+
76
+ user, new = User.objects.update_or_create(
77
+ username=clean_username(token['email']),
78
+ email=token['email'],
79
+ defaults=defaults_for_user(token),
80
+ )
81
+
82
+ if new:
83
+ logger.info(f'New user {user.email} authenticated with Allowedflare')
84
+ else:
85
+ logger.info(f'Existing user {user.email} authenticated with Allowedflare')
86
+ return user
87
+
88
+ def get_user(self, user_id: int) -> User | None:
89
+ try:
90
+ return User.objects.get(pk=user_id)
91
+ except User.DoesNotExist:
92
+ return None
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ['setuptools>=61.0', 'setuptools_scm[toml]>=6.2', 'wheel']
3
+ build-backend = 'setuptools.build_meta'
4
+
5
+ [project]
6
+ classifiers = [
7
+ 'Programming Language :: Python :: 3',
8
+ 'License :: OSI Approved :: BSD License',
9
+ 'Operating System :: OS Independent',
10
+ 'Topic :: Internet :: WWW/HTTP :: Session',
11
+ ]
12
+ description = 'Authenticate to Django with JSON Web Tokens (JWTs) signed by Cloudflare Access'
13
+ dependencies = [
14
+ 'cryptography',
15
+ 'pyjwt',
16
+ 'requests',
17
+ ]
18
+ dynamic = ['version']
19
+ name = 'allowedflare'
20
+ readme = 'README.md'
21
+
22
+ [project.urls]
23
+ Homepage = 'https://github.com/covracer/allowedflare'
24
+
25
+ [tool.black]
26
+ line-length = 100
27
+ skip-magic-trailing-comma = true
28
+ skip-string-normalization = true
29
+
30
+ [tool.django-stubs]
31
+ django_settings_module = 'demodj.settings'
32
+
33
+ [tool.ruff]
34
+ line-length = 100
35
+
36
+ [tool.mypy]
37
+ plugins = ['mypy_django_plugin.main']
38
+
39
+ [tool.setuptools]
40
+ packages = ['django_allowedflare']
41
+
42
+ [tool.setuptools_scm]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+