Authentication

Secure, simple authentication utilities for FastHTML applications

Imports and utils

Quick Start

Ship Kit’s authentication module provides everything you need to add secure authentication to your FastHTML app in minutes:

from ship_kit.auth import *
from fasthtml.common import *

# 1. Initialize the database
db = init_auth_tables()  # Creates users table with indexes

# 2. Create a new user
user = create_user(db, 'john_doe', 'john@example.com', 'secure_password123')

# 3. Authenticate users during login
user = authenticate_user(db, 'john@example.com', 'secure_password123')
if user:
    sess['auth'] = True
    sess['user'] = {'id': user['id'], 'username': user['username']}

# 4. Protect routes with authentication
beforeware = Beforeware(user_auth_before, skip=['/login', '/signup'])
app, rt = fast_app(before=beforeware)

# 5. Use validation helpers
if is_username_available(db, 'new_user'):
    # Username is available for registration
    pass

That’s it! Your routes are now protected and users must authenticate to access them.

Overview

This module provides comprehensive authentication features:

Core Functions

Function Purpose When to Use
hash_password Securely hash passwords with bcrypt User registration/password updates
verify_password Check if password matches hash User login
user_auth_before Beforeware to protect routes App initialization
get_user_from_session Extract user data from session Inside route handlers
create_auth_token Generate secure tokens Remember me/API auth
verify_auth_token Validate tokens Token-based auth

Database Functions

Function Purpose When to Use
init_auth_tables Create database tables and indexes App startup
create_user Register new user User signup
get_user_by_id Fetch user by ID User profile/admin
get_user_by_email Fetch user by email Login/verification
get_user_by_username Fetch user by username Login/profile lookup
update_user Update user attributes Profile editing
delete_user Remove user from database Account deletion

Helper Functions

Function Purpose When to Use
authenticate_user Verify credentials and return user Login endpoint
check_permission Verify user has required role Access control
track_login Record login events Security/analytics
is_username_available Check username uniqueness Registration
is_email_available Check email uniqueness Registration
validate_user_data Validate registration data Form validation

Password Hashing

Launch Kit uses bcrypt for password hashing - the industry standard for secure password storage. Our implementation uses a cost factor of 12, providing excellent security while maintaining reasonable performance (~200ms per hash on modern hardware).


source

hash_password

 hash_password (password:str)

Hash a password using bcrypt with a cost factor of 12.

Type Details
password str The password to hash
Returns str The hashed password
Password Length Limitation

bcrypt has a maximum password length of 72 bytes. Passwords longer than this are silently truncated. For applications requiring longer passwords, consider hashing with SHA-256 first:

import hashlib
long_password = "very_long_password" * 10
# Hash with SHA-256 first, then bcrypt
sha_hash = hashlib.sha256(long_password.encode()).hexdigest()
final_hash = hash_password(sha_hash)

source

verify_password

 verify_password (password:str, hashed:str)

Verify a password against a bcrypt hash.

Type Details
password str The password to verify
hashed str The hashed password to compare against
Returns bool True if password matches hash, False otherwise

User Model

Launch Kit provides a simple, dataclass-based User model that works seamlessly with FastHTML’s MiniDataAPI.

Note: While the User model is defined as a dataclass, fastlite returns query results as dictionaries for flexibility and performance. This is the expected behavior and aligns with fastlite’s design philosophy.


source

User

 User (username:str, email:str, password_hash:str, role:str='user',
       is_active:bool=True, created_at:datetime.datetime=<factory>,
       updated_at:datetime.datetime=<factory>, id:Optional[int]=None)

User model for authentication.


source

init_auth_tables

 init_auth_tables (db_path:str='data.db')

Creates the users table with proper schema and indexes. Uses FastHTML’s MiniDataAPI for simple, transparent database operations.

Type Default Details
db_path str data.db The path to the SQLite database file
Returns Database The FastHTML Database instance configured with the User table

User CRUD Operations

Simple, transparent database operations following MiniDataAPI patterns:


source

create_user

 create_user (db:apswutils.db.Database, username:str, email:str,
              password:str, role:str='user', table_name:str='user')

Create a new user in the database.

