oauth

package
v0.0.14 Latest Latest
Warning

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

Go to latest
Published: Apr 10, 2026 License: Apache-2.0, MIT Imports: 22 Imported by: 0

Documentation

Overview

Package oauth implements the ATProto OAuth 2.0 client with mandatory PKCE, PAR (Pushed Authorization Requests), and DPoP (Demonstration of Proof-of-Possession).

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidState is returned when the callback state parameter doesn't
	// match a pending authorization request.
	ErrInvalidState = errors.New("oauth: invalid or expired state parameter")

	// ErrIssuerMismatch is returned when the callback iss parameter doesn't
	// match the expected authorization server.
	ErrIssuerMismatch = errors.New("oauth: issuer mismatch")

	// ErrMissingIssuer is returned when the callback is missing the required
	// iss parameter and the AS declared support for it.
	ErrMissingIssuer = errors.New("oauth: missing iss parameter in callback")

	// ErrIssuerVerification is returned when the sub DID's resolved PDS does
	// not point to the expected authorization server.
	ErrIssuerVerification = errors.New("oauth: issuer verification failed — DID does not resolve to expected AS")

	// ErrMissingScope is returned when the token response is missing the
	// required "atproto" scope.
	ErrMissingScope = errors.New("oauth: token response missing required atproto scope")

	// ErrNoSession is returned when no session exists for the requested DID.
	ErrNoSession = errors.New("oauth: no session for DID")

	// ErrTokenExpired is returned when a token has expired and cannot be refreshed.
	ErrTokenExpired = errors.New("oauth: token expired and no refresh token available")

	// ErrNoRefreshToken is returned when refresh is attempted without a refresh token.
	ErrNoRefreshToken = errors.New("oauth: no refresh token available")

	// ErrUseDPoPNonce is the error code returned by servers requiring a DPoP nonce.
	ErrUseDPoPNonce = errors.New("oauth: use_dpop_nonce")
)

Functions

func CreateDPoPProof

func CreateDPoPProof(key *crypto.P256PrivateKey, method, targetURL, nonce, accessToken string) (string, error)

CreateDPoPProof creates a signed DPoP proof JWT per RFC 9449.

func RevokeToken

func RevokeToken(ctx context.Context, endpoint, token string, auth ClientAuth, dpopKey *crypto.P256PrivateKey, nonces *NonceStore, httpClient *http.Client)

RevokeToken revokes a token. Errors are silently ignored per spec.

Types

type AuthServerMetadata

type AuthServerMetadata struct {
	Issuer                             string   `json:"issuer"`
	AuthorizationEndpoint              string   `json:"authorization_endpoint"`
	TokenEndpoint                      string   `json:"token_endpoint"`
	PushedAuthorizationRequestEndpoint string   `json:"pushed_authorization_request_endpoint"`
	RevocationEndpoint                 string   `json:"revocation_endpoint,omitempty"`
	DPoPSigningAlgValuesSupported      []string `json:"dpop_signing_alg_values_supported"`
	ScopesSupported                    []string `json:"scopes_supported"`
	ResponseTypesSupported             []string `json:"response_types_supported"`
	GrantTypesSupported                []string `json:"grant_types_supported"`
	CodeChallengeMethodsSupported      []string `json:"code_challenge_methods_supported"`
	TokenEndpointAuthMethodsSupported  []string `json:"token_endpoint_auth_methods_supported"`

	AuthorizationResponseIssParameterSupported bool     `json:"authorization_response_iss_parameter_supported"`
	RequirePushedAuthorizationRequests         bool     `json:"require_pushed_authorization_requests"`
	ClientIDMetadataDocumentSupported          bool     `json:"client_id_metadata_document_supported"`
	ProtectedResources                         []string `json:"protected_resources,omitempty"` // RFC 9728 §4
}

AuthServerMetadata is the OAuth 2.0 Authorization Server Metadata per RFC 8414, with ATProto-specific requirements.

func FetchAuthServerMetadata

func FetchAuthServerMetadata(ctx context.Context, client *http.Client, issuer string) (*AuthServerMetadata, error)

FetchAuthServerMetadata fetches the authorization server metadata. The issuer should be the AS origin (e.g., "https://bsky.social"). No HTTP redirects are followed (SSRF prevention).

type AuthState

type AuthState struct {
	Issuer             string
	DPoPKey            *crypto.P256PrivateKey
	AuthMethod         string // "none" or "private_key_jwt"
	Verifier           string // PKCE verifier
	RedirectURI        string
	AppState           string // Opaque application state
	TokenEndpoint      string
	RevocationEndpoint string
}

