MojoAuth Hosted Login Page with Python (Django)
Introduction
Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. Built by experienced developers, Django takes care of much of the hassle of web development, allowing you to focus on writing your app without needing to reinvent the wheel.
This guide will walk you through the process of integrating MojoAuth's Hosted Login Page with your Django application using OpenID Connect (OIDC). We'll leverage the powerful mozilla-django-oidc
library, which provides robust OIDC client capabilities specifically designed for Django.
Links:
- MojoAuth Hosted Login Page Documentation (opens in a new tab)
- Django Documentation (opens in a new tab)
- mozilla-django-oidc Documentation (opens in a new tab)
Prerequisites
Before you begin, make sure you have:
- A MojoAuth account with an OIDC application configured
- Your OIDC Client ID, Client Secret, and Issuer URL (usually https://your-project.auth.mojoauth.com (opens in a new tab))
- Python 3.7+ installed
- Basic knowledge of Python and Django
Project Setup
Let's start by setting up a new Django project:
# Create a new project directory mkdir mojoauth-django-demo cd mojoauth-django-demo # Create and activate a virtual environment python -m venv venv source venv/bin/activate # On Windows, use: venv\Scripts\activate # Install Django and the OIDC library pip install django mozilla-django-oidc python-dotenv requests
Create a New Django Project
# Create a new Django project django-admin startproject mojoauth_django_demo . # Create a new app for the authentication python manage.py startapp authentication
Project Structure
Your project structure should look like this:
mojoauth-django-demo/ │ ├── mojoauth_django_demo/ # Project directory │ ├── __init__.py │ ├── settings.py # Django settings │ ├── urls.py # URL configuration │ ├── asgi.py │ └── wsgi.py │ ├── authentication/ # Authentication app │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations/ │ ├── models.py │ ├── views.py # Custom views │ ├── urls.py # App URL configuration │ └── templates/ # HTML templates │ ├── authentication/ │ │ └── profile.html # User profile template │ ├── templates/ # Project-wide templates │ ├── base.html # Base template │ ├── home.html # Homepage template │ └── error.html # Error page │ ├── static/ # Static files │ ├── css/ │ │ └── style.css │ └── js/ │ └── main.js │ ├── .env # Environment variables (not in version control) └── manage.py # Django management script
Configure Environment Variables
Create a .env
file in the project root to store sensitive information:
MOJOAUTH_CLIENT_ID=your-client-id MOJOAUTH_CLIENT_SECRET=your-client-secret MOJOAUTH_ISSUER=https://your-project.auth.mojoauth.com MOJOAUTH_REDIRECT_URI=http://localhost:8000/oidc/callback/ DJANGO_SECRET_KEY=your-secure-random-string DEBUG=True
Make sure to add this file to your .gitignore
to keep your credentials secure.
Configure Django Settings
Edit the mojoauth_django_demo/settings.py
file to include the OIDC configuration:
import os from pathlib import Path from dotenv import load_dotenv # Load environment variables from .env file load_dotenv() # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'django-insecure-key') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' ALLOWED_HOSTS = ['localhost', '127.0.0.1'] # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # OIDC 'mozilla_django_oidc', # Local apps 'authentication', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', # OIDC middleware 'mozilla_django_oidc.middleware.SessionRefresh', ] ROOT_URLCONF = 'mojoauth_django_demo.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates')], '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', ], }, }, ] WSGI_APPLICATION = 'mojoauth_django_demo.wsgi.application' # Database DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', } } # Password validation 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 LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_TZ = True # Static files (CSS, JavaScript, Images) STATIC_URL = '/static/' STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')] # Default primary key field type DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # Authentication AUTHENTICATION_BACKENDS = ( 'mozilla_django_oidc.auth.OIDCAuthenticationBackend', 'django.contrib.auth.backends.ModelBackend', ) # OIDC Configuration OIDC_RP_CLIENT_ID = os.getenv('MOJOAUTH_CLIENT_ID') OIDC_RP_CLIENT_SECRET = os.getenv('MOJOAUTH_CLIENT_SECRET') OIDC_OP_JWKS_ENDPOINT = f"{os.getenv('MOJOAUTH_ISSUER')}/.well-known/jwks.json" OIDC_OP_AUTHORIZATION_ENDPOINT = f"{os.getenv('MOJOAUTH_ISSUER')}/oauth/authorize" OIDC_OP_TOKEN_ENDPOINT = f"{os.getenv('MOJOAUTH_ISSUER')}/oauth2/token" OIDC_OP_USER_ENDPOINT = f"{os.getenv('MOJOAUTH_ISSUER')}/oauth/userinfo" # OIDC Behavior OIDC_STORE_ACCESS_TOKEN = True OIDC_STORE_ID_TOKEN = True OIDC_AUTH_REQUEST_EXTRA_PARAMS = {'prompt': 'login'} # Login/Logout URLs LOGIN_URL = '/oidc/authenticate/' LOGOUT_REDIRECT_URL = '/' LOGIN_REDIRECT_URL = '/authentication/profile/' # Custom OIDC Authentication OIDC_RP_SIGN_ALGO = 'RS256' OIDC_USERNAME_ALGO = 'authentication.utils.generate_username'
Create Custom User Model (Optional)
For more flexibility, you might want to create a custom user model in authentication/models.py
:
from django.contrib.auth.models import AbstractUser from django.db import models class CustomUser(AbstractUser): """ Custom user model that extends the default Django user model. This allows for easy addition of custom fields. """ email = models.EmailField(unique=True) picture = models.URLField(blank=True, null=True) # Add additional fields as needed def __str__(self): return self.email
If you decide to use a custom user model, update your settings:
# In settings.py AUTH_USER_MODEL = 'authentication.CustomUser'
Make sure to create and apply migrations before proceeding:
python manage.py makemigrations python manage.py migrate
Create Authentication Utilities
Create a new file authentication/utils.py
for helper functions:
import base64 import hashlib import json import time from urllib.parse import urlencode import requests from django.conf import settings from django.urls import reverse from mozilla_django_oidc.auth import OIDCAuthenticationBackend def generate_username(email): """ Generate a unique username from an email address. """ # Convert email to base64 to avoid invalid characters return base64.urlsafe_b64encode(email.encode()).decode().replace('=', '') class MojoAuthOIDCBackend(OIDCAuthenticationBackend): """ Custom OIDC authentication backend for MojoAuth. Extends the mozilla-django-oidc backend. """ def create_user(self, claims): """ Create a new user from the provided claims. """ user = super().create_user(claims) # Update user with information from claims self.update_user(user, claims) return user def update_user(self, user, claims): """ Update an existing user with the provided claims. """ # Update user fields based on claims user.first_name = claims.get('given_name', '') user.last_name = claims.get('family_name', '') user.email = claims.get('email', '') # For custom user model if hasattr(user, 'picture'): user.picture = claims.get('picture', '') user.save() return user def get_logout_url(request): """ Generate the MojoAuth logout URL. """ params = { 'client_id': settings.OIDC_RP_CLIENT_ID, 'post_logout_redirect_uri': request.build_absolute_uri(settings.LOGOUT_REDIRECT_URL), } logout_url = f"{settings.MOJOAUTH_ISSUER}/oauth2/sessions/logout?{urlencode(params)}" return logout_url def refresh_token(request): """ Refresh the access token using the refresh token. """ refresh_token = request.session.get('oidc_refresh_token') if not refresh_token: return None token_endpoint = settings.OIDC_OP_TOKEN_ENDPOINT data = { 'client_id': settings.OIDC_RP_CLIENT_ID, 'client_secret': settings.OIDC_RP_CLIENT_SECRET, 'grant_type': 'refresh_token', 'refresh_token': refresh_token, } response = requests.post(token_endpoint, data=data) if response.status_code == 200: tokens = response.json() request.session['oidc_access_token'] = tokens.get('access_token') request.session['oidc_id_token'] = tokens.get('id_token') # Store the new refresh token if provided if 'refresh_token' in tokens: request.session['oidc_refresh_token'] = tokens.get('refresh_token') # Update expiration time if 'expires_in' in tokens: request.session['oidc_token_expires_at'] = int(time.time()) + tokens.get('expires_in', 3600) return tokens return None def is_token_expired(request, grace_period_seconds=300): """ Check if the token is expired or will expire within the grace period. """ expiry_time = request.session.get('oidc_token_expires_at') if not expiry_time: return True current_time = time.time() return current_time >= (expiry_time - grace_period_seconds) def get_userinfo(request): """ Get user information from the userinfo endpoint using the access token. """ access_token = request.session.get('oidc_access_token') if not access_token: return None headers = { 'Authorization': f"Bearer {access_token}" } response = requests.get(settings.OIDC_OP_USER_ENDPOINT, headers=headers) if response.status_code == 200: return response.json() return None def get_token_details(token): """ Decode the payload of a JWT token without validation. This is for display purposes only, not for authentication. """ if not token: return None # Split the token into header, payload, and signature parts = token.split('.') if len(parts) != 3: return None # Decode the payload payload = parts[1] # Add padding if needed padding = '=' * (4 - (len(payload) % 4)) payload = payload + padding try: decoded = base64.urlsafe_b64decode(payload) return json.loads(decoded) except Exception: return None
Configure OIDC Authentication Backend
Update your settings.py
to use your custom backend:
# In settings.py AUTHENTICATION_BACKENDS = ( 'authentication.utils.MojoAuthOIDCBackend', 'django.contrib.auth.backends.ModelBackend', ) # Add MojoAuth issuer setting MOJOAUTH_ISSUER = os.getenv('MOJOAUTH_ISSUER')
Create Views and URLs
Create views in authentication/views.py
:
from django.contrib import messages from django.contrib.auth import logout from django.contrib.auth.decorators import login_required from django.http import JsonResponse from django.shortcuts import redirect, render from .utils import (get_logout_url, get_token_details, get_userinfo, is_token_expired, refresh_token) @login_required def profile_view(request): """ Display the user's profile information. """ # Check if token is expired or about to expire needs_refresh = is_token_expired(request) # Get user info directly from the session access_token = request.session.get('oidc_access_token') id_token = request.session.get('oidc_id_token') # Get user info from the userinfo endpoint userinfo = get_userinfo(request) # Decode tokens for display (not for authentication) access_token_details = get_token_details(access_token) id_token_details = get_token_details(id_token) context = { 'user': request.user, 'userinfo': userinfo, 'access_token': access_token, 'id_token': id_token, 'access_token_details': access_token_details, 'id_token_details': id_token_details, 'needs_refresh': needs_refresh, } return render(request, 'authentication/profile.html', context) @login_required def refresh_token_view(request): """ Refresh the access token. """ result = refresh_token(request) if result: messages.success(request, "Token refreshed successfully.") else: messages.error(request, "Failed to refresh token.") return redirect('profile') @login_required def custom_logout(request): """ Custom logout view that redirects to MojoAuth logout endpoint. """ # Get the MojoAuth logout URL logout_url = get_logout_url(request) # Logout from Django logout(request) # Redirect to MojoAuth logout endpoint return redirect(logout_url) @login_required def api_userinfo(request): """ API endpoint that returns user information. """ userinfo = get_userinfo(request) if userinfo: return JsonResponse(userinfo) return JsonResponse({'error': 'Failed to get user information'}, status=400)
Create URL patterns in authentication/urls.py
:
from django.urls import path from . import views urlpatterns = [ path('profile/', views.profile_view, name='profile'), path('refresh-token/', views.refresh_token_view, name='refresh_token'), path('logout/', views.custom_logout, name='custom_logout'), path('api/userinfo/', views.api_userinfo, name='api_userinfo'), ]
Update the project's mojoauth_django_demo/urls.py
:
from django.contrib import admin from django.urls import include, path from django.views.generic import TemplateView urlpatterns = [ path('admin/', admin.site.urls), path('oidc/', include('mozilla_django_oidc.urls')), path('authentication/', include('authentication.urls')), path('', TemplateView.as_view(template_name='home.html'), name='home'), path('error/', TemplateView.as_view(template_name='error.html'), name='error'), ]
Create Templates
Project-wide Templates
Create templates/base.html
:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{% block title %}MojoAuth Django Demo{% endblock %}</title> <link rel="stylesheet" href="/static/css/style.css"> </head> <body> <header> <nav> <div class="logo"> <a href="{% url 'home' %}">MojoAuth Django Demo</a> </div> <ul class="nav-links"> <li><a href="{% url 'home' %}">Home</a></li> {% if user.is_authenticated %} <li><a href="{% url 'profile' %}">Profile</a></li> <li><a href="{% url 'custom_logout' %}">Logout</a></li> {% else %} <li><a href="{% url 'oidc_authentication_init' %}">Login</a></li> {% endif %} </ul> </nav> </header> <div class="container"> {% if messages %} <div class="messages"> {% for message in messages %} <div class="message {{ message.tags }}"> {{ message }} </div> {% endfor %} </div> {% endif %} {% block content %}{% endblock %} </div> <footer> <p>© 2025 MojoAuth Django Demo</p> </footer> </body> </html>
Create templates/home.html
:
{% extends "base.html" %} {% block title %}Home - MojoAuth Django Demo{% endblock %} {% block content %} <div class="hero"> <h1>Welcome to MojoAuth Django Demo</h1> <p>This application demonstrates how to integrate MojoAuth's Hosted Login Page with a Django application.</p> {% if user.is_authenticated %} <div class="welcome-box"> <h2>Welcome, {{ user.get_full_name|default:user.username }}!</h2> <p>You are logged in with MojoAuth.</p> <div class="btn-group"> <a href="{% url 'profile' %}" class="btn primary">View Profile</a> <a href="{% url 'custom_logout' %}" class="btn secondary">Logout</a> </div> </div> {% else %} <div class="auth-box"> <p>Experience secure passwordless authentication with MojoAuth.</p> <a href="{% url 'oidc_authentication_init' %}" class="btn primary">Login with MojoAuth</a> </div> {% endif %} </div> <div class="features"> <div class="feature"> <h2>Secure Authentication</h2> <p>OIDC provides a secure, standardized authentication protocol.</p> </div> <div class="feature"> <h2>Passwordless Login</h2> <p>Improve security and user experience with passwordless options.</p> </div> <div class="feature"> <h2>Easy Integration</h2> <p>Simple setup with Django using mozilla-django-oidc.</p> </div> </div> {% endblock %}
Create templates/error.html
:
{% extends "base.html" %} {% block title %}Error - MojoAuth Django Demo{% endblock %} {% block content %} <div class="error-container"> <h1>{{ error_title|default:"Error" }}</h1> <p class="error-message">{{ error_message|default:"An unexpected error occurred." }}</p> {% if error_details %} <div class="error-details"> <h3>Details:</h3> <pre>{{ error_details }}</pre> </div> {% endif %} <div class="action-buttons"> <a href="{% url 'home' %}" class="btn primary">Back to Home</a> </div> </div> {% endblock %}
Authentication App Templates
Create the directory:
mkdir -p authentication/templates/authentication
Create authentication/templates/authentication/profile.html
:
{% extends "base.html" %} {% block title %}Profile - MojoAuth Django Demo{% endblock %} {% block content %} <div class="profile-container"> <h1>User Profile</h1> <div class="profile-header"> {% if userinfo.picture %} <div class="profile-avatar"> <img src="{{ userinfo.picture }}" alt="Profile Picture"> </div> {% else %} <div class="profile-avatar no-image"> {{ user.get_full_name|default:user.username|slice:":1" }} </div> {% endif %} <div class="profile-info"> <h2>{{ user.get_full_name|default:user.username }}</h2> <p class="email">{{ user.email }}</p> {% if userinfo.email_verified %} <span class="verified-badge">Verified Email</span> {% endif %} </div> </div> {% if needs_refresh %} <div class="token-refresh-alert"> <p>Your session will expire soon.</p> <a href="{% url 'refresh_token' %}" class="btn secondary">Refresh Token</a> </div> {% endif %} <div class="profile-details"> <h3>Profile Information</h3> <table> <tr> <th>Username</th> <td>{{ user.username }}</td> </tr> <tr> <th>Full Name</th> <td>{{ user.get_full_name|default:"Not provided" }}</td> </tr> <tr> <th>Email</th> <td>{{ user.email }}</td> </tr> <tr> <th>User ID</th> <td>{{ userinfo.sub }}</td> </tr> </table> </div> <div class="token-section"> <h3>Token Information</h3> <h4>ID Token</h4> <div class="code-block"> <pre>{{ id_token|slice:":50" }}...</pre> </div> {% if id_token_details %} <h4>ID Token Claims</h4> <div class="code-block"> <pre>{{ id_token_details|pprint }}</pre> </div> {% endif %} <h4>Access Token</h4> <div class="code-block"> <pre>{{ access_token|slice:":50" }}...</pre> </div> {% if access_token_details %} <h4>Access Token Claims</h4> <div class="code-block"> <pre>{{ access_token_details|pprint }}</pre> </div> {% endif %} </div> <div class="raw-profile"> <h3>Raw User Info</h3> <div class="code-block"> <pre>{{ userinfo|pprint }}</pre> </div> </div> <div class="action-buttons"> <a href="{% url 'home' %}" class="btn secondary">Back to Home</a> <a href="{% url 'custom_logout' %}" class="btn danger">Logout</a> </div> </div> {% endblock %}
Add CSS Styling
Create static/css/style.css
:
/* Basic Reset */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; background-color: #f5f7fa; } /* Layout */ .container { width: 100%; max-width: 1200px; margin: 0 auto; padding: 20px; } /* Navigation */ header { background-color: #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.1); } nav { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; max-width: 1200px; margin: 0 auto; } .logo a { color: #4f46e5; font-weight: bold; font-size: 20px; text-decoration: none; } .nav-links { display: flex; list-style: none; } .nav-links li { margin-left: 20px; } .nav-links a { text-decoration: none; color: #555; font-weight: 500; transition: color 0.3s ease; } .nav-links a:hover { color: #4f46e5; } /* Hero Section */ .hero { background-color: #fff; border-radius: 8px; padding: 40px; margin: 20px 0; text-align: center; box-shadow: 0 2px 10px rgba(0,0,0,0.05); } .hero h1 { margin-bottom: 20px; color: #333; font-size: 32px; } .hero p { margin-bottom: 30px; color: #666; font-size: 18px; } /* Buttons */ .btn { display: inline-block; padding: 12px 24px; border-radius: 6px; text-decoration: none; font-weight: 600; font-size: 16px; transition: all 0.3s ease; margin-right: 10px; } .primary { background-color: #4f46e5; color: #fff; } .primary:hover { background-color: #4338ca; } .secondary { background-color: #e5e7eb; color: #374151; } .secondary:hover { background-color: #d1d5db; } .danger { background-color: #ef4444; color: #fff; } .danger:hover { background-color: #dc2626; } .btn-group { margin-top: 20px; } /* Features */ .features { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-top: 40px; } .feature { background-color: #fff; border-radius: 8px; padding: 30px; box-shadow: 0 2px 5px rgba(0,0,0,0.05); transition: transform 0.3s ease, box-shadow 0.3s ease; } .feature:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(0,0,0,0.1); } .feature h2 { margin-bottom: 15px; color: #4f46e5; } /* Profile Page */ .profile-container { background-color: #fff; border-radius: 8px; padding: 40px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); } .profile-header { display: flex; align-items: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #e5e7eb; } .profile-avatar { width: 100px; height: 100px; border-radius: 50%; overflow: hidden; margin-right: 20px; background-color: #4f46e5; color: white; display: flex; align-items: center; justify-content: center; font-size: 40px; font-weight: bold; } .profile-avatar img { width: 100%; height: 100%; object-fit: cover; } .profile-info h2 { margin-bottom: 5px; color: #333; } .profile-info .email { color: #666; margin-bottom: 5px; } .verified-badge { background-color: #10b981; color: white; padding: 2px 8px; border-radius: 10px; font-size: 12px; font-weight: 500; } .profile-details { margin-bottom: 30px; } .profile-details h3, .token-section h3, .token-section h4, .raw-profile h3 { margin-bottom: 15px; color: #4f46e5; font-size: 20px; } .token-section h4 { font-size: 16px; margin-top: 20px; } .profile-details table { width: 100%; border-collapse: collapse; } .profile-details th { text-align: left; padding: 10px; background-color: #f3f4f6; border-bottom: 1px solid #e5e7eb; } .profile-details td { padding: 10px; border-bottom: 1px solid #e5e7eb; } .code-block { background-color: #f8fafc; padding: 15px; border-radius: 6px; overflow: auto; margin-bottom: 30px; border: 1px solid #e5e7eb; } .code-block pre { font-family: 'Courier New', monospace; font-size: 14px; white-space: pre-wrap; } .token-refresh-alert { background-color: #fffbeb; border: 1px solid #fef3c7; border-radius: 6px; padding: 15px; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; } .token-refresh-alert p { color: #92400e; } .action-buttons { margin-top: 30px; } /* Messages */ .messages { margin-bottom: 20px; } .message { padding: 12px 15px; border-radius: 6px; margin-bottom: 10px; } .message.error { background-color: #fee2e2; color: #b91c1c; } .message.success { background-color: #dcfce7; color: #15803d; } .message.warning { background-color: #fef3c7; color: #92400e; } .message.info { background-color: #dbeafe; color: #1e40af; } /* Error Page */ .error-container { text-align: center; padding: 40px; background-color: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); } .error-container h1 { color: #ef4444; margin-bottom: 20px; } .error-message { font-size: 18px; margin-bottom: 20px; } .error-details { text-align: left; margin-bottom: 30px; background-color: #f8fafc; padding: 15px; border-radius: 6px; border: 1px solid #e5e7eb; } /* Footer */ footer { text-align: center; padding: 20px; margin-top: 40px; color: #6b7280; font-size: 14px; } /* Media Queries */ @media (max-width: 768px) { .profile-header { flex-direction: column; text-align: center; } .profile-avatar { margin-right: 0; margin-bottom: 20px; } .token-refresh-alert { flex-direction: column; } .token-refresh-alert .btn { margin-top: 10px; } } @media (max-width: 576px) { nav { flex-direction: column; } .nav-links { margin-top: 15px; } .nav-links li { margin: 0 10px; } .hero { padding: 30px 20px; } }
Create a Template Filter for Pretty Printing (Optional)
To make the JSON output look nicer, you can create a template filter:
mkdir -p authentication/templatetags touch authentication/templatetags/__init__.py
Create authentication/templatetags/json_filters.py
:
import json from django import template from django.utils.safestring import mark_safe register = template.Library() @register.filter def pprint(value): """Pretty print a JSON object.""" try: if isinstance(value, dict): return mark_safe(json.dumps(value, indent=4)) return mark_safe(json.dumps(json.loads(value), indent=4)) except (ValueError, TypeError): return value
Register the App's Template Tags
Update authentication/apps.py
:
from django.apps import AppConfig class AuthenticationConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'authentication'
Make sure to update the __init__.py
to include the template tags:
# In authentication/__init__.py default_app_config = 'authentication.apps.AuthenticationConfig'
Running the Application
Before running the application, make sure to apply migrations:
python manage.py migrate
Now you can start your Django application:
python manage.py runserver
Your application will be available at http://localhost:8000
.
Testing the Authentication Flow
- Open your browser and navigate to
http://localhost:8000
- Click the "Login with MojoAuth" button
- You'll be redirected to the MojoAuth Hosted Login Page
- After successful authentication, you'll be redirected back to your application
- You should now see your user profile information
Production Considerations
1. Environment Configuration
For production deployment, use environment variables or a dedicated configuration system:
# In settings.py DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',')
2. HTTPS in Production
Always use HTTPS in production. Update your settings:
# In settings.py for production SECURE_SSL_REDIRECT = True SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True SECURE_HSTS_SECONDS = 31536000 # 1 year SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_HSTS_PRELOAD = True
3. Database
For production, use a more robust database like PostgreSQL:
# In settings.py for production DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': os.getenv('DB_NAME'), 'USER': os.getenv('DB_USER'), 'PASSWORD': os.getenv('DB_PASSWORD'), 'HOST': os.getenv('DB_HOST', 'localhost'), 'PORT': os.getenv('DB_PORT', '5432'), } }
4. Static Files
For production, configure static files with a CDN or a dedicated static file server:
# In settings.py for production STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
5. Logging
Configure proper logging for production:
# In settings.py for production LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'verbose': { 'format': '{levelname} {asctime} {module} {message}', 'style': '{', }, }, 'handlers': { 'file': { 'level': 'INFO', 'class': 'logging.FileHandler', 'filename': os.path.join(BASE_DIR, 'logs/django.log'), 'formatter': 'verbose', }, }, 'loggers': { 'django': { 'handlers': ['file'], 'level': 'INFO', 'propagate': True, }, 'mozilla_django_oidc': { 'handlers': ['file'], 'level': 'INFO', 'propagate': True, }, 'authentication': { 'handlers': ['file'], 'level': 'INFO', 'propagate': True, }, }, }
Advanced Features
1. Custom OIDC Scopes
To request additional scopes from MojoAuth:
# In settings.py OIDC_RP_SCOPES = 'openid email profile'
2. Role-Based Access Control
Implement role-based access control using custom permissions:
# In authentication/utils.py from django.contrib.auth.mixins import UserPassesTestMixin from django.core.exceptions import PermissionDenied def has_role(user, role_name): """ Check if a user has a specific role from the OIDC claims. """ if not hasattr(user, 'userinfo'): return False roles = user.userinfo.get('roles', []) return role_name in roles class RoleRequiredMixin(UserPassesTestMixin): """ Mixin that checks if a user has a specific role. """ required_role = None def test_func(self): if not self.request.user.is_authenticated: return False if not self.required_role: return True return has_role(self.request.user, self.required_role)
Use the mixin in your views:
from django.views import View from .utils import RoleRequiredMixin class AdminView(RoleRequiredMixin, View): required_role = 'admin' def get(self, request): # Only users with the 'admin' role will reach here return render(request, 'admin_dashboard.html')
3. PKCE Support
For public clients, enable PKCE (Proof Key for Code Exchange):
# In settings.py OIDC_USE_PKCE = True