Skip to main content

Bazis Authing

PyPI version Python Versions License

Extension package for Bazis providing a flexible authentication system with support for various login methods.

Quick Start

# Install the package
uv add bazis-authing

# Configure in settings.py
BAZIS_AUTH_KINDS = [
    'bazis.contrib.authing.services.password',  # Username/password authentication
]

# Register routes in main router.py
from bazis.core.routing import BazisRouter

router = BazisRouter(prefix='/api/v1')
from bazis.contrib.authing.routes import router as authing_router
from bazis.contrib.authing.services.password.router import router as password_router

router.register(prefix='/authing', arg=authing_router)
# the router of every service in BAZIS_AUTH_KINDS
router.register(prefix='/authing', arg=password_router)

Table of Contents

Description

Bazis Authing is an extension package for the Bazis framework that provides a flexible and extensible authentication system. The package includes:

  • Universal /auth/ endpoint — central point for checking authentication status and getting available login methods
  • Built-in password authentication — ready-to-use module for username/password authentication
  • AuthStore — cookie-based authentication state storage system
  • Support for multiple authentication methods — easily add OAuth, SAML, LDAP, and other providers
  • JWT integration — automatic JWT token generation after successful authentication

This package requires bazis and bazis-users packages to be installed.

Requirements

  • Python: 3.12+
  • bazis: latest version
  • bazis-users: latest version
  • PostgreSQL: 12+
  • Redis: For caching

Installation

uv add bazis-authing

Using pip

pip install bazis-authing

Core Components

/auth/ Endpoint

Central endpoint for authentication operations.

URL: GET /api/v1/authing/auth/

Purpose:

  • Check current authentication status
  • Get list of available login methods
  • Get JWT token for authenticated users (a signed-in authorization store gives it once: the store is deleted, the next request with its token is not authenticated)

Behavior:

  1. If user is NOT authenticated → returns 401 error with list of available login methods:
{
  "errors": [
    {
      "status": 401,
      "code": "UNAUTHORIZED",
      "title": "User is not authorized.",
      "detail": "User is not authorized.",
      "meta": {
        "actions": [
          {
            "code": "password",
            "name": "Login/Password",
            "url": "/api/v1/authing/password/",
            "method": "POST"
          }
        ],
        "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
      }
    }
  ]
}
  1. If user is authenticated → returns user data and JWT token:
{
  "user_id": "123e4567-e89b-12d3-a456-426614174000",
  "username": "user1",
  "first_name": "John",
  "last_name": "Doe",
  "email": "user1@example.com",
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "logout_actions": [
    {
      "url": "/api/v1/authing/logout/",
      "method": "POST"
    }
  ]
}

Authentication Methods

Password Authentication

Built-in module for username and password authentication.

Module code: password

Endpoints:

  1. POST /api/v1/authing/password/ — web authentication with redirect

    • Accepts username and password
    • On successful authentication, sets cookie and redirects to /auth/
    • On failed authentication, returns error
  2. POST /api/v1/authing/password/token/ — get JWT token for Swagger/API

    • Accepts username and password via OAuth2PasswordRequestForm
    • Returns JWT token directly without cookie
    • Used for authentication in Swagger UI

Request schema (POST /password/):

{
  "username": "user1",
  "password": "password123"
}

Response schema (POST /password/token/):

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer"
}

AuthStore

Cookie-based authentication state storage system.

Location: bazis.contrib.authing.service.AuthStore

Purpose:

  • Store temporary authentication data in encrypted cookie
  • Track multi-step authentication progress
  • Pass errors between requests

Main methods:

  • login(user, request, auth_type) — registers successful user authentication
  • set_error(code, detail) — saves authentication error
  • data_reset() — clears storage
  • redirect_to_auth() — the answer of a login: 303 to /auth/ with the cookie, without the store token in the URL
  • response_set_cookie(response) — adds the store cookie to a response (HttpOnly, SameSite=Lax, Secure unless BAZIS_AUTH_COOKIE_SECURE is off, for the lifetime of the store)
  • claim() — the signed-in user id, once (deletes the store); used by /auth/

AuthStoreTokenRequired:

Dependency for endpoints requiring authentication token in cookie:

from bazis.contrib.authing.service import AuthStoreTokenRequired
from fastapi import Depends

@router.post('/password/')
def password_auth(auth_store: AuthStoreTokenRequired = Depends()):
    # auth_store guaranteed to contain valid token
    pass

Usage

Project Setup

1. Add to settings.py:

# settings.py

# List of available authentication methods
BAZIS_AUTH_KINDS = [
    'bazis.contrib.authing.services.password',  # Username/password
    # Add other methods here
]

2. Register routes:

# router.py
from bazis.core.routing import BazisRouter

router = BazisRouter(prefix='/api/v1')

# Register authentication routes
from bazis.contrib.authing.routes import router as authing_router
from bazis.contrib.authing.services.password.router import router as password_router

router.register(prefix='/authing', arg=authing_router)
# the router of every service in BAZIS_AUTH_KINDS
router.register(prefix='/authing', arg=password_router)

