Skip to main content

Lightning-Fast Magic Link Authentication

Project description

Haze - Lightning-Fast Magic Link Authentication

PyPI - Downloads GitHub Sponsors PyPI - Python Version PyPI - Version Star

Haze is a high-performance, easy-to-use Magic Link Authentication service for Python applications. Generate secure authentication links that work across devices with minimal setup.

Features

  • Fast & Efficient - Optimized core with minimal overhead
  • 🔒 Ultra Secure - Modern cryptography with JWT
  • 🔧 Highly Configurable - Like Neovim, but for authentication
  • 🧩 Zero Daemon - No background processes required
  • 📦 Minimal Dependencies - Lightweight core with optional extras
  • 📱 Cross-Device Auth - Click on phone, authenticate on desktop
  • 💾 Pluggable Storage - Use any database system
  • 🦄 Modern Defaults - NanoID, JWT, MsgPack by default

Installation

Since Haze is now available on PYPI, you can use any package manager you want.

pip install haze-auth[full]

# Basic installation (install jwt later, with different package version)
pip install git+https://github.com/itsmeadarsh2008/haze.git@main

# With JWT support (Recommended)
pip install "haze[jwt] @ git+https://github.com/itsmeadarsh2008/haze.git@main"

# With all optional dependencies
pip install "haze [full] @ git+https://github.com/itsmeadarsh2008/haze.git@main"

You can also specify individual extra dependencies:

# Pick and choose what you need
pip install "haze[<optional deps>] git+https://github.com/itsmeadarsh2008/haze.git"

Quick Start

import haze
import secrets

# Configure Haze
haze.use(
    base_url="https://myapp.com",
    magic_link_path="/auth/verify",
    secret_key=secrets.token_urlsafe(32)
)

# Simple in-memory storage for demo purposes
token_store = {}

# Define storage handler
@haze.storage
def store_token(token_id, data=None):
    if data is None:
        return token_store.get(token_id)
    token_store[token_id] = data
    return data

# Generate a magic link for a user
link = haze.generate(
    user_id="user123",
    metadata={"name": "John Doe", "email": "john@example.com"}
)
print(f"Magic Link: {link}")

# Verify the magic link
# This is typically done in your web endpoint
@app.route("/auth/verify")
def verify_link():
    token_id = request.args.get("token_id")
    signature = request.args.get("signature")

    try:
        user_data = haze.verify(token_id, signature)
        # Authentication successful
        # Set session, JWT, etc.
        return {"success": True, "user": user_data}
    except Exception as e:
        return {"success": False, "error": str(e)}

Advanced Usage

Custom Configuration

haze.use(
    # Base settings
    base_url="https://myapp.com",
    magic_link_path="/auth/magic",
    link_expiry=3600,  # 1 hour
    allow_reuse=False,  # One-time use by default

    # Token settings
    token_provider="jwt",
    jwt_algorithm="HS256",  # or RS256, ES256
    
    # ID generation
    id_generator="nanoid",  # or "uuid"
    nanoid_size=21,

    # Format settings
    serialization_format="msgpack"  # or "json"
)

Using with JWT

import secrets

# Generate a secure key
secret_key = secrets.token_urlsafe(32)

# Configure Haze to use JWT with HMAC
haze.use(
    token_provider="jwt",
    jwt_algorithm="HS256",
    secret_key=secret_key
)

Using with Asymmetric Keys (JWT)

from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization

# Generate key pair
private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048
)
public_key = private_key.public_key()

# Configure Haze
haze.use(
    token_provider="jwt",
    jwt_algorithm="RS256",
    private_key=private_key,
    public_key=public_key
)

Database Integration Examples

With SQLAlchemy

from sqlalchemy import create_engine, Column, String, Integer, Boolean, JSON, Text
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

# Setup database
Base = declarative_base()
engine = create_engine("sqlite:///haze_tokens.db")
Session = sessionmaker(bind=engine)

class Token(Base):
    __tablename__ = "tokens"

    token_id = Column(String, primary_key=True)
    user_id = Column(String, nullable=False)
    exp = Column(Integer, nullable=False)
    created_at = Column(Integer, nullable=False)
    metadata = Column(JSON, nullable=True)
    consumed = Column(Boolean, default=False)

Base.metadata.create_all(engine)

# Setup Haze storage handler
@haze.storage
def store_token(token_id, data=None):
    session = Session()
    try:
        if data is None:
            # Retrieve token
            token = session.query(Token).filter_by(token_id=token_id).first()
            if not token:
                return None
            return {
                "user_id": token.user_id,
                "exp": token.exp,
                "created_at": token.created_at,
                "metadata": token.metadata,
                "consumed": token.consumed
            }
        else:
            # Create or update token
            token = session.query(Token).filter_by(token_id=token_id).first()
            if token:
                # Update existing token
                token.user_id = data["user_id"]
                token.exp = data["exp"]
                token.created_at = data.get("created_at")
                token.metadata = data.get("metadata")
                token.consumed = data.get("consumed", False)
            else:
                # Create new token
                token = Token(
                    token_id=token_id,
                    user_id=data["user_id"],
                    exp=data["exp"],
                    created_at=data.get("created_at"),
                    metadata=data.get("metadata"),
                    consumed=data.get("consumed", False)
                )
                session.add(token)

            session.commit()
            return data
    finally:
        session.close()

With Redis

import redis
import json
import time

# Setup Redis connection
r = redis.Redis(host='localhost', port=6379, db=0)

@haze.storage
def store_token(token_id, data=None):
    key = f"haze:token:{token_id}"
    if data is None:
        # Retrieve token
        token_data = r.get(key)
        if not token_data:
            return None
        return json.loads(token_data)
    else:
        # Store token with expiration
        ttl = data["exp"] - int(time.time())
        r.setex(key, ttl, json.dumps(data))
        return data

Event Handlers

Haze provides hooks for various authentication events:

# Called when a link is verified
@haze.verification
def on_verification(user_id, token_data):
    print(f"User {user_id} verified with token: {token_data['jti']}")
    # Update last login time, etc.

# Called when a magic link is clicked
@haze.onclick
def on_link_clicked(user_id, user_data):
    print(f"User {user_id} clicked magic link")
    # Track analytics, etc.

Using with Popular Web Frameworks

Flask Example

from flask import Flask, request, redirect, session
import haze
import secrets

app = Flask(__name__)
app.secret_key = secrets.token_urlsafe(32)

# Configure Haze
haze.use(
    base_url="http://localhost:5000",  # For local development
    magic_link_path="/auth/verify",
    secret_key=app.secret_key
)

# Simple in-memory storage for demo purposes
token_store = {}

@haze.storage
def store_token(token_id, data=None):
    if data is None:
        return token_store.get(token_id)
    token_store[token_id] = data
    return data

@app.route("/login", methods=["POST"])
def login():
    email = request.form.get("email")
    if not email:
        return {"error": "Email required"}, 400

    # Generate magic link
    link = haze.generate(
        user_id=email,
        metadata={"email": email}
    )

    # In a real app, send this link via email
    # For demo, we'll just return it
    return {"link": link}

@app.route("/auth/verify")
def verify():
    token_id = request.args.get("token_id")
    signature = request.args.get("signature")

    try:
        user_data = haze.verify(token_id, signature)
        # Set session
        session["user_id"] = user_data["user_id"]
        session["authenticated"] = True

        # Redirect to dashboard
        return redirect("/dashboard")
    except Exception as e:
        return {"error": str(e)}, 400

@app.route("/dashboard")
def dashboard():
    if not session.get("authenticated"):
        return redirect("/login")

    return f"Welcome, {session.get('user_id')}!"

FastAPI Example

from fastapi import FastAPI, Depends, HTTPException, Request, Response
from fastapi.responses import RedirectResponse
from pydantic import BaseModel, EmailStr
import secrets
import haze

app = FastAPI()

# Configure Haze
haze.use(
    base_url="http://localhost:8000",  # For local development
    magic_link_path="/auth/verify",
    secret_key=secrets.token_urlsafe(32)
)

# Simple in-memory storage
token_store = {}

@haze.storage
def store_token(token_id, data=None):
    if data is None:
        return token_store.get(token_id)
    token_store[token_id] = data
    return data

class LoginRequest(BaseModel):
    email: EmailStr

@app.post("/login")
async def login(request: LoginRequest):
    # Generate magic link
    link = haze.generate(
        user_id=request.email,
        metadata={"email": request.email}
    )

    # In a real app, send this link via email
    return {"link": link}

@app.get("/auth/verify")
async def verify(token_id: str, signature: str, response: Response):
    try:
        user_data = haze.verify(token_id, signature)

        # Set cookie for authentication
        response.set_cookie(
            key="session_token",
            value=user_data["user_id"],
            httponly=True,
            secure=False,  # Set to True in production with HTTPS
            samesite="lax"
        )

        return RedirectResponse(url="/dashboard")
    except Exception as e:
        raise HTTPException(status_code=400, detail=str(e))

@app.get("/dashboard")
async def dashboard(request: Request):
    session_token = request.cookies.get("session_token")
    if not session_token:
        return RedirectResponse(url="/login")

    return {"message": f"Welcome, {session_token}!"}

Security Best Practices

  1. Always use HTTPS for production environments
  2. Set appropriate token expiry times - shorter is better
  3. Rotate your secret keys periodically
  4. Use asymmetric cryptography (JWT with RSA/ECDSA) for increased security
  5. Implement rate limiting to prevent brute force attacks
  6. Store tokens securely in a database with proper encryption
  7. Enable one-time use for magic links by setting allow_reuse=False

Troubleshooting

Common Issues

"ModuleNotFoundError" for optional dependencies

pip install "haze[full] @ git+https://github.com/itsmeadarsh2008/haze.git"

"ConfigurationError: secret_key must be set"

Ensure you've set a secure secret key with haze.use(secret_key=...).

"ValidationError: Token expired"

The magic link has expired. Generate a new one or increase the link_expiry setting.

"ValidationError: Token not found"

The token doesn't exist in storage. Check your storage handler implementation.

"ValidationError: Invalid signature"

The signature verification failed. This could indicate a tampered link or configuration issues.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

haze_auth-0.1.2.tar.gz (15.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

haze_auth-0.1.2-py3-none-any.whl (18.9 kB view details)

Uploaded Python 3

File details

Details for the file haze_auth-0.1.2.tar.gz.

File metadata

  • Download URL: haze_auth-0.1.2.tar.gz
  • Upload date:
  • Size: 15.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for haze_auth-0.1.2.tar.gz
Algorithm Hash digest
SHA256 c885e7cad8b6f6fa270b5091b13a2d5002e31f7d84539e35496e675653749fe5
MD5 68fb7577e9aaf6a82f721245bf8d9fe1
BLAKE2b-256 88caa9855e541cd8d761effdc9f8b959d82e76a35f18ef47875ce1de9ccf5b66

See more details on using hashes here.

Provenance

The following attestation bundles were made for haze_auth-0.1.2.tar.gz:

Publisher: publish.yml on itsmeadarsh2008/haze

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file haze_auth-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: haze_auth-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 18.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for haze_auth-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 54b3ead8d0d55e9cec949ba883ce0eacec95a4c988bda6ac06fe37f209b40057
MD5 16617b7f26644783bec8f65d0d178ef2
BLAKE2b-256 8f1e3e57479c57d90aa61389eb07a1ca6fa74d2643193beb8a016639c27e4237

See more details on using hashes here.

Provenance

The following attestation bundles were made for haze_auth-0.1.2-py3-none-any.whl:

Publisher: publish.yml on itsmeadarsh2008/haze

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page