feat(mcp): OAuth so claude.ai and ChatGPT can connect as connectors
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m49s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m49s
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>
This commit is contained in:
1 parent
994f6dc554
commit
75aea4a7d4
31 files changed
+1581
-49
No files matched your search
@@ -150,6 +150,14 @@ func (s *Server) Router() http.Handler {
|
||||
r.Get("/api/v1/ws/lists/{listId}", s.handleWebSocket)
|
||||
r.Get("/api/v1/events", s.handleSSE)
|
||||
|
||||
// OAuth for the MCP endpoint (oauth.go); the consent page /oauth/authorize is part of the app
|
||||
r.Get("/.well-known/oauth-protected-resource", s.oauthOnly(s.handleOAuthProtectedResource))
|
||||
r.Get("/.well-known/oauth-protected-resource/*", s.oauthOnly(s.handleOAuthProtectedResource))
|
||||
r.Get("/.well-known/oauth-authorization-server", s.oauthOnly(s.handleOAuthServerMetadata))
|
||||
r.Post("/oauth/register", s.oauthOnly(s.handleOAuthRegister))
|
||||
r.Post("/oauth/token", s.oauthOnly(s.handleOAuthToken))
|
||||
r.Post("/oauth/revoke", s.oauthOnly(s.handleOAuthRevoke))
|
||||
|
||||
// Authenticated API Routes
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(s.authMiddleware)
|
||||
@@ -304,6 +312,8 @@ func (s *Server) Router() http.Handler {
|
||||
r.Post(mcpPath, s.handleMCP)
|
||||
r.Get(mcpPath, handleMCPNoStream)
|
||||
r.Delete(mcpPath, handleMCPNoStream)
|
||||
r.Get("/api/v1/oauth/authorize", s.oauthOnly(s.handleOAuthAuthorizeInfo))
|
||||
r.Post("/api/v1/oauth/authorize", s.oauthOnly(s.handleOAuthAuthorize))
|
||||
|
||||
// API Keys Management
|
||||
r.Get("/api/v1/keys", s.handleGetAPIKeys)
|
||||
@@ -489,6 +499,16 @@ func (s *Server) authenticateToken(token string) (user *domain.User, key *apiKey
|
||||
}
|
||||
return u, &apiKeyAuth{ID: keyID, Scopes: scopes}
|
||||
}
|
||||
if strings.HasPrefix(token, storage.OAuthAccessPrefix) {
|
||||
if !s.oauthEnabled() {
|
||||
return nil, nil
|
||||
}
|
||||
u, scopes, keyID, err := s.store.ValidateOAuthAccessToken(token, time.Now())
|
||||
if err != nil || u == nil || !u.IsActive {
|
||||
return nil, nil
|
||||
}
|
||||
return u, &apiKeyAuth{ID: keyID, Scopes: scopes, OAuth: true}
|
||||
}
|
||||
claims, err := auth.ValidateToken(s.cfg.JWTSecret, token)
|
||||
if err != nil || claims == nil {
|
||||
return nil, nil
|
||||
@@ -517,9 +537,18 @@ func (s *Server) authMiddleware(next http.Handler) http.Handler {
|
||||
}
|
||||
user, key := s.authenticateToken(token)
|
||||
if user == nil {
|
||||
// Tells MCP clients where to start the OAuth flow (RFC 9728)
|
||||
if r.URL.Path == mcpPath && s.oauthEnabled() {
|
||||
w.Header().Set("WWW-Authenticate", `Bearer resource_metadata="`+s.oauthResourceMetadataURL()+`"`)
|
||||
}
|
||||
writeError(w, http.StatusUnauthorized, "Nicht autorisiert – bitte anmelden")
|
||||
return
|
||||
}
|
||||
// OAuth tokens were issued for the MCP endpoint only
|
||||
if key != nil && key.OAuth && r.URL.Path != mcpPath {
|
||||
writeError(w, http.StatusForbidden, "OAuth-Zugänge gelten nur für den MCP-Endpunkt")
|
||||
return
|
||||
}
|
||||
ctx := auth.WithUserID(r.Context(), user.ID)
|
||||
if key == nil {
|
||||
if claims, err := auth.ValidateToken(s.cfg.JWTSecret, token); err == nil {
|
||||
|
||||
Reference in new issue
Block a user