Basic Password Authentication

Typical web authentication flow:

Step 1: Client requests authentication status:

GET /api/v1/authing/auth/

Response (if not authenticated):

{
  "errors": [
    {
      "status": 401,
      "code": "UNAUTHORIZED",
      "meta": {
        "actions": [
          {
            "code": "password",
            "name": "Login/Password",
            "url": "/api/v1/authing/password/",
            "method": "POST"
          }
        ],
        "token": "auth_token_here"
      }
    }
  ]
}

Step 2: Client performs authentication:

POST /api/v1/authing/password/
Authorization: Bearer auth_token_here
Content-Type: application/json

{
  "username": "user1",
  "password": "password123"
}

Response (successful authentication):

HTTP 303 See Other
Location: /api/v1/authing/auth/
Set-Cookie: bazis_auth=auth_token_here; HttpOnly; Max-Age=60; Path=/; SameSite=lax; Secure

Step 3: Client follows redirect with the same token and gets user data (once: the store is deleted, a new login needs a new store):

GET /api/v1/authing/auth/
Authorization: Bearer auth_token_here

Response:

{
  "user_id": "123e4567-e89b-12d3-a456-426614174000",
  "username": "user1",
  "email": "user1@example.com",
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Getting JWT Token

For direct JWT token retrieval (e.g., for mobile apps or Swagger):

POST /api/v1/authing/password/token/
Content-Type: application/x-www-form-urlencoded

username=user1&password=password123

Response:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer"
}

This token can be used in the Authorization: Bearer <token> header for all subsequent requests.

Logout

POST /api/v1/authing/logout/
Authorization: Bearer auth_token_here

Deletes the authorization stores of the request (bearer token, cookie or query parameter) and the bazis_auth cookie; answers 204 and is idempotent. The session JWT is stateless: the client drops it.

How It Works

System Architecture

┌─────────────┐
│   Client    │
└──────┬──────┘
       │
       │ 1. GET /auth/ (no cookie)
       ▼
┌─────────────────────┐
│  /auth/ endpoint    │◄─── Checks user status
└──────┬──────────────┘
       │
       │ 2. Returns 401 + available actions
       ▼
┌─────────────┐
│   Client    │
└──────┬──────┘
       │
       │ 3. POST /password/ (with credentials)
       ▼
┌─────────────────────┐
│ Password endpoint   │◄─── Authenticates user
└──────┬──────────────┘
       │
       │ 4. Sets cookie + redirects to /auth/
       ▼
┌─────────────────────┐
│  /auth/ endpoint    │◄─── Returns user data + JWT
└──────┬──────────────┘
       │
       │ 5. Returns user + token
       ▼
┌─────────────┐
│   Client    │
└─────────────┘

Authentication Process

  1. Initialization:

    • Client requests /auth/ without cookie
    • System creates temporary token and returns it in meta
    • Client saves this token
  2. Authentication:

    • Client sends credentials with token in cookie
    • System validates credentials via Django authenticate()
    • On success — saves user_id in AuthStore
    • Sets updated cookie and redirects
  3. Token Retrieval:

    • Client requests /auth/ with cookie
    • System extracts user_id from AuthStore
    • Generates JWT token via user.jwt_build()
    • Returns user data and token

Security

  • Single-use store — /auth/ gives the session of a signed-in store once
  • Temporary tokens — a store lives BAZIS_AUTH_COOKIE_LIFETIME seconds (default 600), a signed-in one BAZIS_AUTH_CLAIM_LIFETIME (default 60); the cookie lives as long
  • Cookie attributes — HttpOnly, SameSite=Lax, Secure (BAZIS_AUTH_COOKIE_SECURE=false for development over plain HTTP)
  • No tokens in redirects — the login redirects and the Google state do not carry the store token; only /google-auth-init/?bazis_auth=<store token> has it in the URL
  • Known limitation (login CSRF) — the Google init is a GET with the store token, so an attacker can send a victim /google-auth-init/?bazis_auth=<attacker's store>: a victim who already consented in Google signs the attacker's store in without a prompt, and the attacker takes the session. Planned fix: a POST init with the store token as the bearer token that returns a one-time ticket for the window URL
  • JWT tokens — for further API operations

Examples

Client Application Usage Example

// JavaScript client for authentication

class AuthClient {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
    this.authToken = null;
    this.jwtToken = null;
  }

  async checkAuth() {
    const response = await fetch(`${this.baseUrl}/authing/auth/`, {
      credentials: 'include'
    });

    if (response.ok) {
      const data = await response.json();
      this.jwtToken = data.token;
      return { authenticated: true, user: data };
    }

    const error = await response.json();
    this.authToken = error.errors[0].meta.token;
    return {
      authenticated: false,
      actions: error.errors[0].meta.actions
    };
  }

  async loginWithPassword(username, password) {
    const response = await fetch(`${this.baseUrl}/authing/password/`, {
      method: 'POST',
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json',
        'Cookie': `bazis_auth=${this.authToken}`
      },
      body: JSON.stringify({ username, password }),
      redirect: 'follow'
    });

    if (response.ok) {
      return await this.checkAuth();
    }

    throw new Error('Authentication failed');
  }

  async apiRequest(url, options = {}) {
    return fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        'Authorization': `Bearer ${this.jwtToken}`
      }
    });
  }
}

