Django & Cognito article snippets
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from django.contrib import admin | |
from django.contrib.auth.admin import UserAdmin | |
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, UsernameField | |
from django.utils.translation import ugettext_lazy as _ | |
from account.models import User | |
class CustomUserCreationForm(UserCreationForm): | |
class Meta(UserCreationForm.Meta): | |
model = User | |
class CustomUserChangeForm(UserChangeForm): | |
class Meta(UserCreationForm.Meta): | |
model = User | |
fields = '__all__' | |
field_classes = {'username': UsernameField} | |
@admin.register(User) | |
class CustomUserAdmin(UserAdmin): | |
fieldsets = ( | |
(None, {'fields': ('username', 'email', 'password', )}), | |
( | |
_('Permissions'), | |
{'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions', )} | |
), | |
(_('Important dates'), {'fields': ('created_at', 'updated_at', )}), | |
) | |
readonly_fields = ('created_at', 'updated_at', ) | |
add_fieldsets = ( | |
(None, { | |
'classes': ('wide', ), | |
'fields': ('username', 'email', 'password1', 'password2', ), | |
}), | |
) | |
form = CustomUserChangeForm | |
add_form = CustomUserCreationForm | |
list_display = ('username', 'is_staff', 'is_active', ) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from rest_framework import serializers | |
from account.models import User | |
class UserSerializer(serializers.ModelSerializer): | |
""" Used to retrieve user info """ | |
class Meta: | |
model = User | |
fields = '__all__' | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from rest_framework.generics import GenericAPIView | |
from rest_framework.mixins import RetrieveModelMixin | |
from rest_framework.permissions import IsAuthenticated | |
from account.api.serializers import UserSerializer | |
class UserProfileAPIView(RetrieveModelMixin, GenericAPIView): | |
serializer_class = UserSerializer | |
permission_classes = (IsAuthenticated, ) | |
def get_object(self): | |
return self.request.user | |
def get(self, request, *args, **kwargs): | |
""" | |
User profile | |
Get profile of current logged in user. | |
""" | |
return self.retrieve(request, *args, **kwargs) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from django.db import models | |
from django.contrib.auth.base_user import AbstractBaseUser | |
from django.contrib.auth.models import PermissionsMixin | |
from django.contrib.auth.validators import UnicodeUsernameValidator | |
from core.models import AbstractBaseModel | |
class User(PermissionsMixin, AbstractBaseUser, AbstractBaseModel): | |
""" | |
Table contains cognito-users & django-users. | |
PermissionsMixin leverage built-in django model permissions system | |
(which allows to limit information for staff users via Groups). | |
Note: Django-admin user and app user not split in different tables because of simplicity of development. | |
Some libraries assume there is only one user model, and they can't work with both. | |
For example to have a history log of changes for entities - to save which user made a change of object attribute, | |
perhaps, auth-related libs, and some other. | |
With current implementation we don't need to fork, adapt and maintain third party packages. | |
They should work out of the box. | |
The disadvantage is - cognito-users will have unused fields which always empty. Not critical. | |
""" | |
username_validator = UnicodeUsernameValidator() | |
### Common fields ### | |
# For cognito-users username will contain `sub` claim from jwt token | |
# (unique identifier (UUID) for the authenticated user). | |
# For django-users it will contain username which will be used to login into django-admin site | |
username = models.CharField('Username', max_length=255, unique=True, validators=[username_validator]) | |
is_active = models.BooleanField('Active', default=True) | |
### Cognito-user related fields ### | |
# some additional fields which will be filled-out only for users registered via Cognito | |
pass | |
### Django-user related fields ### | |
# password is inherited from AbstractBaseUser | |
email = models.EmailField('Email address', blank=True) # allow non-unique emails | |
is_staff = models.BooleanField( | |
'staff status', | |
default=False, | |
help_text='Designates whether the user can log into this admin site.' | |
) | |
USERNAME_FIELD = 'username' | |
EMAIL_FIELD = 'email' | |
REQUIRED_FIELDS = ['email'] # used only on createsuperuser | |
@property | |
def is_django_user(self): | |
return self.has_usable_password() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import jwt | |
from jwt import DecodeError | |
from jwt.algorithms import RSAAlgorithm | |
from rest_framework_jwt.settings import api_settings | |
from django.contrib.auth import authenticate | |
def get_username_from_payload_handler(payload): | |
username = payload.get('sub') | |
authenticate(remote_user=username) | |
return username | |
def cognito_jwt_decode_handler(token): | |
""" | |
To verify the signature of an Amazon Cognito JWT, first search for the public key with a key ID that | |
matches the key ID in the header of the token. (c) | |
https://aws.amazon.com/premiumsupport/knowledge-center/decode-verify-cognito-json-token/ | |
Almost the same as default 'rest_framework_jwt.utils.jwt_decode_handler', but 'secret_key' feature is skipped | |
""" | |
options = {'verify_exp': api_settings.JWT_VERIFY_EXPIRATION} | |
unverified_header = jwt.get_unverified_header(token) | |
if 'kid' not in unverified_header: | |
raise DecodeError('Incorrect authentication credentials.') | |
kid = jwt.get_unverified_header(token)['kid'] | |
try: | |
# pick a proper public key according to `kid` from token header | |
public_key = RSAAlgorithm.from_jwk(api_settings.JWT_PUBLIC_KEY[kid]) | |
except KeyError: | |
# in this place we could refresh cached jwks and try again | |
raise DecodeError('Can\'t find proper public key in jwks') | |
else: | |
return jwt.decode( | |
token, | |
public_key, | |
api_settings.JWT_VERIFY, | |
options=options, | |
leeway=api_settings.JWT_LEEWAY, | |
audience=api_settings.JWT_AUDIENCE, | |
issuer=api_settings.JWT_ISSUER, | |
algorithms=[api_settings.JWT_ALGORITHM] | |
) | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class DenyAny(BasePermission): | |
def has_permission(self, request, view): | |
return False | |
def has_object_permission(self, request, view, obj): | |
return False |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import uuid | |
from django.db import models | |
class AbstractBaseModel(models.Model): | |
""" | |
Base abstract model, that has `uuid` instead of `id` and includes `created_at`, `updated_at` fields. | |
""" | |
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True) | |
created_at = models.DateTimeField('Created at', auto_now_add=True) | |
updated_at = models.DateTimeField('Updated at', auto_now=True) | |
class Meta: | |
abstract = True | |
def __repr__(self): | |
return f'<{self.__class__.__name__} {self.uuid}>' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"keys": [ | |
{ | |
"alg": "RS256", | |
"e": "AQAB", | |
"kid": "Mvd6BSFCvQ+PbEOQCqOZd3CCSdd/d/mw+65R5uN1+r0=", | |
"kty": "RSA", | |
"n": "kQgIEUZBMkoN7jU_rRxjH...B1tcoSa4EkYUZtDsQ", | |
"use": "sig" | |
}, | |
{ | |
"alg": "RS256", | |
"e": "AQAB", | |
"kid": "jzr0vtU+c+hY2apVvODttwoYVSpdS/Bhn8D7YLAXe7o=", | |
"kty": "RSA", | |
"n": "l6m0rB8RSQWmp8gijxjYK...Na77QY8cRfzNLuLmzw", | |
"use": "sig" | |
} | |
] | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"sub": "9387dbed-ce4a-44fa-b6ab-6b26327e9305", | |
"event_id": "01c908b7-c15c-42b4-9849-855af5528051", | |
"token_use": "access", | |
"scope": "openid email", | |
"auth_time": 1572171426, | |
"iss": "https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_bUFIXsrqe", | |
"exp": 1572175026, | |
"iat": 1572171426, | |
"version": 2, | |
"jti": "cc5eeb1d-686e-42ad-b65d-5c5583a80140", | |
"client_id": "2bdgd681nmmnickj0coq0j1oq1", | |
"username": "9387dbed-ce4a-44fa-b6ab-6b26327e9305" | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import json | |
from urllib import request | |
... | |
COGNITO_AWS_REGION = 'eu-central-1' | |
COGNITO_USER_POOL = 'eu-central-1_xxxxxx' | |
# Provide this value if `id_token` is used for authentication (it contains 'aud' claim). | |
# `access_token` doesn't have it, in this case keep the COGNITO_AUDIENCE empty | |
COGNITO_AUDIENCE = None | |
COGNITO_POOL_URL = None # will be set few lines of code later, if configuration provided | |
rsa_keys = {} | |
# To avoid circular imports, we keep this logic here. | |
# On django init we download jwks public keys which are used to validate jwt tokens. | |
# For now there is no rotation of keys (seems like in Cognito decided not to implement it) | |
if COGNITO_AWS_REGION and COGNITO_USER_POOL: | |
COGNITO_POOL_URL = 'https://cognito-idp.{}.amazonaws.com/{}'.format(COGNITO_AWS_REGION, COGNITO_USER_POOL) | |
pool_jwks_url = COGNITO_POOL_URL + '/.well-known/jwks.json' | |
jwks = json.loads(request.urlopen(pool_jwks_url).read()) | |
rsa_keys = {key['kid']: json.dumps(key) for key in jwks['keys']} | |
JWT_AUTH = { | |
'JWT_PAYLOAD_GET_USERNAME_HANDLER': 'core.api.jwt.get_username_from_payload_handler', | |
'JWT_DECODE_HANDLER': 'core.api.jwt.cognito_jwt_decode_handler', | |
'JWT_PUBLIC_KEY': rsa_keys, | |
'JWT_ALGORITHM': 'RS256', | |
'JWT_AUDIENCE': COGNITO_AUDIENCE, | |
'JWT_ISSUER': COGNITO_POOL_URL, | |
'JWT_AUTH_HEADER_PREFIX': 'Bearer', | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
REST_FRAMEWORK = { | |
'DEFAULT_PERMISSION_CLASSES': ( | |
'core.api.permissions.DenyAny', | |
), | |
'DEFAULT_AUTHENTICATION_CLASSES': ( | |
'rest_framework_jwt.authentication.JSONWebTokenAuthentication', | |
), | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
REST_FRAMEWORK = { | |
'DEFAULT_PERMISSION_CLASSES': ( | |
'core.api.permissions.DenyAny', | |
), | |
... | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
MIDDLEWARE = [ | |
... | |
'django.contrib.auth.middleware.AuthenticationMiddleware', | |
'django.contrib.auth.middleware.RemoteUserMiddleware', | |
... | |
] | |
AUTHENTICATION_BACKENDS = [ | |
'django.contrib.auth.backends.RemoteUserBackend', | |
'django.contrib.auth.backends.ModelBackend', | |
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from account.api.views import UserProfileAPIView | |
urlpatterns = [ | |
path('admin/', admin.site.urls), | |
path('api/v1/me', UserProfileAPIView.as_view(), name='my_profile'), | |
] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment