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 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 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
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 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 ¶
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 ¶
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
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. |