auth

package
v0.1.3 Latest Latest
Warning

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

Go to latest
Published: May 9, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package auth provides service token caching and management for AppView. Service tokens are JWTs issued by a user's PDS to authorize AppView to act on their behalf when communicating with hold services. Tokens are cached with automatic expiry parsing and 10-second safety margins.

Package auth provides authentication and authorization for ATCR, including ATProto session validation, hold authorization (captain/crew membership), scope parsing, and token caching for OAuth and service tokens.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrIdentityResolution indicates handle/DID resolution failed
	ErrIdentityResolution = errors.New("identity resolution failed")
	// ErrInvalidCredentials indicates PDS returned 401 (bad password/app-password)
	ErrInvalidCredentials = errors.New("invalid credentials")
	// ErrPDSUnavailable indicates PDS is unreachable or returned a server error
	ErrPDSUnavailable = errors.New("PDS unavailable")
)

Sentinel errors for authentication failures

View Source
var ErrHoldNotFound = fmt.Errorf("hold not found")

ErrHoldNotFound is returned when a hold's captain record cannot be found

View Source
var ErrUnauthorized = fmt.Errorf("unauthorized")

ErrUnauthorized is returned when access is denied

Functions

func CheckReadAccessWithCaptain

func CheckReadAccessWithCaptain(captain *atproto.CaptainRecord, userDID string) bool

CheckReadAccessWithCaptain implements the standard read authorization logic This is shared across all HoldAuthorizer implementations Read access rules: - Public hold: allow anyone (even anonymous) - Private hold: require authentication (any authenticated user)

func CheckWriteAccessWithCaptain

func CheckWriteAccessWithCaptain(captain *atproto.CaptainRecord, userDID string, isCrew bool) bool

CheckWriteAccessWithCaptain implements the standard write authorization logic This is shared across all HoldAuthorizer implementations Write access rules: - Must be authenticated - Must be hold owner OR crew member

func CleanExpiredTokens

func CleanExpiredTokens()

CleanExpiredTokens removes expired tokens from the cache Can be called periodically to prevent unbounded growth (though expired tokens are also removed lazily on access)

func CreateAppviewServiceToken

func CreateAppviewServiceToken(privateKey *atcrypto.PrivateKeyP256, appviewDID, holdDID, userDID string) (string, error)

CreateAppviewServiceToken creates a short-lived ES256 JWT for appview→hold communication. The token authenticates the appview when calling hold XRPC endpoints like updateCrewTier.

Claims:

  • iss: appview DID (e.g. did:web:atcr.io)
  • aud: hold DID (e.g. did:web:hold01.atcr.io)
  • sub: user DID being acted upon
  • exp: now + 60s
  • iat: now

func DecodeDIDFromHyphens added in v0.1.3

func DecodeDIDFromHyphens(s string) (string, bool)

DecodeDIDFromHyphens converts a hyphen-encoded DID back to colon-separated form. "did-plc-abc123" → "did:plc:abc123", "did-web-example.com" → "did:web:example.com" Returns the decoded DID and true if the input matched, or ("", false) otherwise.

func GetCacheStats

func GetCacheStats() map[string]any

GetCacheStats returns statistics about the service token cache for debugging

func GetOrFetchServiceToken

func GetOrFetchServiceToken(
	ctx context.Context,
	refresher *oauth.Refresher,
	did, holdDID, pdsEndpoint string,
) (string, error)

GetOrFetchServiceToken gets a service token for hold authentication. Checks cache first, then fetches from PDS with OAuth/DPoP if needed. This is the canonical implementation used by both middleware and crew registration.

IMPORTANT: Uses DoWithSession() to hold a per-DID lock through the entire PDS interaction. This prevents DPoP nonce race conditions when multiple Docker layers upload concurrently.

func GetOrFetchServiceTokenWithAppPassword

func GetOrFetchServiceTokenWithAppPassword(
	ctx context.Context,
	did, holdDID, pdsEndpoint string,
) (string, error)

GetOrFetchServiceTokenWithAppPassword gets a service token using app-password Bearer authentication. Used when auth method is app_password instead of OAuth.

func GetServiceToken

func GetServiceToken(did, holdDID string) (token string, expiresAt time.Time)

GetServiceToken retrieves a cached service token for the given DID and hold DID Returns empty string if no valid cached token exists

func InvalidateServiceToken

func InvalidateServiceToken(did, holdDID string)

