account

package
v0.25.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 25, 2026 License: MIT Imports: 14 Imported by: 0

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

View Source
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.

View Source
var ErrInvalidSignature = errors.New("account: invalid signature")

ErrInvalidSignature is returned when a signed URL fails verification.

View Source
var ErrSignatureExpired = errors.New("account: signature expired")

ErrSignatureExpired is returned when a signed URL's expiry has passed.

View Source
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

func (*MemoryTokenStore) Get

func (s *MemoryTokenStore) Get(_ context.Context, selector string) (*Record, bool, error)

func (*MemoryTokenStore) Save

func (s *MemoryTokenStore) Save(_ context.Context, r *Record) 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).

const (
	// PurposeReset is the namespace for password-reset tokens.
	PurposeReset Purpose = "password_reset"
	// PurposeVerify is the namespace for email-verification tokens.
	PurposeVerify Purpose = "email_verify"
)

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.

func (*Record) Expired

func (r *Record) Expired() bool

Expired reports whether the record's expiry has passed.

func (*Record) Used

func (r *Record) Used() bool

Used reports whether the record has been consumed.

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

func NewSigner

func NewSigner(key []byte) *Signer

NewSigner returns a Signer keyed by key (any length; 32 bytes recommended).

func (*Signer) Sign

func (s *Signer) Sign(base string, ttl time.Duration) (string, error)

Sign returns path with an appended "expires" timestamp and "signature" query parameter. base may already contain query parameters; they are included in the signed payload. ttl<=0 produces a non-expiring link (expires=0), which is verified accordingly.

func (*Signer) Verify

func (s *Signer) Verify(signed string) error

Verify checks a signed URL produced by Sign. It returns ErrInvalidSignature on tampering and ErrSignatureExpired when the embedded expiry has passed.

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

func NewThrottle(maxAttempts int, windowDur time.Duration) *Throttle

NewThrottle returns a Throttle allowing maxAttempts failures per window. Defaults: maxAttempts<=0 -> 5, window<=0 -> 1 minute.

func (*Throttle) Attempts

func (t *Throttle) Attempts(key string) int

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

func (t *Throttle) Check(key string) error

Check reports whether key is currently locked out without recording an attempt. It returns ErrThrottled (with the lock duration) when locked.

func (*Throttle) Clear

func (t *Throttle) Clear(key string)

Clear resets the counter for key. Call on successful authentication.

func (*Throttle) Hit

func (t *Throttle) Hit(key string) error

Hit records one failed attempt for key and returns ErrThrottled if this attempt crosses (or has already crossed) the limit. Call this on each authentication failure.

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

func (t *Tokens) Consume(ctx context.Context, purpose Purpose, plain string) (string, error)

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

func (t *Tokens) Issue(ctx context.Context, purpose Purpose, subject string) (string, error)

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.

func (*Tokens) Verify

func (t *Tokens) Verify(ctx context.Context, purpose Purpose, plain string) (string, error)

Verify checks a plaintext token without consuming it. It returns the subject on success. Use Consume for single-use semantics; Verify is for flows that need to display a form before committing (the actual reset must still call Consume).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL