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 ¶
- Variables
- func CheckReadAccessWithCaptain(captain *atproto.CaptainRecord, userDID string) bool
- func CheckWriteAccessWithCaptain(captain *atproto.CaptainRecord, userDID string, isCrew bool) bool
- func CleanExpiredTokens()
- func CreateAppviewServiceToken(privateKey *atcrypto.PrivateKeyP256, appviewDID, holdDID, userDID string) (string, error)
- func DecodeDIDFromHyphens(s string) (string, bool)
- func GetCacheStats() map[string]any
- func GetOrFetchServiceToken(ctx context.Context, refresher *oauth.Refresher, ...) (string, error)
- func GetOrFetchServiceTokenWithAppPassword(ctx context.Context, did, holdDID, pdsEndpoint string) (string, error)
- func GetServiceToken(did, holdDID string) (token string, expiresAt time.Time)
- func InvalidateServiceToken(did, holdDID string)
- func P256ToECDSA(key *atcrypto.PrivateKeyP256) (*ecdsa.PrivateKey, error)
- func SetServiceToken(did, holdDID, token string) error
- func ValidateAccess(userDID, userHandle string, access []AccessEntry) error
- type AccessEntry
- type Cache
- type CachedSession
- type HoldAuthorizer
- type RemoteHoldAuthorizer
- func (a *RemoteHoldAuthorizer) CheckReadAccess(ctx context.Context, holdDID, userDID string) (bool, error)
- func (a *RemoteHoldAuthorizer) CheckWriteAccess(ctx context.Context, holdDID, userDID string) (bool, error)
- func (a *RemoteHoldAuthorizer) ClearAllDenials() error
- func (a *RemoteHoldAuthorizer) ClearCrewDenial(ctx context.Context, holdDID, userDID string) error
- func (a *RemoteHoldAuthorizer) GetCaptainRecord(ctx context.Context, holdDID string) (*atproto.CaptainRecord, error)
- func (a *RemoteHoldAuthorizer) IsCachedCrewMember(ctx context.Context, holdDID, userDID string) (bool, error)
- func (a *RemoteHoldAuthorizer) IsCrewMember(ctx context.Context, holdDID, userDID string) (bool, error)
- func (a *RemoteHoldAuthorizer) RecordCrewApproval(ctx context.Context, holdDID, userDID string) error
- type SessionResponse
- type SessionValidator
- type TokenCache
- type TokenCacheEntry
Constants ¶
This section is empty.
Variables ¶
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 = errors.New("PDS unavailable") )
Sentinel errors for authentication failures
var ErrHoldNotFound = fmt.Errorf("hold not found")
ErrHoldNotFound is returned when a hold's captain record cannot be found
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 prunes expired entries from the default cache.
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
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 ¶
GetCacheStats returns default-cache statistics 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 ¶
GetServiceToken returns the cached service token for (did, holdDID). Returns ("", zero time) if absent or expired. Delegates to the default package cache; tests that need isolation should construct a Cache.
func InvalidateServiceToken ¶
func InvalidateServiceToken(did, holdDID string)
InvalidateServiceToken removes (did, holdDID) from the default cache.
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 ¶
SetServiceToken stores token under (did, holdDID) in the default cache, applying the standard 10s safety margin against the JWT's exp claim.
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 Cache ¶ added in v0.1.4
type Cache struct {
// contains filtered or unexported fields
}
Cache stores per-(DID, hold DID) service tokens with automatic expiry. The zero value is not usable; construct via NewCache. A default package instance backs the GetServiceToken/SetServiceToken/... wrappers; tests that need isolation can construct their own.
func DefaultCache ¶ added in v0.1.4
func DefaultCache() *Cache
DefaultCache returns the package-level cache that backs the GetServiceToken/SetServiceToken/... wrappers. Callers (notably ServiceAuthFetcher) use this when they want to read back a value that GetOrFetchServiceToken* wrote.
func (*Cache) CleanExpired ¶ added in v0.1.4
func (c *Cache) CleanExpired()
CleanExpired removes all expired entries.
func (*Cache) Clear ¶ added in v0.1.4
func (c *Cache) Clear()
Clear removes every entry. Intended for tests that need isolation between subtests, not for production code.
func (*Cache) Get ¶ added in v0.1.4
Get returns the cached token for (did, holdDID) and its expiry. If the entry is expired it is removed and (zero values, zero time) is returned.
func (*Cache) Invalidate ¶ added in v0.1.4
Invalidate removes the cached entry for (did, holdDID). No-op if absent.
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
type TokenCacheEntry ¶
TokenCacheEntry represents a cached access token
Source Files
¶
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. |