Files
MindSift/Doku/plans/oauth-fuer-mcp.md
T
sandrodachandClaude Opus 5.5 75aea4a7d4
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m49s
feat(mcp): OAuth so claude.ai and ChatGPT can connect as connectors
MindSift becomes the OAuth authorization server for its own MCP
endpoint: discovery (RFC 9728/8414), dynamic client registration,
authorization code with PKCE (S256), rotating refresh tokens with reuse
detection. The consent page lives in the app at /oauth/authorize. A
grant is an api_keys entry (kind 'oauth'), so scopes, origin badges,
renaming and revoking work as for keys; tokens are valid for /mcp only.
Unused connections expire after OAUTH_IDLE_DAYS (default 90). Requires
BASE_URL; off in demo mode. Release 0.51.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 20:01:32 +01:00

11 KiB
Raw Blame History

Plan: OAuth für den MCP-Zugang (Entwurf)

Stand 2026-11-01, umgesetzt in v0.51.0. Ziel: MindSift lässt sich in claude.ai, der Claude-App am Handy und ChatGPT im Browser als Connector eintragen. Diese Clients können keinen X-API-Key-Header setzen, sondern verlangen den OAuth-Ablauf aus der MCP-Spezifikation (Autorisierung, Stand 2025-06-18). MindSift wird dafür selbst zum OAuth-Server für seinen eigenen MCP-Endpunkt.

Nicht Teil dieses Plans: Anmelden bei MindSift über fremde Anbieter (OIDC), siehe registrierung-und-login.md, Abschnitt 4.

Heute

  • MCP-Endpunkt POST /api/v1/mcp, Anmeldung über authMiddleware: API-Schlüssel (mindsift_sec_…) als X-API-Key oder Authorization: Bearer, alternativ eine Sitzung.
  • Ein Schlüssel hat Rechte (tasks:read, lists:write …), mcpToolAllowed zeigt nur die erlaubten Werkzeuge, Änderungen bekommen die Herkunft mcp mit dem Namen des Schlüssels.
  • Die Sitzung liegt im Frontend im localStorage, nicht in einem Cookie. Der Server sieht beim reinen Aufruf einer Seite also nicht, wer angemeldet ist.

So sieht es für Nutzer aus

  1. In claude.ai: Einstellungen → Connectors → Eigenen Connector hinzufügen, Adresse https://<instanz>/api/v1/mcp.
  2. Der Browser öffnet MindSift. Wer nicht angemeldet ist, meldet sich wie gewohnt an.
  3. Eine Zustimmungsseite: „Claude möchte auf dein MindSift zugreifen“, darunter die Rechte als Häkchen (Aufgaben lesen, Aufgaben ändern, Listen lesen, Listen ändern, in die Inbox schreiben), vorbelegt mit dem, was der Client anfragt. Knöpfe Erlauben / Ablehnen.
  4. Zurück in claude.ai sind die Werkzeuge da.
  5. In Einstellungen → API-Schlüssel steht der Zugang als eigener Eintrag „Claude (OAuth)“ mit Rechten, „zuletzt benutzt“ und Widerrufen. Rechte ändern geht dort wie bei jedem Schlüssel.

Was der Agent anlegt, trägt wie heute das Abzeichen mit dem Namen des Eintrags.

Grundidee: ein OAuth-Zugang ist ein API-Schlüssel

Die Zustimmung legt einen normalen Eintrag in api_keys an (neue Spalte kind = 'oauth', dazu oauth_client_id). Ein Schlüssel-Geheimnis zum Abtippen gibt es dafür nicht: key_hash bekommt einen zufälligen Wert, den niemand kennt. Die Tokens hängen an diesem Eintrag.

Damit bleiben Rechteprüfung (mcpToolAllowed, apiKeyRouteDenied), Herkunft (origin), „zuletzt benutzt“, Umbenennen, Rechte ändern und Widerrufen unverändert. Wird der Eintrag gelöscht, fallen die Tokens per Fremdschlüssel mit.

Endpunkte

Alle ohne Anmeldung erreichbar, außer der Zustimmung.

Endpunkt Zweck
GET /.well-known/oauth-protected-resource (auch mit Pfad-Anhang /api/v1/mcp) Beschreibung der Ressource (RFC 9728): resource, authorization_servers, scopes_supported
GET /.well-known/oauth-authorization-server Beschreibung des OAuth-Servers (RFC 8414): Endpunkte, code_challenge_methods_supported: ["S256"], grant_types_supported, token_endpoint_auth_methods_supported: ["none"]
POST /oauth/register Dynamische Client-Registrierung (RFC 7591), wie sie claude.ai und ChatGPT benutzen
GET /oauth/authorize Liefert die App aus (wie jede Seite); die App zeigt die Zustimmung
GET /api/v1/oauth/authorize Prüft die Parameter und liefert Client-Name, Rücksprungadresse und angefragte Rechte für die Zustimmungsseite (angemeldet)
POST /api/v1/oauth/authorize Erlauben oder Ablehnen; gibt die Rücksprung-URL mit code und state (bzw. error=access_denied) zurück, die App leitet weiter (angemeldet)
POST /oauth/token authorization_code gegen Tokens tauschen, refresh_token erneuern
POST /oauth/revoke Token widerrufen (RFC 7009), optional, schadet nicht

