Skip to content

Commit d72f7a0

Browse files
committed
Add cognito user manager, use env vars
Add optional use of a user manager to hold the get_or_create function Use env vars for aws region, aws user pool
1 parent 933658e commit d72f7a0

File tree

8 files changed

+132
-19
lines changed

8 files changed

+132
-19
lines changed

config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@
6161
# The name of the AWS profile to use for the AWS client used for ingestion
6262
AWS_PROFILE_NAME = os.environ.get("AWS_PROFILE_NAME")
6363

64+
# Cognito configuration
65+
COGNITO_AWS_REGION = os.environ.get("COGNITO_AWS_REGION")
66+
COGNITO_USER_POOL = os.environ.get("COGNITO_USER_POOL")
67+
6468
# Database configuration
6569
POSTGRES_DB = os.environ.get("POSTGRES_DB")
6670
POSTGRES_USER = os.environ.get("POSTGRES_USER")
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
__version__ = "0.0.4"
2-
31
from .backend import JSONWebTokenAuthentication # noqa

metrics/api/django_cognito_jwt/backend.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,34 @@
1-
import logging
2-
31
from django.apps import apps as django_apps
42
from django.conf import settings
53
from django.utils.encoding import force_str
4+
from django.utils.module_loading import import_string
65
from django.utils.translation import gettext as _
76
from rest_framework import HTTP_HEADER_ENCODING, exceptions
87
from rest_framework.authentication import BaseAuthentication
98

109
from .validator import TokenError, TokenValidator
1110

12-
logger = logging.getLogger(__name__)
1311
VALID_AUTH_HEADER_LENGTH = 2
1412

1513

1614
def get_authorization_header(request):
1715
"""
18-
Return request's 'Authorization:' header, as a bytestring.
16+
Return request's 'X-UHD-AUTH:' header, as a bytestring.
1917
2018
Hide some test client ickyness where the header can be unicode.
2119
"""
22-
auth = request.META.get('HTTP_X_UHD_AUTH', b'')
20+
auth = request.META.get("HTTP_X_UHD_AUTH", b"")
2321
if isinstance(auth, str):
2422
# Work around django test client oddness
2523
auth = auth.encode(HTTP_HEADER_ENCODING)
2624
return auth
2725

2826

2927
class JSONWebTokenAuthentication(BaseAuthentication):
30-
"""Token based authentication using the JSON Web Token standard."""
28+
"""Token based authentication using the JSON Web Token standard.
29+
Based on https://github.com/labd/django-cognito-jwt and modified
30+
to suit our use case
31+
"""
3132

3233
def authenticate(self, request):
3334
"""Entrypoint for Django Rest Framework"""
@@ -42,10 +43,25 @@ def authenticate(self, request):
4243
except TokenError:
4344
raise exceptions.AuthenticationFailed from None
4445

45-
user_model = self.get_user_model()
46-
user = user_model.objects.get_or_create_for_cognito(jwt_payload)
46+
custom_user_manager = self.get_custom_user_manager()
47+
if custom_user_manager:
48+
user = custom_user_manager.get_or_create_for_cognito(jwt_payload)
49+
else:
50+
user_model = self.get_user_model()
51+
user = user_model.objects.get_or_create_for_cognito(jwt_payload)
4752
return (user, jwt_token)
4853

54+
@staticmethod
55+
def get_custom_user_manager():
56+
"""If COGNITO_USER_MANAGER is set, then the user object is obtained
57+
via get_or_create_for_cognito on the user manager, this allows use
58+
of the default unmodified Django User model"""
59+
result = None
60+
custom_user_manager_path = getattr(settings, "COGNITO_USER_MANAGER", False)
61+
if custom_user_manager_path:
62+
result = import_string(custom_user_manager_path)()
63+
return result
64+
4965
@staticmethod
5066
def get_user_model():
5167
user_model = getattr(settings, "COGNITO_USER_MODEL", settings.AUTH_USER_MODEL)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import logging
2+
3+
from django.contrib.auth import get_user_model
4+
from django.contrib.auth.models import BaseUserManager, User
5+
6+
logger = logging.getLogger(__name__)
7+
8+
9+
class CognitoManager(BaseUserManager):
10+
11+
@staticmethod
12+
def get_or_create_for_cognito(jwt_payload):
13+
username = jwt_payload["entraObjectId"]
14+
try:
15+
user = get_user_model().objects.get(username=username)
16+
logger.debug("Found existing user %s", user.username)
17+
except User.DoesNotExist:
18+
password = None
19+
user = get_user_model().objects.create_user(
20+
username=username,
21+
password=password,
22+
)
23+
logger.info("Created user %s", user.username)
24+
user.is_active = True
25+
user.save()
26+
return user

metrics/api/django_cognito_jwt/validator.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import logging
23

34
import jwt
45
import requests
@@ -7,6 +8,8 @@
78
from django.utils.functional import cached_property
89
from jwt.algorithms import RSAAlgorithm
910

11+
logger = logging.getLogger(__name__)
12+
1013