// Usage
const auth = new AuthClient('https://api.example.com/api/v1');

// Check authentication status
const status = await auth.checkAuth();

if (!status.authenticated) {
  // Perform login
  await auth.loginWithPassword('user1', 'password123');
}

// Now you can make requests to protected endpoints
const response = await auth.apiRequest('/api/v1/protected/resource/');

Authentication Testing

import pytest
from bazis_test_utils.utils import get_api_client
from bazis.contrib.users import get_user_model

User = get_user_model()

@pytest.mark.django_db(transaction=True)
def test_password_authentication_flow(sample_app):
    # Create user
    user = User.objects.create_user(
        'testuser',
        email='test@example.com',
        password='testpass123'
    )

    # Step 1: Get status (not authenticated)
    response = get_api_client(sample_app).get('/api/v1/authing/auth/')
    assert response.status_code == 400
    
    data = response.json()
    assert 'errors' in data
    error = data['errors'][0]
    assert error['code'] == 'UNAUTHORIZED'
    
    auth_token = error['meta']['token']
    actions = error['meta']['actions']
    
    # Check available login methods
    assert len(actions) == 1
    assert actions[0]['code'] == 'password'

    # Step 2: Perform authentication
    response = get_api_client(sample_app, auth_token).post(
        '/api/v1/authing/password/',
        json_data={
            'username': 'testuser',
            'password': 'testpass123',
        },
    )
    assert response.status_code == 200

    # Step 3: Verify received data
    data = response.json()
    assert data['user_id'] == str(user.id)
    assert data['username'] == 'testuser'
    assert data['email'] == 'test@example.com'
    assert 'token' in data  # JWT token

    # Step 4: Test invalid credentials
    response = get_api_client(sample_app, auth_token).post(
        '/api/v1/authing/password/',
        json_data={
            'username': 'testuser',
            'password': 'wrongpassword',
        },
    )
    assert response.status_code == 400

Extension

Adding New Authentication Method

You can add your own authentication method (OAuth, SAML, LDAP, etc.):

1. Create a module (e.g., myapp/auth_oauth.py):

from bazis.core.routing import BazisRouter
from django.utils.translation import gettext_lazy as _

AUTH_CODE = 'oauth'

router = BazisRouter(prefix='/oauth', tags=[_('OAuth Authentication')])

def get_login_action():
    """Returns login method information for /auth/"""
    return {
        'code': AUTH_CODE,
        'name': 'OAuth Login',
        'url': '/api/v1/authing/oauth/',
        'method': 'GET',
    }

def get_logout_actions():
    """Returns logout actions (optional)"""
    return {
        'url': '/api/v1/authing/oauth/logout/',
        'method': 'POST',
    }

@router.get('/')
def oauth_login():
    # OAuth authentication logic
    pass

2. Add to settings.py:

BAZIS_AUTH_KINDS = [
    'bazis.contrib.authing.services.password',
    'myapp.auth_oauth',  # Your module
]

3. Register routes:

# myapp/router.py
from bazis.core.routing import BazisRouter
from . import auth_oauth

router = BazisRouter()
router.include_router(auth_oauth.router)

Now your authentication method will automatically appear in the list of available methods in /auth/!

License

Apache License 2.0

See LICENSE file for details.

Support

If you have questions or issues:


Made with ❤️ by Bazis team

Metadata

Release files for bazis-authing 2.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for bazis-authing 2.6.0
File Size Uploaded
bazis_authing-2.6.0.tar.gz 137.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for bazis-authing 2.6.0
File Interpreter ABI Platform
bazis_authing-2.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 173.4 kB

Release files / bazis_authing-2.6.0.tar.gz

Download URL bazis_authing-2.6.0.tar.gz
Size 137.5 kB
Tags Source
SHA-256 checksum
How to use checksums
744870d14fa083dbe89d95a896a38bab5bfbf63f778e3bc67aa2c195a11c1f9a
BLAKE2b-256 checksum
How to use checksums
58ed3c00bdd464980261a40377f8fde8f721adaa2552b01604df1414a956d32f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / bazis_authing-2.6.0-py3-none-any.whl

Download URL bazis_authing-2.6.0-py3-none-any.whl
Size 35.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8dff19b40431ffd83f19dc32c803e9de51601d431aa4799cc7217581d368eb4e
BLAKE2b-256 checksum
How to use checksums
1d02cb8c5ef05e0ae4a8a9c303bf2dd6adb6b997e1f7388cedc57c1f07b6ec68
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

2.6.0 This release

2 release files

2.5.0

2 release files

2.4.0

2 release files

2.3.1

2 release files

2.3.0

2 release files

2.2.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page