Type Default Details
db Database The FastHTML Database instance
username str The unique username
email str The unique email address
password str The plain text password
role str user The user role
table_name str user The table name
Returns Optional The created user dict or None if user already exists

source

get_user_by_id

 get_user_by_id (db:apswutils.db.Database, user_id:int,
                 table_name:str='user')

Get user by ID.

Type Default Details
db Database The FastHTML Database instance
user_id int The user ID
table_name str user The table name
Returns Optional The user dict or None if not found

source

get_user_by_email

 get_user_by_email (db:apswutils.db.Database, email:str,
                    table_name:str='user')

Get user by email address.

Type Default Details
db Database The FastHTML Database instance
email str The email address
table_name str user The table name
Returns Optional The user dict or None if not found

source

get_user_by_username

 get_user_by_username (db:apswutils.db.Database, username:str,
                       table_name:str='user')

Get user by username.

Type Default Details
db Database The FastHTML Database instance
username str The username
table_name str user The table name
Returns Optional The user dict or None if not found

source

update_user

 update_user (db:apswutils.db.Database, user_id:int,
              table_name:str='user', **kwargs)

Update user attributes.

Type Default Details
db Database The FastHTML Database instance
user_id int The user ID
table_name str user The table name
kwargs VAR_KEYWORD
Returns Optional The updated user dict or None if not found

source

delete_user

 delete_user (db:apswutils.db.Database, user_id:int,
              table_name:str='user')

Delete a user from the database.

Type Default Details
db Database The FastHTML Database instance
user_id int The user ID
table_name str user The table name
Returns bool True if deleted, False if not found

Authentication Helpers

Convenient functions for common authentication operations:


source

check_permission

 check_permission (user:__main__.User, required_role:str)

*Check if a user has the required role or higher.

Role hierarchy: - ‘admin’ has all permissions - ‘user’ has basic permissions - Custom roles can be added as needed*

Type Details
user User The User object to check
required_role str The required role
Returns bool True if user has permission, False otherwise

source

authenticate_user

 authenticate_user (db:apswutils.db.Database, username_or_email:str,
                    password:str, table_name:str='user')

Authenticate a user by username/email and password.

Type Default Details
db Database The FastHTML Database instance
username_or_email str The username or email address
password str The plain text password
table_name str user The table name
Returns Optional The authenticated user dict or None if invalid credentials

source

track_login

 track_login (db:apswutils.db.Database, user_id:int,
              ip_address:Optional[str]=None,
              user_agent:Optional[str]=None, table_name:str='user_logins')

*Track user login for security and analytics using MiniDataAPI pattern.

Creates a login record with timestamp and metadata. Returns the created login record.*

Type Default Details
db Database The FastHTML Database instance
user_id int The user ID
ip_address Optional None The IP address of the login
user_agent Optional None The user agent string
table_name str user_logins The table name
Returns Dict The login record

source

UserLogin

 UserLogin (user_id:int, ip_address:Optional[str]=None,
            user_agent:Optional[str]=None,
            login_time:datetime.datetime=<factory>, id:Optional[int]=None)

Login tracking record for security and analytics.

Comparison: Raw SQL vs MiniDataAPI

Aspect Current (Raw SQL) Proposed (MiniDataAPI)
Table Creation Manual SQL with CREATE TABLE db.create(UserLogin)
Data Insertion SQL INSERT with placeholders logins.insert(login_data)
Return Value None Login record dict
Type Safety No type hints for data Dataclass with types
Consistency Different from other functions Same pattern as User CRUD
Database Portability SQLite-specific syntax Works across databases
Foreign Keys Defined in CREATE TABLE Need separate index creation

Migration Considerations

To migrate existing databases: 1. The table structure created by MiniDataAPI is compatible with the existing schema 2. Foreign key constraints would need to be added separately 3. Existing data would remain intact

Validation Helpers

Functions to ensure data integrity and check uniqueness:


source

is_username_available

 is_username_available (db:apswutils.db.Database, username:str,
                        table_name:str='user')

Check if username is available for registration.

Type Default Details
db Database The FastHTML Database instance
username str The username to check
table_name str user The table name
Returns bool True if username is available, False if taken

source

is_email_available

 is_email_available (db:apswutils.db.Database, email:str,
                     table_name:str='user')