1114
class TokenError(Exception):
1215
pass
@@ -62,10 +65,13 @@ def validate(self, token):
6265
"jwt": token,
6366
"key": public_key,
6467
"issuer": self.pool_url,
65-
"algorithms": ["RS256"]
68+
"algorithms": ["RS256"],
6669
}
6770

71+
logger.debug("JWT - %s", params)
6872
token_payload = jwt.decode(token, options={"verify_signature": False})
73+
logger.debug("JWT decoded - %s", token_payload)
74+
6975
if "aud" in token_payload:
7076
params.update({"audience": self.audience})
7177

metrics/api/settings/default.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,9 @@
111111
},
112112
]
113113

114-
COGNITO_AWS_REGION = "eu-west-2"
115-
COGNITO_USER_POOL = "eu-west-2_m44aoyGeK"
114+
COGNITO_USER_MANAGER = "metrics.api.django_cognito_jwt.user_manager.CognitoManager"
115+
COGNITO_AWS_REGION = config.COGNITO_AWS_REGION
116+
COGNITO_USER_POOL = config.COGNITO_USER_POOL
116117
COGNITO_AUDIENCE = None
117118
COGNITO_PUBLIC_KEYS_CACHING_ENABLED = True
118119
COGNITO_PUBLIC_KEYS_CACHING_TIMEOUT = 60 * 60 * 24 # 24h caching, default is 300s

tests/unit/metrics/api/django_cognito_jwt/test_backend.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,42 @@ def test_authenticate_no_token(rf):
1717
assert auth.authenticate(request) is None
1818

1919

20+
@pytest.mark.parametrize(
21+
"cognito_user_manager",
22+
["metrics.api.django_cognito_jwt.user_manager.CognitoManager", None],
23+
)
2024
def test_authenticate_valid(
21-
rf, monkeypatch, cognito_well_known_keys, jwk_private_key_one
25+
rf, monkeypatch, cognito_well_known_keys, jwk_private_key_one, cognito_user_manager
2226
):
27+
settings.COGNITO_USER_MANAGER = cognito_user_manager
2328
token = create_jwt_token(
2429
jwk_private_key_one,
2530
{
2631
"iss": "https://cognito-idp.eu-central-1.amazonaws.com/bla",
2732
"aud": settings.COGNITO_AUDIENCE,
2833
"sub": "username",
34+
"entraObjectId": "entraOID",
2935
},
3036
)
3137

38+
@staticmethod
3239
def func(payload):
33-
return USER_MODEL(username=payload["sub"])
40+
return USER_MODEL(username=payload["entraObjectId"])
3441

35-
monkeypatch.setattr(
36-
USER_MODEL.objects, "get_or_create_for_cognito", func, raising=False
37-
)
42+
if cognito_user_manager:
43+
monkeypatch.setattr(
44+
f"{cognito_user_manager}.get_or_create_for_cognito", func, raising=False
45+
)
46+
else:
47+
monkeypatch.setattr(
48+
USER_MODEL.objects, "get_or_create_for_cognito", func, raising=False
49+
)
3850

3951
request = rf.get("/", HTTP_X_UHD_AUTH=b"bearer %s" % token.encode("utf8"))
4052
auth = backend.JSONWebTokenAuthentication()
4153
user, auth_token = auth.authenticate(request)
4254
assert user
43-
assert user.username == "username"
55+
assert user.username == "entraOID"
4456
assert auth_token == token.encode("utf8")
4557

4658

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from unittest import mock
2+
from django.contrib.auth import get_user_model
3+
from django.contrib.auth.models import User
4+
5+
from metrics.api.django_cognito_jwt.user_manager import CognitoManager
6+
7+
USER_MODEL = get_user_model()
8+
9+
10+
def test_get_or_create_for_cognito_get_existing_user():
11+
jwt_payload = {
12+
"entraObjectId": "unique_user_id",
13+
}
14+
15+
mock_user = mock.MagicMock()
16+
mock_user.username = jwt_payload["entraObjectId"]
17+
mock_user.is_active = True
18+
19+
with mock.patch.object(USER_MODEL.objects, "get", return_value=mock_user):
20+
user = CognitoManager.get_or_create_for_cognito(jwt_payload)
21+
assert user
22+
assert user.username == jwt_payload["entraObjectId"]
23+
assert user.is_active is True
24+
25+
26+
def test_get_or_create_for_cognito_create_user():
27+
jwt_payload = {
28+
"entraObjectId": "unique_user_id",
29+
}
30+
31+
def create_user_mock(*args, username, **kwargs):
32+
mock_user = mock.MagicMock()
33+
mock_user.username = username
34+
return mock_user
35+
36+
with (
37+
mock.patch.object(USER_MODEL.objects, "get", side_effect=User.DoesNotExist),
38+
mock.patch.object(
39+
USER_MODEL.objects, "create_user", side_effect=create_user_mock
40+
) as create_user,
41+
):
42+
user = CognitoManager.get_or_create_for_cognito(jwt_payload)
43+
assert user
44+
assert user.username == jwt_payload["entraObjectId"]
45+
assert user.is_active is True
46+
47+
create_user.assert_called_once_with(
48+
username=jwt_payload["entraObjectId"],
49+
password=None,
50+
)

0 commit comments

Comments
 (0)