Files
synapbus/internal/auth/provider.go
T
Algis DumbrisandClaude Opus 4.6 2575ce2626 feat: OAuth 2.1 with PKCE, MCP auth, dead letters, channel management, and UX polish
- Add OAuth 2.1 identity provider with PKCE S256 (ory/fosite)
- Add RFC 7591 dynamic client registration for MCP clients
- Add RFC 8414 OAuth metadata discovery endpoint
- Add branded OAuth login/authorize pages with SynapBus design
- Add SYNAPBUS_BASE_URL env var for remote/LAN deployments
- Add OAuth bearer token authentication for MCP connections
- Add dead letter queue with Web UI management page
- Add channel leave, member list, and improved channel management
- Add agent auth middleware for MCP-authenticated requests
- Add console printer for structured server startup output
- Hide human accounts from agent management UI
- Fix SSE through middleware (Flush/Unwrap support)
- Fix graceful shutdown by closing SSE clients before server stop
- Fix localhost/127.0.0.1 redirect URI normalization for OAuth
- Remove agent self-registration MCP tools (manage via Web UI only)
- Update README with OAuth setup guide and MCP client config example

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 12:54:02 +02:00

93 lines
2.6 KiB
Go

package auth
import (
"context"
"crypto/rand"
"net/url"
"time"
"github.com/ory/fosite"
"github.com/ory/fosite/compose"
"github.com/ory/fosite/token/hmac"
)
// NewOAuthProvider creates a configured fosite OAuth 2.1 provider.
// It supports authorization code with PKCE (S256 only), client credentials, and refresh token rotation.
func NewOAuthProvider(cfg Config, store *FositeStore) fosite.OAuth2Provider {
secret := cfg.Secret
if len(secret) < 32 {
// Generate a random secret if not configured
secret = make([]byte, 32)
rand.Read(secret)
}
config := &fosite.Config{
AccessTokenLifespan: cfg.AccessTokenTTL,
RefreshTokenLifespan: cfg.RefreshTokenLifetime,
AuthorizeCodeLifespan: 10 * time.Minute,
GlobalSecret: secret,
SendDebugMessagesToClients: cfg.DevMode,
EnforcePKCE: true,
EnforcePKCEForPublicClients: true,
EnablePKCEPlainChallengeMethod: false,
TokenURL: tokenURL(cfg.IssuerURL),
HashCost: cfg.BcryptCost,
// Allow localhost with any port for native MCP clients (RFC 8252 Section 7.3)
RedirectSecureChecker: func(_ context.Context, u *url.URL) bool {
if u == nil {
return false
}
return u.Scheme == "https" || u.Hostname() == "localhost" || u.Hostname() == "127.0.0.1"
},
}
// HMACSHAStrategy for token generation
hmacStrategy := &hmac.HMACStrategy{
Config: config,
}
_ = hmacStrategy
return compose.Compose(
config,
store,
&compose.CommonStrategy{
CoreStrategy: compose.NewOAuth2HMACStrategy(config),
},
compose.OAuth2AuthorizeExplicitFactory,
compose.OAuth2ClientCredentialsGrantFactory,
compose.OAuth2RefreshTokenGrantFactory,
compose.OAuth2PKCEFactory,
compose.OAuth2TokenIntrospectionFactory,
)
}
// tokenURL returns the token endpoint URL. When issuerURL is set, it's used as the base.
// When empty (auto-detect mode), fosite's TokenURL is set to a relative path since
// the actual URL is exposed via OAuth metadata from the request Host header.
func tokenURL(issuerURL string) string {
if issuerURL != "" {
return issuerURL + "/oauth/token"
}
return "/oauth/token"
}
// NewSession creates a new fosite session for a user.
func NewSession(user *User) fosite.Session {
return &fositeSession{
UserID: user.ID,
Username: user.Username,
Subject: user.Username,
}
}
// NewSessionWithAgent creates a new fosite session for a user with an agent identity.
func NewSessionWithAgent(user *User, agentName string) fosite.Session {
return &fositeSession{
UserID: user.ID,
Username: user.Username,
Subject: user.Username,
AgentName: agentName,
}
}