I am doing a django project, using dj-rest-auth, and jwt token. When registering a user, the program generates a refresh token along with access token, but when logging in, it only generates access token and refresh token is an empty string "".
I cannot find a similar problem, nor any solution to this.
Here is my settings.py:
"""
Django settings for backend project.
Generated by 'django-admin startproject' using Django 4.2.1.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
from pathlib import Path
from datetime import timedelta
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-$!z5s1dryft$&tjajmulo+kb7^vi$mfujnzor$_zi(7qv9jxwj'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
#3rd Party
'rest_framework',
'rest_framework_simplejwt',
'dj_rest_auth',
'allauth',
'allauth.account',
'allauth.socialaccount',
'dj_rest_auth.registration',
'corsheaders',
#local
'accounts.apps.AccountsConfig',
]
SITE_ID = 1
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
"corsheaders.middleware.CorsMiddleware",
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'backend.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.request',
],
},
},
]
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
WSGI_APPLICATION = 'backend.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL = "accounts.CustomUser"
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.AllowAny",
],
"DEFAULT_AUTHENTICATION_CLASSES": [
'rest_framework_simplejwt.authentication.JWTAuthentication',
"rest_framework.authentication.SessionAuthentication",
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10
}
# REST_AUTH = {
# 'TOKEN_MODEL': None,
# 'USE_JWT': True,
# 'JWT_AUTH_COOKIE': 'my-app-auth',
# 'JWT_AUTH_REFRESH_COOKIE': 'my-refresh-token',
# 'USER_DETAILS_SERIALIZER': "accounts.serializers.CustomUserDetailsSerializer"
# }
REST_AUTH = {
'LOGIN_SERIALIZER': 'dj_rest_auth.serializers.LoginSerializer',
'TOKEN_SERIALIZER': 'dj_rest_auth.serializers.TokenSerializer',
'JWT_SERIALIZER': 'dj_rest_auth.serializers.JWTSerializer',
'JWT_SERIALIZER_WITH_EXPIRATION': 'dj_rest_auth.serializers.JWTSerializerWithExpiration',
'JWT_TOKEN_CLAIMS_SERIALIZER': 'rest_framework_simplejwt.serializers.TokenObtainPairSerializer',
'USER_DETAILS_SERIALIZER': 'accounts.serializers.CustomUserDetailsSerializer',
'PASSWORD_RESET_SERIALIZER': 'dj_rest_auth.serializers.PasswordResetSerializer',
'PASSWORD_RESET_CONFIRM_SERIALIZER': 'dj_rest_auth.serializers.PasswordResetConfirmSerializer',
'PASSWORD_CHANGE_SERIALIZER': 'dj_rest_auth.serializers.PasswordChangeSerializer',
'REGISTER_SERIALIZER': 'dj_rest_auth.registration.serializers.RegisterSerializer',
'REGISTER_PERMISSION_CLASSES': ('rest_framework.permissions.AllowAny',),
'TOKEN_MODEL': None,
'TOKEN_CREATOR': 'dj_rest_auth.utils.default_create_token',
'PASSWORD_RESET_USE_SITES_DOMAIN': False,
'OLD_PASSWORD_FIELD_ENABLED': False,
'LOGOUT_ON_PASSWORD_CHANGE': False,
'SESSION_LOGIN': True,
'USE_JWT': True,
'JWT_AUTH_COOKIE': 'my-app-auth',
'JWT_AUTH_REFRESH_COOKIE': 'my-refresh-token',
'JWT_AUTH_REFRESH_COOKIE_PATH': '/',
'JWT_AUTH_SECURE': False,
'JWT_AUTH_HTTPONLY': True,
'JWT_AUTH_SAMESITE': 'Lax',
'JWT_AUTH_RETURN_EXPIRATION': False,
'JWT_AUTH_COOKIE_USE_CSRF': False,
'JWT_AUTH_COOKIE_ENFORCE_CSRF_ON_UNAUTHENTICATED': False,
}
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"http://localhost:8000",
]
CSRF_TRUSTED_ORIGINS = ["http://localhost:3000"]
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=5),
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
"ROTATE_REFRESH_TOKENS": False,
"BLACKLIST_AFTER_ROTATION": False,
"UPDATE_LAST_LOGIN": False,
"ALGORITHM": "HS256",
"SIGNING_KEY": SECRET_KEY,
"VERIFYING_KEY": "",
"AUDIENCE": None,
"ISSUER": None,
"JSON_ENCODER": None,
"JWK_URL": None,
"LEEWAY": 0,
"AUTH_HEADER_TYPES": ("Bearer",),
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
"USER_ID_FIELD": "id",
"USER_ID_CLAIM": "user_id",
"USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule",
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
"TOKEN_TYPE_CLAIM": "token_type",
"TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser",
"JTI_CLAIM": "jti",
"SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
"SLIDING_TOKEN_LIFETIME": timedelta(minutes=5),
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
"TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer",
"TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSerializer",
"TOKEN_VERIFY_SERIALIZER": "rest_framework_simplejwt.serializers.TokenVerifySerializer",
"TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer",
"SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer",
"SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer",
}
Here is my urls.py
from django.urls import include, path
from rest_framework import routers
from .views import CustomUserViewSet
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
from dj_rest_auth.views import PasswordResetConfirmView
router = routers.DefaultRouter()
router.register(r'users', CustomUserViewSet)
urlpatterns = [
path('', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('dj-rest-auth/', include('dj_rest_auth.urls')),
path('dj-rest-auth/registration/', include('dj_rest_auth.registration.urls')),
path('dj-rest-auth/password/reset/confirm/<str:uidb64>/<str:token>', PasswordResetConfirmView.as_view(),
name='password_reset_confirm'),
]
Here is what I get when i go the dj-rest-auth/login endpoint and put in my info:
As you can see no refresh token was generated.
Also here is my serializers:
from .models import CustomUser
from rest_framework import serializers
from dj_rest_auth.serializers import UserDetailsSerializer
class CustomUserSerializer(serializers.ModelSerializer):
class Meta:
model = CustomUser
fields = ('id', 'username', 'email', 'first_name', 'last_name', 'name')
class CustomUserDetailsSerializer(UserDetailsSerializer):
class Meta:
model = CustomUser
fields = UserDetailsSerializer.Meta.fields + ('name',)
EDIT: I found in the source code of the dj-rest-auth, in the LoginView, their code is written like this:
def get_response(self):
serializer_class = self.get_response_serializer()
if api_settings.USE_JWT:
from rest_framework_simplejwt.settings import (
api_settings as jwt_settings,
)
access_token_expiration = (timezone.now() + jwt_settings.ACCESS_TOKEN_LIFETIME)
refresh_token_expiration = (timezone.now() + jwt_settings.REFRESH_TOKEN_LIFETIME)
return_expiration_times = api_settings.JWT_AUTH_RETURN_EXPIRATION
auth_httponly = api_settings.JWT_AUTH_HTTPONLY
data = {
'user': self.user,
'access': self.access_token,
}
if not auth_httponly:
data['refresh'] = self.refresh_token
else:
# Wasnt sure if the serializer needed this
data['refresh'] = ''
As you can see the last sentence it is setting to empty string, when I change that to self.refresh_token, it does work, is this a problem from their end, meaning a bug in the dj-rest-auth package, or there is another solution to this? And if it is a bug, how can I workaround this?
I was understanding the httponly refresh token incorrectly. After some more research this is how refresh token work. Storing and sending it happens automatically.
When using an HttpOnly cookie for the refresh token, you don't directly access the refresh token from the frontend. Instead, the browser automatically sends it with every HTTP request made to the same domain. Here's how you can work with such a setup:
Login:
When the user logs in, the server sets the refresh token as an HttpOnly cookie in the HTTP response. The frontend does not have access to this cookie, but the browser stores it. If you're also getting an access token during login (which is not stored as an HttpOnly cookie), the frontend can store this in memory (or other less secure client-side storages like localStorage or sessionStorage if required). Using Access Token:
For accessing protected resources, the frontend sends requests with the access token (often in the Authorization header). If the access token is valid, the server processes the request. Access Token Expiry & Refresh:
When the access token expires and a request to a protected resource fails because of that, the frontend recognizes this expiration (usually through a 401 Unauthorized response). The frontend then sends a request to a token refresh endpoint on the server. Since the refresh token is stored as an HttpOnly cookie, it's automatically sent with this request. The server checks the refresh token, and if it's valid, issues a new access token and sends it back in the response. The frontend then retries the original request with the new access token. Logout:
On logout, the frontend sends a request to a logout endpoint. The server then invalidates the refresh token and sends a response that tells the browser to delete the refresh token cookie.