Files
WYGIWYH/app/apps/common/management/commands/setup_oauth.py
T
obervinov 4273c541c5 Add API tokens and OAuth2 client support for external integrations
- Personal API tokens (model, user-settings UI, admin, management command,
  DRF auth class) for non-interactive API access from automations like n8n.
  Raw token shown once; only a SHA-256 hash is stored; last_used_at writes
  are throttled.
- OAuth2 authorization server via django-oauth-toolkit with authorization
  server metadata and optional, off-by-default Dynamic Client Registration
  (RFC 7591), so remote OAuth/MCP clients can authenticate and self-register.
- Tests for token auth, DCR gating and the management commands, plus
  .env.example and README documentation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 19:15:31 +04:00

130 lines
4.6 KiB
Python

import os
from django.contrib.auth.hashers import check_password
from django.core.exceptions import ValidationError
from django.core.management.base import BaseCommand, CommandError
from oauth2_provider.models import get_application_model
Application = get_application_model()
def _get_env(name: str) -> str:
return os.getenv(name, "").strip()
def _get_bool_env(name: str, default: bool = False) -> bool:
raw = _get_env(name)
if not raw:
return default
return raw.lower() in {"1", "true", "yes", "on"}
class Command(BaseCommand):
help = (
"Creates or updates the OAuth application used by MCP clients when "
"MCP_OAUTH_CLIENT_* environment variables are configured."
)
def handle(self, *args, **options):
client_id = _get_env("MCP_OAUTH_CLIENT_ID")
client_secret = _get_env("MCP_OAUTH_CLIENT_SECRET")
redirect_uris = " ".join(_get_env("MCP_OAUTH_REDIRECT_URIS").split())
name = _get_env("MCP_OAUTH_CLIENT_NAME") or "WYGIWYH MCP"
skip_authorization = _get_bool_env("MCP_OAUTH_SKIP_AUTHORIZATION", default=False)
if not any([client_id, client_secret, redirect_uris]):
self.stdout.write(
self.style.NOTICE(
"MCP OAuth client env vars are not set. Skipping OAuth application setup."
)
)
return
missing = []
if not client_id:
missing.append("MCP_OAUTH_CLIENT_ID")
if not client_secret:
missing.append("MCP_OAUTH_CLIENT_SECRET")
if not redirect_uris:
missing.append("MCP_OAUTH_REDIRECT_URIS")
if missing:
raise CommandError(
"Missing required MCP OAuth settings: " + ", ".join(missing)
)
application, created = Application.objects.get_or_create(
client_id=client_id,
defaults={
"name": name,
"client_type": Application.CLIENT_CONFIDENTIAL,
"authorization_grant_type": Application.GRANT_AUTHORIZATION_CODE,
"redirect_uris": redirect_uris,
"skip_authorization": skip_authorization,
"client_secret": client_secret,
"hash_client_secret": True,
},
)
updated_fields = []
if application.name != name:
application.name = name
updated_fields.append("name")
if application.client_type != Application.CLIENT_CONFIDENTIAL:
application.client_type = Application.CLIENT_CONFIDENTIAL
updated_fields.append("client_type")
if (
application.authorization_grant_type
!= Application.GRANT_AUTHORIZATION_CODE
):
application.authorization_grant_type = Application.GRANT_AUTHORIZATION_CODE
updated_fields.append("authorization_grant_type")
if application.redirect_uris != redirect_uris:
application.redirect_uris = redirect_uris
updated_fields.append("redirect_uris")
if application.skip_authorization != skip_authorization:
application.skip_authorization = skip_authorization
updated_fields.append("skip_authorization")
if application.hash_client_secret is not True:
application.hash_client_secret = True
updated_fields.append("hash_client_secret")
if not application.client_secret or not check_password(
client_secret,
application.client_secret,
):
application.client_secret = client_secret
updated_fields.append("client_secret")
try:
application.full_clean()
except ValidationError as exc:
errors = "; ".join(
f"{field}: {', '.join(messages)}"
for field, messages in exc.message_dict.items()
)
raise CommandError(f"Invalid MCP OAuth application settings: {errors}") from exc
if created:
application.save()
self.stdout.write(
self.style.SUCCESS(
f"Created MCP OAuth application '{application.client_id}'."
)
)
return
if updated_fields:
application.save(update_fields=updated_fields)
self.stdout.write(
self.style.SUCCESS(
f"Updated MCP OAuth application '{application.client_id}'."
)
)
return
self.stdout.write(
self.style.SUCCESS(
f"MCP OAuth application '{application.client_id}' is already up to date."
)
)