InvalidateServiceToken removes a service token from the cache Used when we detect that a token is invalid or the user's session has expired

func P256ToECDSA

func P256ToECDSA(key *atcrypto.PrivateKeyP256) (*ecdsa.PrivateKey, error)

P256ToECDSA converts an atcrypto P-256 private key to a stdlib *ecdsa.PrivateKey. This is needed because golang-jwt requires stdlib crypto types, while atcrypto wraps them in its own types. We re-parse via PKCS8 encoding round-trip.

func SetServiceToken

func SetServiceToken(did, holdDID, token string) error

SetServiceToken stores a service token in the cache Automatically parses the JWT to extract the expiry time Applies a 10-second safety margin (cache expires 10s before actual JWT expiry)

func ValidateAccess

func ValidateAccess(userDID, userHandle string, access []AccessEntry) error

ValidateAccess checks if the requested access is allowed for the user For ATCR, users can only push to repositories under their own handle/DID

Types

type AccessEntry

type AccessEntry struct {
	Type    string   `json:"type"`              // "repository"
	Name    string   `json:"name,omitempty"`    // e.g., "alice/myapp"
	Actions []string `json:"actions,omitempty"` // e.g., ["pull", "push"]
}

AccessEntry represents access permissions for a resource

func ParseScope

func ParseScope(scopes []string) ([]AccessEntry, error)

ParseScope parses Docker registry scope strings into AccessEntry structures Scope format: "repository:alice/myapp:pull,push" Multiple scopes can be provided

type CachedSession

type CachedSession struct {
	DID         string
	Handle      string
	PDS         string
	AccessToken string
	ExpiresAt   time.Time
}

CachedSession represents a cached session

type HoldAuthorizer

type HoldAuthorizer interface {
	// CheckReadAccess checks if userDID can read from holdDID
	// Returns: (allowed bool, error)
	CheckReadAccess(ctx context.Context, holdDID, userDID string) (bool, error)

	// CheckWriteAccess checks if userDID can write to holdDID
	// Returns: (allowed bool, error)
	CheckWriteAccess(ctx context.Context, holdDID, userDID string) (bool, error)

	// GetCaptainRecord retrieves the captain record for a hold
	// Used to check public flag and allowAllCrew settings
	GetCaptainRecord(ctx context.Context, holdDID string) (*atproto.CaptainRecord, error)

	// IsCrewMember checks if userDID is a crew member of holdDID
	IsCrewMember(ctx context.Context, holdDID, userDID string) (bool, error)

	// ClearCrewDenial removes any cached denial for a user/hold pair
	// Called when user successfully becomes a crew member to ensure immediate access
	// Returns nil if no denial cache exists or invalidation succeeds
	ClearCrewDenial(ctx context.Context, holdDID, userDID string) error

	// IsCachedCrewMember returns true only if there is a non-expired approval
	// in the cache. It MUST NOT make any network calls. Cache miss returns (false, nil).
	IsCachedCrewMember(ctx context.Context, holdDID, userDID string) (bool, error)

	// RecordCrewApproval writes an approval to the cache with the implementation's
	// standard TTL. Used to warm the cache after an out-of-band confirmation of crew
	// membership (e.g. a successful requestCrew POST). No-op for implementations
	// without a cache.
	RecordCrewApproval(ctx context.Context, holdDID, userDID string) error
}

HoldAuthorizer checks if a DID has read/write access to a hold Implementations can query local PDS (hold service) or remote XRPC (appview)

func NewRemoteHoldAuthorizer

func NewRemoteHoldAuthorizer(db *sql.DB, testMode bool) HoldAuthorizer

NewRemoteHoldAuthorizer creates a new remote authorizer for AppView with production defaults

func NewRemoteHoldAuthorizerWithBackoffs

func NewRemoteHoldAuthorizerWithBackoffs(db *sql.DB, testMode bool, firstDenialBackoff, cleanupInterval, cleanupGracePeriod time.Duration, dbBackoffDurations []time.Duration) HoldAuthorizer

NewRemoteHoldAuthorizerWithBackoffs creates a new remote authorizer with custom backoff durations Used for testing to avoid long sleeps

type RemoteHoldAuthorizer

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

RemoteHoldAuthorizer queries a hold's PDS via XRPC endpoints Used by AppView to authorize access to remote holds Implements caching for captain records to reduce XRPC calls

func (*RemoteHoldAuthorizer) CheckReadAccess

func (a *RemoteHoldAuthorizer) CheckReadAccess(ctx context.Context, holdDID, userDID string) (bool, error)