AuthState stores the pending authorization flow state.

func (*AuthState) MarshalJSON

func (s *AuthState) MarshalJSON() ([]byte, error)

func (*AuthState) UnmarshalJSON

func (s *AuthState) UnmarshalJSON(data []byte) error

type AuthorizeOptions

type AuthorizeOptions struct {
	// Input is the user's handle or DID.
	Input string

	// RedirectURI is the callback URL. Must be in ClientMetadata.RedirectURIs.
	RedirectURI string

	// Scope overrides the scope from ClientMetadata. Optional.
	Scope string

	// State is opaque application state passed through the flow. Optional.
	State string
}

AuthorizeOptions configures an authorization request.

type AuthorizeResult

type AuthorizeResult struct {
	URL   string
	State string
}

AuthorizeResult is returned by Authorize with the URL to redirect the user to.

type CallbackParams

type CallbackParams struct {
	Code  string
	State string
	Iss   string // Authorization server issuer (for mix-up prevention)
}

CallbackParams are the parameters received from the OAuth callback.

type Client

type Client struct {
	// ClientMetadata is this client's metadata document.
	ClientMetadata ClientMetadata

	// Identity resolves DIDs and handles.
	Identity *identity.Directory

	// HTTPClient is the HTTP client for OAuth requests.
	// None = default 30s timeout client.
	HTTPClient gt.Option[*http.Client]

	// SessionStore persists authenticated sessions. Required.
	//
	// Implementors are responsible for encrypting tokens at rest.
	// The Session struct contains access and refresh tokens in plaintext.
	SessionStore SessionStore

	// StateStore stores pending authorization flow state. Required.
	StateStore StateStore

	// Key is the signing key for confidential clients.
	// Nil for public clients.
	Key *crypto.P256PrivateKey

	// KeyID is the key identifier for confidential client assertions.
	KeyID string
	// contains filtered or unexported fields
}

Client is an ATProto OAuth 2.0 client that handles the complete authorization flow including PKCE, PAR, and DPoP.

func (*Client) AuthenticatedClient

func (c *Client) AuthenticatedClient(ctx context.Context, did string) (*xrpc.Client, error)

AuthenticatedClient returns an *xrpc.Client configured with DPoP authentication for the given user DID. The client is long-lived: it automatically refreshes stale tokens on each request, protected by a mutex so concurrent requests coalesce on a single refresh.

func (*Client) Authorize

func (c *Client) Authorize(ctx context.Context, opts AuthorizeOptions) (*AuthorizeResult, error)

Authorize initiates the OAuth authorization flow. Returns the URL to redirect the user's browser to.

func (*Client) Callback

func (c *Client) Callback(ctx context.Context, params CallbackParams) (*Session, error)

Callback handles the OAuth redirect callback and completes token exchange.

func (*Client) SignOut

func (c *Client) SignOut(ctx context.Context, did string) error

SignOut revokes tokens and deletes the session for the given DID.

type ClientAuth

type ClientAuth interface {
	// Apply adds authentication parameters to the form values.
	Apply(params url.Values, issuer string) error
}

ClientAuth adds client authentication parameters to token endpoint requests.

type ClientMetadata

type ClientMetadata struct {
	ClientID                    string   `json:"client_id"`
	ApplicationType             string   `json:"application_type,omitempty"`
	GrantTypes                  []string `json:"grant_types"`
	Scope                       string   `json:"scope"`
	ResponseTypes               []string `json:"response_types"`
	RedirectURIs                []string `json:"redirect_uris"`
	DPoPBoundAccessTokens       bool     `json:"dpop_bound_access_tokens"`
	TokenEndpointAuthMethod     string   `json:"token_endpoint_auth_method"`
	TokenEndpointAuthSigningAlg string   `json:"token_endpoint_auth_signing_alg,omitempty"`
	JWKS                        *JWKSet  `json:"jwks,omitempty"`
	ClientName                  string   `json:"client_name,omitempty"`
	ClientURI                   string   `json:"client_uri,omitempty"`
	LogoURI                     string   `json:"logo_uri,omitempty"`
	TOSURI                      string   `json:"tos_uri,omitempty"`
	PolicyURI                   string   `json:"policy_uri,omitempty"`
}

ClientMetadata is the OAuth client metadata document. The client_id is the URL where this document is hosted.

type ConfidentialClientAuth

type ConfidentialClientAuth struct {
	ClientID string
	Key      *crypto.P256PrivateKey
	KeyID    string
}

