Documentation
¶
Overview ¶
Package fs provides filesystem-backed (JSON-file-per-record) implementations of OneAuth's KeyStore, KidStore, AppRegistrationStore, User/Username/Identity/ Channel/Token/RefreshToken/APIKey store interfaces, intended for single-process dev and small deployments.
<!-- design:start --> Every store in this package shares one shape: each record is a JSON file under {StoragePath}/{kind}/, written atomically (temp file + rename + chmod 0600 via writeAtomicFile) so a crash mid-write leaves either the old or the new file but never a half-written one. User-supplied identifiers are run through safeName, the single point of defense against path traversal: it rejects empty names, null bytes, absolute paths, and embedded "..", and folds /, \, : into underscores. Backends that need real cross-process transaction semantics should use a database-backed store instead (e.g. GORM); the FS stores assume a single writer process and mediate in-process concurrency with sync.RWMutex (or, for the older user/identity/channel/token stores, atomic writes alone). Directories are created lazily on first write, and reverse-lookup and list operations scan the relevant directory, skipping unreadable or corrupt files rather than failing the whole call.
ENTITIES ¶
FSKeyStore — keys.KeyStorage impl; one signing-key JSON file per clientID under signing_keys/. Keys must be []byte (else ErrAlgorithmMismatch); GetKeyByKid scans all files because the layout is clientID-indexed. Also exposes backward-compatible aliases (RegisterKey, GetVerifyKey, GetSigningKey, GetExpectedAlg, ListKeys, GetCurrentKid). NewFSKeyStore constructs it.
FSKidStore — keys.KidStorage impl; one kid-indexed JSON file per kid under kid_keys/, each carrying a grace-period expires_at (zero = never). Mirrors FSKeyStore but is kid-keyed, so GetKey(clientID) always returns ErrKeyNotFound by design (matching the in-memory KidStore). Expired entries are filtered on read and purged by CleanExpired; Remove is idempotent. NewFSKidStore constructs it.
FSAppStore — admin.AppRegistrationStore impl; one AppRegistration JSON file per client_id under apps/. GetApp distinguishes a corrupt file (parse error) from an absent registration (ErrAppNotFound), while ListApps silently skips corrupt files so one hand-corrupted file cannot lock out admin tooling. Single-process only. NewFSAppStore constructs it.
FSUserStore — core UserStore impl; one user JSON file per userId under users/. SaveUser converts foreign core.User implementations into FSUser, best-effort preserving created_at from the profile map. FSUser is the persisted record (id, active flag, open profile map, timestamps). NewFSUserStore constructs it.
FSUsernameStore — UsernameStore impl enforcing username uniqueness via one file per normalized (lowercase) username under usernames/. FSUsername stores both the normalized name (used as filename/key) and the original case for display, plus a Version for optimistic concurrency. ChangeUsername is not atomic across files and best-effort restores the old name on failure; concurrent same-name reservations resolve last-write-wins. NewFSUsernameStore constructs it.
FSIdentityStore — IdentityStore impl; one identity JSON file per type+value key under identities/. createIfMissing seeds an unassigned (empty UserID, unverified) identity. Reverse lookups such as GetUserIdentities scan the directory. NewFSIdentityStore constructs it.
FSChannelStore — ChannelStore impl; one channel JSON file per provider+identityKey under channels/. SaveChannel auto-bumps Version and CreatedAt/UpdatedAt; createIfMissing seeds empty credentials/profile maps; lookup by identityKey scans the directory. NewFSChannelStore constructs it.
FSTokenStore — verification/reset token store; one AuthToken JSON file per token value under tokens/, with the (sanitized) token as the filename. GetToken auto-deletes and rejects expired tokens; DeleteUserTokens scans the directory filtering by userID and type. NewFSTokenStore constructs it.
FSRefreshTokenStore — RefreshToken store with rotation and family-based theft detection; one file per token under refresh_tokens/. The filename is the SHA256 hash of the token, not the raw value, so the secret never appears in a path. RotateRefreshToken revokes the old token and mints a successor in the same family, returning ErrTokenReused when an already-revoked token is replayed. NewFSRefreshTokenStore constructs it.
FSAPIKeyStore — APIKey store; one file per keyID under api_keys/ with the secret stored only as a bcrypt hash. CreateAPIKey returns the full "oa_keyid_secret" once; ValidateAPIKey parses that triple and bcrypt-compares; listings clear KeyHash before returning. NewFSAPIKeyStore constructs it. <!-- design:end -->
Index ¶
- type FSAPIKeyStore
- func (s *FSAPIKeyStore) CreateAPIKey(userID, name string, scopes []string, expiresAt *time.Time) (fullKey string, apiKey *core.APIKey, err error)
- func (s *FSAPIKeyStore) GetAPIKeyByID(keyID string) (*core.APIKey, error)
- func (s *FSAPIKeyStore) ListUserAPIKeys(userID string) ([]*core.APIKey, error)
- func (s *FSAPIKeyStore) RevokeAPIKey(keyID string) error
- func (s *FSAPIKeyStore) UpdateAPIKeyLastUsed(keyID string) error
- func (s *FSAPIKeyStore) ValidateAPIKey(fullKey string) (*core.APIKey, error)
- type FSAppStore
- type FSChannelStore
- type FSIdentityStore
- func (s *FSIdentityStore) GetIdentity(identityType, identityValue string, createIfMissing bool) (*core.Identity, bool, error)
- func (s *FSIdentityStore) GetUserIdentities(userId string) ([]*core.Identity, error)
- func (s *FSIdentityStore) MarkIdentityVerified(identityType, identityValue string) error
- func (s *FSIdentityStore) SaveIdentity(identity *core.Identity) error
- func (s *FSIdentityStore) SetUserForIdentity(identityType, identityValue string, newUserId string) error
- type FSKeyStore
- func (s *FSKeyStore) DeleteKey(clientID string) error
- func (s *FSKeyStore) GetCurrentKid(clientID string) (string, error)
- func (s *FSKeyStore) GetExpectedAlg(clientID string) (string, error)
- func (s *FSKeyStore) GetKey(clientID string) (*keys.KeyRecord, error)
- func (s *FSKeyStore) GetKeyByKid(kid string) (*keys.KeyRecord, error)
- func (s *FSKeyStore) GetSigningKey(clientID string) (any, error)
- func (s *FSKeyStore) GetVerifyKey(clientID string) (any, error)
- func (s *FSKeyStore) ListKeyIDs() ([]string, error)
- func (s *FSKeyStore) ListKeys() ([]string, error)
- func (s *FSKeyStore) PutKey(rec *keys.KeyRecord) error
- func (s *FSKeyStore) RegisterKey(clientID string, key any, algorithm string) error
- type FSKidStore
- func (s *FSKidStore) Add(kid string, key any, algorithm string, clientID string, expiresAt time.Time) error
- func (s *FSKidStore) CleanExpired() error
- func (s *FSKidStore) GetKey(clientID string) (*keys.KeyRecord, error)
- func (s *FSKidStore) GetKeyByKid(kid string) (*keys.KeyRecord, error)
- func (s *FSKidStore) Remove(kid string) error
- type FSRefreshTokenStore
- func (s *FSRefreshTokenStore) CleanupExpiredTokens() error
- func (s *FSRefreshTokenStore) CreateRefreshToken(userID, clientID string, deviceInfo map[string]any, scopes []string) (*core.RefreshToken, error)
- func (s *FSRefreshTokenStore) GetRefreshToken(token string) (*core.RefreshToken, error)
- func (s *FSRefreshTokenStore) GetUserTokens(userID string) ([]*core.RefreshToken, error)
- func (s *FSRefreshTokenStore) RevokeRefreshToken(token string) error
- func (s *FSRefreshTokenStore) RevokeTokenFamily(family string) error
- func (s *FSRefreshTokenStore) RevokeUserTokens(userID string) error
- func (s *FSRefreshTokenStore) RotateRefreshToken(oldToken string) (*core.RefreshToken, error)
- type FSTokenStore
- func (s *FSTokenStore) CreateToken(userID, email string, tokenType core.TokenType, expiryDuration time.Duration) (*core.AuthToken, error)
- func (s *FSTokenStore) DeleteToken(token string) error
- func (s *FSTokenStore) DeleteUserTokens(userID string, tokenType core.TokenType) error
- func (s *FSTokenStore) GetToken(token string) (*core.AuthToken, error)
- type FSUser
- type FSUserStore
- type FSUsername
- type FSUsernameStore
- func (s *FSUsernameStore) ChangeUsername(oldUsername, newUsername, userID string) error
- func (s *FSUsernameStore) GetUserByUsername(username string) (string, error)
- func (s *FSUsernameStore) ReleaseUsername(username string) error
- func (s *FSUsernameStore) ReserveUsername(username string, userID string) error
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type FSAPIKeyStore ¶
type FSAPIKeyStore struct {
StoragePath string
// contains filtered or unexported fields
}
FSAPIKeyStore stores API keys as JSON files
func NewFSAPIKeyStore ¶
func NewFSAPIKeyStore(storagePath string) *FSAPIKeyStore
NewFSAPIKeyStore creates a new file-based API key store
func (*FSAPIKeyStore) CreateAPIKey ¶
func (s *FSAPIKeyStore) CreateAPIKey(userID, name string, scopes []string, expiresAt *time.Time) (fullKey string, apiKey *core.APIKey, err error)
CreateAPIKey creates a new API key and returns the full key (only shown once) The key format is: keyID + "_" + secret
func (*FSAPIKeyStore) GetAPIKeyByID ¶
func (s *FSAPIKeyStore) GetAPIKeyByID(keyID string) (*core.APIKey, error)
GetAPIKeyByID retrieves an API key by its public ID
func (*FSAPIKeyStore) ListUserAPIKeys ¶
func (s *FSAPIKeyStore) ListUserAPIKeys(userID string) ([]*core.APIKey, error)
ListUserAPIKeys returns all API keys for a user (without secrets)
func (*FSAPIKeyStore) RevokeAPIKey ¶
func (s *FSAPIKeyStore) RevokeAPIKey(keyID string) error
RevokeAPIKey marks an API key as revoked
func (*FSAPIKeyStore) UpdateAPIKeyLastUsed ¶
func (s *FSAPIKeyStore) UpdateAPIKeyLastUsed(keyID string) error
UpdateAPIKeyLastUsed updates the last used timestamp
func (*FSAPIKeyStore) ValidateAPIKey ¶
func (s *FSAPIKeyStore) ValidateAPIKey(fullKey string) (*core.APIKey, error)
ValidateAPIKey validates a full API key and returns the key metadata if valid The fullKey format is: keyID + "_" + secret
type FSAppStore ¶ added in v0.0.81
type FSAppStore struct {
StoragePath string
// contains filtered or unexported fields
}
FSAppStore implements admin.AppRegistrationStore by persisting each registration as a JSON file under {basePath}/apps/{client_id}.json.
Mirrors the FSKeyStore design: file-per-record (so concurrent ops don't serialize on a single mutex outside this process), atomic writes via temp-file + rename (so a crash mid-write leaves either the old file or the new file intact, never a half-written one), and safeName guarding path-traversal in client_ids.
In-process concurrency is mediated by sync.RWMutex. Multi-process deployments need a backend with real transaction semantics — see GORMAppStore (#167).
func NewFSAppStore ¶ added in v0.0.81
func NewFSAppStore(storagePath string) *FSAppStore
NewFSAppStore creates a filesystem-backed AppRegistrationStore. The {basePath}/apps/ directory is created lazily on first write.
func (*FSAppStore) DeleteApp ¶ added in v0.0.81
func (s *FSAppStore) DeleteApp(clientID string) error
DeleteApp removes the registration for clientID. Returns admin.ErrAppNotFound if no such registration exists, matching InMemoryAppStore semantics.
func (*FSAppStore) GetApp ¶ added in v0.0.81
func (s *FSAppStore) GetApp(clientID string) (*admin.AppRegistration, error)
GetApp returns the registration for clientID, or admin.ErrAppNotFound if no such file exists. A corrupt JSON file surfaces as a parse error rather than ErrAppNotFound — callers should distinguish "registration absent" from "registration unreadable" so an unexpected on-disk corruption isn't silently treated as a missing client.
func (*FSAppStore) ListApps ¶ added in v0.0.81
func (s *FSAppStore) ListApps() ([]*admin.AppRegistration, error)
ListApps returns every registration in the store. Files that fail to parse are skipped (with no error returned) so a single hand-corrupted file does not lock out admin tooling — partial recovery beats total failure here. Callers needing strict integrity should validate at the store level (e.g., periodic fsck) rather than relying on ListApps.
Returns an empty slice (not an error) when the apps dir does not yet exist, matching the InMemory backend's "fresh store has no apps" shape.
func (*FSAppStore) SaveApp ¶ added in v0.0.81
func (s *FSAppStore) SaveApp(app *admin.AppRegistration) error
SaveApp persists app to disk. Overwrites any existing registration with the same client_id. Empty client_id is rejected.
type FSChannelStore ¶
type FSChannelStore struct {
StoragePath string
}
FSChannelStore stores channels as JSON files
func NewFSChannelStore ¶
func NewFSChannelStore(storagePath string) *FSChannelStore
func (*FSChannelStore) GetChannel ¶
func (*FSChannelStore) GetChannelsByIdentity ¶
func (s *FSChannelStore) GetChannelsByIdentity(identityKey string) ([]*core.Channel, error)
func (*FSChannelStore) SaveChannel ¶
func (s *FSChannelStore) SaveChannel(channel *core.Channel) error
type FSIdentityStore ¶
type FSIdentityStore struct {
StoragePath string
}
FSIdentityStore stores identities as JSON files
func NewFSIdentityStore ¶
func NewFSIdentityStore(storagePath string) *FSIdentityStore
func (*FSIdentityStore) GetIdentity ¶
func (*FSIdentityStore) GetUserIdentities ¶
func (s *FSIdentityStore) GetUserIdentities(userId string) ([]*core.Identity, error)
func (*FSIdentityStore) MarkIdentityVerified ¶
func (s *FSIdentityStore) MarkIdentityVerified(identityType, identityValue string) error
func (*FSIdentityStore) SaveIdentity ¶
func (s *FSIdentityStore) SaveIdentity(identity *core.Identity) error
func (*FSIdentityStore) SetUserForIdentity ¶
func (s *FSIdentityStore) SetUserForIdentity(identityType, identityValue string, newUserId string) error
type FSKeyStore ¶ added in v0.0.32
type FSKeyStore struct {
StoragePath string
// contains filtered or unexported fields
}
FSKeyStore implements keys.KeyStorage using filesystem storage.
func NewFSKeyStore ¶ added in v0.0.32
func NewFSKeyStore(storagePath string) *FSKeyStore
NewFSKeyStore creates a new filesystem-backed KeyStore.
func (*FSKeyStore) DeleteKey ¶ added in v0.0.32
func (s *FSKeyStore) DeleteKey(clientID string) error
func (*FSKeyStore) GetCurrentKid ¶ added in v0.0.38
func (s *FSKeyStore) GetCurrentKid(clientID string) (string, error)
func (*FSKeyStore) GetExpectedAlg ¶ added in v0.0.32
func (s *FSKeyStore) GetExpectedAlg(clientID string) (string, error)
func (*FSKeyStore) GetKey ¶ added in v0.0.38
func (s *FSKeyStore) GetKey(clientID string) (*keys.KeyRecord, error)
func (*FSKeyStore) GetKeyByKid ¶ added in v0.0.38
func (s *FSKeyStore) GetKeyByKid(kid string) (*keys.KeyRecord, error)
func (*FSKeyStore) GetSigningKey ¶ added in v0.0.32
func (s *FSKeyStore) GetSigningKey(clientID string) (any, error)
func (*FSKeyStore) GetVerifyKey ¶ added in v0.0.32
func (s *FSKeyStore) GetVerifyKey(clientID string) (any, error)
func (*FSKeyStore) ListKeyIDs ¶ added in v0.0.38
func (s *FSKeyStore) ListKeyIDs() ([]string, error)
func (*FSKeyStore) ListKeys ¶ added in v0.0.32
func (s *FSKeyStore) ListKeys() ([]string, error)
func (*FSKeyStore) RegisterKey ¶ added in v0.0.32
func (s *FSKeyStore) RegisterKey(clientID string, key any, algorithm string) error
type FSKidStore ¶ added in v0.0.83
type FSKidStore struct {
StoragePath string
// contains filtered or unexported fields
}
FSKidStore implements keys.KidStorage using filesystem storage. One file per kid under {StoragePath}/kid_keys/, mirroring FSKeyStore's file-per-record layout.
func NewFSKidStore ¶ added in v0.0.83
func NewFSKidStore(storagePath string) *FSKidStore
NewFSKidStore creates a new filesystem-backed KidStorage.
func (*FSKidStore) CleanExpired ¶ added in v0.0.83
func (s *FSKidStore) CleanExpired() error
func (*FSKidStore) GetKey ¶ added in v0.0.83
func (s *FSKidStore) GetKey(clientID string) (*keys.KeyRecord, error)
GetKey always returns ErrKeyNotFound — KidStorage is kid-indexed and has no clientID→key lookup. Matches the in-memory KidStore.
func (*FSKidStore) GetKeyByKid ¶ added in v0.0.83
func (s *FSKidStore) GetKeyByKid(kid string) (*keys.KeyRecord, error)
func (*FSKidStore) Remove ¶ added in v0.0.83
func (s *FSKidStore) Remove(kid string) error
Remove is idempotent — deleting an absent kid is not an error.
type FSRefreshTokenStore ¶
type FSRefreshTokenStore struct {
StoragePath string
// contains filtered or unexported fields
}
FSRefreshTokenStore stores refresh tokens as JSON files
func NewFSRefreshTokenStore ¶
func NewFSRefreshTokenStore(storagePath string) *FSRefreshTokenStore
NewFSRefreshTokenStore creates a new file-based refresh token store
func (*FSRefreshTokenStore) CleanupExpiredTokens ¶
func (s *FSRefreshTokenStore) CleanupExpiredTokens() error
CleanupExpiredTokens removes expired tokens (for maintenance)
func (*FSRefreshTokenStore) CreateRefreshToken ¶
func (s *FSRefreshTokenStore) CreateRefreshToken(userID, clientID string, deviceInfo map[string]any, scopes []string) (*core.RefreshToken, error)
CreateRefreshToken creates a new refresh token for a user
func (*FSRefreshTokenStore) GetRefreshToken ¶
func (s *FSRefreshTokenStore) GetRefreshToken(token string) (*core.RefreshToken, error)
GetRefreshToken retrieves a refresh token by its value
func (*FSRefreshTokenStore) GetUserTokens ¶
func (s *FSRefreshTokenStore) GetUserTokens(userID string) ([]*core.RefreshToken, error)
GetUserTokens lists all active (non-revoked, non-expired) refresh tokens for a user
func (*FSRefreshTokenStore) RevokeRefreshToken ¶
func (s *FSRefreshTokenStore) RevokeRefreshToken(token string) error
RevokeRefreshToken marks a token as revoked
func (*FSRefreshTokenStore) RevokeTokenFamily ¶
func (s *FSRefreshTokenStore) RevokeTokenFamily(family string) error
RevokeTokenFamily revokes all tokens in a family (theft detection)
func (*FSRefreshTokenStore) RevokeUserTokens ¶
func (s *FSRefreshTokenStore) RevokeUserTokens(userID string) error
RevokeUserTokens revokes all refresh tokens for a user
func (*FSRefreshTokenStore) RotateRefreshToken ¶
func (s *FSRefreshTokenStore) RotateRefreshToken(oldToken string) (*core.RefreshToken, error)
RotateRefreshToken invalidates old token and creates new one in same family Returns ErrTokenReused if the old token was already revoked (theft detection)
type FSTokenStore ¶
type FSTokenStore struct {
StoragePath string
}
FSTokenStore stores verification and reset tokens as JSON files
func NewFSTokenStore ¶
func NewFSTokenStore(storagePath string) *FSTokenStore
func (*FSTokenStore) CreateToken ¶
func (*FSTokenStore) DeleteToken ¶
func (s *FSTokenStore) DeleteToken(token string) error
func (*FSTokenStore) DeleteUserTokens ¶
func (s *FSTokenStore) DeleteUserTokens(userID string, tokenType core.TokenType) error
type FSUser ¶
type FSUser struct {
UserId string `json:"user_id"`
IsActive bool `json:"is_active"`
UserProfile map[string]any `json:"profile"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
FSUser implements the oneauth.User interface
type FSUserStore ¶
type FSUserStore struct {
StoragePath string
}
FSUserStore stores users as JSON files
func NewFSUserStore ¶
func NewFSUserStore(storagePath string) *FSUserStore
func (*FSUserStore) CreateUser ¶
func (*FSUserStore) GetUserById ¶
func (s *FSUserStore) GetUserById(userId string) (core.User, error)
type FSUsername ¶ added in v0.0.26
type FSUsername struct {
NormalizedUsername string `json:"normalized_username"` // Lowercase for lookups
Username string `json:"username"` // Original case-preserved
UserID string `json:"user_id"`
Version int `json:"version"` // For optimistic concurrency
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
FSUsername represents a username reservation stored as JSON
type FSUsernameStore ¶ added in v0.0.26
type FSUsernameStore struct {
StoragePath string
}
FSUsernameStore implements oa.UsernameStore using filesystem storage.
Purpose ¶
Provides username uniqueness enforcement and username-based login lookup using JSON files. Each username is stored as a separate file with the normalized (lowercase) username as the filename.
File Structure ¶
{StoragePath}/
└── usernames/
├── johndoe.json # {"username": "JohnDoe", "user_id": "abc123", ...}
├── janedoe.json # {"username": "JaneDoe", "user_id": "xyz789", ...}
└── ...
Concurrency Model ¶
Uses optimistic locking with version numbers. The atomic file write (write to temp, rename) provides basic safety, but concurrent modifications to the same username should be serialized at the application level or will result in last-write-wins behavior.
Setup ¶
usernameStore := fs.NewFSUsernameStore("/var/data/myapp")
localAuth := &oneauth.LocalAuth{
UsernameStore: usernameStore,
SignupPolicy: &oneauth.SignupPolicy{
RequireUsername: true,
EnforceUsernameUnique: true,
},
}
func NewFSUsernameStore ¶ added in v0.0.26
func NewFSUsernameStore(storagePath string) *FSUsernameStore
NewFSUsernameStore creates a new filesystem-backed UsernameStore
func (*FSUsernameStore) ChangeUsername ¶ added in v0.0.26
func (s *FSUsernameStore) ChangeUsername(oldUsername, newUsername, userID string) error
ChangeUsername atomically changes a username using optimistic concurrency. Returns error if new username is already taken.
Usage ¶
Called from a "Change Username" profile page handler.
Concurrency Note ¶
This is not fully atomic across files. In a race condition where another process takes the new username between our check and creation, we attempt to restore the old username. For production systems with high concurrency, consider using a database-backed store instead.
func (*FSUsernameStore) GetUserByUsername ¶ added in v0.0.26
func (s *FSUsernameStore) GetUserByUsername(username string) (string, error)
GetUserByUsername looks up a userID by username (case-insensitive).
Usage ¶
Called by NewCredentialsValidatorWithUsername during login when user enters a username instead of email.
func (*FSUsernameStore) ReleaseUsername ¶ added in v0.0.26
func (s *FSUsernameStore) ReleaseUsername(username string) error
ReleaseUsername removes a username reservation.
When to Use ¶
Call this when:
- User deletes their account
- Admin removes a username (e.g., for policy violations)
func (*FSUsernameStore) ReserveUsername ¶ added in v0.0.26
func (s *FSUsernameStore) ReserveUsername(username string, userID string) error
ReserveUsername reserves a username for a user. Returns error if username is already taken by a different user.
Concurrency ¶
Uses file system as implicit lock. The atomic write (temp file + rename) ensures no partial writes, but concurrent reservations of the same username will result in one succeeding (the last to complete the rename).