Check if email is available for registration.

Type Default Details
db Database The FastHTML Database instance
email str The email address to check
table_name str user The table name
Returns bool True if email is available, False if taken

source

validate_user_data

 validate_user_data (username:str, email:str, password:str)

Validate user registration data.

Type Details
username str The username to validate
email str The email address to validate
password str The password to validate
Returns List The list of validation errors (empty if valid)

Session Management

FastHTML uses server-side sessions to maintain authentication state. Launch Kit provides utilities to make session management simple and secure.

Protecting Routes with Beforeware


source

user_auth_before

 user_auth_before (req, sess, login_path='/login')

*Beforeware function to check authentication status.

Following FastHTML’s standard authentication pattern: - Sets req.scope[‘auth’] for automatic injection in route handlers - Returns None if authenticated (continue processing) - Returns RedirectResponse if not authenticated*

Type Default Details
req The FastHTML Request object
sess The FastHTML Session object
login_path str /login The login path

The user_auth_before follows FastHTML’s recommended authentication pattern. It sets req.scope[‘auth’] for automatic injection in route handlers.

Session Schema

Launch Kit uses a consistent session schema: - sess['auth']: Boolean or auth token - indicates if user is authenticated - sess['user']: Dict with user data (id, username, email, role, etc.)

This separation allows flexibility while maintaining consistency:

# During login:
sess['auth'] = True  # or sess['auth'] = create_auth_token(user['id'])
sess['user'] = {
    'id': user['id'],
    'username': user['username'],
    'email': user['email'],
    'role': user['role']
}

Usage:

beforeware = Beforeware(
    user_auth_before,
    skip=['/login', '/signup', '/static/.*']
)

# Or with a custom login path like /auth/login:
def auth_custom_login(req, sess):
    return user_auth_before(req, sess, login_path='/auth/login')
    
beforeware = Beforeware(
    auth_custom_login,
    skip=['/auth/login', '/auth/signup', '/static/.*']
)

source

get_user_from_session

 get_user_from_session (sess)

Extract user data dictionary from session.

Type Details
sess The FastHTML Session object
Returns Optional The user data dict or None if not authenticated

Note: With FastHTML’s authentication pattern, you can also access auth status via the ‘auth’ parameter in route handlers.

Working with User Sessions

from fasthtml.common import *
from ship_kit.auth import user_auth_before

# Configure authentication beforeware
beforeware = Beforeware(
    user_auth_before,  # Uses default /login path
    skip=[
        '/login',      # Don't require auth for login page
        '/signup',     # Don't require auth for signup
        '/static/.*',  # Static files don't need auth
        r'.*\.css',    # CSS files
        r'.*\.js'      # JavaScript files
    ]
)

# Or with custom login path:
def auth_custom_login(req, sess):
    return user_auth_before(req, sess, login_path='/auth/login')

beforeware = Beforeware(
    auth_custom_login,
    skip=['/auth/login', '/auth/signup', '/static/.*']
)

# Create app with authentication
app, rt = fast_app(before=beforeware)

# This route is protected - redirects to login if not authenticated
@rt('/')
def get(auth):  # auth is automatically injected from req.scope['auth']
    return H1(f"Welcome! You are {'authenticated' if auth else 'not authenticated'}")

Authentication Tokens

Tokens enable stateless authentication for APIs and “remember me” functionality. Launch Kit provides simple utilities for secure token generation and verification.


source

create_auth_token

 create_auth_token (user_id:int)

Create a secure authentication token for a user.

Type Details
user_id int The user ID
Returns str The authentication token
Token Storage

In production, you’d store this token in your database with an expiration date and associate it with the user.


source

verify_auth_token

 verify_auth_token (token:str)

Verify an authentication token and return the user ID.

Type Details
token str The authentication token
Returns Optional The user ID or None if invalid
Token Lookup

In production, you’d look up this token in your database and check expiration.

Security Best Practices

Critical Security Guidelines

Following these practices is essential for maintaining a secure authentication system.

🔐 Password Security

Practice Implementation
Minimum Length Enforce 12+ characters
Complexity Require mixed case, numbers, symbols
Common Passwords Check against haveibeenpwned.com
Password History Prevent reuse of last 5 passwords
Account Lockout Lock after 5 failed attempts

