Initial minimal mcp server for KorAP
diff --git a/auth/oauth.go b/auth/oauth.go
new file mode 100644
index 0000000..acbc978
--- /dev/null
+++ b/auth/oauth.go
@@ -0,0 +1,152 @@
+package auth
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "time"
+
+ "golang.org/x/oauth2"
+ "golang.org/x/oauth2/clientcredentials"
+
+ "github.com/korap/korap-mcp/config"
+)
+
+// OAuthClient handles OAuth2 authentication for KorAP API
+type OAuthClient struct {
+ config *config.OAuthConfig
+ oauth2Config *oauth2.Config
+ token *oauth2.Token
+ httpClient *http.Client
+}
+
+// NewOAuthClient creates a new OAuth2 client
+func NewOAuthClient(cfg *config.OAuthConfig) (*OAuthClient, error) {
+ if cfg == nil {
+ return nil, fmt.Errorf("oauth config cannot be nil")
+ }
+
+ if err := cfg.Validate(); err != nil {
+ return nil, fmt.Errorf("invalid oauth config: %w", err)
+ }
+
+ client := &OAuthClient{
+ config: cfg,
+ oauth2Config: cfg.ToOAuth2Config(),
+ }
+
+ return client, nil
+}
+
+// GetAuthURL returns the authorization URL for the OAuth2 flow
+func (c *OAuthClient) GetAuthURL(state string) string {
+ if c.oauth2Config == nil {
+ return ""
+ }
+ return c.oauth2Config.AuthCodeURL(state, oauth2.AccessTypeOnline)
+}
+
+// ExchangeCode exchanges an authorization code for an access token
+func (c *OAuthClient) ExchangeCode(ctx context.Context, code string) error {
+ if c.oauth2Config == nil {
+ return fmt.Errorf("oauth2 not configured")
+ }
+
+ token, err := c.oauth2Config.Exchange(ctx, code)
+ if err != nil {
+ return fmt.Errorf("failed to exchange code for token: %w", err)
+ }
+
+ c.token = token
+ c.httpClient = c.oauth2Config.Client(ctx, token)
+ return nil
+}
+
+// SetToken sets the OAuth2 token directly
+func (c *OAuthClient) SetToken(token *oauth2.Token) {
+ c.token = token
+ if c.oauth2Config != nil {
+ c.httpClient = c.oauth2Config.Client(context.Background(), token)
+ }
+}
+
+// GetToken returns the current OAuth2 token
+func (c *OAuthClient) GetToken() *oauth2.Token {
+ return c.token
+}
+
+// GetHTTPClient returns an HTTP client with OAuth2 authentication
+func (c *OAuthClient) GetHTTPClient() *http.Client {
+ if c.httpClient != nil {
+ return c.httpClient
+ }
+
+ // Return default client if not authenticated
+ return &http.Client{
+ Timeout: time.Second * 30,
+ }
+}
+
+// IsAuthenticated checks if the client has a valid token
+func (c *OAuthClient) IsAuthenticated() bool {
+ if c.token == nil {
+ return false
+ }
+
+ // Check if token is expired (with 5 minute buffer)
+ return c.token.Valid() && c.token.Expiry.After(time.Now().Add(5*time.Minute))
+}
+
+// ClientCredentialsFlow performs client credentials OAuth2 flow
+func (c *OAuthClient) ClientCredentialsFlow(ctx context.Context) error {
+ if c.config == nil || !c.config.Enabled {
+ return fmt.Errorf("oauth2 not configured")
+ }
+
+ ccConfig := &clientcredentials.Config{
+ ClientID: c.config.ClientID,
+ ClientSecret: c.config.ClientSecret,
+ TokenURL: c.config.TokenURL,
+ Scopes: c.config.Scopes,
+ }
+
+ token, err := ccConfig.Token(ctx)
+ if err != nil {
+ return fmt.Errorf("failed to get client credentials token: %w", err)
+ }
+
+ c.token = token
+ c.httpClient = ccConfig.Client(ctx)
+ return nil
+}
+
+// RefreshToken refreshes the OAuth2 token if possible
+func (c *OAuthClient) RefreshToken(ctx context.Context) error {
+ if c.oauth2Config == nil {
+ return fmt.Errorf("oauth2 not configured")
+ }
+
+ if c.token == nil {
+ return fmt.Errorf("no token to refresh")
+ }
+
+ tokenSource := c.oauth2Config.TokenSource(ctx, c.token)
+ newToken, err := tokenSource.Token()
+ if err != nil {
+ return fmt.Errorf("failed to refresh token: %w", err)
+ }
+
+ c.token = newToken
+ c.httpClient = c.oauth2Config.Client(ctx, newToken)
+ return nil
+}
+
+// AddAuthHeader adds authentication header to an HTTP request
+func (c *OAuthClient) AddAuthHeader(req *http.Request) error {
+ if c.token == nil {
+ return fmt.Errorf("no authentication token available")
+ }
+
+ c.token.SetAuthHeader(req)
+ return nil
+}