Am MCP-Endpunkt antwortet MindSift ohne gültiges Token mit 401 und WWW-Authenticate: Bearer resource_metadata="https://<instanz>/.well-known/oauth-protected-resource". Daran erkennen die Clients, dass sie den Ablauf starten müssen. Heute fehlt dieser Header.

Kein eigenes CORS für diese Endpunkte: claude.ai und ChatGPT rufen sie vom Server aus auf. Browser-Clients wie der MCP Inspector erreichen ohnehin auch den MCP-Endpunkt nicht, solange BASE_URL die erlaubten Ursprünge festlegt.

Ablauf im Detail

Registrierung: Der Client schickt client_name und redirect_uris. MindSift speichert ihn in oauth_clients und gibt eine client_id zurück. Es gibt nur öffentliche Clients (kein Client-Geheimnis); die Sicherheit kommt aus PKCE und den festen Rücksprungadressen.

  • Rücksprungadressen nur https://… oder http://localhost/127.0.0.1 (Desktop-Clients), keine Fragmente.
  • Bremse pro IP wie beim Login. Clients, die 24 Stunden nach der Registrierung noch keine Zustimmung bekommen haben, werden beim Aufräumen gelöscht.

Zustimmung: response_type=code, client_id, redirect_uri (muss exakt registriert sein), code_challenge mit code_challenge_method=S256 (Pflicht), state, scope (optional), resource (RFC 8707, falls angegeben gleich <BASE_URL>/api/v1/mcp).

  • Passen client_id oder redirect_uri nicht, zeigt die App einen Fehler und leitet nicht weiter. Alle anderen Fehler gehen als error=… an die Rücksprungadresse.
  • Bei Erlauben legt MindSift den Eintrag in api_keys an (Name = client_name + „ (OAuth)“, Rechte = gewählte Häkchen). Hat dieselbe Person demselben Client schon zugestimmt, wird der vorhandene Eintrag weiterverwendet, und die Rechte werden aktualisiert, damit keine Doppelten entstehen.
  • Der Code ist 10 Minuten gültig, nur einmal verwendbar und wird nur als Hash gespeichert.

Token: Der Tausch prüft Code, client_id, redirect_uri und code_verifier. Ergebnis:

  • access_token mindsift_oat_…, 1 Stunde gültig
  • refresh_token mindsift_ort_…, 90 Tage gültig, wird bei jeder Erneuerung ausgetauscht. Wird ein bereits ersetzter Refresh-Token noch einmal benutzt, verfallen alle Tokens dieses Eintrags (Schutz bei Diebstahl).
  • scope mit den tatsächlich erteilten Rechten

Nutzung: authenticateToken erkennt mindsift_oat_ und liefert Nutzer und apiKeyAuth des zugehörigen Eintrags. Ab da läuft alles wie bei einem Schlüssel. OAuth-Tokens gelten nur für den MCP-Endpunkt (dafür wurden sie ausgestellt), nicht für die REST-Endpunkte.

Rechte (Scopes)

Die vorhandenen Rechte werden unverändert als OAuth-Scopes verwendet (tasks:read, tasks:write, lists:read, lists:write, webhook:triage; die schmalen webhook:task/webhook:list erscheinen auf der Zustimmungsseite nicht). Fragt ein Client kein scope an (claude.ai tut das meist nicht), sind auf der Zustimmungsseite Lesen und Ändern für Aufgaben und Listen vorbelegt; Inbox bleibt aus. Notizen bleiben wie heute unerreichbar.

Datenbank

ALTER TABLE api_keys ADD COLUMN kind TEXT NOT NULL DEFAULT 'key';   -- key | oauth
ALTER TABLE api_keys ADD COLUMN oauth_client_id TEXT NOT NULL DEFAULT '';

CREATE TABLE oauth_clients (
  id TEXT PRIMARY KEY,               -- client_id
  name TEXT NOT NULL,
  redirect_uris TEXT NOT NULL,       -- JSON-Array
  created_at TEXT NOT NULL
);

CREATE TABLE oauth_codes (
  code_hash TEXT PRIMARY KEY,
  client_id TEXT NOT NULL,
  key_id TEXT NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE,
  redirect_uri TEXT NOT NULL,
  code_challenge TEXT NOT NULL,
  expires_at TEXT NOT NULL
);

CREATE TABLE oauth_tokens (
  token_hash TEXT PRIMARY KEY,
  kind TEXT NOT NULL,                -- access | refresh
  key_id TEXT NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE,
  expires_at TEXT NOT NULL,
  replaced_at TEXT NOT NULL DEFAULT ''  -- refresh: wann ausgetauscht (Wiederverwendung erkennen)
);

Abgelaufene Codes und Tokens räumt der vorhandene Aufräum-Lauf weg. Als Migration über schema_migrations.

Voraussetzungen und Grenzen

  • BASE_URL muss gesetzt sein. Sie ist issuer und Ressource; aus dem Host-Header gebaut wäre sie fälschbar. Ohne BASE_URL ist OAuth aus: kein WWW-Authenticate-Hinweis, und die Endpunkte antworten mit 404. Die Einstellungen sagen dann „OAuth braucht BASE_URL“.
  • Reverse-Proxy: /.well-known/ und /oauth/ müssen bei MindSift ankommen. Mit der Beispielkonfiguration in reverse-proxy.md (location /) ist das so; dort ergänze ich einen Hinweis.
  • Demo-Modus: OAuth aus (Demo-Daten werden zurückgesetzt, Einträge wären sinnlos).
  • CSP: Die Zustimmungsseite leitet per window.location weiter, nicht per Formular, also kein form-action nötig.
  • Kein eigener Admin-Schalter für OAuth. Ein Schalter für den ganzen MCP-Zugang ist eine eigene Aufgabe; ist MCP aus, ist auch OAuth aus.
  • Unbenutzte Zugänge verfallen: Ein OAuth-Eintrag, der 90 Tage nicht benutzt wurde, wird beim Aufräumen gelöscht (samt Tokens). Einstellbar mit OAUTH_IDLE_DAYS (Standard 90, 0 = nie).

Oberfläche

  • Neue Ansicht Zustimmung (/oauth/authorize), Handy-tauglich: Logo, Client-Name, Rücksprung-Domain gut sichtbar („Weiterleitung zu claude.ai“), Rechte-Häkchen mit den vorhandenen Bezeichnungen aus ApiKeysPanel, Erlauben/Ablehnen (44 px). Nicht angemeldet → Anmeldung, danach zurück zur Zustimmung (Parameter bleiben erhalten).
  • API-Schlüssel-Liste: OAuth-Einträge mit Kennzeichen „OAuth“ und Client-Name; „Schlüssel anzeigen/kopieren“ entfällt bei ihnen.
  • Hilfe-Text in ApiKeysPanel ersetzen: statt „claude.ai … wird noch nicht unterstützt“ eine kurze Anleitung „In claude.ai als Connector eintragen: <BASE_URL>/api/v1/mcp“ mit Kopier-Knopf.

Tests

  • Go: kompletter Ablauf (registrieren → zustimmen → Code tauschen → MCP aufrufen → erneuern), falsche redirect_uri, falscher code_verifier, Code doppelt benutzt, abgelaufene Tokens, Refresh-Wiederverwendung, Widerruf des Eintrags, OAuth-Token an REST-Endpunkt (abgelehnt), ohne BASE_URL (aus), Demo (aus). Ablaufzeiten mit übergebener Zeit, Tests unter mehreren TZ.
  • Frontend: Zustimmungsseite (Vorbelegung der Rechte, Ablehnen).
  • Von Hand: Verbindung aus claude.ai gegen die dev-Instanz bzw. mit dem MCP Inspector.

Doku und Release

  • Doku/api/rest.md (Abschnitt MCP, neue OAuth-Endpunkte), Doku/architecture.md, README.md (Funktionsliste), Doku/reverse-proxy.md.
  • Neue Funktion → Minor-Version, CHANGELOG „Added: MindSift lässt sich in claude.ai und ChatGPT als Connector verbinden“.
  • Die Website erwähnt evtl. „claude.ai noch nicht unterstützt“; das melde ich, statt es zu ändern.

Reihenfolge und Aufwand (grob)

  1. Datenbank, Token-Speicher, authenticateToken für mindsift_oat_, WWW-Authenticate – klein.
  2. Metadaten, Registrierung, Token-Endpunkt mit PKCE und Refresh – mittel.
  3. Zustimmungsseite und API-Schlüssel-Liste – mittel.
  4. Tests, Doku, Ausprobieren mit claude.ai – klein bis mittel.

Keine neue Go-Abhängigkeit nötig: Für den Server-Teil reicht die Standardbibliothek (SHA-256 für PKCE, crypto/rand), golang.org/x/oauth2 ist eine Client-Bibliothek und hilft hier kaum.

Entscheidungen (2026-11-01)

  • Ohne scope sind Lesen und Ändern für Aufgaben und Listen vorbelegt.
  • Kein OAuth-Schalter; MCP insgesamt abschaltbar kommt separat.
  • Refresh-Token 90 Tage, bei jeder Erneuerung neu.
  • Unbenutzte OAuth-Einträge verfallen nach 90 Tagen, OAUTH_IDLE_DAYS.