I currently have Django basic auth setup with Knox token authentication. Basic Auth doesn't seem sufficient for production work, so I want to replace that. Does Django have another password-based authentication_class that I can easily replace BasicAuthentication with, or is this a more involved process? If so, where do I start?
my login api view:
class UserLoginView(GenericAPIView):
serializer_class = UserOrganizationSerializer
authentication_classes = (BasicAuthentication,)
permission_classes = (IsAuthenticated,)
def post(self, request):
"""User login with username and password."""
token = AuthToken.objects.create(request.user)
return Response({
'user': self.get_serializer(request.user).data,
'token': token
})
my default authentication classes:
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
],
One very common way is to use a JSON Web Token (JWT). The basic package is django-rest-framework-jwt. The instructions are pretty clear in the documentation, but here is an overview:
$> pip install djangorestframework-jwt
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
...
),
Add the URL patterns
url_patterns = [
path('jwt/', obtain_jwt_token, name='jwt'),
...
]
Get a token (using the excellent httpie utility)
http post localhost:8000/api/jwt/ username=u password=p
{
"token": "REALLY-LONG-TOKEN"
}
Use that token to make a request:
http get localhost:8000/api/some-endpoint/ Authorization:"JWT REALLY-LONG_TOKEN"
Some Notes
JWT tokens are meant to be decodable by the client. They are protected by a signature, so they can't be modified
You can decode the token (online) and see the expiration time & other data
Tokens can be refreshed or verified through other urls provided by the package
Eventually refreshing will fail and the user will need to login again. This timespan is configurable (see my response to this other question)
Related
I am using Django REST Framework and following this tutorial for retrieving all users when admin user is authenticated.
Class-based APIView of Django REST Framework
I am using Postman to test and trying to retrieve the list of all users registered in the system.
At first I try to use my "User Login with Token" API in Postman to create the necessary token as shown below:
I copied the value of the "token" key and pasted it as the value of the "Authorization" key in the "Headers" section of "Get All Users" API in Postman as shown below. It is a GET request and I get the error "detail": "Authentication credentials were not provided." as the response.
Necessary code snippets are as follows:
views.py
class UserAccountListView(APIView):
"""
List of All Users in the System / Application
* Requires Token Authentication.
* Only Admin Users are able to access this view.
"""
authentication_classes = (TokenAuthentication, )
permission_classes = (IsAdminUser, )
def get(self, request, format=None):
"""
Returns a List of All Users
"""
full_names = [user.full_name for user in UsersAccount.objects.all()]
return Response(full_names)
settings.py
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
'rest_framework.permissions.IsAdminUser',
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
'rest_framework.authentication.TokenAuthentication',
],
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=30),
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
"ROTATE_REFRESH_TOKENS": False,
"BLACKLIST_AFTER_ROTATION": False,
"UPDATE_LAST_LOGIN": True,
"ALGORITHM": "HS256",
"SIGNING_KEY": SECRET_KEY,
"VERIFYING_KEY": None,
"AUDIENCE": None,
"ISSUER": None,
"AUTH_HEADER_TYPES": ("Bearer", ),
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
"USER_ID_FIELD": "id",
"USER_ID_CLAIM": "user_id",
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken", ),
"TOKEN_TYPE_CLAIM": "token_type",
"JTI_CLAIM": "jti",
"SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
"SLIDING_TOKEN_LIFETIME": timedelta(minutes=5),
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
}
urls.py
urlpatterns = [
path('', UsersAccountAPIOverview.as_view()),
path("all", UserAccountListView.as_view()),
path("register", UsersAccountRegistrationView.as_view()),
path("token", UserAccountTokenObtainPairView.as_view()),
path("token/refresh", TokenRefreshView.as_view()),
path("token/verify", TokenVerifyView.as_view()),
]
Looking forward for your kind support and help. If you need further information, I will provide you.
Thank you.
In your views.py remove the line:
authentication_classes = (TokenAuthentication, )
This is because in your settings.py file, the first line:
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
...
],
mentions JWTAuthentication and not TokenAuthentication
You must add Bearer in postman :
"Authorization" : "Bearer [token]"
I am using the Django Restful API Framework together with Simple JWT and have successfully created a URL for receiving and refreshing a user token.
In order to try out the authentication using the token, I have created a view that simply lists all the posts inside the database. I have then assigned the IsAuthenticated class to the view.
As expected, I get an error message saying that the authentication credentials were not provided. I then went ahead and made a simple GET request using Postman, with the authentication token provided in the "Authorization" tab. The type was set to "Bear Token". Unfortunately, I still get the message "Authentication credentials were not provided." with a 403 Forbidden code.
I have also tried to provide the token in the Headers, as well as make CURL requests, everything to no avail.
My view looks like this:
class PostListView(generics.ListAPIView):
permission_classes = (IsAuthenticated,)
queryset = Post.objects.filter()
This is the serializer:
class PostListSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = ('__all__')
The settings.py of the Django project:
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.AllowAny'],
'DEFAULT_AUTHENTICATION_CLASSES:': ('rest_framework_simplejwt.authentication.JWTAuthentication',)
}
CORS_ALLOW_ALL_ORIGINS = True # For testing purposes
I have followed several different tutorials online, read through numerous posts as well as followed the official documentation of Simple JWT.
Well what you are doing is trying to filter the data while your basic purpose is to just list your model. For filtering make sure your go through the documentation DRF Filtering.
Try these changes in your code. I hope it will work for you.
Settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
}
Views.py
class UserList(generics.ListAPIView):
permission_classes = (IsAuthenticated,)
queryset = Post.objects.all()
serializer_class = PostListSerializer
After this try to hit your API with access token. To learn more about generic views you can go through this link Generic Views in DRF.
I am using JWT authentication for my django-rest-framework and react project. So, I have defined a URL path that provides the JWT token.
path('api/auth/token/', obtain_jwt_token),
I have defined another path which retrieves the current logged-in user:
path('current_user/', current_user, name='current-user'),
current_user:
#api_view(['GET'])
def current_user(request):
if not request.user.is_authenticated:
return Response('User is not authenticated')
profile = Profile.objects.get(user=request.user)
serializer = CurrentProfileSerializer(profile)
return Response(serializer.data)
The problem is, after I log in at api/auth/token/ and then go to current_user/, I am getting 'User is not authenticated' response. I thought that obtain_jwt_token returns a token and logs in the user to request.user. Am I wrong for assuming this? Please ask if I need to provide any more details.
settings.py
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
}
when you receive token in api/auth/token reuqest, you should store it in frontend. then in currect_user request, use this stored token in header of request. like this:
Authorization: JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyLCJ1c2VybmFtZSI6ImFsaUBtYWlsLmNvbSIsImV4cCI6MTUzMzcxNjUzNCwiZW1haWwiOiJhbGlAbWFpbC5jb20ifQWfVfp6Nfj9gvedTkqhqlwZhAwzi2YK64cx2FpRLms
TLDR; It seems that my POSTs (to DRF endpoints) are only CSRF protected, if the client has an authenticated session. This is wrong, and leaves the application option to login CSRF attacks. How can I fix this?
I'm starting to build a django rest framework API for a ReactJS frontend, and we want everything, including the authentication, to be handled via API. We are using SessionAuthentication.
If I have an authenticated session, then CSRF works entirely as expected (when auth'd the client should have a CSRF cookie set, and this needs to be paired with the csrfmiddlewaretoken in the POST data).
However, when not authenticated, no POSTs seem to be subject to CSRF checks. Including the (basic) login APIView that has been created. This leaves the site vulnerable to login CSRF exploits.
Does anyone know how to enforce CSRF checks even on unathenticated sessions? and/or how DRF seems to bypass CSRF checks for login?
Below is my rough setup ...
settings.py:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
views.py:
class Login(APIView):
permission_classes = (permissions.AllowAny,)
#method_decorator(csrf_protect) # shouldn't be needed
def post(self, request, format=None):
user = authenticate(
request,
username=request.POST['username'],
password=request.POST['password']
)
# ... perform login logic ...
def get(self, request, format=None):
"""
Client must GET the login to obtain CSRF token
"""
# Force generation of CSRF token so that it's set in the client
get_token(request)
return Response(None)
urls.py:
urlpatterns = [
url(r'^login/$', views.Login.as_view(), name='login'),
]
expected behaviour:
login_url = reverse('login')
login_details = {
'username': self.user.email,
'password': self.password,
}
client = APIClient(enforce_csrf_checks=True)
# Try to just POST to a CSRF protected view with no CSRF
response = client.post(reverse('login'), login_details)
# response status should be 403 Missing or incorrect CSRF
# GET the login API first to obtain CSRF
client.get(reverse('login'))
login_details['csrfmiddlewaretoken'] = client.cookies.get('csrftoken').value
# Now POST to the login API with the CSRF cookie and CSRF token in the POST data
response = client.post(reverse('login'), login_details)
# response status should now be 200 (and a newly rotated CSRF token delivered)
actual behaviour:
client = APIClient(enforce_csrf_checks=True)
# Try to just to a CSRF protected view with no CSRF
response = client.post(reverse('login'), login_details)
# BROKEN: response status is 200, client is now logged in
# Post to the exact same view again, still with no CSRF
response = client.post(reverse('login'), login_details)
# response status is now 403
# BROKEN: This prooves that this view is protected against CSRF, but ONLY for authenticated sessions.
Django REST Framework is disabling CSRF token requirement when using SessionAuthentication and user is not authenticated. This is by design to not mess up other authentication method that don't require CSRF authentication (because they're not based on cookies) and you should ensure by yourself that CSRF is validated on login request and it is mentioned in last paragraph of SessionAuthentication documentation. It is advised to either use non-API login process or ensure that API-based login process is fully protected.
You can check how DRFs SessionAuthentication is enforcing CSRF validation when you are logged in and base your view on that.
You can create a child class of APIView that forces CSRF.
from rest_framework import views
class ForceCRSFAPIView(views.APIView):
#classmethod
def as_view(cls, **initkwargs):
# Force enables CSRF protection. This is needed for unauthenticated API endpoints
# because DjangoRestFramework relies on SessionAuthentication for CSRF validation
view = super().as_view(**initkwargs)
view.csrf_exempt = False
return view
Then all you need to do is change your login view to descend from this
class Login(ForceCRSFAPIView)
# ...
I'm following the DRF docs to setup TokenAuthentication, and can't get it working with the browsable API. I believe I've added the proper lines in settings.py:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
),
INSTALLED_APPS = (
...
'rest_framework',
'rest_framework.authtoken',
...
As well as generated tokens for existing users with the code snippet from the docs. I can see tokens for each user if I query the authtoken_token table, so I know they exist.
Everytime I try to log in to the browsable API, I get the following content returned:
HTTP 401 Unauthorized
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
WWW-Authenticate: Token
{
"detail": "Authentication credentials were not provided."
}
So it appears to be attempting Token authentication, but this message is a little odd. When I enter an incorrect password, I get the 'enter a correct password' message on the login form. When I enter the correct password, it appears to login, but takes me to the API root with the above message, and displays "Log In" on the top menu, rather than the username.
Could this be related to my custom user model somehow? Or could it be due to the fact that I'm currently developing with the dev server, which doesn't support https- the DRF docs mention needing HTTPS with TokenAuthentication, though I wasn't sure if that was a best practice or actually required.
You can't use the browsable api with TokenAuthentication. You have to add SessionAuthtication to your settings (http://www.django-rest-framework.org/api-guide/authentication/#sessionauthentication):
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
),
You can use a browser plugin to set token in the header. I'm using Modheader which is free.
The example of setting the header:
I wrote a blog post on how this can be done: link to post.
I like this solution because you don't need to change the auth classes.
I did:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
),
and I added a custom auth class in api.py
class CustomAuthToken(ObtainAuthToken):
authentication_classes = [TokenAuthentication]
def post(self, request, *args, **kwargs):
...
return Response({...})
See https://www.django-rest-framework.org/api-guide/authentication/#by-exposing-an-api-endpoint