From 16d04bd5f757ec7bb54ce1e1c43d439cbdf4c610 Mon Sep 17 00:00:00 2001 From: James Bhattarai Date: Fri, 6 Mar 2026 01:36:23 +0545 Subject: [PATCH] Chore: Setup and Env --- .env.example | 123 ++++++++++++++++++++-------- config/base.py | 2 + manage.py | 73 ++++++++++++++--- scripts/configure_oauth_provider.py | 2 +- scripts/init_db.py | 7 ++ 5 files changed, 162 insertions(+), 45 deletions(-) diff --git a/.env.example b/.env.example index 204b2bf..537face 100644 --- a/.env.example +++ b/.env.example @@ -1,58 +1,117 @@ -# Flask Configuration -FLASK_APP=wsgi.py +FLASK_APP=manage.py FLASK_ENV=development -SECRET_KEY=your-secret-key-here-change-in-production +FLASK_DEBUG=1 # Database -DATABASE_URL=postgresql://user:password@localhost:5432/authy2_dev +DATABASE_URL=postgresql://user:password@localhost:5432/gatehouse_dev SQLALCHEMY_ECHO=False SQLALCHEMY_LOG_LEVEL=WARNING -# Security +# Security / Encryption +SECRET_KEY=change-me-in-production +ENCRYPTION_KEY=change-me-in-production-32-bytes!! +# Used to encrypt SSH CA private keys stored in the database +CA_ENCRYPTION_KEY=change-me-in-production BCRYPT_LOG_ROUNDS=12 -ENCRYPTION_KEY=your-encryption-key-here-change-in-production + +# Session cookies SESSION_COOKIE_SECURE=False -SESSION_COOKIE_HTTPONLY=True SESSION_COOKIE_SAMESITE=Lax +# Only needed when sharing cookies across subdomains (e.g. api.example.com + ui.example.com) +# SESSION_COOKIE_DOMAIN=example.com MAX_SESSION_DURATION=86400 -# CORS -#CORS_ORIGINS=http://localhost:3000,http://localhost:5173,https://oidc-playpen.lovable.app/,http://localhost:8080/ -CORS_ORIGINS=* - - -# JWT (if using JWT instead of sessions) -JWT_SECRET_KEY=your-jwt-secret-key-here +# ───────────────────────────────────────────────────────────────────────────── +# JWT +# ───────────────────────────────────────────────────────────────────────────── +JWT_SECRET_KEY=change-me-in-production JWT_ACCESS_TOKEN_EXPIRES=3600 JWT_REFRESH_TOKEN_EXPIRES=2592000 -# Redis (for session storage) +# ───────────────────────────────────────────────────────────────────────────── +# Redis (session storage + rate limiting) +# ───────────────────────────────────────────────────────────────────────────── REDIS_URL=redis://localhost:6379/0 +SESSION_REDIS_URL=redis://localhost:6379/0 +RATELIMIT_STORAGE_URL=redis://localhost:6379/1 -# OIDC +# ───────────────────────────────────────────────────────────────────────────── +# CORS +# ───────────────────────────────────────────────────────────────────────────── +CORS_ORIGINS=http://localhost:8080,http://localhost:5173 + +# ───────────────────────────────────────────────────────────────────────────── +# Frontend / App URLs +# All three should point at the browser-facing SPA. They are used for: +# FRONTEND_URL → OAuth callback redirects after provider auth +# APP_URL → Password-reset and email-verify links in emails +# OIDC_UI_URL → OIDC /authorize redirects to the React consent/login UI +# ───────────────────────────────────────────────────────────────────────────── +FRONTEND_URL=http://localhost:8080 +APP_URL=http://localhost:8080 +OIDC_UI_URL=http://localhost:8080 + +# ───────────────────────────────────────────────────────────────────────────── +# OIDC / OAuth issuer +# ───────────────────────────────────────────────────────────────────────────── OIDC_ISSUER_URL=http://localhost:5000 +OIDC_BASE_URL=http://localhost:5000 +# ───────────────────────────────────────────────────────────────────────────── +# WebAuthn +# ───────────────────────────────────────────────────────────────────────────── +WEBAUTHN_RP_ID=localhost +WEBAUTHN_RP_NAME=Gatehouse +WEBAUTHN_ORIGIN=http://localhost:8080 + +# ───────────────────────────────────────────────────────────────────────────── +# SSH CA (pick one) +# ───────────────────────────────────────────────────────────────────────────── +SSH_CA_KEY_PATH=/path/to/ca-users +# SSH_CA_PRIVATE_KEY= # raw key content; takes priority over SSH_CA_KEY_PATH + +# ───────────────────────────────────────────────────────────────────────────── +# Email / SMTP +# ───────────────────────────────────────────────────────────────────────────── +EMAIL_ENABLED=False +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USE_TLS=True +SMTP_USERNAME= +SMTP_PASSWORD= +FROM_ADDRESS=noreply@gatehouse.local + +# ───────────────────────────────────────────────────────────────────────────── # Logging +# ───────────────────────────────────────────────────────────────────────────── LOG_LEVEL=INFO LOG_TO_STDOUT=True +# ───────────────────────────────────────────────────────────────────────────── # Rate Limiting +# ───────────────────────────────────────────────────────────────────────────── RATELIMIT_ENABLED=True -RATELIMIT_STORAGE_URL=redis://localhost:6379/1 - -# SSH CA -# Path to CA private key file (alternative to SSH_CA_PRIVATE_KEY env var) -SSH_CA_KEY_PATH=/path/to/ca-users -# Or set the key content directly (takes priority over SSH_CA_KEY_PATH): -# SSH_CA_PRIVATE_KEY= - -EMAIL_ENABLED= -SMTP_HOST= -SMTP_PORT= -SMTP_USERNAME= -SMTP_PASSWORD= -FROM_ADDRESS= -WEBAUTHN_ORIGIN= +# Per-endpoint auth limits (optional — defaults shown) +# RATELIMIT_AUTH_REGISTER=10 per minute; 50 per hour +# RATELIMIT_AUTH_LOGIN=20 per minute; 100 per hour +# RATELIMIT_AUTH_TOTP_VERIFY=20 per minute; 100 per hour +# RATELIMIT_AUTH_FORGOT_PASSWORD=5 per minute; 20 per hour +# RATELIMIT_AUTH_RESET_PASSWORD=10 per minute; 30 per hour ZEROTIER_API_TOKEN= -ZEROTIER_API_URL= \ No newline at end of file +ZEROTIER_API_URL= + +# ───────────────────────────────────────────────────────────────────────────── +# OIDC token lifetimes & security (optional — defaults shown) +# ───────────────────────────────────────────────────────────────────────────── +# OIDC_ACCESS_TOKEN_LIFETIME=3600 +# OIDC_REFRESH_TOKEN_LIFETIME=2592000 +# OIDC_ID_TOKEN_LIFETIME=3600 +# OIDC_AUTHORIZATION_CODE_LIFETIME=600 +# OIDC_REQUIRE_PKCE=True +# OIDC_ALLOW_IMPLICIT_FLOW=False +# OIDC_KEY_ROTATION_DAYS=90 +# OIDC_KEY_GRACE_PERIOD_DAYS=30 +# OIDC_RATE_LIMIT_AUTHORIZE=10/minute +# OIDC_RATE_LIMIT_TOKEN=20/minute +# OIDC_RATE_LIMIT_USERINFO=60/minute diff --git a/config/base.py b/config/base.py index b940767..e4d9329 100644 --- a/config/base.py +++ b/config/base.py @@ -128,6 +128,8 @@ class BaseConfig: # Frontend URL (for OAuth callback redirects) FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:8080") + APP_URL = os.getenv("APP_URL", os.getenv("FRONTEND_URL", "http://localhost:8080")) + OIDC_UI_URL = os.getenv("OIDC_UI_URL", os.getenv("FRONTEND_URL", "http://localhost:8080")) # ZeroTier Configuration ZEROTIER_API_TOKEN = os.getenv("ZEROTIER_API_TOKEN", "") diff --git a/manage.py b/manage.py index 3f694fc..06d3fc7 100644 --- a/manage.py +++ b/manage.py @@ -1,5 +1,6 @@ """Management script for Flask application.""" import os +import click from dotenv import load_dotenv # Load environment variables FIRST, before any app imports @@ -153,36 +154,75 @@ def mfa_compliance_status(): @cli.command("configure_oauth") -def configure_oauth(): - """Interactively configure an OAuth provider at the application level. +@click.argument("provider", required=False) +@click.option("--client-id", default=None, help="OAuth client ID") +@click.option("--client-secret", default=None, help="OAuth client secret") +@click.option("--redirect-url", default=None, help="Default redirect URL (e.g. https://yourdomain.com/api/v1/auth/external//callback)") +def configure_oauth(provider, client_id, client_secret, redirect_url): + """Configure an OAuth provider at the application level. - Usage: + Usage (interactive): python manage.py configure_oauth + Usage (non-interactive): + python manage.py configure_oauth google --client-id ID --client-secret SECRET + Supported providers: google, github, microsoft """ import getpass - from gatehouse_app.models.authentication_method import ApplicationProviderConfig + from gatehouse_app.models.auth.authentication_method import ApplicationProviderConfig from gatehouse_app.extensions import db SUPPORTED = ["google", "github", "microsoft"] - print("=" * 60) - print("OAuth Provider Configuration") - print("=" * 60) - print(f"Supported providers: {', '.join(SUPPORTED)}") + # Well-known endpoints — stored in additional_config so the adapter can + # resolve auth_url / token_url / userinfo_url without extra logic. + PROVIDER_DEFAULTS = { + "google": { + "auth_url": "https://accounts.google.com/o/oauth2/v2/auth", + "token_url": "https://oauth2.googleapis.com/token", + "userinfo_url": "https://www.googleapis.com/oauth2/v3/userinfo", + }, + "github": { + "auth_url": "https://github.com/login/oauth/authorize", + "token_url": "https://github.com/login/oauth/access_token", + "userinfo_url": "https://api.github.com/user", + }, + "microsoft": { + "auth_url": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", + "token_url": "https://login.microsoftonline.com/common/oauth2/v2.0/token", + "userinfo_url": "https://graph.microsoft.com/oidc/userinfo", + }, + } - provider = input("Provider [google/github/microsoft]: ").strip().lower() + if not provider: + print("=" * 60) + print("OAuth Provider Configuration") + print("=" * 60) + print(f"Supported providers: {', '.join(SUPPORTED)}") + provider = input("Provider [google/github/microsoft]: ").strip().lower() + + provider = provider.strip().lower() if provider not in SUPPORTED: print(f"❌ Unknown provider: {provider}") return - client_id = input("Client ID: ").strip() + if not client_id: + client_id = input("Client ID: ").strip() if not client_id: print("❌ client_id is required") return - client_secret = getpass.getpass("Client Secret (leave blank to keep existing): ").strip() + if not client_secret: + client_secret = getpass.getpass("Client Secret (leave blank to keep existing): ").strip() + + if not redirect_url: + base_url = os.getenv("API_BASE_URL", "http://localhost:5000/api/v1") + default = f"{base_url}/auth/external/{provider}/callback" + entered = input(f"Default redirect URL [{default}]: ").strip() + redirect_url = entered or default + + additional_config = PROVIDER_DEFAULTS[provider].copy() with app.app_context(): config = ApplicationProviderConfig.query.filter_by(provider_type=provider).first() @@ -191,6 +231,11 @@ def configure_oauth(): if client_secret: config.set_client_secret(client_secret) config.is_enabled = True + config.default_redirect_url = redirect_url + config.additional_config = { + **(config.additional_config or {}), + **additional_config, + } db.session.commit() print(f"✅ Updated {provider} provider config.") else: @@ -198,12 +243,16 @@ def configure_oauth(): provider_type=provider, client_id=client_id, is_enabled=True, + default_redirect_url=redirect_url, + additional_config=additional_config, ) if client_secret: config.set_client_secret(client_secret) db.session.add(config) db.session.commit() print(f"✅ Created {provider} provider config.") + print(f" redirect_url : {redirect_url}") + print(f" auth_url : {additional_config['auth_url']}") @cli.command("list_oauth") @@ -213,7 +262,7 @@ def list_oauth(): Usage: python manage.py list_oauth """ - from gatehouse_app.models.authentication_method import ApplicationProviderConfig + from gatehouse_app.models.auth.authentication_method import ApplicationProviderConfig with app.app_context(): configs = ApplicationProviderConfig.query.all() diff --git a/scripts/configure_oauth_provider.py b/scripts/configure_oauth_provider.py index fe8bcc0..5372bd7 100755 --- a/scripts/configure_oauth_provider.py +++ b/scripts/configure_oauth_provider.py @@ -58,7 +58,7 @@ if os.path.exists(env_file): # Import after path setup from gatehouse_app import create_app -from gatehouse_app.services.external_auth_service import ExternalAuthService, ExternalAuthError +from gatehouse_app.services.external_auth import ExternalAuthService, ExternalAuthError def _microsoft_defaults() -> dict: diff --git a/scripts/init_db.py b/scripts/init_db.py index 6f620bd..9398243 100644 --- a/scripts/init_db.py +++ b/scripts/init_db.py @@ -1,4 +1,11 @@ """Initialize database script.""" +import sys +import os +import time +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent)) + from gatehouse_app import create_app from gatehouse_app.extensions import db from sqlalchemy import text