Django OID token validation solutions - django-rest-framework

I am working on a Django rest-framework API. The frontend app (react) authenticates with a OpenID provider, and passes the authentication token to the API. I need to verify the authenticity of the token before serving the client requests.
As far as I understand, OID libraries that I have seen give a client and a provider implementation, but the above scenario seems to not be covered, i.e the API is neither a client, or a provider.
I have found one source describing the validation steps required, but I was wondering if there is an opensource solution that I have not found yet that will perform validation of the token for me.
Update
This is the implementation I have made:
import requests
import datetime
import six
from logging import getLogger
from requests.auth import HTTPBasicAuth
from urllib.parse import urljoin
from requests.exceptions import HTTPError
from calendar import timegm
from jwkest import JWKESTException
from jwkest.jwk import KEYS
from jwkest.jws import JWS
from django.utils.encoding import smart_text
from django.utils.functional import cached_property
from django.utils.translation import ugettext as _
from django.conf import settings
from rest_framework.authentication import BaseAuthentication, get_authorization_header
from rest_framework.exceptions import AuthenticationFailed
from .utils import class_cache
logger = getLogger(__name__)
def setting(key):
return getattr(settings, f'OIDC_AUTH_{key}')
class AuthenticatedServiceClient:
def __init__(self, roles, user_id):
self.roles = roles
self.id = user_id
def is_authenticated(self):
return True
#staticmethod
def create(payload, request):
roles = payload.get('role', [])
user_id = payload.get('sub', None) # framework specific code
if not user_id:
msg = _('No sub claims provided')
logger.error(msg)
raise AuthenticationFailed(msg)
return AuthenticatedServiceClient(roles, user_id)
class NoAuthentication(BaseAuthentication):
def authenticate(self, request):
no_user_id = '00000000-0000-0000-0000-000000000000'
return AuthenticatedServiceClient.create({'sub': no_user_id}, request), True
class BaseOidcAuthentication(BaseAuthentication):
#cached_property
def oidc_config(self):
url = urljoin(setting('OIDC_ENDPOINT'), '.well-known/openid-configuration')
return requests.get(url, verify=setting("SSL_VERIFY")).json()
class BearerTokenAuthentication(BaseOidcAuthentication):
www_authenticate_realm = 'api'
def authenticate(self, request):
bearer_token = self.get_bearer_token(request)
if bearer_token is None:
return None
try:
token_info = self.introspect_token(bearer_token)
except HTTPError:
msg = _('Invalid Authorization header. Unable to verify bearer token')
logger.error(msg)
raise AuthenticationFailed(msg)
logger.debug(f"Received token: {token_info}")
self.validate_bearer_token(token_info)
return AuthenticatedServiceClient.create(token_info, request), True
def validate_bearer_token(self, token_info):
if token_info['active'] is False:
msg = _('Authentication Failed. Received Inactive Token')
logger.error(msg)
raise AuthenticationFailed(msg)
if setting('OIDC_SCOPE') not in token_info['scope']:
msg = _('Authentication Failed. Invalid token scope')
logger.error(msg)
raise AuthenticationFailed(msg)
utc_timestamp = timegm(datetime.datetime.utcnow().utctimetuple())
if utc_timestamp > int(token_info.get('exp', 0)):
msg = _('Invalid Authorization header. Bearer token has expired.')
logger.error(msg)
raise AuthenticationFailed(msg)
def get_bearer_token(self, request):
auth = get_authorization_header(request).split()
auth_header_prefix = setting('BEARER_AUTH_HEADER_PREFIX').lower()
if not auth or smart_text(auth[0].lower()) != auth_header_prefix:
msg = _('Authorization failed. No bearer token found in header')
logger.error(msg)
return None
if len(auth) == 1:
msg = _('Invalid Authorization header. No credentials provided')
logger.error(msg)
raise AuthenticationFailed(msg)
elif len(auth) > 2:
msg = _('Invalid Authorization header. Credentials string should not contain spaces.')
logger.error(msg)
raise AuthenticationFailed(msg)
elif smart_text(auth[1]).count('.') == 2:
msg = _('Authorization failed. Unexpected token format')
logger.error(msg)
return None
return auth[1]
#class_cache(ttl=setting('BEARER_TOKEN_EXPIRATION_TIME'))
def introspect_token(self, token):
response = requests.post(
self.oidc_config['introspection_endpoint'],
auth=HTTPBasicAuth(setting('OIDC_SCOPE'), setting('OIDC_INTROSPECT_PASSWORD')),
data={'token': token.decode('ascii')},
verify=setting("SSL_VERIFY"))
return response.json()
class JSONWebTokenAuthentication(BaseOidcAuthentication):
"""Token based authentication using the JSON Web Token standard"""
www_authenticate_realm = 'api'
def authenticate(self, request):
jwt_value = self.get_jwt_value(request)
if jwt_value is None:
return None
payload = self.decode_jwt(jwt_value)
logger.debug(f"Received token: {payload}")
self.validate_claims(payload)
return AuthenticatedServiceClient.create(payload, request), True
def get_jwt_value(self, request):
auth = get_authorization_header(request).split()
auth_header_prefix = setting('BEARER_AUTH_HEADER_PREFIX').lower()
if not auth or smart_text(auth[0].lower()) != auth_header_prefix:
return None
if len(auth) == 1:
msg = _('Invalid Authorization header. No credentials provided')
logger.error(msg)
raise AuthenticationFailed(msg)
elif len(auth) > 2:
msg = _('Invalid Authorization header. Credentials string should not contain spaces.')
logger.error(msg)
raise AuthenticationFailed(msg)
elif smart_text(auth[1]).count('.') != 2:
return None
return auth[1]
def jwks(self):
keys = KEYS()
keys.load_from_url(self.oidc_config['jwks_uri'], verify=setting("SSL_VERIFY"))
return keys
#cached_property
def issuer(self):
return self.oidc_config['issuer']
#class_cache(ttl=setting('JWKS_EXPIRATION_TIME'))
def decode_jwt(self, jwt_value):
keys = self.jwks()
try:
id_token = JWS().verify_compact(jwt_value, keys=keys)
except JWKESTException:
msg = _('Invalid Authorization header. JWT Signature verification failed.')
logger.error(msg)
raise AuthenticationFailed(msg)
except UnicodeDecodeError:
msg = _('Bad token format. Token decoding failed.')
logger.error(msg)
raise AuthenticationFailed(msg)
return id_token
def get_audiences(self, id_token):
return setting('AUDIENCES')
def validate_claims(self, id_token):
if isinstance(id_token.get('aud'), six.string_types):
# Support for multiple audiences
id_token['aud'] = [id_token['aud']]
if id_token.get('iss') != self.issuer:
msg = _('Invalid Authorization header. Invalid JWT issuer.')
logger.error(msg)
raise AuthenticationFailed(msg)
if not any(aud in self.get_audiences(id_token) for aud in id_token.get('aud', [])):
msg = _('Invalid Authorization header. Invalid JWT audience.')
logger.error(msg)
raise AuthenticationFailed(msg)
utc_timestamp = timegm(datetime.datetime.utcnow().utctimetuple())
if utc_timestamp > id_token.get('exp', 0):
msg = _('Invalid Authorization header. JWT has expired.')
logger.error(msg)
raise AuthenticationFailed(msg)
if setting("ENABLE_NBF_CHECK") and 'nbf' in id_token and utc_timestamp < id_token['nbf']:
msg = _('Invalid Authorization header. JWT not yet valid.')
logger.error(msg)
raise AuthenticationFailed(msg)
if 'iat' in id_token and utc_timestamp > id_token['iat'] + setting('LEEWAY'):
msg = _('Invalid Authorization header. JWT too old.')
logger.error(msg)
raise AuthenticationFailed(msg)
if setting('OIDC_SCOPE') not in id_token.get('scope'):
msg = _('Invalid Authorization header. Invalid JWT scope.')
logger.error(msg)
raise AuthenticationFailed(msg)
def authenticate_header(self, request):
return 'JWT realm="{0}"'.format(self.www_authenticate_real

At this time it doesn't seem like there is an official library supporting it.
I have put together the following implementation, using https://github.com/ByteInternet/drf-oidc-auth as a base:
https://gist.github.com/latusaki/0f015643d55c2481bb7acd023c4203e3

Related

print(self.request.user.id) returns None

I have a Django REST App where I want to do the log in. I need the id of the currend logged in user and when I print it, it returns None. This is my code:
serializer.py
class LoginSerializer(serializers.Serializer):
username = serializers.CharField(
label="Username",
write_only=True
)
password = serializers.CharField(
label="Password",
# This will be used when the DRF browsable API is enabled
style={'input_type': 'password'},
trim_whitespace=False,
write_only=True
)
view.py
class LoginAPI(APIView):
def post(self, request):
username = request.data['username']
password = request.data['password']
user = User.objects.filter(username=username).first()
if user is None:
raise AuthenticationFailed('User not found!')
if not user.check_password(password):
raise AuthenticationFailed('Incorrect password!')
payload = {
'id': user.id,
}
token = jwt.encode(payload, 'secret', algorithm='HS256').decode('utf-8')
response = Response()
response.set_cookie(key='token', value=token, httponly=True)
response.data = {
'token': token
}
print(self.request.user.id)
return response
I don't know what I have to change in my log in view in order to print the id, not None.
You didn't login yet in that post function. So self.request.user does not exist.
print(user.id)

ObtainAuthToken post function customization. How to get token and name in the response?

I´m trying to get response that include name apart from token.
In the documentation https://www.django-rest-framework.org/api-guide/authentication/ about ObtainAuthToken I see we can overwrite the post fn but I´m not sure how to apply it in my CreateTokenView class.
serializers.py
class AuthTokenSerializer(serializers.Serializer):
"""Serializer for the user authentication object"""
email = serializers.CharField()
password = serializers.CharField(
style={'input_type': 'password'},
trim_whitespace=False
)
def validate(self,attrs):
"""Overwriting the validate() fn"""
email = attrs.get('email')
password = attrs.get('password')
user = authenticate(
request=self.context.get('request'),
username=email,
password=password
)
if not user:
msg = _('Unable to authenticate with provided credentials')
raise serializers.ValidationError(msg, code='authentication')
attrs['user'] = user
return attrs
views.py
from rest_framework.authtoken.views import ObtainAuthToken
class CreateTokenView(ObtainAuthToken):
"""Create a new auth token for user"""
serializer_class = AuthTokenSerializer
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
Add the post fn and customize the kwargs
from rest_framework.authtoken.models import Token
from rest_framework.response import Response
class CreateTokenView(ObtainAuthToken):
"""Create a new auth token for user"""
serializer_class = AuthTokenSerializer
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data,
context={'request': request})
serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user']
token, created = Token.objects.get_or_create(user=user)
return Response({
'token': token.key,
'name': user.name
})

