Skip to content

Instantly share code, notes, and snippets.

@glebpushkov
Last active July 19, 2022 19:45
Embed
What would you like to do?
Django & Cognito article snippets
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', )
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__'
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)
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()
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]
)
class DenyAny(BasePermission):
def has_permission(self, request, view):
return False
def has_object_permission(self, request, view, obj):
return False
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}>'
{
"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"
}
]
}
{
"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"
}
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',
}
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'core.api.permissions.DenyAny',
),
...
}
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'core.api.permissions.DenyAny',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
),
}
MIDDLEWARE = [
...
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.RemoteUserMiddleware',
...
]
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.RemoteUserBackend',
'django.contrib.auth.backends.ModelBackend',
]
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