Documentation
¶
Overview ¶
Package auth authenticates the admin by proving control of a Stellar key, then issues a JWT session token. It follows the SEP-10 challenge-response shape: the server hands out a transaction to sign, and a valid signature stands in for a password.
The flow ¶
GenerateChallenge builds a Stellar transaction — sourced by the server account and carrying a ManageData "auth" operation sourced by the admin account with a random nonce, time-bounded, tagged with the challenge ID in the memo — signs it with the server key, and stores it (see ChallengeStore). The transaction is never submitted to the network; it exists only to be signed. The caller countersigns it with the admin private key and returns it to VerifySignedChallenge, which confirms the challenge still exists and is unexpired, that the signed transaction hashes to the original (signatures excluded), and that an admin signature is present. Challenges are single-use: they are deleted on success to block replay. A verified caller is then issued a token with JWTService.GenerateToken.
Key separation ¶
The challenge signing key (StellarConfig.ServerSecretKey) must differ from the admin key it authenticates, and both NewChallengeService and config loading reject a shared key. Because a challenge is handed out already signed, a shared key would let an unsigned echo of that challenge satisfy verification. Verification additionally skips any signature made by the server key before looking for the admin's.
Pieces ¶
ChallengeService (challenge.go) runs the challenge lifecycle. ChallengeStore (store.go) persists challenges with automatic expiry; RedisStore is the production implementation, backed by Redis TTLs. JWTService (jwt.go) mints and validates HMAC-SHA256 tokens whose Claims carry the admin public key. errors.go holds the sentinel errors handlers map onto HTTP status codes.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrChallengeNotFound indicates the requested challenge does not exist or has expired. ErrChallengeNotFound = errors.New("challenge not found") // ErrChallengeExpired indicates the challenge has exceeded its validity period. ErrChallengeExpired = errors.New("challenge expired") // ErrInvalidTransaction indicates the provided transaction is malformed or invalid. ErrInvalidTransaction = errors.New("invalid transaction") // ErrTransactionMismatch indicates the signed transaction does not match the original challenge. ErrTransactionMismatch = errors.New("transaction does not match challenge") // ErrInvalidSignature indicates the transaction is not signed by the expected keypair. ErrInvalidSignature = errors.New("invalid or missing signature") // ErrInvalidToken indicates the JWT token is malformed, expired, or has an invalid signature. ErrInvalidToken = errors.New("invalid token") // ErrInvalidTokenClaims indicates the token claims are invalid or cannot be parsed. ErrInvalidTokenClaims = errors.New("invalid token claims") // ErrUnexpectedSigningMethod indicates the token uses an unsupported signing algorithm. ErrUnexpectedSigningMethod = errors.New("unexpected signing method") )
Authentication errors that can be mapped to HTTP status codes by handlers.
Functions ¶
This section is empty.
Types ¶
type Challenge ¶
type Challenge struct {
ID string `json:"id"` // Unique identifier for the challenge
Transaction string `json:"transaction"` // Base64-encoded Stellar transaction envelope
CreatedAt time.Time `json:"created_at"` // Timestamp when the challenge was created
ExpiresAt time.Time `json:"expires_at"` // Timestamp when the challenge expires
}
Challenge represents a cryptographic challenge used in the authentication flow. It contains a Stellar transaction that must be signed by the user to prove ownership of their private key. Challenges are temporary and expire after a configured duration.
type ChallengeService ¶
type ChallengeService interface {
// GenerateChallenge creates a new authentication challenge containing a Stellar transaction
// that must be signed by the user. The challenge expires after a configured duration.
GenerateChallenge() (*Challenge, error)
// VerifySignedChallenge validates that the provided transaction was signed by the correct
// keypair and matches the original challenge. Challenges are single-use and deleted after verification.
VerifySignedChallenge(challengeID, signedTxB64 string) error
}
ChallengeService defines the interface for Stellar transaction-based challenge-response authentication. This implements a cryptographic proof-of-ownership flow where users must sign a server-generated Stellar transaction with their private key to prove they control a specific public key.
func NewChallengeService ¶
func NewChallengeService(authConfig *config.AuthConfig, stellarConfig *config.StellarConfig, store ChallengeStore) (ChallengeService, error)
NewChallengeService creates a new ChallengeService with the provided configuration. Challenges are signed with StellarConfig.ServerSecretKey, which must differ from the admin key it authenticates. Returns an error if that key is not a valid Stellar private key.
type ChallengeStore ¶
type ChallengeStore interface {
// Store persists a challenge with automatic expiration based on the challenge's ExpiresAt field.
// Returns an error if the challenge is already expired or if storage fails.
Store(challengeID string, challenge *Challenge) error
// Get retrieves a challenge by its ID. Returns an error if the challenge is not found,
// has expired, or if retrieval fails.
Get(challengeID string) (*Challenge, error)
// Delete removes a challenge from storage. This should be called after successful
// authentication to prevent challenge reuse. Returns an error if deletion fails.
Delete(challengeID string) error
}
ChallengeStore defines the interface for storing and retrieving authentication challenges. Implementations must provide thread-safe operations with automatic expiration support.
func NewRedisStore ¶
func NewRedisStore(client *redis.Client, keyPrefix string) (ChallengeStore, error)
NewRedisStore creates a new RedisStore instance for storing authentication challenges. The keyPrefix parameter is used to namespace challenge keys in Redis, allowing multiple applications or environments to share the same Redis instance without key collisions.
Example:
store := NewRedisStore(redisClient, "microvault:auth") // This will create keys like: "microvault:auth:challenge:abc123"
type Claims ¶
type Claims struct {
AdminPublicKey string `json:"admin_pk"`
jwt.RegisteredClaims
}
Claims represents the JWT token payload containing user authentication information. It embeds the standard JWT registered claims and adds the admin's Stellar public key.
type JWTService ¶
type JWTService struct {
// contains filtered or unexported fields
}
JWTService provides JWT token generation and validation functionality. It uses HMAC-SHA256 signing and includes configurable expiration and refresh windows.
func NewJWTService ¶
func NewJWTService(config *config.AuthConfig) *JWTService
NewJWTService creates a new JWTService instance with the provided authentication configuration.
func (*JWTService) GenerateToken ¶
GenerateToken creates a new JWT token for the specified admin public key. The token is signed using HMAC-SHA256 and includes standard claims (issuer, subject, timestamps). Returns the signed token string, expiration time, and any error encountered during generation.
func (*JWTService) ShouldRefresh ¶
func (s *JWTService) ShouldRefresh(claims *Claims) bool
ShouldRefresh determines if a token should be refreshed based on its remaining lifetime. Returns true if the token will expire within the configured refresh window, allowing clients to obtain a new token before the current one expires.
func (*JWTService) ValidateToken ¶
func (s *JWTService) ValidateToken(tokenString string) (*Claims, error)
ValidateToken parses and validates a JWT token string. It verifies the signature using HMAC-SHA256 and ensures the token hasn't expired. Returns the extracted claims if valid, or an error if validation fails.
type RedisStore ¶
type RedisStore struct {
// contains filtered or unexported fields
}
RedisStore is a Redis-backed implementation of ChallengeStore. It uses Redis's native TTL functionality for automatic challenge expiration.
func (*RedisStore) Delete ¶
func (s *RedisStore) Delete(challengeID string) error
Delete removes a challenge from Redis by its ID. This method should be called after successful authentication to prevent replay attacks where an attacker might try to reuse a valid signed challenge.
Returns an error if:
- Redis DEL operation fails
- The operation times out (3 second timeout)
Note: This method does not return an error if the challenge doesn't exist. DEL operations in Redis are idempotent.
func (*RedisStore) Get ¶
func (s *RedisStore) Get(challengeID string) (*Challenge, error)
Get retrieves a challenge from Redis by its ID. The challenge data is retrieved from Redis and deserialized from JSON.
Returns an error if:
- The challenge is not found (may have expired or never existed)
- Redis GET operation fails
- JSON unmarshaling fails
- The operation times out (3 second timeout)
Note: If a challenge has expired, Redis will automatically delete it and this method will return a "challenge not found" error.
func (*RedisStore) Store ¶
func (s *RedisStore) Store(challengeID string, challenge *Challenge) error
Store persists a challenge to Redis with automatic expiration. The challenge is serialized to JSON and stored with a TTL based on the ExpiresAt field. Redis will automatically remove the challenge when the TTL expires.
Returns an error if:
- The challenge is already expired (ExpiresAt is in the past)
- JSON marshaling fails
- Redis SET operation fails
- The operation times out (3 second timeout)