Documentation
¶
Overview ¶
Package account implements the credential-lifecycle flows that sit around authentication: password-reset tokens, email-verification tokens, signed URLs, and a login throttler.
Design rules (secure-by-default):
- Reset and verification tokens are high-entropy random values. The server stores only a SHA-256 hash, so a store leak does not yield usable tokens.
- Tokens expire and are single-use: Consume succeeds at most once, which defeats replay.
- Signed URLs use the crypt package's HMAC primitive over a canonical string and embed an expiry, verified in constant time.
- The login throttler is a sliding-style fixed-window counter that locks an identifier out after too many failures, mitigating brute force and credential stuffing.
The TokenStore interface is small enough for a SQL/Redis driver to replace MemoryTokenStore without changing call sites. All timestamps are UTC.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrTokenNotFound is returned when no token matches. ErrTokenNotFound = errors.New("account: token not found") // ErrTokenExpired is returned when a token matched but has expired. ErrTokenExpired = errors.New("account: token expired") // ErrTokenUsed is returned when a single-use token was already consumed. ErrTokenUsed = errors.New("account: token already used") )
Errors returned by token flows.
var ErrInvalidSignature = errors.New("account: invalid signature")
ErrInvalidSignature is returned when a signed URL fails verification.
var ErrSignatureExpired = errors.New("account: signature expired")
ErrSignatureExpired is returned when a signed URL's expiry has passed.
var ErrThrottled = errors.New("account: too many attempts")
ErrThrottled is returned by Throttle.Hit / Check when an identifier is currently locked out.
Functions ¶
This section is empty.
Types ¶
type MemoryTokenStore ¶
type MemoryTokenStore struct {
// contains filtered or unexported fields
}
MemoryTokenStore is an in-process TokenStore backed by a sync.RWMutex.
func NewMemoryTokenStore ¶
func NewMemoryTokenStore() *MemoryTokenStore
NewMemoryTokenStore returns an empty in-memory token store.
func (*MemoryTokenStore) Delete ¶
func (s *MemoryTokenStore) Delete(_ context.Context, selector string) error
type Purpose ¶
type Purpose string
Purpose distinguishes token namespaces so a password-reset token can never be replayed as an email-verification token (and vice versa).
type Record ¶
type Record struct {
Selector string
Purpose Purpose
Subject string // opaque app identifier: user id, email, etc.
Hash string // hex SHA-256 of the secret half
CreatedAt time.Time
ExpiresAt time.Time
UsedAt time.Time // zero until consumed
}
Record is a stored token entry. The plaintext is never persisted — only Hash (hex SHA-256). Selector is the public lookup key, so verification is O(1) and does not require scanning rows.
type Signer ¶
type Signer struct {
// contains filtered or unexported fields
}
Signer produces and verifies tamper-proof, expiring URLs using an HMAC key (reuse APP_KEY bytes). The signature covers the path and a canonical, sorted encoding of the query parameters plus the expiry, so reordering or adding parameters invalidates it.
Example ¶
ExampleSigner shows a tamper-proof signed URL: Sign produces a link with an embedded signature, Verify accepts the untouched link and rejects a tampered one.
package main
import (
"errors"
"fmt"
"time"
"github.com/devituz/lagodev/auth/account"
)
func main() {
signer := account.NewSigner([]byte("0123456789abcdef0123456789abcdef"))
signed, err := signer.Sign("https://app.test/invite?team=acme", time.Hour)
if err != nil {
panic(err)
}
fmt.Println("valid:", signer.Verify(signed) == nil)
fmt.Println("tampered invalid:", errors.Is(signer.Verify(signed+"x"), account.ErrInvalidSignature))
}
Output: valid: true tampered invalid: true
type Throttle ¶
type Throttle struct {
// contains filtered or unexported fields
}
Throttle is a fixed-window failure counter keyed by an arbitrary identifier (e.g. "login:"+email or an IP). After MaxAttempts failures within Window, the identifier is locked until the window elapses. A successful login should call Clear to reset the counter.
Example ¶
ExampleThrottle shows the login throttler locking an identifier out after the configured number of failed attempts.
package main
import (
"errors"
"fmt"
"time"
"github.com/devituz/lagodev/auth/account"
)
func main() {
th := account.NewThrottle(3, time.Minute)
key := "login:alice@example.com"
// Record three failed logins; the third crosses the limit.
for i := 1; i <= 3; i++ {
err := th.Hit(key)
fmt.Printf("attempt %d locked: %v\n", i, errors.Is(err, account.ErrThrottled))
}
// Further checks report the lock without recording a new attempt.
fmt.Println("check locked:", errors.Is(th.Check(key), account.ErrThrottled))
// A successful login clears the counter.
th.Clear(key)
fmt.Println("after clear:", th.Check(key) == nil)
}
Output: attempt 1 locked: false attempt 2 locked: false attempt 3 locked: true check locked: true after clear: true
func NewThrottle ¶
NewThrottle returns a Throttle allowing maxAttempts failures per window. Defaults: maxAttempts<=0 -> 5, window<=0 -> 1 minute.
func (*Throttle) Attempts ¶
Attempts returns the current recorded failure count for key (0 if none or the window has elapsed). Useful for surfacing "N attempts remaining".
func (*Throttle) Check ¶
Check reports whether key is currently locked out without recording an attempt. It returns ErrThrottled (with the lock duration) when locked.
type TokenStore ¶
type TokenStore interface {
Save(ctx context.Context, r *Record) error
Get(ctx context.Context, selector string) (*Record, bool, error)
Delete(ctx context.Context, selector string) error
}
TokenStore persists token records keyed by selector. Implementations must be safe for concurrent use.
type Tokens ¶
type Tokens struct {
// contains filtered or unexported fields
}
Tokens issues and verifies single-use, expiring, hashed tokens for a given Purpose. Use one Tokens per purpose-family or pass the purpose per call.
func NewTokens ¶
func NewTokens(store TokenStore, ttl time.Duration) *Tokens
NewTokens returns a Tokens with the given store and default TTL. ttl<=0 defaults to 1 hour.
func (*Tokens) Consume ¶
Consume validates a plaintext token and atomically marks it used, so a second call with the same token returns ErrTokenUsed. It returns the subject on success.
Errors: ErrTokenNotFound, ErrTokenExpired, ErrTokenUsed.
Example ¶
ExampleTokens_Consume issues a password-reset token, consumes it once (success), then shows a second Consume of the same token is rejected as already-used — the single-use property that defeats link replay.
package main
import (
"context"
"errors"
"fmt"
"time"
"github.com/devituz/lagodev/auth/account"
)
func main() {
ctx := context.Background()
tokens := account.NewTokens(account.NewMemoryTokenStore(), time.Hour)
// Mint a reset token for a user; the plaintext goes into the email link.
plain, err := tokens.Issue(ctx, account.PurposeReset, "user-42")
if err != nil {
panic(err)
}
// User clicks the link: consume the token to perform the reset.
subject, err := tokens.Consume(ctx, account.PurposeReset, plain)
fmt.Println("first consume:", subject, err == nil)
// Replaying the same link must fail.
_, err = tokens.Consume(ctx, account.PurposeReset, plain)
fmt.Println("second consume used:", errors.Is(err, account.ErrTokenUsed))
}
Output: first consume: user-42 true second consume used: true
func (*Tokens) Issue ¶
Issue mints a token for (purpose, subject) and returns the one-time plaintext "<selector>.<secret>". The caller embeds it in an email link; it is unrecoverable afterwards because only its hash is stored.