chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
// Package oauth implements the user-facing OAuth 2.1 login flows the stdio
|
||||
// server uses to obtain a GitHub token without a pre-provisioned Personal
|
||||
// Access Token.
|
||||
//
|
||||
// It supports both GitHub OAuth Apps and GitHub Apps (user-to-server). The
|
||||
// only practical difference is that GitHub App user tokens expire and carry a
|
||||
// refresh token; this package always returns a refreshing [golang.org/x/oauth2.TokenSource]
|
||||
// so callers never have to special-case the app type.
|
||||
//
|
||||
// The package depends only on golang.org/x/oauth2 and the standard library. MCP
|
||||
// concerns (sessions, elicitation) are abstracted behind the [Prompter]
|
||||
// interface so the flows can be tested without a live client.
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// Config describes an OAuth client and the GitHub endpoints it talks to.
|
||||
type Config struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
// Scopes requested during authorization. GitHub Apps ignore these (their
|
||||
// access is governed by installed permissions); OAuth Apps honor them.
|
||||
Scopes []string
|
||||
// Endpoint holds the authorization, token, and device endpoints. Build one
|
||||
// with [GitHubEndpoint].
|
||||
Endpoint oauth2.Endpoint
|
||||
// CallbackPort is the fixed local port for the PKCE callback server. Zero
|
||||
// requests a random port, which is the secure default for native binaries
|
||||
// but cannot be reached through Docker port mapping (see the Manager).
|
||||
CallbackPort int
|
||||
}
|
||||
|
||||
// NewGitHubConfig builds a Config for the given GitHub host. An empty host
|
||||
// targets github.com; otherwise the host may be a GHES or ghe.com hostname,
|
||||
// with or without a scheme.
|
||||
func NewGitHubConfig(clientID, clientSecret string, scopes []string, host string, callbackPort int) Config {
|
||||
return Config{
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
Scopes: scopes,
|
||||
Endpoint: GitHubEndpoint(host),
|
||||
CallbackPort: callbackPort,
|
||||
}
|
||||
}
|
||||
|
||||
// GitHubEndpoint returns the OAuth authorization, token, and device endpoints
|
||||
// for a GitHub host. An empty host targets github.com.
|
||||
func GitHubEndpoint(host string) oauth2.Endpoint {
|
||||
base := NormalizeHost(host)
|
||||
return oauth2.Endpoint{
|
||||
AuthURL: base + "/login/oauth/authorize",
|
||||
TokenURL: base + "/login/oauth/access_token",
|
||||
DeviceAuthURL: base + "/login/device/code",
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizeHost turns a user-supplied host into a scheme+host base URL with no
|
||||
// trailing slash. The API subdomain is stripped because OAuth endpoints live on
|
||||
// the web host, not the API host (api.github.com -> github.com). An empty host
|
||||
// yields the github.com default, so callers can also use it to recognize the
|
||||
// default host (NormalizeHost(host) == "https://github.com").
|
||||
func NormalizeHost(host string) string {
|
||||
host = strings.TrimSpace(host)
|
||||
if host == "" {
|
||||
return "https://github.com"
|
||||
}
|
||||
|
||||
scheme := "https"
|
||||
switch {
|
||||
case strings.HasPrefix(host, "https://"):
|
||||
host = strings.TrimPrefix(host, "https://")
|
||||
case strings.HasPrefix(host, "http://"):
|
||||
scheme = "http"
|
||||
host = strings.TrimPrefix(host, "http://")
|
||||
}
|
||||
|
||||
// Drop any path, query, or fragment; we only need scheme://host.
|
||||
if i := strings.IndexAny(host, "/?#"); i >= 0 {
|
||||
host = host[:i]
|
||||
}
|
||||
|
||||
host = strings.TrimPrefix(host, "api.")
|
||||
|
||||
return fmt.Sprintf("%s://%s", scheme, host)
|
||||
}
|
||||
|
||||
// randomState returns a cryptographically random URL-safe string used as the
|
||||
// OAuth state parameter (CSRF protection) and elicitation IDs.
|
||||
func randomState() (string, error) {
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", fmt.Errorf("generating random state: %w", err)
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
Reference in New Issue
Block a user