🍪 Session Security

Practice Implementation
Secure Cookies secure=True, httponly=True, samesite='Lax'
Session Timeout 30 min idle, 12 hour absolute
Session Rotation New ID after login
CSRF Protection Use CSRF tokens for state changes

🎫 Token Security

Practice Implementation
Storage Hash tokens before storing
Expiration 30 days for remember me
Rotation New token on each use
Revocation Allow users to revoke all tokens

🌐 General Security

Practice Implementation
HTTPS Enforce TLS 1.2+ everywhere
Rate Limiting 5 attempts per minute
Audit Logging Log all auth events
2FA Support TOTP/WebAuthn

Complete Database Example

Here’s how to use the User model and database functions together:

from ship_kit.auth import *
from fasthtml.common import *

# Initialize database
db = init_auth_tables('app.db')

# Create FastHTML app with auth
beforeware = Beforeware(
    user_auth_before,
    skip=['/login', '/signup', '/']
)
app, rt = fast_app(before=beforeware)

@rt('/')
def get():
    return Div(
        H1('Welcome to Ship Kit Auth Demo'),
        A('Sign Up', href='/signup'),
        ' | ',
        A('Login', href='/login')
    )

@rt('/signup')
def get():
    return Div(
        H2('Sign Up'),
        Form(
            Input(name='username', placeholder='Username', required=True),
            Input(name='email', type='email', placeholder='Email', required=True),
            Input(name='password', type='password', placeholder='Password', required=True),
            Button('Sign Up', type='submit'),
            method='post'
        )
    )

@rt('/signup')
def post(username: str, email: str, password: str):
    # Validate input
    errors = validate_user_data(username, email, password)
    if errors:
        return Div(
            *[P(error, style='color: red') for error in errors],
            get()
        )
    
    # Check availability
    if not is_username_available(db, username):
        return Div(P('Username already taken', style='color: red'), get())
    if not is_email_available(db, email):
        return Div(P('Email already registered', style='color: red'), get())
    
    # Create user
    user = create_user(db, username, email, password)
    if user:
        return Redirect('/login')
    
    return Div(P('Registration failed', style='color: red'), get())

@rt('/login')
def get():
    return Div(
        H2('Login'),
        Form(
            Input(name='username_or_email', placeholder='Username or Email', required=True),
            Input(name='password', type='password', placeholder='Password', required=True),
            Button('Login', type='submit'),
            method='post'
        )
    )

@rt('/login')
def post(username_or_email: str, password: str, sess, req):
    # Authenticate user
    user = authenticate_user(db, username_or_email, password)
    
    if user:
        # Set session
        sess['auth'] = True
        sess['user'] = {'id': user['id'], 'username': user['username'], 'email': user['email']}
        
        # Track login (now returns a dictionary with login details)
        ip = req.headers.get('x-forwarded-for', req.client.host)
        user_agent = req.headers.get('user-agent')
        login_record = track_login(db, user['id'], ip, user_agent)
        
        # You can use the login_record if needed, e.g. for analytics
        # login_record contains: id, user_id, ip_address, user_agent, login_time
        
        return Redirect('/dashboard')
    
    return Div(P('Invalid credentials', style='color: red'), get())

@rt('/dashboard')
def get(auth, sess):
    user_data = get_user_from_session(sess)
    if not user_data:
        return Redirect('/login')
    
    return Div(
        H2(f"Welcome, {user_data['username']}!"),
        P(f"Email: {user_data['email']}"),
        A('Logout', href='/logout')
    )

@rt('/logout')
def get(sess):
    sess.clear()
    return Redirect('/')

Database Tests

Testing the User model and database operations:

Quick Example

Complete Login/Signup Example

Here’s how to implement a complete authentication flow:

from fasthtml.common import *
from ship_kit.auth import hash_password, verify_password

# Initialize FastHTML app
app, rt = fast_app()

# Simulated user database
users = {}

@rt('/signup')
def post(email: str, password: str):
    # Check if user exists
    if email in users:
        return "User already exists", 400
    
    # Create new user with hashed password
    users[email] = {
        'email': email,
        'password': hash_password(password)
    }
    return "Signup successful! Please login."

