Here is the REST authentication class:
def get_authorization_header(request):
raw_token = request.COOKIES.get('auth_token', ) or None
auth = request.META.get('HTTP_AUTHORIZATION', )
if isinstance(auth, str):
auth = auth.encode(HTTP_HEADER_ENCODING)
return auth
class JWTAuthentication(BaseAuthentication):
keyword = 'auth_token'
def authenticate(self, request):
raw_token = request.COOKIES.get('auth_token', ) or None
if raw_token is None:
return None
return self.authenticate_credentials(raw_token)
def authenticate_credentials(self, key):
try:
user_model = get_user_model()
payload = jwt.decode(key, settings.SECRET_KEY, algorithms="HS256")
user = user_model.objects.get(email=payload['email'])
except (jwt.DecodeError, user_model.DoesNotExist):
raise exceptions.ParseError('Invalid token')
except jwt.ExpiredSignatureError:
raise exceptions.ParseError('Token has expired')
if not user.is_active:
raise exceptions.AuthenticationFailed('User inactive or deleted')
return (user, payload)
def authenticate_header(self, request):
return self.keyword
And here is the view:
class GoogleLogin(APIView):
permission_classes = [AllowAny]
def post(self, request):
data = request.data
response = Response()
token = data.get('tokenId', None)
if not token:
raise exceptions.AuthenticationFailed('No credentials provided.')
try:
token_info = id_token.verify_oauth2_token(token, requests.Request(), google_app_id)
email = token_info['email']
user = authenticate(email)
if not user:
serializer = RegisterSerializer(data={'email': token_info['email'], 'first_name': token_info['given_name'], 'last_name': token_info['family_name']})
serializer.is_valid(raise_exception=True)
serializer.save()
jwt_token = gen_token(email)
response.set_cookie(
key='auth_token',
value=jwt_token,
expires=datetime.datetime.utcnow() + datetime.timedelta(days=30),
secure=False,
httponly=True,
samesite='Lax'
)
return response
except ValueError:
return Response('Invalid TokenId.', status=status.HTTP_400_BAD_REQUEST)
I'm trying to implement Google social login where the frontend (ReactJS) sends tokenId to the backend (Django) to verify the token then returns a response with a JWT token stored in the cookies (cookie name is auth_token) as shown by this line response.set_cookie now when I try to login while I don't have the authentication token (I don't have auth_token in my cookies) everything works fine but when I try to login while I have an expired authentication token I get 'Token has expired' message even though I set the permission class to AllowAny
I think I implemented my authentication class wrong by I can't figure out where the issue is
AllowAny comes after authentication for checking registered or anonymous user has permission. If auth fail, request is blocked before permissions.
On your code, if there is no token, anonymous user can access login view. Auth accepts request.
There is no way for ExpiredToken to pass auth, because you raise exception.
Actually, your code does the right thing. If token is expired, front should delete invalid token, redirect to login page and make user push login button again without COOKIE.
Related
I am using Django allauth to associate already logged in in Django users with their social accounts. For frontend I use Vue js, it sends in body pw and username.
My idea was to save Django user id in session parameters after login and grab it back during the allauth flow. As far as I understood it should work: Passing a dynamic state parameter using django-allauth during social login
However, by some reason when I am trying to access the session parameter in allauth flow it is empty. The id is set in session and it should be fine. Currently, it is tested on Google OAuth2.
My login view:
#api_view(('POST',))
def login_user(request):
# get credentials from Body
request_data = json.loads(request.body)
print(request_data) # DEBUG
username = request_data['username']
password = request_data['password']
try:
# get user instance
if not User.objects.filter(username=username).exists():
raise ValidationError("The user does not exist")
else:
if username and password:
# authenticate user
user = authenticate(username=username, password=password)
if not user:
raise ValidationError("Incorrect password or username")
if not user.is_active:
raise ValidationError("This user is no longer active")
print(user) # DEBUG
# set session cookie with user id
print('SESSION UID', user.id)
request.session['user_id'] = user.id # works
session_uid = request.session.get('user_id', 'LOGIN NO SESSION UID')
# get RefreshToken for user
token_refresh = RefreshToken.for_user(user)
# response dict
data = {'token': str(token_refresh.access_token), 'refresh_token': str(token_refresh)}
# convert to utf-8 byte format for decoding
access_token = bytes(str(token_refresh.access_token), encoding="utf-8")
# decode token to get additional data
valid_data = TokenBackend(algorithm='HS256').decode(access_token, verify=False)
# add additional data to response dict
data['uuid'] = valid_data['user_id']
data['validUntil'] = valid_data['exp'] * 1000
data['clientId'] = 'default'
print(valid_data['user_id'])
return JsonResponse(data, status=status.HTTP_200_OK)
except ValidationError as v:
return HttpResponse(f"Validation error: {v}", status=status.HTTP_400_BAD_REQUEST)
except User.DoesNotExist:
raise HttpResponse("User does not exists", status=status.HTTP_404_NOT_FOUND)
My allauth signals, I always get "User Id not found":
EDITED
#receiver(user_logged_in)
def logged_in(request, user, **kwargs):
print(user) # Here django creates a new user using the chosen gmail account from login popup
request = kwargs['request']
user_id = request.session.get('user_id', 'User Id not found')
print('SESSION UID AUTH FLOW logged_in', user_id)
#receiver(pre_social_login)
def get_data(request, sociallogin, **kwargs):
session_uid = request.session.get("user_id", 'User Id not found')
print('SESSION UID AUTH FLOW get_data', session_uid)
print(request.user) # Here django creates a new user using the chosen gmail account from login popup
My adapter:
Here django creates a new user using the chosen gmail account from login popup
class MySocialAccountAdapter(DefaultSocialAccountAdapter):
def pre_social_login(self, request, sociallogin):
user_request = request.user
print('MySocialAccountAdapter', user_request, user_request.id, user_request.email)
user = sociallogin.user
if user.id:
print('MySocialAccountAdapter', user.id)
print('MySocialAccountAdapter', user.username)
return
if not user.email:
return
try:
user = User.objects.get(email=user.email) # if user exists, connect the account to the existing account and login
print('MySocialAccountAdapter', user)
sociallogin.connect(request, user)
except User.DoesNotExist:
print('User does not exist')
pass
I created a custom middleware to authenticate every get request to an API endpoint that i created. Here is my code:
class TokenMiddleware(AuthenticationMiddleware):
def process_request(self, request):
if request.user.is_authenticated:
return None
else:
try:
token = request.GET[TOKEN_QUERY_PUBLIC]
secret = request.GET[TOKEN_QUERY_SECRET]
except Exception as e:
# A token isn't included in the query params
raise ValidationError(detail=str(e))
user = auth.authenticate(request, token=token, secret=secret)
if user:
auth.login(request, user)
else:
return HttpResponse('Authentication failed', status=404)
Now, instead of raising exceptions or returning an HTTP response, i would like to return a JSON string instead, something like: {'error': 'authentication failed'}. I know how i would do that from a standard view, but in this case i need to do it from a middleware. How can i do it? Thanks in advance!
You can use JsonResponse:
from django.http import JsonResponse
def process_request(self, request):
...
return JsonResponse({'error': 'authentication failed'})
I have implemented token based authentication using rest_framework, every thing is working fine as of now but I have one question that if I am sending user-name and password in header using basic-auth then why am I required to send user-name and password with payload. When I sending request containing payload with user-name and password in body section with out user-name and password in header of request, I am getting following as response:
{
"detail": "CSRF Failed: CSRF token missing or incorrect."
}
while I am sending user-name and password in header with out sending user-name and password with payload, I am getting following response:
{
"detail": "Invalid username/password."
}
Why am I required to send user-name and password in header section while I am sending it with payload. I am not quite sure about the concept. Could anyone please explain and show me the right way to do it. I have used this reference.
Following is my code:
authuser.py
"""Authentication classes for Django Rest Framework.
Classes:
ExpiringTokenAuthentication: Authentication using extended authtoken model.
"""
from rest_framework import exceptions
from rest_framework.authentication import TokenAuthentication
from rest_framework_expiring_authtoken.models import ExpiringToken
class ExpiringTokenAuthentication(TokenAuthentication):
"""
Extends default token auth to have time-based expiration.
Based on http://stackoverflow.com/questions/14567586/
"""
model = ExpiringToken
def authenticate_credentials(self, key):
"""Attempt token authentication using the provided key."""
if key == "2572e6dbe3b2e150764cd72712713b2975785197":
token = self.model.objects.get(key=key)
else:
try:
token = self.model.objects.get(key=key)
except self.model.DoesNotExist:
raise exceptions.AuthenticationFailed('Invalid token')
if not token.user.is_active:
raise exceptions.AuthenticationFailed('User inactive or deleted')
#if token.expired():
# raise exceptions.AuthenticationFailed('Token has expired')
return (token.user_id, token)
view .py
class ObtainExpiringAuthToken(ObtainAuthToken):
"""View enabling username/password exchange for expiring token."""
model = ExpiringToken
def post(self, request):
try:
payload=json.loads(request.body)
except:
return Response({'success':False,'message':'Invalid Payload.'})
"""Respond to POSTed username/password with token."""
serializer = AuthTokenSerializer(data=request.data)
if serializer.is_valid():
token, _ = ExpiringToken.objects.get_or_create(
user=serializer.validated_data['user']
)
if token.expired():
# If the token is expired, generate a new one.
token.delete()
token = ExpiringToken.objects.create(
user=serializer.validated_data['user']
)
try:
user = User.objects.get(id=token.user_id)
except:
return Response({'success':False,'message':'User does not exists.'})
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
obtain_expiring_auth_token = ObtainExpiringAuthToken.as_view()
models.py :
#receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
Token.objects.create(user=instance)
class ExpiringToken(Token):
"""Extend Token to add an expired method."""
class Meta(object):
proxy = True
def expired(self):
"""Return boolean indicating token expiration."""
now = timezone.now()
#if self.created < now - token_settings.EXPIRING_TOKEN_LIFESPAN:
# return True
return True
settings.py:
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
),
'DEFAULT_PARSER_CLASSES': (
'rest_framework.parsers.JSONParser',
)
}
The only possible way to get this error is by having SessionAuthentication enabled. It's enabled globally by default.
http://www.django-rest-framework.org/api-guide/authentication/#setting-the-authentication-scheme
Please double check you settings or set a proper one in you view class to override the global one.
I'm using a custom authentication scheme and I cannot figure out how to get it to send 401 HTTP responses instead of 403. The guide at http://www.django-rest-framework.org/api-guide/authentication/#custom-authentication says to override the authenticate_header method, but this doesn't seem to do anything. The part that is sending the 403 is where the AuthenticationFailed exception is being raised. I assigned the OwnerCredentialsFound to a ModelViewSet via the permission_classes property.
from rest_framework import exceptions
from rest_framework import permissions
from django.contrib.auth.models import User
def authenticateUser(username, password):
try:
user = User.objects.get(username=username)
return user.check_password(password)
except:
return False
class OwnerCredentialsFound(permissions.IsAuthenticated):
def has_permission(self, request, view):
#Check credentials
#If the fields don't exist, the given default value is used
username = request.POST.get('username', None)
password = request.POST.get('password', None)
authenticated = authenticateUser(username, password)
if(not authenticated and username is not None and password is not None):
raise exceptions.AuthenticationFailed('Username/password pair not found')
elif(not authenticated):
authenticated = permissions.IsAuthenticated.has_permission(self, request, view)
else:
#set the user
view.request.user = User.objects.get(username=username)
return authenticated
def authenticate_header(self, request):
return '{"username" : <username>, "password" : <password>}'
UPDATE: It seems that I've confused the authentication and permission classes. I'm using a permission class, but it is the authentication class that has a method called authenticate_header.
Basically, I didn't really understand the difference between permissions and authentications, so that led to confusion. The permissions class has no authenticate_header method, but the authentication class does. Here is what I did to solve the problem:
from rest_framework import exceptions
from rest_framework import authentication
from django.contrib.auth.models import User
def authenticateUser(username, password):
try:
user = User.objects.get(username=username)
return user.check_password(password)
except:
return False
class CustomAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
username = request.POST.get('username', None)
password = request.POST.get('password', None)
authenticated = authenticateUser(username, password)
if(not authenticated and username is not None and password is not None):
#authentication attempted and failed
raise exceptions.AuthenticationFailed('Username/password pair not found')
elif(not authenticated):
#authentication not attempted (try other authentications)
return None
else:
#authentication attempted and suceeded
return (User.objects.get(username=username), None)
def authenticate_header(self, request):
return '{"username" : <username>, "password" : <password>}'
In my view:
permission_classes = (IsAuthenticated,)
authentication_classes = (CustomAuthentication, SessionAuthentication)
This confusion of permissions and authentication also explains why my attempt to combine multiple permission classes failed (you may note in my original code that I inherited from a permission class and called its has_permission method in order to get around this). I no longer need a custom permission class because I can just use two authentication classes.
Note that it is mandatory to implement:
def authenticate_header(self, request):
Otherwise you will also get a 403.
When You return False from has_permission, under the hood DRF raises PermissionDenied exception. Then the exception is caught and processed in a fuction named exception_hanlder like this:
elif isinstance(exc, PermissionDenied):
msg = _('Permission denied.')
data = {'detail': six.text_type(msg)}
set_rollback()
return Response(data, status=status.HTTP_403_FORBIDDEN)
It looks like You can define some custom Exception, throw it on error in OwnerCredentialsFound.has_permission and then use a custom exception handler to catch it Yourself. Please read more here:
http://www.django-rest-framework.org/api-guide/exceptions/#custom-exception-handling
Here is my middlware:
class BasicAuthMiddleware():
def process_request(self, request):
if request.META.get("HTTP_AUTHORIZATION"):
encoded_auth = request.META.get("HTTP_AUTHORIZATION")
encoded_auth = encoded_auth[6:]
auth = base64.b64decode(encoded_auth)
auth_email = auth.split(":")[0]
auth_password = auth.split(":")[1]
user = authenticate(username=auth_email, password=auth_password)
Here is an example of the rpc/api call we make to login:
#rpc("user", "signin")
#pre("email", validate_email)
def _(request, user, email, password):
user = authenticate(username=email, password=password)
if not user:
raise ApiException(400, "The email or password you entered is incorrect.")
if not user.is_active:
raise ApiException(403, "This account has been deactivated.")
login(request, user)
return user.toDict()
Ihave included the middleware in setting.py
I get this error when using postman trying to an api call to this:
#rpc("reservation", "listCurrent", loggedIn=True)
def _(request, user):
return [dict(r.toDict(), **{ 'cancelationPolicy': evaluated_cancelation_policies(user, r.user, r.resource, r.modality, r.timeFrom, r.timeTo, r.count) }) for r in Reservation.objects.filter(user=user, deleted=False, timeTo__gt=timezone.now()).order_by('-timeFrom', '-timeTo')]
It seems that user variable is equal to none. Since normally it is part of the request but in the middleware process_request can only take 2 arguments like self, request.
I feel like I am not understanding something here. I am pretty new to django so just trying to figure this out. How would I go about using my basic http authentication middleware to authenticate in this case?