Mobile Authentication

JWT-based authentication for mobile and native app clients using allauth app endpoints

Mobile Authentication

Mobile and native app authentication uses JWT tokens via the allauth app client endpoints.

JWT Token Strategy

Enable JWT authentication:

HEADLESS_TOKEN_STRATEGY=apps.accounts.token_strategy.HeadlessJWTTokenStrategy

JWT Configuration

# config/settings.py

# Token expiration
HEADLESS_JWT_ACCESS_TOKEN_EXPIRES_IN = 300      # 5 minutes
HEADLESS_JWT_REFRESH_TOKEN_EXPIRES_IN = 86400   # 24 hours

# Stateful validation (recommended)
HEADLESS_JWT_STATEFUL_VALIDATION_ENABLED = True

# Private key (base64 encoded in production)
HEADLESS_JWT_PRIVATE_KEY = "<base64_encoded_pem_key>"

API Endpoints

Mobile clients use the /_allauth/app/v1/ endpoints:

EndpointMethodPurpose
/auth/loginPOSTAuthenticate and get tokens
/auth/signupPOSTCreate account
/auth/sessionGETGet current session
/auth/sessionDELETELogout
/auth/token/refreshPOSTRefresh access token

Authentication Flow

Login

POST /_allauth/app/v1/auth/login
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "password123"
}

Response

{
  "meta": {
    "is_authenticated": true,
    "access_token": "eyJhbGciOiJFUzI1NiIs...",
    "refresh_token": "eyJhbGciOiJFUzI1NiIs..."
  },
  "data": {
    "user": {
      "id": 1,
      "email": "user@example.com"
    }
  }
}

Authenticated Requests

GET /api/v1/users/profile/
Authorization: Bearer eyJhbGciOiJFUzI1NiIs...

Token Refresh

POST /_allauth/app/v1/auth/token/refresh
Content-Type: application/json

{
  "refresh": "eyJhbGciOiJFUzI1NiIs..."
}

JWT Token Claims

The custom HeadlessJWTTokenStrategy adds tenant claims to JWT tokens:

# apps/accounts/token_strategy.py
class HeadlessJWTTokenStrategy(JWTTokenStrategy):
    def get_claims(self, user) -> Dict[str, Any]:
        claims = {}
        if hasattr(user, "get_primary_tenant"):
            tenant = user.get_primary_tenant()
            if tenant:
                claims["tenant_id"] = str(tenant.id)
        return claims

Decoded Token

{
  "sub": "1",
  "sid": "abc123",
  "iat": 1704672000,
  "exp": 1704672300,
  "jti": "unique-token-id",
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000"
}

Client Implementation

React Native Example

import AsyncStorage from '@react-native-async-storage/async-storage';
import axios from 'axios';

const API_URL = 'https://api.example.com';

const apiClient = axios.create({
  baseURL: API_URL,
});

// Token storage
async function storeTokens(access: string, refresh: string) {
  await AsyncStorage.multiSet([
    ['access_token', access],
    ['refresh_token', refresh],
  ]);
}

// Request interceptor
apiClient.interceptors.request.use(async (config) => {
  const token = await AsyncStorage.getItem('access_token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Response interceptor for token refresh
apiClient.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401) {
      const refresh = await AsyncStorage.getItem('refresh_token');
      if (refresh) {
        const { data } = await axios.post(
          `${API_URL}/_allauth/app/v1/auth/token/refresh`,
          { refresh }
        );
        await storeTokens(
          data.meta.access_token,
          data.meta.refresh_token
        );
        // Retry original request
        error.config.headers.Authorization = 
          `Bearer ${data.meta.access_token}`;
        return apiClient.request(error.config);
      }
    }
    return Promise.reject(error);
  }
);

// Login function
async function login(email: string, password: string) {
  const { data } = await axios.post(
    `${API_URL}/_allauth/app/v1/auth/login`,
    { email, password }
  );
  await storeTokens(
    data.meta.access_token,
    data.meta.refresh_token
  );
  return data.data.user;
}

Swift Example

import Foundation

class AuthService {
    static let shared = AuthService()
    private let baseURL = "https://api.example.com"
    
    func login(email: String, password: String) async throws -> User {
        let url = URL(string: "\(baseURL)/_allauth/app/v1/auth/login")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = try JSONEncoder().encode([
            "email": email,
            "password": password
        ])
        
        let (data, _) = try await URLSession.shared.data(for: request)
        let response = try JSONDecoder().decode(LoginResponse.self, from: data)
        
        // Store tokens in Keychain
        KeychainService.store(key: "access_token", value: response.meta.accessToken)
        KeychainService.store(key: "refresh_token", value: response.meta.refreshToken)
        
        return response.data.user
    }
}

Security Considerations

  1. Token Storage: Store tokens securely (Keychain on iOS, EncryptedSharedPreferences on Android)
  2. Token Refresh: Implement automatic refresh before expiration
  3. Logout: Clear all stored tokens on logout
  4. SSL Pinning: Consider certificate pinning for sensitive apps
  5. Biometric Auth: Use biometrics to unlock stored tokens

Testing

# tests/account/test_jwt_auth.py
class TestJWTLogin:
    def test_login_returns_jwt_tokens(self, dj_client, user_with_tenant):
        response = dj_client.post(
            "/_allauth/app/v1/auth/login",
            data=json.dumps({"email": user.email, "password": TEST_PASSWORD}),
            content_type="application/json",
        )
        assert response.status_code == 200
        assert "access_token" in response.json()["meta"]
        assert "refresh_token" in response.json()["meta"]

Last updated on

On this page