Files
at-container-registry/pkg/auth/oauth/client.go
2025-10-10 15:01:47 -05:00

139 lines
4.6 KiB
Go

package oauth
import (
"context"
"fmt"
"net/url"
"strings"
"atcr.io/pkg/atproto"
"github.com/bluesky-social/indigo/atproto/auth/oauth"
"github.com/bluesky-social/indigo/atproto/identity"
"github.com/bluesky-social/indigo/atproto/syntax"
)
// App wraps indigo's ClientApp with ATCR-specific configuration
type App struct {
clientApp *oauth.ClientApp
baseURL string
directory identity.Directory
}
// NewApp creates a new OAuth app for ATCR with default scopes
func NewApp(baseURL string, store oauth.ClientAuthStore) (*App, error) {
return NewAppWithScopes(baseURL, store, GetDefaultScopes())
}
// NewAppWithScopes creates a new OAuth app for ATCR with custom scopes
func NewAppWithScopes(baseURL string, store oauth.ClientAuthStore, scopes []string) (*App, error) {
config := NewClientConfigWithScopes(baseURL, scopes)
clientApp := oauth.NewClientApp(&config, store)
return &App{
clientApp: clientApp,
baseURL: baseURL,
directory: identity.DefaultDirectory(),
}, nil
}
// NewClientConfig creates an OAuth client configuration for ATCR
func NewClientConfig(baseURL string) oauth.ClientConfig {
return NewClientConfigWithScopes(baseURL, GetDefaultScopes())
}
// NewClientConfigWithScopes creates an OAuth client configuration with custom scopes
func NewClientConfigWithScopes(baseURL string, scopes []string) oauth.ClientConfig {
clientID := ClientIDWithScopes(baseURL, scopes)
redirectURI := RedirectURI(baseURL)
// Check if this is localhost (public client) or production (confidential client)
if strings.Contains(baseURL, "127.0.0.1") || strings.Contains(baseURL, "localhost") {
return oauth.NewPublicConfig(clientID, redirectURI, scopes)
}
// Production: confidential client
// Note: Client secrets would be configured separately if needed
return oauth.NewPublicConfig(clientID, redirectURI, scopes)
}
// StartAuthFlow initiates an OAuth authorization flow for a given handle
// Returns the authorization URL (state is stored in the auth store)
func (a *App) StartAuthFlow(ctx context.Context, handle string) (authURL string, err error) {
// Start auth flow with handle as identifier
// Indigo will resolve the handle internally
authURL, err = a.clientApp.StartAuthFlow(ctx, handle)
if err != nil {
return "", fmt.Errorf("failed to start auth flow: %w", err)
}
return authURL, nil
}
// ProcessCallback processes an OAuth callback with authorization code and state
// Returns ClientSessionData which contains the session information
func (a *App) ProcessCallback(ctx context.Context, params url.Values) (*oauth.ClientSessionData, error) {
sessionData, err := a.clientApp.ProcessCallback(ctx, params)
if err != nil {
return nil, fmt.Errorf("failed to process OAuth callback: %w", err)
}
return sessionData, nil
}
// ResumeSession resumes an existing OAuth session
// Returns a ClientSession that can be used to make authenticated requests
func (a *App) ResumeSession(ctx context.Context, did syntax.DID, sessionID string) (*oauth.ClientSession, error) {
session, err := a.clientApp.ResumeSession(ctx, did, sessionID)
if err != nil {
return nil, fmt.Errorf("failed to resume session: %w", err)
}
return session, nil
}
// GetClientApp returns the underlying indigo ClientApp
// This is useful for advanced use cases that need direct access
func (a *App) GetClientApp() *oauth.ClientApp {
return a.clientApp
}
// Directory returns the identity directory used by the OAuth app
func (a *App) Directory() identity.Directory {
return a.directory
}
// ClientID generates the OAuth client ID for ATCR
func ClientID(baseURL string) string {
return ClientIDWithScopes(baseURL, GetDefaultScopes())
}
// ClientIDWithScopes generates a client ID with custom scopes
func ClientIDWithScopes(baseURL string, scopes []string) string {
scopeStr := strings.Join(scopes, " ")
if strings.Contains(baseURL, "127.0.0.1") || strings.Contains(baseURL, "localhost") {
// Localhost: use query-based client ID
return fmt.Sprintf("http://localhost?redirect_uri=%s&scope=%s",
url.QueryEscape(RedirectURI(baseURL)),
url.QueryEscape(scopeStr))
}
// Production: use metadata URL
return baseURL + "/client-metadata.json"
}
// RedirectURI returns the OAuth redirect URI for ATCR
func RedirectURI(baseURL string) string {
return baseURL + "/auth/oauth/callback"
}
// GetDefaultScopes returns the default OAuth scopes for ATCR registry operations
func GetDefaultScopes() []string {
return []string{
"atproto",
"blob:application/vnd.docker.distribution.manifest.v2+json",
fmt.Sprintf("repo:%s", atproto.ManifestCollection),
fmt.Sprintf("repo:%s", atproto.TagCollection),
fmt.Sprintf("repo:%s", atproto.StarCollection),
fmt.Sprintf("repo:%s", atproto.SailorProfileCollection),
}
}