I am trying implement authentication using the jwt token using djangorest framework but am getting assertion error

AssertionError: 'LoginView' should either include a serializer_class attribute, or override the get_serializer_class() method
Here is the code
class LoginView(GenericAPIView):
def post(self, request):
data = request.data
username = data.get('username', '')
password = data.get('password', '')
user = auth.authenticate(username=username, password=password)
if user:
auth_token = jwt.encode({'username': user.username}, settings.JWT_SECRET_KEY)
serializer = UserSerializer(user)
data = {'user': serializer.data, 'token': auth_token}
return Response(data, status=status.HTTP_200_OK)
return Response({'detail': 'Invalid credential'}, status=status.HTTP_401_UNAUTHORIZED)

Django channel jwt custom middelware passing data

I'm trying to authenticate users using JWT through custom middleware for websocket communication. I'm sending the token from clientside by key, value and decode it from the serverside to get the user. But, when I try to access scope['user'] from comsumers, there is nothing. I want to know how I can pass on the scope['user'] to consumers. And I am not sure if this is a proper way to authenticate.
jwt_auth.py
#database_sync_to_async
def get_user(self, scope, decoded_data):
user = get_user_model().objects.get(id=decoded_data['user_id'])
return user
class JWTAuthMiddleware(BaseMiddleware):
def populate_scope(self, scope):
if b'token' not in scope['query_string']:
raise ValueError(
'JWTMiddleware cannot find token in scope.'
)
async def resolve_scope(self, scope):
token = parse_qs(scope['query_string'].decode('utf8'))['token'][0]
# print('token -> ' + str(token))
try:
UntypedToken(token) # Verify JWT
decoded_data = jwt_decode(token, settings.SECRET_KEY, algorithms=['HS256'])
# print('decoded_data -> ' + str(decoded_data))
scope['user'] = await get_user(self, scope, decoded_data)
print('scope -> ' + str(scope))
# return self.inner(scope)
except (InvalidToken, TokenError) as e:
print(e)

