oauth

package
v0.1.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 17, 2026 License: MIT Imports: 28 Imported by: 0

Documentation

Overview

Package oauth provides OAuth client configuration and helper functions for ATCR. It provides helpers for setting up indigo's OAuth library with ATCR-specific configuration, including default scopes, confidential client setup, and interactive browser-based authentication flows.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ExpandIncludeScopes

func ExpandIncludeScopes(scopes []string) []string

ExpandIncludeScopes expands any "include:" prefixed scopes to their full form by looking up the corresponding permission-set in the embedded lexicon files. For example, "include:io.atcr.authFullApp" expands to "repo?collection=io.atcr.manifest&..."

func GenerateKeyID

func GenerateKeyID(privateKey *atcrypto.PrivateKeyP256) (string, error)

GenerateKeyID generates a stable key ID from a P256 public key Uses the first 8 characters of the hex-encoded SHA256 hash of the public key bytes

func GenerateOrLoadClientKey

func GenerateOrLoadClientKey(keyPath string) (*atcrypto.PrivateKeyP256, error)

GenerateOrLoadClientKey generates a new P256 key pair or loads an existing one This is a convenience wrapper for OAuth client keys

func GenerateOrLoadKey

func GenerateOrLoadKey(keyPath string, keyType KeyType) (atcrypto.PrivateKey, error)

GenerateOrLoadKey generates a new key pair or loads an existing one Supports both P-256 (OAuth) and K-256 (ATProto PDS) key types

func GenerateOrLoadPDSKey

func GenerateOrLoadPDSKey(keyPath string) (*atcrypto.PrivateKeyK256, error)

GenerateOrLoadPDSKey generates a new K256 key pair or loads an existing one This is a convenience wrapper for ATProto PDS signing keys

func GetDefaultScopes

func GetDefaultScopes(did string) []string

GetDefaultScopes returns the default OAuth scopes for ATCR registry operations. Includes io.atcr.authFullApp permission-set plus individual scopes for PDS compatibility. Blob scopes are listed explicitly (not supported in Lexicon permission-sets).

func IsPEMFormat

func IsPEMFormat(data []byte) bool

IsPEMFormat checks if bytes are in PEM format (for migration detection) Exported for testing and migration utilities

func MultibaseToPrivateKey

func MultibaseToPrivateKey(encoded string) (*atcrypto.PrivateKeyP256, error)

MultibaseToPrivateKey parses a multibase-encoded P256 private key

func NewClientApp

func NewClientApp(baseURL string, store oauth.ClientAuthStore, scopes []string, keyPath string, clientName string) (*oauth.ClientApp, error)

NewClientApp creates an indigo OAuth ClientApp with ATCR-specific configuration Automatically configures confidential client for production deployments keyPath specifies where to store/load the OAuth client P-256 key (ignored for localhost) clientName is added to OAuth client metadata (currently unused, reserved for future)

func NewClientAppWithKey

func NewClientAppWithKey(baseURL string, store oauth.ClientAuthStore, scopes []string, privateKey *atcrypto.PrivateKeyP256, clientName string) (*oauth.ClientApp, error)

NewClientAppWithKey creates an indigo OAuth ClientApp with a pre-loaded P-256 key. Used by AppView when loading keys from the database instead of disk. For localhost development, privateKey is ignored (public client).

func OpenBrowser

func OpenBrowser(url string) error

OpenBrowser opens the default browser to the given URL. This is the public API that maintains backward compatibility.

func PrivateKeyToMultibase

func PrivateKeyToMultibase(key *atcrypto.PrivateKeyP256) string

PrivateKeyToMultibase converts a P256 private key to multibase format Required by indigo's SetClientSecret() API

func RedirectURI

func RedirectURI(baseURL string) string

RedirectURI returns the OAuth redirect URI for ATCR

func ScopesMatch

func ScopesMatch(stored, desired []string) bool

ScopesMatch checks if two scope lists are equivalent (order-independent) Returns true if both lists contain the same scopes, regardless of order. Expands any "include:" prefixed scopes in the desired list before comparing, since the PDS returns expanded scopes in the stored session.

func StartSignupFlow added in v0.1.3

func StartSignupFlow(ctx context.Context, clientApp *oauth.ClientApp, authServerHost string) (string, error)

StartSignupFlow starts an OAuth authorization flow pointed at a specific authorization server with prompt=create, asking the server to show its signup UI rather than its login UI.

indigo's ClientApp.StartAuthFlow does not expose prompt, so this duplicates the PAR + redirect build using only exported indigo helpers. The resulting AuthRequestData is persisted via clientApp.Store so the existing ServeCallback path handles the return leg unchanged.

authServerHost is the PDS origin (e.g. "https://eurosky.social"). It gets resolved to the actual OAuth auth server URL before the PAR call.

Types

type CommandExecutor

type CommandExecutor interface {
	Execute(name string, args ...string) error
}

CommandExecutor is an interface for executing system commands. This allows for dependency injection and mocking in tests.

type InteractiveResult

type InteractiveResult struct {
	SessionData *oauth.ClientSessionData
	Session     *oauth.ClientSession
	ClientApp   *oauth.ClientApp
}

InteractiveResult contains the result of an interactive OAuth flow

func InteractiveFlowWithCallback

func InteractiveFlowWithCallback(
	ctx context.Context,
	baseURL string,
	handle string,
	scopes []string,
	clientName string,
	registerCallback func(handler http.HandlerFunc) error,
	displayAuthURL func(string) error,
) (*InteractiveResult, error)

