fix: resolve OAuth compatibility issues for login-flow deployment

- Drop OIDC fork: comment out third_party/oidc mount, use upstream
  v1.16.3 from app store (fixes consent redirect race, PR #631)
- Support client_secret_basic auth: add _extract_basic_auth() helper
  so TS MCP SDK can authenticate at token endpoint (RFC 6749 §2.3.1)
- Multi-issuer JWT validation: accept tokens with internal Docker
  issuer (http://app:80) or public URL (NEXTCLOUD_PUBLIC_ISSUER_URL)
  since AS proxy obtains tokens server-to-server
- Introspection fallback: try token introspection when JWT verification
  fails, supporting both JWT and opaque token types
- Register all tool scopes in DCR: add semantic:read, collectives:read,
  collectives:write to OIDC client allowed_scopes so tokens include
  them and semantic search tools are visible to authenticated clients
- Auto-create Astrolabe OAuth client: new app-hook creates OIDC client
  and stores credentials in config.php so the "Authorize via OAuth"
  button works without manual setup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-03-29 15:05:26 +02:00
co-authored by Claude Opus 4.6
parent 3cf4c777ed
commit fe8799a133
7 changed files with 151 additions and 24 deletions
+19 -2
View File
@@ -24,10 +24,10 @@ import logging
import os
import secrets
import time
from base64 import urlsafe_b64encode
from base64 import b64decode, urlsafe_b64encode
from dataclasses import dataclass, field
from typing import Any
from urllib.parse import urlencode
from urllib.parse import unquote, urlencode
from urllib.parse import urlparse as parse_url
import jwt
@@ -905,6 +905,19 @@ async def _oauth_callback_as_proxy(
return RedirectResponse(redirect_url, status_code=302)
def _extract_basic_auth(request: Request) -> tuple[str | None, str | None]:
"""Extract client_id and client_secret from HTTP Basic Auth header (RFC 6749 §2.3.1)."""
auth_header = request.headers.get("authorization", "")
if not auth_header.startswith("Basic "):
return None, None
try:
decoded = b64decode(auth_header[6:]).decode("utf-8")
client_id, _, client_secret = decoded.partition(":")
return unquote(client_id), unquote(client_secret) if client_secret else None
except Exception:
return None, None
def _verify_pkce_s256(code_verifier: str, code_challenge: str) -> bool:
"""Verify PKCE S256 code_verifier against stored code_challenge.
@@ -958,6 +971,10 @@ async def _token_authorization_code(request: Request, form) -> JSONResponse:
code_verifier = form.get("code_verifier")
client_id = form.get("client_id")
# RFC 6749 §2.3.1: clients may authenticate via HTTP Basic Auth
if not client_id:
client_id, _ = _extract_basic_auth(request)
logger.debug(
"AS proxy token: received code=%s client_id=%s redirect_uri=%s "
"code_verifier=%s",