The admin authentication system uses an environment-first approach with database fallback for robust, secure access control.
def is_super_admin(email):
"""Environment-first authentication with database fallback."""
# 1. Check environment variables first (deployment-time config)
env_emails = os.environ.get("SUPER_ADMIN_EMAILS", "")
if env_emails:
emails_list = [e.strip().lower() for e in env_emails.split(",") if e.strip()]
if email.lower() in emails_list:
return True
# 2. Fallback to database configuration (runtime config)
try:
with get_db_session() as db:
config = db.query(TenantManagementConfig).filter_by(
config_key="super_admin_emails"
).first()
if config and config.config_value:
db_emails = [e.strip().lower() for e in config.config_value.split(",")]
return email.lower() in db_emails
except Exception as e:
logger.error(f"Database auth check failed: {e}")
return False- Session Caching: Super admin status cached in session to avoid redundant database calls
- Trust Session State:
require_tenant_access()checks session first, then validates if needed - Automatic Caching: Session updated when admin status is confirmed
# Add to require_tenant_access decorator
max_session_age = 3600 # 1 hour
session_start = session.get("authenticated_at", 0)
if time.time() - session_start > max_session_age:
session.clear()
return redirect(url_for("auth.login"))
# Re-validate admin status every 5 minutes
last_check = session.get("admin_check_at", 0)
if time.time() - last_check > 300:
session["is_super_admin"] = is_super_admin(email)
session["admin_check_at"] = time.time()def audit_admin_access(email, tenant_id, action, success=True):
"""Log all admin access attempts with IP and user agent."""
audit_log = AuditLog(
email=email,
tenant_id=tenant_id,
action=f"admin_access_{action}",
success=success,
ip_address=request.remote_addr,
user_agent=request.user_agent.string,
timestamp=datetime.utcnow()
)app.config.update(
SESSION_COOKIE_SECURE=True, # HTTPS only
SESSION_COOKIE_HTTPONLY=True, # No JavaScript access
SESSION_COOKIE_SAMESITE='Lax', # CSRF protection
PERMANENT_SESSION_LIFETIME=3600 # 1 hour timeout
)Move from .env files to proper secrets management:
- Fly.io secrets for production
- Kubernetes secrets for k8s deployments
- AWS Secrets Manager or similar for AWS
To prevent subdomain squatting and brand impersonation, tenant subdomains are automatically generated using random UUIDs.
Implementation:
- Subdomains are 8-character hexadecimal strings (e.g.,
a7f3d92b) - Generated from UUID4 (first 8 characters)
- Users cannot choose custom subdomains during signup
- Eliminates risk of claiming subdomains like "nytimes" or "cnn"
Branding Solution: Publishers who want branded URLs should use virtual hosts (custom domains):
- Configure custom domain (e.g.,
sales.publisher.com) in tenant settings - Domain ownership verified by Approximated proxy service
- Proper branding without security risks
Ad Server Configuration Check: Tenants without configured ad servers show a "Pending Configuration" page instead of active agent endpoints. This prevents incomplete registrations from appearing operational.
- Full access to all tenants
- Configured via environment variables or database
- Can create/modify/delete tenants and users
- Limited access to specific tenants via User model
- Cannot access other tenants' data
- Can manage their own tenant's configuration
- Each advertiser (principal) has isolated access tokens
- Tokens scoped to specific tenant
- Cannot access other tenants or principals
- All admin actions logged to
audit_logstable - Includes timestamp, user, action, and result
- Used for compliance and security monitoring
All authentication changes must include tests for:
- Session timeout behavior
- Re-validation logic
- Environment vs database precedence
- Audit logging completeness
- Session security headers
- CSRF protection
Test Location: tests/integration/test_product_deletion.py contains comprehensive authentication tests including environment-first approach validation.
✅ Working: OAuth authentication works correctly within the sales-agent.scope3.com domain and its subdomains.
test-agent.adcontextprotocol.org) has limitations due to browser cookie security restrictions.
- User visits
https://tenant.sales-agent.scope3.com/admin/ - OAuth flow works perfectly with session cookies
- User redirected back to tenant subdomain after authentication
- User visits external domain (e.g.,
https://test-agent.adcontextprotocol.org/admin/) - OAuth initiation works and stores external domain in session
- OAuth callback cannot retrieve session data due to cookie domain restrictions
- User redirected to login page instead of back to external domain
# Production session config
SESSION_COOKIE_DOMAIN = ".sales-agent.scope3.com" # Scoped to internal domain
SESSION_COOKIE_SECURE = True # HTTPS only
SESSION_COOKIE_SAMESITE = "None" # Required for OAuth
SESSION_COOKIE_PATH = "/admin/" # Admin interface only# OAuth Initiation (stores external domain in session)
session["oauth_external_domain"] = request.headers.get("Apx-Incoming-Host")
# OAuth Callback (retrieves from session - fails cross-domain)
external_domain = session.pop("oauth_external_domain", None)The limitation is due to fundamental browser security: cookies cannot be shared across different domains. When a user comes from test-agent.adcontextprotocol.org, the browser cannot access session cookies scoped to .sales-agent.scope3.com.
- ✅ OAuth session handling within same domain
- ✅ Approximated header detection and processing
- ✅ Session cookie configuration
- ✅ Redirect URI integrity (no modifications)
- ✅ CSRF protection preservation (Authlib state management)
- ✅ Cross-domain limitation documentation
Key Test File: tests/integration/test_oauth_session_handling.py
Potential approaches for cross-domain OAuth:
- Alternative State Storage: Redis, database, or external service
- Modified Redirect URI Approach: Register additional redirect URIs with domain-specific query parameters
- Authentication Architecture Changes: Different authentication flow for external domains
- Proxy-Based Solution: Handle authentication at the proxy/gateway level
For immediate needs, direct users to use https://tenant.sales-agent.scope3.com/admin/ for OAuth authentication rather than external domain URLs.
The Admin UI supports authentication via any OpenID Connect (OIDC) compliant provider, not just Google. This allows organizations to use their existing identity provider.
Any OIDC-compliant provider works, including:
- Google (default)
- Microsoft Azure AD / Entra ID
- Okta
- Auth0
- Keycloak
- OneLogin
- Ping Identity
- Custom OIDC providers
# Required for generic OIDC
OAUTH_DISCOVERY_URL=https://your-provider.com/.well-known/openid-configuration
OAUTH_CLIENT_ID=your-client-id
OAUTH_CLIENT_SECRET=your-client-secret
# Optional: customize scopes (defaults to "openid email profile")
OAUTH_SCOPES=openid email profile custom_scope# Google-specific (backwards compatible)
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret| Provider | Discovery URL |
|---|---|
https://accounts.google.com/.well-known/openid-configuration |
|
| Microsoft | https://login.microsoftonline.com/{tenant-id}/v2.0/.well-known/openid-configuration |
| Okta | https://{your-domain}.okta.com/.well-known/openid-configuration |
| Auth0 | https://{your-tenant}.auth0.com/.well-known/openid-configuration |
| Keycloak | https://{server}/realms/{realm}/.well-known/openid-configuration |
When creating your OAuth application in your identity provider:
- Application Type: Web application
- Redirect URI:
http://localhost:8001/auth/google/callback(local) orhttps://your-domain/admin/auth/google/callback(production) - Scopes: At minimum
openid,email, andprofile - Grant Type: Authorization Code
The system automatically handles different claim formats from various providers:
| Claim | Providers |
|---|---|
email, preferred_username, upn, sub |
|
| Name | name, display_name, given_name + family_name |
| Picture | picture, avatar_url, photo |
When multiple OAuth configurations exist, they're used in this order:
- Generic OIDC (
OAUTH_DISCOVERY_URL+ credentials) - highest priority - Named Provider (
OAUTH_PROVIDER+ generic credentials) - Google OAuth (
GOOGLE_CLIENT_ID+ secret) - backwards compatible - File-based (
client_secret.json) - legacy support
🔒 Security: All secrets MUST be in .env.secrets file (no environment variables).
Create your .env.secrets file:
# API Keys
GEMINI_API_KEY=your-gemini-api-key-here
# OAuth Configuration (choose ONE option)
# Option A: Generic OIDC (works with ANY provider)
OAUTH_DISCOVERY_URL=https://your-provider.com/.well-known/openid-configuration
OAUTH_CLIENT_ID=your-client-id
OAUTH_CLIENT_SECRET=your-client-secret
# Option B: Google OAuth (simpler if only using Google)
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
# Super Admin Configuration
SUPER_ADMIN_EMAILS=user1@example.com,user2@example.com
# GAM OAuth Configuration (required for Google Ad Manager functionality)
# Note: This is separate from Admin UI OAuth - it's for GAM API access
GAM_OAUTH_CLIENT_ID=your-gam-client-id.apps.googleusercontent.com
GAM_OAUTH_CLIENT_SECRET=your-gam-client-secret
# Optional
SUPER_ADMIN_DOMAINS=example.com- Single Source: All secrets in one place
- Gitignore Protection: File not committed to repository
- Workspace Isolation: Each workspace can have different secrets
- Reduced Risk: No accidental secret exposure via environment variables
- Never commit secrets to version control
- Use different secrets for dev/staging/production
- Rotate secrets regularly (at least quarterly)
- Audit secret access via logs
- Use secrets managers in production (Fly.io secrets, AWS Secrets Manager, etc.)