InteractiveFlowWithCallback runs an interactive OAuth flow with explicit callback handling This version allows the caller to register the callback handler before starting the flow

type KeyType

type KeyType int

KeyType represents the elliptic curve algorithm for key generation

const (
	// KeyTypeP256 uses NIST P-256 (ES256) - standard for OAuth/OIDC
	KeyTypeP256 KeyType = iota
	// KeyTypeK256 uses secp256k1 (ES256K) - used for ATProto PDS signing
	KeyTypeK256
)

type PostAuthCallback

type PostAuthCallback func(ctx context.Context, did, handle, pdsEndpoint, sessionID string) error

PostAuthCallback is called after successful OAuth authentication. Parameters: ctx, did, handle, pdsEndpoint, sessionID This allows AppView to perform business logic (profile creation, avatar fetch, etc.) without coupling the OAuth package to AppView-specific dependencies.

type Refresher

type Refresher struct {
	// contains filtered or unexported fields
}

Refresher manages OAuth sessions and token refresh for AppView Sessions are loaded fresh from database on every request (database is source of truth)

func NewRefresher

func NewRefresher(clientApp *oauth.ClientApp) *Refresher

NewRefresher creates a new session refresher

func (*Refresher) DeleteSession

func (r *Refresher) DeleteSession(ctx context.Context, did string) error

DeleteSession removes an OAuth session from storage and optionally invalidates the UI session This is called when OAuth authentication fails to force re-authentication

func (*Refresher) DoWithSession

func (r *Refresher) DoWithSession(ctx context.Context, did string, fn func(session *oauth.ClientSession) error) error

DoWithSession executes a function with a locked OAuth session. The lock is held for the entire duration of the function, preventing DPoP nonce races.

This is the preferred way to make PDS requests that require OAuth/DPoP authentication. The lock is held through the entire PDS interaction, ensuring that:

  1. Only one goroutine at a time can negotiate DPoP nonces with the PDS for a given DID
  2. The session's PersistSessionCallback saves the updated nonce before other goroutines load
  3. Concurrent layer uploads don't race on stale nonces

Why locking is critical: During docker push, multiple layers upload concurrently. Each layer creates a new ClientSession by loading from database. Without locking, this race condition occurs:

  1. Layer A loads session with stale DPoP nonce from DB
  2. Layer B loads session with same stale nonce (A hasn't updated DB yet)
  3. Layer A makes request → 401 "use_dpop_nonce" → gets fresh nonce → saves to DB
  4. Layer B makes request → 401 "use_dpop_nonce" (using stale nonce from step 2)
  5. DPoP nonce thrashing continues, eventually causing 500 errors

With per-DID locking:

  1. Layer A acquires lock, loads session, handles nonce negotiation, saves, releases lock
  2. Layer B acquires lock AFTER A releases, loads fresh nonce from DB, succeeds

Example usage:

var result MyResult
err := refresher.DoWithSession(ctx, did, func(session *oauth.ClientSession) error {
    resp, err := session.DoWithAuth(session.Client, req, "com.atproto.server.getServiceAuth")
    if err != nil {
        return err
    }
    // Parse response into result...
    return nil
})

func (*Refresher) SetUISessionStore

func (r *Refresher) SetUISessionStore(store UISessionStore)

SetUISessionStore sets the UI session store for invalidating sessions on OAuth failures

func (*Refresher) ValidateSession

func (r *Refresher) ValidateSession(ctx context.Context, did string) error

ValidateSession checks if an OAuth session is usable by attempting to load it. This triggers token refresh if needed (via indigo's auto-refresh in DoWithSession). Returns nil if session is valid, error if session is invalid/expired/needs re-auth.

This is used by the token handler to validate OAuth sessions before issuing JWTs, preventing the flood of errors that occurs when a stale session is discovered during parallel layer uploads.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server handles OAuth authorization for the AppView

func NewServer

func NewServer(clientApp *oauth.ClientApp) *Server

NewServer creates a new OAuth server

func (*Server) ServeAuthorize

func (s *Server) ServeAuthorize(w http.ResponseWriter, r *http.Request)

ServeAuthorize handles GET /auth/oauth/authorize

func (*Server) ServeCallback

func (s *Server) ServeCallback(w http.ResponseWriter, r *http.Request)

ServeCallback handles GET /auth/oauth/callback

func (*Server) SetPostAuthCallback

func (s *Server) SetPostAuthCallback(callback PostAuthCallback)

SetPostAuthCallback sets the callback to be invoked after successful OAuth authentication This allows AppView to inject business logic without coupling the OAuth package

func (*Server) SetRefresher

func (s *Server) SetRefresher(refresher *Refresher)

SetRefresher sets the refresher for invalidating session cache

func (*Server) SetUISessionStore

func (s *Server) SetUISessionStore(store UISessionStore)

SetUISessionStore sets the UI session store for web login

type SessionCache

type SessionCache struct {
	Session   *oauth.ClientSession
	SessionID string
}

SessionCache represents a cached OAuth session

type UISessionStore

type UISessionStore interface {
	Create(did, handle, pdsEndpoint string, duration time.Duration) (string, error)
	DeleteByDID(did string)
}

UISessionStore interface for managing UI sessions Shared between refresher and server

type UserStore

type UserStore interface {
	UpsertUser(did, handle, pdsEndpoint, avatar string) error
}

UserStore is the interface for user management

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL