Chore: Setup and Env
This commit is contained in:
+90
-31
@@ -1,58 +1,117 @@
|
|||||||
# Flask Configuration
|
FLASK_APP=manage.py
|
||||||
FLASK_APP=wsgi.py
|
|
||||||
FLASK_ENV=development
|
FLASK_ENV=development
|
||||||
SECRET_KEY=your-secret-key-here-change-in-production
|
FLASK_DEBUG=1
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
DATABASE_URL=postgresql://user:password@localhost:5432/authy2_dev
|
DATABASE_URL=postgresql://user:password@localhost:5432/gatehouse_dev
|
||||||
SQLALCHEMY_ECHO=False
|
SQLALCHEMY_ECHO=False
|
||||||
SQLALCHEMY_LOG_LEVEL=WARNING
|
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
|
BCRYPT_LOG_ROUNDS=12
|
||||||
ENCRYPTION_KEY=your-encryption-key-here-change-in-production
|
|
||||||
|
# Session cookies
|
||||||
SESSION_COOKIE_SECURE=False
|
SESSION_COOKIE_SECURE=False
|
||||||
SESSION_COOKIE_HTTPONLY=True
|
|
||||||
SESSION_COOKIE_SAMESITE=Lax
|
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
|
MAX_SESSION_DURATION=86400
|
||||||
|
|
||||||
# CORS
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
#CORS_ORIGINS=http://localhost:3000,http://localhost:5173,https://oidc-playpen.lovable.app/,http://localhost:8080/
|
# JWT
|
||||||
CORS_ORIGINS=*
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
JWT_SECRET_KEY=change-me-in-production
|
||||||
|
|
||||||
# JWT (if using JWT instead of sessions)
|
|
||||||
JWT_SECRET_KEY=your-jwt-secret-key-here
|
|
||||||
JWT_ACCESS_TOKEN_EXPIRES=3600
|
JWT_ACCESS_TOKEN_EXPIRES=3600
|
||||||
JWT_REFRESH_TOKEN_EXPIRES=2592000
|
JWT_REFRESH_TOKEN_EXPIRES=2592000
|
||||||
|
|
||||||
# Redis (for session storage)
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Redis (session storage + rate limiting)
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
REDIS_URL=redis://localhost:6379/0
|
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_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
|
# Logging
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
LOG_LEVEL=INFO
|
LOG_LEVEL=INFO
|
||||||
LOG_TO_STDOUT=True
|
LOG_TO_STDOUT=True
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# Rate Limiting
|
# Rate Limiting
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
RATELIMIT_ENABLED=True
|
RATELIMIT_ENABLED=True
|
||||||
RATELIMIT_STORAGE_URL=redis://localhost:6379/1
|
# Per-endpoint auth limits (optional — defaults shown)
|
||||||
|
# RATELIMIT_AUTH_REGISTER=10 per minute; 50 per hour
|
||||||
# SSH CA
|
# RATELIMIT_AUTH_LOGIN=20 per minute; 100 per hour
|
||||||
# Path to CA private key file (alternative to SSH_CA_PRIVATE_KEY env var)
|
# RATELIMIT_AUTH_TOTP_VERIFY=20 per minute; 100 per hour
|
||||||
SSH_CA_KEY_PATH=/path/to/ca-users
|
# RATELIMIT_AUTH_FORGOT_PASSWORD=5 per minute; 20 per hour
|
||||||
# Or set the key content directly (takes priority over SSH_CA_KEY_PATH):
|
# RATELIMIT_AUTH_RESET_PASSWORD=10 per minute; 30 per hour
|
||||||
# SSH_CA_PRIVATE_KEY=
|
|
||||||
|
|
||||||
EMAIL_ENABLED=
|
|
||||||
SMTP_HOST=
|
|
||||||
SMTP_PORT=
|
|
||||||
SMTP_USERNAME=
|
|
||||||
SMTP_PASSWORD=
|
|
||||||
FROM_ADDRESS=
|
|
||||||
WEBAUTHN_ORIGIN=
|
|
||||||
|
|
||||||
ZEROTIER_API_TOKEN=
|
ZEROTIER_API_TOKEN=
|
||||||
ZEROTIER_API_URL=
|
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
|
||||||
|
|||||||
@@ -128,6 +128,8 @@ class BaseConfig:
|
|||||||
|
|
||||||
# Frontend URL (for OAuth callback redirects)
|
# Frontend URL (for OAuth callback redirects)
|
||||||
FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:8080")
|
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 Configuration
|
||||||
ZEROTIER_API_TOKEN = os.getenv("ZEROTIER_API_TOKEN", "")
|
ZEROTIER_API_TOKEN = os.getenv("ZEROTIER_API_TOKEN", "")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""Management script for Flask application."""
|
"""Management script for Flask application."""
|
||||||
import os
|
import os
|
||||||
|
import click
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# Load environment variables FIRST, before any app imports
|
# Load environment variables FIRST, before any app imports
|
||||||
@@ -153,37 +154,76 @@ def mfa_compliance_status():
|
|||||||
|
|
||||||
|
|
||||||
@cli.command("configure_oauth")
|
@cli.command("configure_oauth")
|
||||||
def configure_oauth():
|
@click.argument("provider", required=False)
|
||||||
"""Interactively configure an OAuth provider at the application level.
|
@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/<provider>/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
|
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
|
Supported providers: google, github, microsoft
|
||||||
"""
|
"""
|
||||||
import getpass
|
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
|
from gatehouse_app.extensions import db
|
||||||
|
|
||||||
SUPPORTED = ["google", "github", "microsoft"]
|
SUPPORTED = ["google", "github", "microsoft"]
|
||||||
|
|
||||||
|
# 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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if not provider:
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("OAuth Provider Configuration")
|
print("OAuth Provider Configuration")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print(f"Supported providers: {', '.join(SUPPORTED)}")
|
print(f"Supported providers: {', '.join(SUPPORTED)}")
|
||||||
|
|
||||||
provider = input("Provider [google/github/microsoft]: ").strip().lower()
|
provider = input("Provider [google/github/microsoft]: ").strip().lower()
|
||||||
|
|
||||||
|
provider = provider.strip().lower()
|
||||||
if provider not in SUPPORTED:
|
if provider not in SUPPORTED:
|
||||||
print(f"❌ Unknown provider: {provider}")
|
print(f"❌ Unknown provider: {provider}")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not client_id:
|
||||||
client_id = input("Client ID: ").strip()
|
client_id = input("Client ID: ").strip()
|
||||||
if not client_id:
|
if not client_id:
|
||||||
print("❌ client_id is required")
|
print("❌ client_id is required")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not client_secret:
|
||||||
client_secret = getpass.getpass("Client Secret (leave blank to keep existing): ").strip()
|
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():
|
with app.app_context():
|
||||||
config = ApplicationProviderConfig.query.filter_by(provider_type=provider).first()
|
config = ApplicationProviderConfig.query.filter_by(provider_type=provider).first()
|
||||||
if config:
|
if config:
|
||||||
@@ -191,6 +231,11 @@ def configure_oauth():
|
|||||||
if client_secret:
|
if client_secret:
|
||||||
config.set_client_secret(client_secret)
|
config.set_client_secret(client_secret)
|
||||||
config.is_enabled = True
|
config.is_enabled = True
|
||||||
|
config.default_redirect_url = redirect_url
|
||||||
|
config.additional_config = {
|
||||||
|
**(config.additional_config or {}),
|
||||||
|
**additional_config,
|
||||||
|
}
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
print(f"✅ Updated {provider} provider config.")
|
print(f"✅ Updated {provider} provider config.")
|
||||||
else:
|
else:
|
||||||
@@ -198,12 +243,16 @@ def configure_oauth():
|
|||||||
provider_type=provider,
|
provider_type=provider,
|
||||||
client_id=client_id,
|
client_id=client_id,
|
||||||
is_enabled=True,
|
is_enabled=True,
|
||||||
|
default_redirect_url=redirect_url,
|
||||||
|
additional_config=additional_config,
|
||||||
)
|
)
|
||||||
if client_secret:
|
if client_secret:
|
||||||
config.set_client_secret(client_secret)
|
config.set_client_secret(client_secret)
|
||||||
db.session.add(config)
|
db.session.add(config)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
print(f"✅ Created {provider} provider config.")
|
print(f"✅ Created {provider} provider config.")
|
||||||
|
print(f" redirect_url : {redirect_url}")
|
||||||
|
print(f" auth_url : {additional_config['auth_url']}")
|
||||||
|
|
||||||
|
|
||||||
@cli.command("list_oauth")
|
@cli.command("list_oauth")
|
||||||
@@ -213,7 +262,7 @@ def list_oauth():
|
|||||||
Usage:
|
Usage:
|
||||||
python manage.py list_oauth
|
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():
|
with app.app_context():
|
||||||
configs = ApplicationProviderConfig.query.all()
|
configs = ApplicationProviderConfig.query.all()
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ if os.path.exists(env_file):
|
|||||||
|
|
||||||
# Import after path setup
|
# Import after path setup
|
||||||
from gatehouse_app import create_app
|
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:
|
def _microsoft_defaults() -> dict:
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
"""Initialize database script."""
|
"""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 import create_app
|
||||||
from gatehouse_app.extensions import db
|
from gatehouse_app.extensions import db
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|||||||
Reference in New Issue
Block a user