Integrating Quasar/VueJS With Django Session Authentication
The decoupling of frontend and backend systems has become a popular architectural choice. This approach not only enhances scalability but also promotes a clear division of responsibilities, making projects more manageable and efficient. However, building working development environments can be challenging, especially when setting up authentication. This article aims to demystify the process of integrating a Quasar/VueJS frontend with a Django backend, focusing primarily on setting up a seamless session based authentication system.
If you would like to skip this article and just get the starter project, you can find that here: https://github.com/bklik/quasar-django-starter
Setting Up An Environment
We’ll start by setting up our environment for our project. We’ll need a project folder, python virtual environment, Quasar frontend project, and Django backend project.
# Create an navigate to our project folder
mkdir vue-django-session
cd vue-django-session
# Create a python virtual environment and source it
python -m venv .venv
source .venv/bin/activate
# Windows: .venv/Scripts/activate
# Install Django and setup a backend project
pip install django
django-admin startproject backend
# Set up a quasar fontend project
# - Quasar CLI
# - Project folder: frontend
# - Vue 3
# - Typescript
# - Webpack
# - <script setup>
# - SCSS
# - ESLint/Pinia/Axios
# - Prettier
npm init quasar
Building Our Backend
Our Django development environment runs by default on port 8000, but our Quasar/VueJS development environment runs by default on port 8080. This will cause Cross-Origin Resource Sharing (CORS) errors, as our frontend tries to communicate with our backend. We need to change some settings to get this to work.
We will need a few more project requirements first
# Django REST Framework for delivering our API
pip install djangorestframework
# CORS headers for allowing us to accept requests from other ports
pip install django-cors-headers
cd backend/
python manage.py migrate
python manage.py createsuperuser
Apply the following settings changes.
# backend/backend/settings.py
import os
...
INSTALLED_APPS = [
...
# Requirements
'djangorestframework',
'django-cors-headers'
]
MIDDLEWARE = [
...
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
...
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
# This will remove these settings when served by Apache during production
if not os.environ.get('WSGI_APPLICATION'):
# Allow all origins (not recommended for production)
CORS_ALLOW_CREDENTIALS = True
# Or allow specific origins
CORS_ALLOWED_ORIGINS = [
"http://localhost:8080",
]
CSRF_TRUSTED_ORIGINS = [
"http://localhost:8080",
]
# Name of token in header
CSRF_COOKIE_NAME = "csrftoken"
# 20 minutes in seconds
SESSION_COOKIE_AGE = 1200
# Resets the cookie are after each request
SESSION_SAVE_EVERY_REQUEST = True
...
Create An App To Manage Sessions
There is no built-in Django session administration for seeing what users are logged in and when their session will expire. We will create a small app for viewing active sessions and managing the views for login and checking authentication.
# from backend/
python manage.py startapp app_auth
Apply the following file changes.
# app_auth/admin.py
from django.contrib import admin
from django.contrib.auth.models import User
from django.contrib.sessions.models import Session
from django.urls import reverse
from django.utils.html import format_html
@admin.register(Session)
class SessionAdmin(admin.ModelAdmin):
# Ceates an easy way to view/expire current sessions
list_display = ('session_key', 'username', 'expire_date', 'expire_session')
def username(self, obj):
session_data = obj.get_decoded()
user_id = session_data.get('_auth_user_id')
if user_id:
user = User.objects.get(id=user_id)
return user.username
return None
def expire_session(self, obj):
return format_html(
'<a href="{}" class="button">Expire Session</a>',
reverse('expire_session', args=[obj.pk])
)
# app_auth/serializers.py
from django.contrib.auth.models import User
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.serializers import Serializer
class UserSerializer(serializers.ModelSerializer):
permissions = serializers.SerializerMethodField()
def get_permissions(self, obj):
return list(obj.get_all_permissions())
class Meta:
model = User
fields = [
'id',
'username',
'first_name',
'last_name',
'permissions'
]
class LoginSerializer(Serializer):
username = serializers.CharField()
password = serializers.CharField(write_only=True)
def validate(self, data):
user = User.objects.filter(username=data['username']).first()
if user and user.check_password(data['password']):
return user
raise ValidationError({'error': 'Invalid username or password.'})
# app_auth/views.py
from django.contrib.auth import login
from django.contrib.sessions.models import Session
from django.http import HttpResponseRedirect
from rest_framework import permissions, status
from rest_framework.response import Response
from rest_framework.views import APIView
from .serializers import LoginSerializer, UserSerializer
class AuthCheck(APIView):
permission_classes = []
def get(self, request):
if request.user.is_authenticated:
user = UserSerializer(request.user, context={'request': request})
return Response({"isAuthenticated": True, "user": user.data})
else:
return Response({"isAuthenticated": False})
class LoginView(APIView):
authentication_classes = []
permission_classes = (permissions.AllowAny,)
def post(self, request):
serializer = LoginSerializer(data=request.data)
if serializer.is_valid():
user = serializer.validated_data
login(request, user)
return Response({"detail": "Login successful."}, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def expire_session_view(request, session_key):
try:
session = Session.objects.get(session_key=session_key)
session.delete()
except Session.DoesNotExist:
pass
return HttpResponseRedirect('/admin/sessions/session/')
# app_auth/urls.py
from django.urls import path
from .views import AuthCheck, LoginView, expire_session_view
urlpatterns = [
path('auth-check', AuthCheck.as_view(), name='auth_check'),
path('login/', LoginView.as_view(), name='login'),
path('expire-session/<str:session_key>/',
expire_session_view, name='expire_session'),
]
# backend/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('app_auth/', include('app_auth.urls')),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
path('admin/', admin.site.urls),
]
# bachend/settings.py
...
INSTALLED_APPS = [
...
# Apps
'app_auth'
]
If we test our progress using `python manage.py runserver`, we should be able to access the Django admin at: http://127.0.0.1:8000/admin
We can log in using the superuser credentials, and see our session by clicking our Sessions admin. While we are in the admin, create a new user to test the auth-check view.
We can use localhost in our url to login with a different user: http://localhost:8000/app_auth/auth-check
Using the ‘Log in’ link, we can authenticate with our newly created test user credentials, and see the user information.
If we go back to our Django adminstration tab and refresh Sessions, we should now see the session of our test user. We can expire this Session, which will log our test user out.
Building Our Frontend
Now that we we have our backend ready, we need to build out the settings, components, stores, and communications needed on our frontend.
First, we’ll setup axios. We need axios to know what URL to use, that it needs to include credentials, and to pass in the csfrtoken cookie in the header when making post/put requests.
// src/boot/axios.ts
...
const api = axios.create({
baseURL: 'http://localhost:8000',
withCredentials: true,
});
// Helper function to get CSRF token from cookie
function getCookie(name: string): string | null {
const value = '; ' + document.cookie;
const parts = value.split('; ' + name + '=');
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
}
export default boot(({ app }) => {
...
// Set the X-CSRFToken header for all POST/PUT requests
api.interceptors.request.use((config) => {
if (
config.method?.toLowerCase() === 'post' ||
config.method?.toLowerCase() === 'put'
) {
const csrfToken = getCookie('csrftoken');
if (csrfToken) {
config.headers['X-CSRFToken'] = csrfToken;
}
}
return config;
});
});
Next we need a store that will make user credential requests to the backend.
// src/stores/useAppAuthStore.ts
import { defineStore } from "pinia";
import { api } from "src/boot/axios";
type LoginFormType = {
username: string;
password: string;
};
type UserType = {
id: number;
username: string;
first_name: string;
last_name: string;
permissions: Array<string>;
};
type AuthCheckType = {
isAuthenticated: boolean;
user?: UserType;
};
export const useAppAuthStore = defineStore("app_auth", {
state: () => ({
loggedOut: true,
error: "" as string,
user: {} as UserType,
loading: false,
authInterval: null as ReturnType<typeof setInterval> | null,
}),
getters: {},
actions: {
async login(credentials: LoginFormType) {
this.loading = true;
await api
.post("/app_auth/login/", credentials)
.then(() => {
this.authCheck();
})
.catch((error) => {
console.error(error.response.data);
this.error = error.response.data.error;
this.loggedOut = true;
this.user = {} as UserType;
this.loading = false;
});
},
async authCheck() {
this.loading = true;
await api
.get("/app_auth/auth-check")
.then((response) => {
const data = response.data as AuthCheckType;
if (data.isAuthenticated && data.user) {
this.user = data.user;
this.error = "";
this.loggedOut = false;
this.loading = false;
if (!this.authInterval) {
this.authInterval = setInterval(() => {
this.authCheck();
}, 1000 * 60 * 21); // 21 minutes in miliseconds
}
} else {
this.error = "";
this.loggedOut = true;
this.loading = false;
this.authInterval = null;
}
})
.catch((error) => {
console.error(error.response.data);
this.error = error;
this.loggedOut = true;
this.loading = false;
});
},
async logout() {
this.loading = true;
await api
.get("/api-auth/logout/")
.then(() => {
this.authCheck();
})
.catch((error) => {
console.error(error.response.data);
this.error = error.response.data.error;
this.loading = false;
});
},
},
});
Create component that provides the user with a form for logging in.
<!-- src/components/app_auth/LoginForm.vue -->
<template>
<q-dialog v-model="appAuthStore.loggedOut" persistent>
<q-card class="login-form">
<q-form @submit="submitLogin()">
<q-card-section>
<div class="text-h4">Sign in</div>
</q-card-section>
<q-card-section class="column q-gutter-md">
<q-input
type="text"
label="Username"
required
autofocus
v-model="loginModel.username"
:rules="[
(val) =>
(val && val.length > 0) || 'Required field.',
]"
></q-input>
<q-input
type="password"
label="Password"
required
v-model="loginModel.password"
:rules="[
(val) =>
(val && val.length > 0) || 'Required field.',
]"
></q-input>
<div v-if="appAuthStore.error" class="text-negative">
<q-icon :name="mdiAlertCircle" size="sm"></q-icon>
{{ appAuthStore.error }}
</div>
</q-card-section>
<q-card-actions align="right" class="text-primary">
<q-btn
type="submit"
color="primary"
:loading="appAuthStore.loading"
label="Sign in"
:icon="mdiLoginVariant"
></q-btn>
</q-card-actions>
</q-form>
</q-card>
</q-dialog>
</template>
<style lang="scss">
.login-form {
width: 100%;
& input:required + .q-field__label::after {
color: $negative;
content: " *";
}
}
</style>
<script lang="ts" setup>
import { reactive } from "vue";
import { mdiAlertCircle, mdiLoginVariant } from "@quasar/extras/mdi-v6";
import { useAppAuthStore } from "src/stores/useAppAuthStore";
type LoginFormType = {
username: string;
password: string;
};
const defaultModel = {
username: "",
password: "",
};
const appAuthStore = useAppAuthStore();
const loginModel = reactive<LoginFormType>({ ...defaultModel });
function submitLogin() {
appAuthStore.login(loginModel).then(() => {
if (appAuthStore.error === "") {
Object.assign(loginModel, { ...defaultModel });
}
});
}
</script>
Quasar starts with an ExampleComponent that we can reuse to display user information when they successfully log in.
<!-- src/components/ExampleComponent.vue -->
<template>
<pre>{{ appAuthStore.user }}</pre>
<q-btn
v-if="!appAuthStore.loggedOut"
label="Logout"
color="primary"
@click="appAuthStore.logout()"
></q-btn>
</template>
<script setup lang="ts">
import { useAppAuthStore } from "src/stores/useAppAuthStore";
const appAuthStore = useAppAuthStore();
</script>
Since the IndexPage was using the ExampleComponent, we need to remove all the unnecessary code.
<!-- src/pages/IndexPage.vue -->
<template>
<q-page class="row items-center justify-evenly">
<example-component></example-component>
</q-page>
</template>
<script setup lang="ts">
import ExampleComponent from "components/ExampleComponent.vue";
</script>
Include the LoginForm component on the MainLayout so it will show when the user is not authenticated.
<!-- src/layouts/MainLayout.vue -->
<template>
...
<LoginForm></LoginForm>
</template>
<script setup lang="ts">
...
import LoginForm from 'src/components/app_auth/LoginForm.vue';
</script>
Last, we need to check if the user is authenticated whenever they navigate around the app to see if they are logged in.
// src/router/index.ts
export default route(function (/* { store, ssrContext } */) {
...
// Before each rount, check if the user is logged in
Router.beforeEach(() => {
appAuthStore.authCheck();
});
});
Run Developer Environmant
We should now have everything we need to run the frontend, backend, and test authentication.
# Terminal 1
python manage.py runserver
# Terminal 2
quasar dev
Your app should look something like this.
That’s everything you need to get started with an authenticated environment. Hope this has been helpful.