ConfidentialClientAuth authenticates confidential clients using private_key_jwt.

func (*ConfidentialClientAuth) Apply

func (a *ConfidentialClientAuth) Apply(params url.Values, issuer string) error

type ECPublicJWK

type ECPublicJWK struct {
	KTY string `json:"kty"`
	CRV string `json:"crv"`
	X   string `json:"x"`
	Y   string `json:"y"`
}

ECPublicJWK is the JSON Web Key representation of a P-256 public key, used in DPoP proof JWT headers.

func PublicJWK

func PublicJWK(pub *crypto.P256PublicKey) ECPublicJWK

PublicJWK returns the JWK representation of a P-256 public key.

type ExchangeCodeConfig

type ExchangeCodeConfig struct {
	TokenEndpoint      string
	RevocationEndpoint string
	Code               string
	CodeVerifier       string
	RedirectURI        string
	ClientAuth         ClientAuth
	DPoPKey            *crypto.P256PrivateKey
	Nonces             *NonceStore
	HTTPClient         *http.Client
}

ExchangeCodeConfig configures a token exchange request.

type JWKSet

type JWKSet struct {
	Keys []ECPublicJWK `json:"keys"`
}

JWKSet is a JSON Web Key Set containing public keys for confidential clients.

type MemorySessionStore

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

MemorySessionStore is an in-memory SessionStore for testing. Not safe for concurrent use.

func NewMemorySessionStore

func NewMemorySessionStore() *MemorySessionStore

NewMemorySessionStore creates an empty in-memory session store.

func (*MemorySessionStore) DeleteSession

func (s *MemorySessionStore) DeleteSession(_ context.Context, did string) error

func (*MemorySessionStore) GetSession

func (s *MemorySessionStore) GetSession(_ context.Context, did string) (*Session, error)

func (*MemorySessionStore) SetSession

func (s *MemorySessionStore) SetSession(_ context.Context, did string, session *Session) error

type MemoryStateStore

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

MemoryStateStore is an in-memory StateStore for testing. Not safe for concurrent use.

func NewMemoryStateStore

func NewMemoryStateStore() *MemoryStateStore

NewMemoryStateStore creates an empty in-memory state store.

func (*MemoryStateStore) DeleteState

func (s *MemoryStateStore) DeleteState(_ context.Context, state string) error

func (*MemoryStateStore) GetState

func (s *MemoryStateStore) GetState(_ context.Context, state string) (*AuthState, error)

func (*MemoryStateStore) SetState

func (s *MemoryStateStore) SetState(_ context.Context, state string, data *AuthState) error

type NonceStore

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

NonceStore stores DPoP nonces per server origin. Safe for concurrent use.

func NewNonceStore

func NewNonceStore() *NonceStore

NewNonceStore creates a new empty nonce store.

func (*NonceStore) Get

func (s *NonceStore) Get(origin string) string

Get returns the stored nonce for the given origin, or "".

func (*NonceStore) Set

func (s *NonceStore) Set(origin string, nonce string)

Set stores a nonce for the given origin.

type OAuthError

type OAuthError struct {
	Code        string `json:"error"`
	Description string `json:"error_description,omitempty"`
}

OAuthError represents an error response from an OAuth endpoint.

func (*OAuthError) Error

func (e *OAuthError) Error() string

type PKCEChallenge

type PKCEChallenge struct {
	Verifier  string // base64url-encoded 32 random bytes (43 chars)
	Challenge string // base64url(SHA-256(Verifier))
	Method    string // "S256"
}

PKCEChallenge holds PKCE code challenge parameters for an authorization request.

func GeneratePKCE

func GeneratePKCE() (*PKCEChallenge, error)

GeneratePKCE creates a new PKCE challenge with a cryptographically random verifier.

type ProtectedResourceMetadata

type ProtectedResourceMetadata struct {
	Resource             string   `json:"resource"`
	AuthorizationServers []string `json:"authorization_servers"`
}

ProtectedResourceMetadata is the OAuth 2.0 Protected Resource Metadata per RFC 9728, used to discover the authorization server for a PDS.

func FetchProtectedResourceMetadata

func FetchProtectedResourceMetadata(ctx context.Context, client *http.Client, pdsURL string) (*ProtectedResourceMetadata, error)

FetchProtectedResourceMetadata fetches the protected resource metadata from a PDS. The pdsURL should be the PDS origin (e.g., "https://bsky.social"). No HTTP redirects are followed (SSRF prevention).

