A complete, self-hosted OAuth 2.0 / OIDC Authorization Server that you control.
No third-party dependencies. No cloud providers. No data sharing. Deploy it locally, own it completely, and never worry about external OAuth services again.
- No vendor lock-in - Your auth infrastructure, your control
- Complete data privacy - User credentials never leave your infrastructure
- Zero external dependencies - No Auth0, Okta, or cloud provider required
- Unlimited users - No per-user pricing or subscription fees
- Industry-standard protocols - OAuth 2.0, OIDC, PKCE (S256)
- Production-ready - Used in real-world applications
- Audit everything - Full access to logs, tokens, and user activity
- Custom policies - Define your own consent flows and token lifetimes
- Deploy anywhere - Local network, private cloud, or air-gapped environments
- Customize everything - Scopes, claims, token formats, consent screens
- Integrate seamlessly - Works with any OAuth2-compatible application
- Scale on your terms - From 10 users to 10,000+
# Clone and run
git clone https://github.com/Sbussiso/LOauth2.git
cd LOauth2
export ENABLE_DEV_ENDPOINTS=true
uv run server.py
# Complete setup at http://127.0.0.1:8000/setup
# Your OAuth2 server is now running!That's it. No accounts to create. No credit card required. No data sent to third parties.
- β PKCE (S256) - Protection against authorization code interception
- β Refresh Token Rotation - Automatic token rotation for enhanced security
- β RS256 Signing Keys - Industry-standard JWT signing with key rotation
- β Consent Management - Granular user consent with configurable policies
- β Client Authentication - Multiple auth methods (none, secret_post, secret_basic)
- β Admin Web UI - Manage clients, scopes, policies, and keys through intuitive interface
- β Admin REST APIs - Automate configuration and management
- β Custom Scopes - Define your own scopes with descriptions and claims
- β Flexible Policies - Per-client token lifetimes, consent rules, and formats
- β SQLite/PostgreSQL/MySQL - Choose your database backend
- β OpenID Connect (OIDC) - Full OIDC discovery, JWKS, UserInfo, ID tokens
- β OAuth 2.0 - Authorization Code, Refresh Token grants
- β Token Operations - Revocation, introspection, refresh
- β Multiple Client Types - Public (SPAs, mobile) and confidential (backend)
- β Working Examples - Complete demo apps included (todo, camera)
- β Dev Tools - Seed data, PKCE generator, quick client creation
- β Comprehensive Docs - Troubleshooting guides and code examples
- β Easy Integration - Works with any OAuth2 client library
- Installation & Setup - Deploy in minutes
- Configuration - Environment variables and database setup
- OAuth2 Flow Walkthrough - Complete authorization guide
- Troubleshooting Guide - Solve integration issues fast
β οΈ - Client Code Examples - Working Python/Flask examples with PKCE
- API Reference - Complete endpoint documentation
- Admin UI Guide - Configure clients and policies
- Developer Guide - Build apps with LOauth2 (flows, PKCE, BFF, tokens, examples)
- Production Deployment - Scale with Gunicorn, Docker, PostgreSQL
- Security Best Practices - Harden your deployment
| Feature | Self-Hosted (This Project) | Auth0 / Okta / etc. |
|---|---|---|
| Data Privacy | β 100% on your infrastructure | β Data on their servers |
| Cost | β Free (MIT License) | β $$$+ per user/month |
| Vendor Lock-in | β None | β Tied to provider |
| Customization | β Unlimited | |
| Network Requirements | β Works offline/air-gapped | β Requires internet |
| Control | β You own everything | β Subject to their terms |
| Audit & Compliance | β Full access to all data | |
| Scalability | β Scale on your hardware |
- π’ Enterprise Teams - Own your authentication without vendor fees
- π¬ Research Labs - Air-gapped or secure environments
- π Homelab Enthusiasts - Self-host all your services with OAuth2
- π Startups - Start free, scale without per-user costs
- π‘οΈ Privacy-Focused Orgs - Keep user data on your infrastructure
- π Education - Learn OAuth2/OIDC with a real server
- Python 3.10+
- SQLite (included) or PostgreSQL/MySQL
- 512MB RAM minimum (scales with usage)
# Using uv (recommended)
uv run server.py
# Using pip
python3 -m venv .venv
source .venv/bin/activate
pip install flask authlib sqlalchemy
python server.pyServer starts at http://127.0.0.1:8000
# Set database URL
export DATABASE_URL="postgresql://user:pass@localhost/oauth"
export APP_SECRET="your-secure-random-secret-key"
# Run with Gunicorn
pip install gunicorn
gunicorn -w 4 -b 0.0.0.0:8000 server:appdocker build -t oauth2-server .
docker run -p 8000:8000 \
-e DATABASE_URL="sqlite:////data/oauth.db" \
-e APP_SECRET="..." \
-v oauth2_app_data:/data \
oauth2-server# Optional: set a strong secret for sessions
export APP_SECRET=$(python -c 'import secrets; print(secrets.token_hex(32))')
# Build and start (no .env required)
docker compose up -d --build
# Then open http://127.0.0.1:8000/setup- Compose defaults to
DATABASE_URL=sqlite:////data/oauth.dband persists data in theapp_datavolume. - To enable dev helpers (e.g. /dev/seed, /dev/pkce):
Then seed:
export ENABLE_DEV_ENDPOINTS=true docker compose up -dcurl "http://127.0.0.1:8000/dev/seed?reset=1" -H "X-Admin-Token: <ADMIN_TOKEN>"
- SQLite data is stored inside the container at:
/data/oauth.db. - It is persisted via the named volume
app_dataindocker-compose.yml. - The volume keeps your data when you stop/remove the container.
- To remove all data, including the volume:
docker compose down -v # WARNING: deletes volumes and your data
| Variable | Description | Default |
|---|---|---|
APP_SECRET |
Flask session secret (use strong random value) | Random (dev only) |
DATABASE_URL |
SQLAlchemy database URL | sqlite:///oauth.db |
ENABLE_DEV_ENDPOINTS |
Enable /dev/* helper routes |
false |
ADMIN_TOKEN |
Bootstrap admin token (first-time setup only) | Generated at /setup |
SQLite (development/small deployments):
# Automatic - database file created at ./oauth.db
uv run server.pyPostgreSQL (recommended for production):
export DATABASE_URL="postgresql://user:password@host:5432/dbname"
uv run server.pyMySQL/MariaDB:
export DATABASE_URL="mysql+pymysql://user:password@host:3306/dbname"
pip install pymysql
uv run server.pyexport ENABLE_DEV_ENDPOINTS=true
uv run server.pyOpen http://127.0.0.1:8000/setup in your browser:
- System generates a secure Admin Token
- Copy it immediately (shown only once)
- Token is hashed and stored in database
- Use it to access Admin UI and APIs
export BASE=http://127.0.0.1:8000
curl "$BASE/dev/seed" -H "X-Admin-Token: <YOUR_ADMIN_TOKEN>"Creates:
- Demo users:
alice/alice,bob/bob - Demo client:
demo-web(public, PKCE-enabled) - Default scopes:
openid,profile,email,offline_access
# Run the included todo demo
uv run todo_demo.py
# Open http://localhost:3000
# Sign in with alice/aliceπ You now have a working OAuth2 server!
Access Admin UI at http://127.0.0.1:8000/admin/login
Create a new client:
Client ID: my-app
Redirect URI: http://localhost:3000/callback
Grant Types: authorization_code refresh_token
Response Types: code
Scope: openid profile email offline_access
Client Type: Public (for SPAs/mobile) or Confidential (for backends)
For confidential clients: Copy the generated client_secret
# Generate PKCE
import os, base64, hashlib
verifier = base64.urlsafe_b64encode(os.urandom(40)).decode().rstrip("=")
challenge = base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode()).digest()
).decode().rstrip("=")
# Redirect user to authorization endpoint
auth_url = (
"http://127.0.0.1:8000/authorize?"
"client_id=my-app&"
"response_type=code&"
"redirect_uri=http://localhost:3000/callback&"
"scope=openid profile email offline_access&"
"code_challenge_method=S256&"
f"code_challenge={challenge}&"
"state=random-state-value"
)User approves consent β Redirected to your callback URL with code
# Exchange code for tokens
response = requests.post(
"http://127.0.0.1:8000/token",
data={
"grant_type": "authorization_code",
"client_id": "my-app",
"client_secret": "...", # Only for confidential clients
"code": code_from_callback,
"redirect_uri": "http://localhost:3000/callback",
"code_verifier": verifier, # PKCE verifier
}
)
tokens = response.json()
# {
# "access_token": "...",
# "refresh_token": "...",
# "id_token": "...",
# "token_type": "Bearer",
# "expires_in": 3600
# }# Call UserInfo endpoint
response = requests.get(
"http://127.0.0.1:8000/userinfo",
headers={"Authorization": f"Bearer {access_token}"}
)
user_info = response.json()
# {
# "sub": "1",
# "preferred_username": "alice",
# "email": "alice@example.com",
# ...
# }# When access token expires
response = requests.post(
"http://127.0.0.1:8000/token",
data={
"grant_type": "refresh_token",
"client_id": "my-app",
"refresh_token": refresh_token,
}
)
new_tokens = response.json()
# Old refresh_token is revoked, new one issued (rotation)π See Client Examples for complete working code.
β Getting 401 during token exchange?
This is usually one of three things:
-
Missing client_secret (confidential clients)
# Check your client configuration python3 -c "import sqlite3; conn = sqlite3.connect('oauth.db'); print(conn.execute('SELECT client_id, token_endpoint_auth_method, client_secret FROM oauth2_client WHERE client_id=\"YOUR_CLIENT_ID\"').fetchone())"
-
Missing PKCE (public clients) β See PKCE implementation guide
-
redirect_uri mismatch β Must match exactly in both authorize and token requests
β Session/state lost during OAuth redirect?
- Use the stateless approach (embed PKCE verifier in signed state)
β Still having issues?
- π Complete Troubleshooting Guide - Covers all error scenarios
- π» Working Code Examples - Copy and adapt
- β Integration Checklist - Verify your setup
Two complete, working OAuth2 client applications:
- Complete SPA-style OAuth flow
- PKCE implementation
- Session management
- Refresh token handling
- Custom scopes demonstration
- File access permissions
- Real-world use case
# Run any demo
uv run todo_demo.py
# Open http://localhost:3000OpenSentry - Smart security camera system with OAuth2 authentication
OpenSentry is a complete production application that integrates with this OAuth2 server for centralized authentication across multiple camera devices.
Key Features:
- πΉ Live video streaming with motion/object/face detection
- π OAuth2/OIDC authentication (fallback to local auth)
- π mDNS device discovery
- π’ Multi-device SSO support
- π³ Docker-ready deployment
Quick Integration:
# 1. Start the OAuth2 server
cd Oauth2
uv run server.py
# 2. Register OpenSentry as a client
cat > add_opensentry_client.py << 'EOF'
#!/usr/bin/env python3
import os
os.environ['DATABASE_URL'] = os.environ.get('DATABASE_URL', 'sqlite:///oauth.db')
from server import SessionLocal, OAuth2Client
db = SessionLocal()
existing = db.query(OAuth2Client).filter_by(client_id='opensentry-device').first()
redirect_uris = 'http://localhost:5000/oauth2/callback http://127.0.0.1:5000/oauth2/callback'
scope = 'openid profile email offline_access'
if existing:
print("Updating existing client...")
existing.client_secret = None
existing.client_name = 'OpenSentry Device'
existing.redirect_uris = redirect_uris
existing.scope = scope
existing.grant_types = 'authorization_code refresh_token'
existing.response_types = 'code'
existing.token_endpoint_auth_method = 'none'
existing.require_consent = True
db.commit()
print("β Client 'opensentry-device' updated")
else:
print("Creating new client...")
client = OAuth2Client(
client_id='opensentry-device',
client_secret=None,
client_name='OpenSentry Device',
redirect_uris=redirect_uris,
scope=scope,
grant_types='authorization_code refresh_token',
response_types='code',
token_endpoint_auth_method='none',
require_consent=True
)
db.add(client)
db.commit()
print("β Client 'opensentry-device' created")
db.close()
EOF
uv run python add_opensentry_client.py
# 3. Start OpenSentry
cd ../OpenSentry
uv run server.py
# 4. Configure OAuth2 in OpenSentry settings
# Navigate to http://127.0.0.1:5000/settings
# Select OAuth2 Authentication
# Base URL: http://127.0.0.1:8000
# Client ID: opensentry-device
# Save and restartBenefits of OAuth2 with Multiple OpenSentry Devices:
- β Single Sign-On - One login for all your security cameras
- β Centralized User Management - Add/remove users in one place
- β Audit Trail - Track authentication across all devices
- β Enhanced Security - MFA, token rotation, PKCE
- β Graceful Fallback - Local auth available if OAuth2 server is down
See OpenSentry documentation for complete setup guide.
Before deploying to production:
- Replace demo auth - Integrate your production authentication (LDAP, SAML, etc.)
- Generate strong
APP_SECRET- 32+ random bytes - Use environment variables - Never hardcode secrets
- Enforce strict
redirect_uris- No wildcards, exact matches only - Enable PKCE for public clients - Mandatory in client policy
- Use
client_secret_basic- For confidential client authentication - Enable rate limiting - Protect
/tokenand/authorizeendpoints - Set up monitoring - Track failed auth attempts, token usage
- Rotate signing keys - Periodic key rotation via Admin UI
- Use HTTPS - TLS termination at reverse proxy (nginx, ALB)
- Regular backups - Backup database (contains keys and settings)
- Audit logging - Log all admin actions and token operations
pip install gunicorn
gunicorn -w 4 -b 0.0.0.0:8000 server:appFROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "server:app"]docker build -t oauth2-server .
docker run -d -p 8000:8000 \
-e DATABASE_URL="postgresql://..." \
-e APP_SECRET="..." \
oauth2-serverversion: '3.8'
services:
oauth2:
build: .
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql://oauth:password@db:5432/oauth
APP_SECRET: ${APP_SECRET}
depends_on:
- db
db:
image: postgres:15
environment:
POSTGRES_DB: oauth
POSTGRES_USER: oauth
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:server {
listen 443 ssl http2;
server_name oauth.yourdomain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}- Use PostgreSQL or MySQL (not SQLite)
- Configure connection pooling
- Set up TLS-terminating reverse proxy
- Ensure consistent external URL (stable OIDC
issuer) - Enable database replication/backups
- Set up centralized logging
- Configure health checks (
/healthendpoint) - Use secrets management (Vault, AWS Secrets Manager, etc.)
- Set up alerting for auth failures
- Document disaster recovery procedures
| Type | Auth Method | Secret? | PKCE? | Best For |
|---|---|---|---|---|
| Public | none |
β No | β Required | SPAs, mobile apps, desktop apps |
| Confidential | client_secret_postclient_secret_basic |
β Required | Backend services, server-to-server |
Public clients can't securely store secrets (e.g., JavaScript in browser), so they must use PKCE for security.
Confidential clients run on secure servers where secrets can be protected.
Is this production-ready?
Yes! This server implements industry-standard OAuth 2.0 and OIDC protocols. It's used in real-world applications. Follow the Security Checklist for production deployments.
Can I use this commercially?
Absolutely! MIT License means you can use it for any purpose, including commercial projects. No attribution required (but appreciated!).
How does this compare to Auth0/Okta?
See the comparison table above. Main advantages: zero cost, complete control, data privacy, no vendor lock-in.
Can I integrate with my existing user database?
Yes! Replace the demo login system in server.py with your own authentication (LDAP, database, SAML, etc.). The OAuth2/OIDC layer remains the same.
Does it work offline/air-gapped?
Yes! Self-hosted means no external dependencies. Perfect for secure environments without internet access.
How do I reset the Admin Token?
Update server_settings.admin_token_hash in the database, or reinitialize the database for development.
Can I run multiple instances for high availability?
Yes! Use a shared database (PostgreSQL with replication) and deploy multiple app instances behind a load balancer. Sessions are stored server-side in the database.
What about user registration?
User registration is intentionally not included (out of scope for OAuth2). Integrate your own user management system or use the /dev/seed helper for development.
Contributions welcome! This project thrives on community feedback.
- π Found a bug? Open an issue
- π‘ Have an idea? Start a discussion
- π§ Want to contribute code? Fork, commit, and submit a PR!
MIT License - See LICENSE for details.
Use it, modify it, sell it, deploy it anywhere. No restrictions.
We believe authentication should be:
- β Under your control - Not locked behind a vendor
- β Privacy-respecting - Your users' data stays with you
- β Cost-effective - No per-user fees that scale with success
- β Transparent - Open source means you can audit everything
- β Standards-based - Works with any OAuth2-compatible app
Cloud OAuth providers have their place, but you should have the choice to self-host.
- π Troubleshooting Guide - Common issues and solutions
- π» Client Code Examples - Working implementations
- π API Reference - Complete endpoint docs
- βοΈ Admin UI Guide - Configuration reference
- πΉ OpenSentry - Smart security camera system with OAuth2 integration
- ποΈ OpenSentry Command - Device discovery and management dashboard
Take back control of your authentication.
Self-hosted. Open source. Yours forever.