@rt('/login') 
def post(email: str, password: str, sess):
    # Get user from database
    user = users.get(email)
    
    if user and verify_password(password, user['password']):
        # Set session authentication
        sess['auth'] = True
        sess['user_id'] = email
        return Redirect('/')
    
    return "Invalid credentials", 401
# User registration
user_password = "MySecureP@ssw0rd!"
hashed_password = hash_password(user_password)
print(f"Store this in database: {hashed_password[:20]}...")

# User login
login_attempt = "MySecureP@ssw0rd!"
if verify_password(login_attempt, hashed_password):
    print("✅ Login successful!")
else:
    print("❌ Invalid password")

# Wrong password attempt
if not verify_password("wrong_password", hashed_password):
    print("❌ Invalid credentials rejected")
Store this in database: $2b$12$J7K88iTYaU2pD...
✅ Login successful!
❌ Invalid credentials rejected

Token Usage Example

Remember Me Implementation

Here’s how to implement “remember me” functionality using tokens:

from fasthtml.common import *
from ship_kit.auth import *

# Initialize app
app, rt = fast_app()

# Helper functions (would be in your database module)
def get_user_by_email(email): 
    # Fetch from database
    pass

def store_remember_token(user_id, token, days): 
    # Store token with expiration in database
    pass

def is_token_valid_in_db(token): 
    # Check token validity in database
    pass

@rt('/login')
def post(email: str, password: str, remember_me: bool, sess, resp):
    user = get_user_by_email(email)
    
    if user and verify_password(password, user['password']):
        # Set session auth
        sess['auth'] = True
        sess['user_id'] = user['id']
        
        # If remember me is checked, create a token
        if remember_me:
            token = create_auth_token(user['id'])
            # Store token in database with expiration
            store_remember_token(user['id'], token, days=30)
            # Set cookie
            resp.set_cookie('remember_token', token, max_age=30*24*60*60)
        
        return Redirect('/')
    
    return "Invalid credentials"

@rt('/')
def get(sess, req):
    # Check session first
    if sess.get('auth'):
        return "Welcome back!"
    
    # Check remember me token
    token = req.cookies.get('remember_token')
    if token:
        user_id = verify_auth_token(token)
        if user_id and is_token_valid_in_db(token):
            # Restore session
            sess['auth'] = True
            sess['user_id'] = user_id
            return "Welcome back (remembered)!"
    
    return Redirect('/login')
# Generate a token for user 42
user_id = 42
token = create_auth_token(user_id)
print(f"Generated token: {token[:20]}...")

# Later, verify the token
verified_user_id = verify_auth_token(token)
if verified_user_id:
    print(f"✅ Valid token for user {verified_user_id}")
else:
    print("❌ Invalid or expired token")

# Invalid tokens return None
assert verify_auth_token("tampered_token") is None
print("❌ Tampered tokens are rejected")
Generated token: 42:l97Cko11shUwHASMC...
✅ Valid token for user 42
❌ Tampered tokens are rejected

Complete Example: Interactive Demo

Here’s a complete FastHTML application demonstrating all authentication features. Run it right here in the notebook!

from fasthtml.common import *
from ship_kit.auth import *

# Simulated database
users_db = {'test@example.com': {'id': 1, 'email': 'test@example.com', 'password': hash_password('password123')}}

# Configure authentication with custom login path
# Create a wrapper function instead of using partial
def auth_with_custom_login(req, sess):
    return user_auth_before(req, sess, login_path='/auth/login')

beforeware = Beforeware(
    auth_with_custom_login,
    skip=[r'/auth/.*', r'/public/.*', '/']
)
app, rt = fast_app(before=beforeware)

# Public home page
@rt('/')
def get(): 
    return Div(
        H1('Welcome to SecureApp'),
        P('A demo of Ship Kit authentication'),
        A('Login', href='/auth/login', cls='button'),
        A('Protected Area', href='/protected', cls='button')
    )

# Login page
@rt('/auth/login')
def get():
    return Div(
        H2('Login'),
        Form(
            Input(name='email', type='email', placeholder='Email', required=True),
            Input(name='password', type='password', placeholder='Password', required=True),
            Button('Login', type='submit'),
            method='post'
        )
    )