type PublicClientAuth

type PublicClientAuth struct {
	ClientID string
}

PublicClientAuth authenticates public clients (token_endpoint_auth_method: "none").

func (*PublicClientAuth) Apply

func (a *PublicClientAuth) Apply(params url.Values, _ string) error

type RefreshTokenConfig

type RefreshTokenConfig struct {
	TokenEndpoint      string
	RevocationEndpoint string
	RefreshToken       string
	ClientAuth         ClientAuth
	DPoPKey            *crypto.P256PrivateKey
	Nonces             *NonceStore
	HTTPClient         *http.Client
}

RefreshTokenConfig configures a token refresh request.

type Session

type Session struct {
	DPoPKey  *crypto.P256PrivateKey
	TokenSet TokenSet
}

Session represents an authenticated OAuth session for a user.

func (*Session) MarshalJSON

func (s *Session) MarshalJSON() ([]byte, error)

func (*Session) UnmarshalJSON

func (s *Session) UnmarshalJSON(data []byte) error

type SessionStore

type SessionStore interface {
	// GetSession retrieves a session by DID. Returns [ErrNoSession] if not found.
	GetSession(ctx context.Context, did string) (*Session, error)
	// SetSession stores or replaces a session for the given DID.
	SetSession(ctx context.Context, did string, session *Session) error
	// DeleteSession removes the session for the given DID.
	DeleteSession(ctx context.Context, did string) error
}

SessionStore persists OAuth sessions. Keyed by user DID.

type StateStore

type StateStore interface {
	// GetState retrieves pending auth state. Returns [ErrInvalidState] if not found.
	GetState(ctx context.Context, state string) (*AuthState, error)
	// SetState stores pending auth state for the given state parameter.
	SetState(ctx context.Context, state string, data *AuthState) error
	// DeleteState removes the pending auth state.
	DeleteState(ctx context.Context, state string) error
}

StateStore stores pending authorization flow state. Keyed by state parameter. Entries should be short-lived (auto-expire after ~10 minutes).

type StaticTokenSource

type StaticTokenSource struct {
	AccessToken string
	Key         *crypto.P256PrivateKey
}

StaticTokenSource is a TokenSource that always returns the same token. Useful for tests and short-lived operations.

func (*StaticTokenSource) Token

type TokenSet

type TokenSet struct {
	Issuer             string    `json:"iss"`
	Sub                string    `json:"sub"`
	Aud                string    `json:"aud"`
	Scope              string    `json:"scope"`
	AccessToken        string    `json:"access_token"`
	TokenType          string    `json:"token_type"`
	ExpiresAt          time.Time `json:"expires_at"`
	RefreshDeadline    time.Time `json:"refresh_deadline"` // ExpiresAt minus random jitter; refresh when Now > this
	RefreshToken       string    `json:"refresh_token,omitempty"`
	TokenEndpoint      string    `json:"token_endpoint"`
	RevocationEndpoint string    `json:"revocation_endpoint,omitempty"`
}

TokenSet holds the tokens from an OAuth token response.

func ExchangeCode

func ExchangeCode(ctx context.Context, cfg *ExchangeCodeConfig) (*TokenSet, error)

ExchangeCode exchanges an authorization code for tokens.

func RefreshToken

func RefreshToken(ctx context.Context, cfg *RefreshTokenConfig) (*TokenSet, error)

RefreshToken exchanges a refresh token for new tokens.

func (*TokenSet) IsStale

func (t *TokenSet) IsStale() bool

IsStale returns true if the token should be refreshed. The refresh deadline is precomputed with random jitter when the token is received, so this check is a simple time comparison with no syscalls.

type TokenSource

type TokenSource interface {
	// Token returns the current access token and DPoP key.
	// It may refresh the token if stale.
	Token(ctx context.Context) (accessToken string, key *crypto.P256PrivateKey, err error)
}

TokenSource provides the current access token for DPoP-authenticated requests. Implementations must be safe for concurrent use.

type Transport

type Transport struct {
	// Base is the underlying transport. If nil, http.DefaultTransport is used.
	Base http.RoundTripper

	// Source provides the current access token and DPoP key.
	Source TokenSource

	// Nonces stores per-origin DPoP nonces.
	Nonces *NonceStore
}

Transport is an http.RoundTripper that adds DPoP proof headers and handles nonce retry transparently. It uses a TokenSource to get the current (possibly refreshed) access token on each request.

func (*Transport) RoundTrip

func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error)

Jump to

Keyboard shortcuts

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