JWT Token authentication - generate token

I have created a login screen which uses a username and password to login. I have a jwt authentication but I'm getting a bit confused because I have two login urls and I want only one. The jwt url is providing me the token meanwhile the other one that I have created myself I can login but no token is getting generated. This is my code:
serializers.py
class UserLoginSerializer(ModelSerializer):
token = CharField(allow_blank=True, read_only=True)
username = CharField(required=False, allow_blank=True)
class Meta:
model = User
fields = [
'username',
'password',
'token',
]
extra_kwargs = {"password":{"write_only": True}}
def validate(self, data):
user = authenticate(**data)
if user:
if user.is_active:
data['user'] = user
return data
raise exceptions.AuthenticationFailed('Account is not activated')
raise exceptions.AuthenticationFailed('User is not active')
def validate(self, data):
user_obj = None
username = data.get("username", None)
password = data["password"]
if not username:
raise ValidationError("A username is required")
user = User.objects.filter(
Q(username=username)
).distinct()
if user.exists() and user.count() == 1:
user_obj = user.first()
else:
raise ValidationError("This username is not valid")
if user_obj:
if not user_obj.check_password(password):
raise ValidationError("Incorrect credentials, please try again")
data["token"] = "SOME RANDOM TOKEN"
return data
views.py
class UserLoginAPIView(APIView):
permission_classes = [AllowAny]
serializer_class = UserLoginSerializer
def post(self, request, *args, **kwargs):
data = request.data
serializer = UserLoginSerializer(data=data)
if serializer.is_valid(raise_exception=True):
new_data = serializer.data
return Response(new_data, status=HTTP_200_OK)
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
You can re-write your login serializer like this:
from rest_framework_jwt.serializers import JSONWebTokenSerializer
class SignInJWTSerializer(JSONWebTokenSerializer):
def validate(self, attrs):
email = attrs.get('email')
password = attrs.get('password')
if email is None or password is None:
message = 'Must include email and password'
raise serializers.ValidationError({'message': message})
...
And in url:
from rest_framework_jwt.views import ObtainJSONWebToken
path('login/', ObtainJSONWebToken.as_view(serializer_class=serializers.SignInJWTSerializer), name='login'),
also remove view class

Resources