# Handle login
@rt('/auth/login')
def post(email: str, password: str, sess):
    user = users_db.get(email)
    if user and verify_password(password, user['password']):
        sess['auth'] = True
        sess['user'] = user
        return Redirect('/protected')
    return Div(P('Invalid credentials', style='color: red'), get())

# Protected page
@rt('/protected')
def get(auth, sess):
    user = get_user_from_session(sess)
    return Div(
        H2(f"Welcome {user['email']}!"),
        P('This is a protected page.'),
        A('Logout', href='/auth/logout', cls='button')
    )

# Logout
@rt('/auth/logout')
def get(sess):
    sess.clear()
    return Redirect('/')

# Start the server in Jupyter [https://fastht.ml/docs/tutorials/jupyter_and_fasthtml.html]
from fasthtml.jupyter import JupyUvi
server = JupyUvi(app)
print("Server running at http://localhost:8000")
print("\nTest credentials:")
print("- Email: test@example.com")
print("- Password: password123")
Server running at http://localhost:8000

Test credentials:
- Email: test@example.com
- Password: password123
# View the app right here in the notebook by uncommenting the line below
from fasthtml.jupyter import HTMX
#HTMX()

Automated Testing

You can also test the authentication flow programmatically:

import httpx
import asyncio

# Test the authentication flow
async def test_auth_flow():
    async with httpx.AsyncClient(base_url="http://localhost:8000", follow_redirects=False) as client:
        print("Testing authentication flow...")
        
        # 1. Try protected page (should redirect)
        resp = await client.get("/protected")
        assert resp.status_code == 303
        assert resp.headers['location'] == '/auth/login'
        print("✓ Protected page redirects to /auth/login")
        
        # 2. Login with valid credentials
        resp = await client.post("/auth/login", data={
            "email": "test@example.com",
            "password": "password123"
        })
        assert resp.status_code == 303
        assert resp.headers['location'] == '/protected'
        print("✓ Login successful")
        
        # 3. Access protected page
        resp = await client.get("/protected", follow_redirects=True)
        assert "Welcome test@example.com" in resp.text
        print("✓ Protected page accessible")
        
        # 4. Logout
        resp = await client.get("/auth/logout")
        assert resp.status_code == 303
        print("✓ Logout successful")
        
        print("\n✅ All tests passed!")

# Run the tests
await test_auth_flow()
Testing authentication flow...
✓ Protected page redirects to /auth/login
✓ Login successful
✓ Protected page accessible
✓ Logout successful

✅ All tests passed!

Manual Testing

You can test the app manually in a browser by visiting: - http://localhost:8000 - Home page - http://localhost:8000/protected - Will redirect to login - http://localhost:8000/auth/login - Login with test@example.com / password123

# Stop the server gracefully
# Note: Always run this after testing to clean up otherwise there will be a dangling thread
# https://fastht.ml/docs/tutorials/jupyter_and_fasthtml.html#graceful-shutdowns
print("Stopping server...")
server.stop()
print("✓ Server stopped gracefully")
print("\n🎉 All tests passed! Authentication utilities are working correctly with FastHTML.")
Stopping server...
✓ Server stopped gracefully

🎉 All tests passed! Authentication utilities are working correctly with FastHTML.

Auth Tests

Performance Benchmarks

Understanding the performance characteristics of authentication operations is crucial for capacity planning.

Summary

Ship Kit’s authentication module provides a complete, secure foundation for FastHTML applications:

  • Simple API - Core authentication functions plus comprehensive database operations
  • Database Integration - User model with FastHTML’s MiniDataAPI for transparent persistence
  • Secure by Default - Industry-standard bcrypt hashing with sensible defaults
  • FastHTML Native - Seamless integration with sessions, beforeware, and database patterns
  • Production Ready - Battle-tested patterns with comprehensive security guidelines
  • Flexible - Works for both traditional web apps and API authentication
  • Complete Solution - From user registration to login tracking, all in one module

Breaking Changes in v2.0

  • Consistent Session Schema - Always use sess['auth'] for auth status and sess['user'] for user data
  • Simplified user_auth_before - Now explicitly documents return behavior and sets req.scope[‘auth’]