CheckReadAccess implements read authorization using shared logic

func (*RemoteHoldAuthorizer) CheckWriteAccess

func (a *RemoteHoldAuthorizer) CheckWriteAccess(ctx context.Context, holdDID, userDID string) (bool, error)

CheckWriteAccess implements write authorization using shared logic

func (*RemoteHoldAuthorizer) ClearAllDenials

func (a *RemoteHoldAuthorizer) ClearAllDenials() error

ClearAllDenials removes all crew denials from both in-memory and database caches Called on startup to ensure a clean slate

func (*RemoteHoldAuthorizer) ClearCrewDenial

func (a *RemoteHoldAuthorizer) ClearCrewDenial(ctx context.Context, holdDID, userDID string) error

ClearCrewDenial removes crew denial from both in-memory and database caches This allows immediate access after a user becomes a crew member

func (*RemoteHoldAuthorizer) GetCaptainRecord

func (a *RemoteHoldAuthorizer) GetCaptainRecord(ctx context.Context, holdDID string) (*atproto.CaptainRecord, error)

GetCaptainRecord retrieves a captain record with caching 1. Check database cache 2. If cache miss or expired, query hold's XRPC endpoint 3. Update cache

func (*RemoteHoldAuthorizer) IsCachedCrewMember added in v0.1.3

func (a *RemoteHoldAuthorizer) IsCachedCrewMember(ctx context.Context, holdDID, userDID string) (bool, error)

IsCachedCrewMember returns true if there is a non-expired approval row. Never makes network calls. Cache miss or no DB returns (false, nil).

func (*RemoteHoldAuthorizer) IsCrewMember

func (a *RemoteHoldAuthorizer) IsCrewMember(ctx context.Context, holdDID, userDID string) (bool, error)

IsCrewMember checks if userDID is a crew member with caching 1. Check approval cache (15min TTL) 2. Check denial cache with exponential backoff 3. If cache miss, query XRPC endpoint and update cache

func (*RemoteHoldAuthorizer) RecordCrewApproval added in v0.1.3

func (a *RemoteHoldAuthorizer) RecordCrewApproval(ctx context.Context, holdDID, userDID string) error

RecordCrewApproval writes an approval to the cache with the standard 15-min TTL. No-op if there is no DB.

type SessionResponse

type SessionResponse struct {
	DID         string `json:"did"`
	Handle      string `json:"handle"`
	AccessJWT   string `json:"accessJwt"`
	RefreshJWT  string `json:"refreshJwt"`
	Email       string `json:"email,omitempty"`
	AccessToken string `json:"access_token,omitempty"` // Alternative field name
}

SessionResponse represents the response from createSession

type SessionValidator

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

SessionValidator validates ATProto credentials

func NewSessionValidator

func NewSessionValidator() *SessionValidator

NewSessionValidator creates a new ATProto session validator

func (*SessionValidator) CreateSessionAndGetToken

func (v *SessionValidator) CreateSessionAndGetToken(ctx context.Context, identifier, password string) (did, handle, accessToken string, err error)

CreateSessionAndGetToken creates a session and returns the DID, handle, and access token

type TokenCache

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

TokenCache is a simple in-memory cache for ATProto access tokens

func GetGlobalTokenCache

func GetGlobalTokenCache() *TokenCache

GetGlobalTokenCache returns the global token cache instance

func (*TokenCache) Delete

func (tc *TokenCache) Delete(did string)

Delete removes a cached token

func (*TokenCache) Get

func (tc *TokenCache) Get(did string) (string, bool)

Get retrieves an access token for a DID

func (*TokenCache) Set

func (tc *TokenCache) Set(did, accessToken string, ttl time.Duration)

Set stores an access token for a DID

type TokenCacheEntry

type TokenCacheEntry struct {
	AccessToken string
	ExpiresAt   time.Time
}

TokenCacheEntry represents a cached access token

Directories

Path Synopsis
Package holdlocal provides a HoldAuthorizer implementation that queries the hold's own embedded PDS directly.
Package holdlocal provides a HoldAuthorizer implementation that queries the hold's own embedded PDS directly.
Package oauth provides OAuth client configuration and helper functions for ATCR.
Package oauth provides OAuth client configuration and helper functions for ATCR.
Package token provides JWT claims and token handling for registry authentication.
Package token provides JWT claims and token handling for registry authentication.

Jump to

Keyboard shortcuts

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