auth

package module
v2.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: GPL-2.0, GPL-3.0 Imports: 21 Imported by: 0

README

auth

Go Reference Go version Test coverage Mutation OpenSSF Best Practices OpenSSF Scorecard

Go authentication library: Argon2id passwords, WebAuthn/passkeys, OIDC, sessions, API keys, and RBAC.

A standalone Go authentication library providing password hashing (Argon2id with OWASP parameters), WebAuthn/FIDO2 passkey ceremonies, OIDC provider integration with PKCE, session management with idle/absolute timeouts, API key generation and verification, CSRF token helpers, password-reset/email-verification token primitives, and role-based access control helpers.

Dependencies: golang.org/x/crypto, github.com/go-webauthn/webauthn, github.com/coreos/go-oidc/v3, golang.org/x/oauth2.

Note: HTTP handlers are app-specific and intentionally not included. Consumers should implement their own HTTP layer using the exported authentication primitives.

Install

go get github.com/cplieger/auth/v2@latest

Usage

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/cplieger/auth/v2"
)

func main() {
	// Hash a password (package-level with OWASP defaults)
	hash, _ := auth.HashPassword("my-secure-password")

	// Verify
	ok, _ := auth.VerifyPassword("my-secure-password", hash)
	_ = ok

	// Or use a configurable Hasher with custom params and optional pepper
	hasher, _ := auth.NewHasher(auth.Argon2Params{
		Memory: 65536, Iterations: 3, Parallelism: 2,
		SaltLength: 16, KeyLength: 32,
	}, auth.WithPepper([]byte("my-secret-pepper")))
	hash2, _ := hasher.Hash("my-secure-password")
	ok2, _ := hasher.Verify("my-secure-password", hash2)
	_, _ = hash2, ok2

	// Set up authenticator with your store implementation (functional options).
	// NewAuthenticator returns an error if the configuration is unusable (e.g. a
	// __Host- cookie posture combined with a Domain or a non-root Path).
	authenticator, err := auth.NewAuthenticator(
		myStore, // implements auth.SessionStore
		auth.WithIdleTimeout(1*time.Hour),
		auth.WithAbsTimeout(24*time.Hour),
		auth.WithLoginPath("/login"),
		auth.WithCookie(auth.DefaultCookieConfig()),
	)
	if err != nil {
		log.Fatalf("auth: %v", err)
	}

	// Use in HTTP handler
	http.HandleFunc("/api/protected", func(w http.ResponseWriter, r *http.Request) {
		user, _, ok := authenticator.RequireAuth(w, r)
		if !ok {
			return
		}
		_ = user
	})
}

Configuration

All configuration is via functional options and function parameters — no import-time side effects, no environment variable reads, no global state initialization.

  • WithLogger(l): optional *slog.Logger; if nil, uses slog.Default()
  • WithLoginPath(path): redirect path for unauthenticated browser requests (default: "/login")
  • WithCookie(cfg): configurable cookie Name, Posture, Path, SameSite, Domain, TrustForwardedHeaders (see CookieConfig)
  • WithIdleTimeout(d): session idle timeout (default: 1h)
  • WithAbsTimeout(d): session absolute timeout (default: 24h)
  • WithBypass(fn): bypass function for development (synthetic admin user)
  • WithVerifiers(vs []CredentialVerifier): override the default verifier chain. When set, Authenticate iterates the supplied chain instead of the hardcoded default (SessionVerifier + APIKeyVerifier).
  • WithActivityThrottle(d time.Duration): when d>0, SessionVerifier maintains a per-hash last-write map and calls UpdateSessionActivity at most once per d per hash. d==0 (default) preserves the current write-on-every-request behavior. d must be less than the idle timeout, or construction returns an error (the persisted last-activity lags by up to d, so a throttle at or above the idle timeout would expire active sessions).
  • WithUnauthorizedResponse(fn): replace RequireAuth's default unauthorized response (302 to the login path for browsers, 401 JSON otherwise) with your own writer. The hook owns both branches; call IsBrowserRequest inside it to keep a redirect path.
  • WithTimeoutSource(fn): resolve the idle/absolute session timeouts per verification from a callback (for hot-reloadable config) instead of the static WithIdleTimeout/WithAbsTimeout values. Non-positive callback values fall back to the static values, and the activity throttle is clamped to at most half the resolved idle timeout so a shrunken idle can never expire actively-used sessions through write throttling.
  • NewHasher(params, ...HasherOption): configurable Argon2id parameters; use WithPepper([]byte) for HMAC peppering
  • GenerateAPIKey(prefix): pass your key prefix (e.g. "ak_")
  • ValidatePasswordContext(password, username, forbiddenWords): pass app-specific forbidden words
cfg := auth.CookieConfig{
    Name:     "my_session",          // base name (default: "auth_session")
    Posture:  auth.PostureSecure,    // __Host- + Secure (default); see CookiePosture table below
    Path:     "/",                   // cookie path (default: "/")
    Domain:   "",                    // cookie domain (default: unset; must stay unset under a __Host- posture)
    SameSite: http.SameSiteLaxMode,  // (default: Lax)
    // TrustForwardedHeaders: true,  // only behind a proxy that always sets X-Forwarded-Proto
}
authenticator, err := auth.NewAuthenticator(myStore, auth.WithCookie(cfg))
if err != nil {
    log.Fatalf("auth: %v", err) // cfg rejected: see CookieConfig.Validate
}
CookiePosture

CookiePosture controls the cookie name prefix and Secure flag strategy:

Value Behavior
(default) Static __Host- prefix when Secure is true/auto-HTTPS
PosturePerRequest Selects cookie name and Secure flag at request time via isHTTPS(r). HTTPS requests get __Host-+base+Secure; plain HTTP gets the bare base name without the Secure flag. Respects TrustForwardedHeaders for X-Forwarded-Proto detection.

PosturePerRequest is useful for services that accept both HTTP and HTTPS traffic (e.g., behind a load balancer that terminates TLS for some paths but not others).

API

Password Hashing
  • HashPassword(password) (string, error) — Argon2id hash in PHC string format (OWASP defaults)
  • VerifyPassword(password, hash) (bool, error) — verify hash
  • NeedsRehash(encodedHash) bool — check if hash uses outdated parameters
  • DummyHash() string — pre-computed hash for constant-time login timing equalization
  • DefaultArgon2Params() Argon2Params — OWASP-recommended Argon2id parameters
  • NewHasher(params, ...HasherOption) (*Hasher, error) — configurable Argon2id hasher
  • WithPepper(pepper) HasherOption — enable HMAC peppering on a Hasher
  • Hasher.Hash(password) (string, error) — hash with custom params
  • Hasher.Verify(password, hash) (bool, error) — verify with custom params
  • Hasher.NeedsRehash(hash) bool — check against custom params
  • ValidatePasswordLength(password, passwordOnly) error — NIST length check (max 128)
  • ValidatePasswordContext(password, username, forbiddenWords) error — contextual check
  • CheckBreachedPassword(ctx, client, password) (bool, error) — HIBP k-anonymity
Sessions & Tokens
  • GenerateSessionToken() (plaintext, hash, error) — 256-bit session token
  • RotateSessionToken(oldPlaintext) (newPlaintext, newHash, oldHash, error) — rotation helper
  • ValidateSession(sess, idle, abs, now) error — session expiry check
  • SessionHash(token) string — SHA-256 hash of plaintext token
  • HexSHA256(s) string — hex-encoded SHA-256
  • CSRFToken(key, sessionHash) (string, error) — generate CSRF token bound to session
  • VerifyCSRFToken(key, sessionHash, token, maxAge) error — verify CSRF token
  • GenerateOpaqueToken() (plaintext, hash, error) — for password-reset/email-verification
  • VerifyOpaqueToken(plaintext, storedHash, expiresAt) error — verify opaque token
  • CookieConfig.CookieName(r) string — resolve cookie name for request
  • CookieConfig.SetCookie(w, r, token, maxAge) — set session cookie
  • CookieConfig.ReadCookie(r) string — read session token
  • CookieConfig.ClearCookie(w, r) — clear session cookie
  • SessionCookieName(r) / SetSessionCookie / ReadSessionCookie / ClearSessionCookie — default-config free functions
API Keys
  • GenerateAPIKey(keyPrefix) (plaintext, hash, displayPrefix, displaySuffix, error) — API key generation
  • VerifyAPIKey(ctx, store, key) (*Key, error) — API key verification (constant-time hash equality + expiry check)
  • APIKeyHash(key) string — SHA-256 hash of API key
WebAuthn

All in the github.com/cplieger/auth/v2/webauthn subpackage:

  • webauthn.NewWebAuthn(rpID, rpDisplayName, rpOrigins) (*webauthn.WebAuthn, error) — WebAuthn setup (enforced 5-minute ceremony timeout)
  • webauthn.NewWebAuthnUser(user, creds) (*webauthn.User, error) — adapt auth.User + credentials to the go-webauthn User interface
  • webauthn.BeginRegistration / FinishRegistration / BeginLogin / FinishLogin — WebAuthn ceremonies
  • webauthn.CompleteLogin(ctx, wa, store, sessionData, r) (*auth.User, *webauthn.Credential, error) — store-backed login completion: resolves the asserting user from the user handle, verifies the assertion, and persists sign-count/flag custody (including CloneWarning). Best-effort custody write; the caller still owns account-status policy (check User.Enabled) and session creation.
  • webauthn.BeginConditionalLogin(wa) (*protocol.CredentialAssertion, *webauthn.SessionData, error) — conditional mediation (autofill UI)
OIDC
  • oidc.NewProvider(ctx, cfg) (*oidc.Provider, error) — OIDC provider with PKCE (cfg is oidc.Config)
  • oidc.ValidateConfig(cfg) error — validate OIDC configuration
  • oidc.GenerateState() (string, error) — random state parameter
  • oidc.GeneratePKCE() (verifier, challenge, error) — PKCE S256
  • provider.AuthorizationURL(state, nonce, codeChallenge) string — build the authorization redirect URL (binds the nonce and the PKCE S256 challenge)
  • provider.Exchange(ctx, code, codeVerifier, nonce) (*oidc.Claims, *time.Time, error) — exchange the auth code and verify the ID token; nonce must be the non-empty value bound into AuthorizationURL (an empty nonce is rejected with ErrOIDCNonceMismatch, fail-closed)
  • oidc.ResolveUser(claims, existingBySub) (user *auth.User, isNew bool, err error) — map an OIDC identity by (issuer, sub) to a user; for a new identity it provisions from the claims and returns ErrOIDCNoUsername when the token carries neither preferred_username nor email
Auth Middleware
  • NewAuthenticator(store, ...Option) (*Authenticator, error) — create authenticator with functional options; errors on an unusable config (see CookieConfig.Validate)
  • NewSessionVerifier(store, ...Option) (*SessionVerifier, error) — session-cookie credential verifier; errors on an unusable config
  • NewAPIKeyVerifier(store, ...Option) *APIKeyVerifier — API-key credential verifier (reads the X-Api-Key header only, never a URL query parameter — CWE-598)
  • Authenticator.Authenticate(r) (*User, string, error) — resolve request to user
  • Authenticator.RequireAuth(w, r) (*User, string, bool) — auth guard
  • HasRole(user, role) bool — RBAC check
  • ValidateRedirectURI(uri) string — safe relative-path redirect validation
  • CanDisableAuthMethod(method, hasPassword, passkeyCount, oidcEnabled, oidcLinked) bool — check method removal safety
  • IsBrowserRequest(r) bool — detect browser vs API client
  • WithVerifiers(vs []CredentialVerifier) — override the default verifier chain
  • WithActivityThrottle(d time.Duration) — throttle UpdateSessionActivity writes (see Configuration)
  • WithUnauthorizedResponse(fn) — custom unauthorized response for RequireAuth (see Configuration)
  • WithTimeoutSource(fn) — per-request idle/absolute timeout resolution for hot-reloadable config (see Configuration)
  • CredentialVerifier — interface for pluggable credential verifiers
  • SessionStore / webauthn.Store (subpackage github.com/cplieger/auth/v2/webauthn) — interfaces for consumer to implement; webauthn.Store is the contract webauthn.CompleteLogin consumes
  • store.Composite — composite interface (subpackage github.com/cplieger/auth/v2/store)

Subpackages

auth/store

Composite interface store.Composite (formerly store.AuthStore).

auth/ratelimit

Dual sliding-window per-IP + per-account authentication brute-force rate limiter (OWASP ASVS 2.2.1). Standard library only (context, log/slog, sync, time).

rl := ratelimit.NewRateLimiter(ctx, ratelimit.DefaultConfig())
defer rl.Stop()
if allowed, retryAfter := rl.Allow(clientIP, username); !allowed {
    // reject; retry after retryAfter
}
// On each FAILED login attempt, record it so it counts toward the limit:
rl.Record(clientIP, username)
// On successful login, clear the failure counters:
rl.Reset(clientIP, username)
auth/authtest

Exported in-memory SessionStore implementation for consumer tests.

store := authtest.NewMemStore()
store.AddUser(&auth.User{Username: "test", Role: auth.RoleUser, Enabled: true})

Unsupported Features (by design)

The following features are intentionally out of scope. Each has a documented rationale and, where applicable, a recommended alternative.

Feature Rationale
Full OIDC token-refresh orchestration Library handles authentication, not long-lived API access. Consumer uses oauth2.TokenSource.
Multi-provider OIDC registry Consumer instantiates multiple OIDCProvider instances.
WebAuthn MDS verification Enterprise feature with large surface. Consumer can call credential.Verify(mdsProvider) using stored RawAttestation.
OIDC back-channel logout Enterprise SSO feature beyond scope of auth-primitive library.
Hierarchical RBAC / permission sets Library provides HasRole for flat role check. Use casbin/ory-keto for complex RBAC.
Cookie encryption/signing Opaque-token architecture; cookie value is a random token, not sensitive data.
OIDC userinfo endpoint ID token claims sufficient for authentication. Consumer can call provider.UserInfo().
WebAuthn attestation conveyance Default none is correct for most RPs per FIDO Alliance guidance.
WebAuthn credential filtering (AAGUID) Enterprise policy. Consumer can use go-webauthn's filtering directly.
Passkey well-known endpoints Browser/credential-manager concern, not server-auth-library concern.
CSRF middleware (full HTTP layer) Library provides CSRFToken/VerifyCSRFToken primitives; full middleware is HTTP-framework-specific.

Disclaimer

This project is built with care and follows security best practices, but it is intended for personal / self-hosted use. No guarantees of fitness for production environments. Use at your own risk.

This project was built with AI-assisted tooling using Claude Opus and Kiro. The human maintainer defines architecture, supervises implementation, and makes all final decisions.

License

GPL-3.0 — see LICENSE.

Documentation

Overview

Package auth implements authentication primitives: password hashing (Argon2id in PHC string format), WebAuthn/FIDO2 passkey ceremonies, OIDC provider integration with PKCE, session management with idle/absolute timeouts, API key generation and verification, and role-based access control helpers.

Index

Examples

Constants

View Source
const (
	CookieNameSecure = "__Host-auth_session"
	CookieNameHTTP   = "auth_session"
)

Legacy constants for backward compatibility.

View Source
const (
	DefaultIdleTimeout = 1 * time.Hour
	DefaultAbsTimeout  = 24 * time.Hour
)

Default session timeout values matching the pre-refactor struct-field defaults.

View Source
const HeaderXAPIKey = "X-Api-Key" //nolint:gosec // G101 false positive: header name, not a credential

HeaderXAPIKey is the HTTP header carrying the API key. Keys are accepted only via this header, never a URL query parameter; a query-string key leaks into access logs, browser history, and the Referer header (CWE-598).

View Source
const PasswordMaxLength = 128

PasswordMaxLength is the maximum password length to prevent DoS via extremely long inputs to Argon2id (OWASP recommendation).

View Source
const PasswordMinLengthMultiFactor = 8

PasswordMinLengthMultiFactor is the minimum password length when password login is not the sole sufficient factor.

View Source
const PasswordMinLengthSolo = 15

PasswordMinLengthSolo is the minimum password length when password login is enabled and thus a sole sufficient factor.

Variables

View Source
var (
	ErrSessionExpired  = errors.New("session expired")
	ErrSessionNotFound = errors.New("session not found")
)

Sentinel errors for session operations.

View Source
var (
	ErrTokenExpired = errors.New("auth: token expired")
	ErrTokenInvalid = errors.New("auth: token invalid")
)

Token errors.

View Source
var ErrInvalidAPIKey = errors.New("invalid API key")

ErrInvalidAPIKey is returned when an API key cannot be verified.

View Source
var ErrUnauthenticated = errors.New("unauthenticated")

ErrUnauthenticated is returned when no valid credential is found.

Functions

func APIKeyHash

func APIKeyHash(key string) string

APIKeyHash returns the hex-encoded SHA-256 hash of a key string.

func CSRFToken

func CSRFToken(key []byte, sessionHash string) (string, error)

CSRFToken generates a CSRF token bound to the given session hash using HMAC-SHA256 with a random 16-byte nonce per OWASP signed double-submit. Format: base64url(nonce[16] ∥ issuedAt[8] ∥ HMAC-SHA256(key, nonce ∥ sessionHash ∥ issuedAt))

func CanDisableAuthMethod

func CanDisableAuthMethod(method Method, hasPassword bool, passkeyCount int, oidcEnabled, oidcLinked bool) bool

CanDisableAuthMethod checks whether disabling the given auth method would leave the user with no viable authentication method.

func CheckBreachedPassword

func CheckBreachedPassword(ctx context.Context, client *http.Client, password string) (bool, error)

CheckBreachedPassword checks a password against the Have I Been Pwned Passwords API using k-anonymity. Returns true if the password has been found in a breach.

func ClearSessionCookie

func ClearSessionCookie(w http.ResponseWriter, r *http.Request)

ClearSessionCookie clears the session cookie using the default CookieConfig.

func DummyHash

func DummyHash() string

DummyHash returns a pre-computed Argon2id hash used by the login handler to equalize timing when the username doesn't exist (H2 mitigation). The hash is computed lazily on first call.

func GenerateAPIKey

func GenerateAPIKey(keyPrefix string) (plaintext, hash, displayPrefix, displaySuffix string, err error)

GenerateAPIKey generates a new API key with 256 bits of entropy. The keyPrefix is prepended to the random hex string (e.g. "ak_"). It returns the plaintext key, its SHA-256 hash, a display prefix (first 8 chars), and a display suffix (last 4 chars).

Example
package main

import (
	"fmt"

	"github.com/cplieger/auth/v2"
)

func main() {
	plaintext, hash, prefix, suffix, err := auth.GenerateAPIKey("ak_")
	if err != nil {
		panic(err)
	}
	fmt.Println(plaintext[:3], len(hash) == 64, len(prefix) == 8, len(suffix) == 4)
}
Output:
ak_ true true true

func GenerateOpaqueToken

func GenerateOpaqueToken() (plaintext, hash string, err error)

GenerateOpaqueToken generates a cryptographically random opaque token suitable for password-reset or email-verification flows. Returns the plaintext token (to send to the user) and its SHA-256 hash (to store in the database).

func GenerateSessionToken

func GenerateSessionToken() (plaintext, hash string, err error)

GenerateSessionToken generates a cryptographically random session token (256 bits / 32 bytes). It returns the hex-encoded plaintext token and its SHA-256 hash (also hex-encoded).

Example
package main

import (
	"fmt"

	"github.com/cplieger/auth/v2"
)

func main() {
	plaintext, hash, err := auth.GenerateSessionToken()
	if err != nil {
		panic(err)
	}
	fmt.Println(len(plaintext) == 64, len(hash) == 64, plaintext != hash)
}
Output:
true true true

func HasRole

func HasRole(user *User, role Role) bool

HasRole reports whether the user is authorized for the given role.

Example
package main

import (
	"fmt"

	"github.com/cplieger/auth/v2"
)

func main() {
	admin := &auth.User{Role: auth.RoleAdmin}
	user := &auth.User{Role: auth.RoleUser}
	fmt.Println(auth.HasRole(admin, auth.RoleUser))
	fmt.Println(auth.HasRole(user, auth.RoleAdmin))
}
Output:
true
false

func HashPassword

func HashPassword(password string) (string, error)

HashPassword hashes a password using Argon2id with OWASP parameters. Returns the hash in PHC string format: $argon2id$v=19$m=19456,t=2,p=1$<base64-salt>$<base64-hash>

Example
package main

import (
	"fmt"

	"github.com/cplieger/auth/v2"
)

func main() {
	hash, err := auth.HashPassword("my-secure-password")
	if err != nil {
		panic(err)
	}
	ok, err := auth.VerifyPassword("my-secure-password", hash)
	if err != nil {
		panic(err)
	}
	fmt.Println(ok)
}
Output:
true

func HexSHA256

func HexSHA256(s string) string

HexSHA256 returns the hex-encoded SHA-256 hash of s.

func IsBrowserRequest

func IsBrowserRequest(r *http.Request) bool

IsBrowserRequest returns true if the request appears to be from a browser (Accept header contains text/html and no X-API-Key header).

func NeedsRehash

func NeedsRehash(encodedHash string) bool

NeedsRehash reports whether the encoded hash was produced with parameters different from the current OWASP-recommended defaults. Returns true if the hash is invalid or uses outdated parameters.

func ReadSessionCookie

func ReadSessionCookie(r *http.Request) string

ReadSessionCookie reads the session token from the cookie using the default CookieConfig.

func RotateSessionToken

func RotateSessionToken(oldPlaintext string) (newPlaintext, newHash, oldHash string, err error)

RotateSessionToken generates a new session token, returning the new plaintext, new hash, and old hash. The caller is responsible for atomically replacing the session record in the store (delete old hash, insert new hash with same session data).

func SessionCookieName

func SessionCookieName(_ *http.Request) string

SessionCookieName returns the stable cookie name using the default CookieConfig.

func SessionHash

func SessionHash(token string) string

SessionHash returns the hex-encoded SHA-256 hash of a plaintext token.

func SetSessionCookie

func SetSessionCookie(w http.ResponseWriter, r *http.Request, token string, maxAge int)

SetSessionCookie sets the session cookie on the response using the default CookieConfig.

func ValidatePasswordContext

func ValidatePasswordContext(password, username string, forbiddenWords []string) error

ValidatePasswordContext rejects passwords that trivially embed the username or any of the provided forbidden words.

func ValidatePasswordLength

func ValidatePasswordLength(password string, passwordOnly bool) error

ValidatePasswordLength enforces minimum and maximum password length. Maximum length (128 chars) prevents DoS via Argon2id processing of extremely long inputs.

Example
package main

import (
	"fmt"

	"github.com/cplieger/auth/v2"
)

func main() {
	err := auth.ValidatePasswordLength("short", true)
	fmt.Println(err != nil)
}
Output:
true

func ValidateRedirectURI

func ValidateRedirectURI(uri string) string

ValidateRedirectURI ensures the URI is a safe relative path.

Example
package main

import (
	"fmt"

	"github.com/cplieger/auth/v2"
)

func main() {
	fmt.Println(auth.ValidateRedirectURI("/dashboard"))
	fmt.Println(auth.ValidateRedirectURI("https://evil.com"))
}
Output:
/dashboard
/

func ValidateSession

func ValidateSession(sess *Session, idleTimeout, absTimeout time.Duration, now time.Time) error

ValidateSession checks whether a session is still valid given the idle and absolute timeout durations.

func VerifyCSRFToken

func VerifyCSRFToken(key []byte, sessionHash, token string, maxAge time.Duration) error

VerifyCSRFToken verifies a CSRF token against the session hash and checks that it has not expired (maxAge duration from creation).

func VerifyOpaqueToken

func VerifyOpaqueToken(plaintext, storedHash string, expiresAt time.Time) error

VerifyOpaqueToken checks that the plaintext token matches the stored hash and that the token has not expired. Returns nil on success.

func VerifyPassword

func VerifyPassword(password, encodedHash string) (bool, error)

VerifyPassword verifies a password against an encoded Argon2id hash in PHC string format. Uses constant-time comparison.

Types

type APIKeyReader

type APIKeyReader interface {
	GetAPIKeyByHash(ctx context.Context, hash string) (*Key, error)
}

APIKeyReader validates API keys (looked up by hash).

type APIKeyVerifier

type APIKeyVerifier struct {
	// contains filtered or unexported fields
}

APIKeyVerifier authenticates requests via the X-API-Key header. Create with NewAPIKeyVerifier.

func NewAPIKeyVerifier

func NewAPIKeyVerifier(store APIKeyVerifierStore, opts ...Option) *APIKeyVerifier

NewAPIKeyVerifier creates an APIKeyVerifier with the given store and options. Of the shared Option set it consults only WithLogger; every other option (cookie, timeouts, throttle, bypass, hooks) configures session or authenticator behavior this verifier does not have and is silently ignored.

func (*APIKeyVerifier) Verify

func (v *APIKeyVerifier) Verify(ctx context.Context, r *http.Request) (*User, string, error)

Verify checks the X-API-Key header and returns the user if the key is valid. API keys are accepted only via the header, never via a URL query parameter; a key in a query string leaks into access logs, browser history, and the Referer header (CWE-598).

type APIKeyVerifierStore

type APIKeyVerifierStore interface {
	APIKeyReader
	UserReader
}

APIKeyVerifierStore is the minimal interface for API key verification.

type Argon2Params

type Argon2Params struct {
	// Memory in KiB. Default: 19456 (19 MiB, OWASP recommendation #2).
	Memory uint32
	// Iterations (time cost). Default: 2.
	Iterations uint32
	// Parallelism (threads). Default: 1.
	Parallelism uint8
	// SaltLength in bytes. Default: 16.
	SaltLength uint32
	// KeyLength in bytes. Default: 32.
	KeyLength uint32
}

Argon2Params describes the Argon2id parameters for password hashing.

func DefaultArgon2Params

func DefaultArgon2Params() Argon2Params

DefaultArgon2Params returns the OWASP-recommended Argon2id parameters.

func (Argon2Params) Validate

func (p Argon2Params) Validate() error

Validate checks that the params are within safe bounds.

type AuthStore

AuthStore is the composed interface needed by Authenticator — session lookup, user lookup, and API key lookup.

type Authenticator

type Authenticator struct {
	// contains filtered or unexported fields
}

Authenticator resolves an HTTP request to an authenticated user. Create with NewAuthenticator.

func NewAuthenticator

func NewAuthenticator(store AuthStore, opts ...Option) (*Authenticator, error)

NewAuthenticator creates an Authenticator with the given store and options. The store must implement SessionReader, UserReader, and APIKeyReader. It returns an error when the assembled configuration is unusable (see CookieConfig.Validate and WithActivityThrottle). If no idle/absolute timeout is provided, defaults of 1h and 24h are applied.

func (*Authenticator) Authenticate

func (a *Authenticator) Authenticate(r *http.Request) (*User, string, error)

Authenticate resolves the request to a user by running the credential verifier chain in order. The default chain checks the session cookie first, then the API key (accepted only via the X-Api-Key header, never a URL query parameter -- CWE-598). It returns the user and session hash, or ErrUnauthenticated when no verifier authenticates the request.

func (*Authenticator) RequireAuth

func (a *Authenticator) RequireAuth(w http.ResponseWriter, r *http.Request) (*User, string, bool)

RequireAuth checks authentication and returns the user. If not authenticated, it writes the appropriate response and returns ok=false. The default response is a 302 redirect to the login path for browser requests and a 401 JSON envelope otherwise; a hook installed via WithUnauthorizedResponse replaces both branches.

Example
package main

import (
	"fmt"
	"time"

	"github.com/cplieger/auth/v2"
	"github.com/cplieger/auth/v2/authtest"
)

func main() {
	store := authtest.NewMemStore()
	store.AddUser(&auth.User{
		Username: "alice",
		Role:     auth.RoleAdmin,
		Enabled:  true,
	})

	authn, err := auth.NewAuthenticator(
		store,
		auth.WithIdleTimeout(1*time.Hour),
		auth.WithAbsTimeout(24*time.Hour),
	)
	if err != nil {
		panic(err)
	}
	_ = authn
	fmt.Println("authenticator configured")
}
Output:
authenticator configured

type CookieConfig

type CookieConfig struct {
	// Name is the base cookie name (without __Host- prefix).
	// Default: "auth_session".
	Name string

	// Path is the cookie Path attribute. Default: "/".
	Path string

	// Domain is the cookie Domain attribute. Default: "" (unset).
	// Note: __Host- prefix requires Domain to be unset.
	Domain string

	// SameSite is the cookie SameSite attribute. Default: http.SameSiteLaxMode.
	SameSite http.SameSite

	// Posture selects the deploy-time cookie security posture.
	// Default: PostureSecure.
	Posture CookiePosture

	// TrustForwardedHeaders enables honoring X-Forwarded-Proto to detect HTTPS.
	// MUST only be enabled when the app is behind a reverse proxy that always
	// sets/overwrites this header. When false (default), only r.TLS is used.
	TrustForwardedHeaders bool
}

CookieConfig holds configurable cookie attributes for session cookies. Under every posture except PosturePerRequest the deployment has ONE stable cookie name; PosturePerRequest alternates between the __Host--prefixed and bare forms of the configured base name per request scheme.

func DefaultCookieConfig

func DefaultCookieConfig() CookieConfig

DefaultCookieConfig returns a CookieConfig with secure defaults.

func (*CookieConfig) ClearCookie

func (c *CookieConfig) ClearCookie(w http.ResponseWriter, r *http.Request)

ClearCookie clears the session cookie.

func (*CookieConfig) CookieName

func (c *CookieConfig) CookieName(r *http.Request) string

CookieName returns the cookie name for this config. In PosturePerRequest mode the name is selected from the request scheme (__Host-<base> over HTTPS, bare <base> over plain HTTP); in all other postures the request is ignored and the stable EffectiveName() is returned.

func (*CookieConfig) EffectiveName

func (c *CookieConfig) EffectiveName() string

EffectiveName returns the ONE stable cookie name for this deployment. Determined entirely by Posture at config time — no per-request logic.

For PosturePerRequest the actual emitted/read name varies per request (see CookieName / SetCookie / ReadCookie); EffectiveName returns the secure __Host-<base> form as the canonical name for request-less callers and validation.

func (*CookieConfig) ReadCookie

func (c *CookieConfig) ReadCookie(r *http.Request) string

ReadCookie reads the session token from the cookie using the request-appropriate name (per-request in PosturePerRequest mode, otherwise the stable EffectiveName()).

func (*CookieConfig) SetCookie

func (c *CookieConfig) SetCookie(w http.ResponseWriter, r *http.Request, token string, maxAge int)

SetCookie sets the session cookie on the response using this config.

The Secure attribute follows the posture via isSecureCookieForRequest: it is set for every HTTPS posture and omitted only for plain-HTTP delivery, namely PostureInsecureLAN or PosturePerRequest over an HTTP request. Omitting Secure there is deliberate: a browser never sends a Secure cookie over plain HTTP, so forcing it would silently break sessions on HTTP-only LAN deployments. The default posture (PostureSecure) always sets Secure. Static analysis flags the conditional Secure (gosec G124, CodeQL go/cookie-secure-not-set); that is a documented false positive for the HTTP-LAN support, exercised by cookie_perrequest_test.go and redteam_test.go.

func (*CookieConfig) Validate

func (c *CookieConfig) Validate() error

Validate reports whether the configuration will produce a usable session cookie. It returns an error when:

  • Name, Domain, or Path contains a control character, or Name contains a character invalid in a cookie name -- cases that would make http.SetCookie emit a malformed Set-Cookie header; or
  • the posture emits a __Host--prefixed name (every posture except PostureInsecureLAN) while Domain is set or Path is not "/". Browsers silently reject a __Host- cookie that carries a Domain or a non-root Path, breaking every session with no server-side error.

Validate is the single authority for cookie-config validity: the constructors (NewAuthenticator, NewSessionVerifier) call it so an unusable configuration fails fast at construction, and consumers assembling a CookieConfig by hand may call it directly.

type CookiePosture

type CookiePosture int

CookiePosture determines the cookie security posture. Every posture except PosturePerRequest is a deploy-time decision with one stable cookie name; PosturePerRequest selects the name and Secure flag per request from the request scheme.

const (
	// PostureSecure is the default: __Host- prefix + Secure + HttpOnly + SameSite=Lax.
	// Works on any HTTPS deployment including self-signed certificates.
	PostureSecure CookiePosture = iota

	// PostureInsecureLAN is for HTTP-only LAN/Docker deployments (ip:port, dockername:port).
	// Uses a non-prefixed name, no Secure flag. Explicitly opt-in only.
	PostureInsecureLAN

	// PostureForceSecure forces Secure flag even behind a TLS-terminating proxy
	// where r.TLS is nil. Requires TrustForwardedHeaders=true to detect HTTPS.
	PostureForceSecure

	// PosturePerRequest selects the cookie name and Secure flag per request,
	// driven by isHTTPS(r) (which honors TrustForwardedHeaders): __Host-<base>
	// with Secure over HTTPS, and the bare <base> without Secure over plain
	// HTTP. Intended for a single instance serving both HTTP-LAN (ip:port) and
	// HTTPS-proxied traffic. The base name stays configurable via Name.
	PosturePerRequest
)

func (CookiePosture) String

func (p CookiePosture) String() string

String returns the posture's name so structured-log attributes render "PostureSecure" rather than the bare iota integer.

type CredentialVerifier

type CredentialVerifier interface {
	Verify(ctx context.Context, r *http.Request) (*User, string, error)
}

CredentialVerifier resolves an HTTP request to an authenticated user using a specific credential type (session, API key, passkey). Implementations return (nil, "", nil) to indicate "not my credential type" and allow the next verifier in the chain to attempt authentication.

Chain semantics (see Authenticator.Authenticate): the chain stops at the first verifier that returns a non-nil *User (granted) OR a non-nil error. A non-nil error ABORTS the whole chain - verifiers ordered after it never run - and is surfaced to the caller as a failed authentication. A verifier that wants the chain to continue to the next credential type MUST return (nil, "", nil), never an error, for a credential it cannot positively authenticate. The built-in verifiers differ deliberately: SessionVerifier treats every session problem as (nil, "", nil) and falls through, whereas APIKeyVerifier returns ErrUnauthenticated for a present-but-invalid key. In the default NewAuthenticator chain APIKeyVerifier runs last, so this is unobservable; but a custom WithVerifiers chain that places an error-returning verifier before others will reject a request (HTTP 401) whose earlier-type credential is present-but-invalid even when a later verifier holds a valid credential. This fails closed (denial, never a bypass); to avoid masking a later valid credential, order error-returning verifiers last or have them return (nil, "", nil) on "not my credential".

type Hasher

type Hasher struct {
	// contains filtered or unexported fields
}

Hasher provides configurable Argon2id password hashing with optional pepper.

func NewHasher

func NewHasher(params Argon2Params, opts ...HasherOption) (*Hasher, error)

NewHasher creates a Hasher with the given params. Returns an error if params are invalid. Use WithPepper to enable HMAC peppering.

func (*Hasher) Hash

func (h *Hasher) Hash(password string) (string, error)

Hash hashes a password using Argon2id with the configured parameters. Returns the hash in PHC string format.

func (*Hasher) NeedsRehash

func (h *Hasher) NeedsRehash(encodedHash string) bool

NeedsRehash reports whether the hash uses different parameters than this Hasher.

func (*Hasher) Verify

func (h *Hasher) Verify(password, encodedHash string) (bool, error)

Verify verifies a password against an encoded Argon2id hash in PHC format.

type HasherOption

type HasherOption func(*hasherConfig)

HasherOption configures a Hasher.

func WithPepper

func WithPepper(pepper []byte) HasherOption

WithPepper sets the HMAC pepper applied to passwords before hashing. If not provided, no pepper is applied.

type Key

type Key struct {
	CreatedAt time.Time  `json:"created_at"`
	ExpiresAt *time.Time `json:"expires_at,omitempty"`
	KeyHash   string     `json:"-"`
	KeyPrefix string     `json:"key_prefix"`
	KeySuffix string     `json:"key_suffix"`
	Label     string     `json:"label"`
	ID        int64      `json:"id"`
	UserID    int64      `json:"user_id"`
}

Key represents a machine-to-machine API key for a user.

func VerifyAPIKey

func VerifyAPIKey(ctx context.Context, store APIKeyReader, key string) (*Key, error)

VerifyAPIKey hashes the provided key, looks it up in the store, and returns the matching APIKey record. Returns ErrInvalidAPIKey if the key is not found or has expired.

Example
package main

import (
	"context"
	"fmt"

	"github.com/cplieger/auth/v2"
	"github.com/cplieger/auth/v2/authtest"
)

func main() {
	store := authtest.NewMemStore()
	store.AddUser(&auth.User{
		Username: "bot",
		Role:     auth.RoleUser,
		Enabled:  true,
	})

	plaintext, hash, prefix, suffix, _ := auth.GenerateAPIKey("ak_")
	store.AddAPIKey(&auth.Key{
		UserID:    1,
		KeyHash:   hash,
		KeyPrefix: prefix,
		KeySuffix: suffix,
		Label:     "ci",
	})

	key, err := auth.VerifyAPIKey(context.Background(), store, plaintext)
	fmt.Println(key != nil, err == nil)
}
Output:
true true

type Method

type Method string

Method is a typed identifier for the authentication mechanism used to establish a session.

const (
	MethodPassword Method = "password"
	MethodPasskey  Method = "passkey"
	MethodOIDC     Method = "oidc"
)

Auth method identifiers stored in sessions and used for method guards.

type OIDCConfig

type OIDCConfig struct {
	IssuerURL    string `json:"issuer_url" yaml:"issuer_url"`
	ClientID     string `json:"client_id" yaml:"client_id"`
	ClientSecret string `json:"-" yaml:"client_secret"`
	RedirectURI  string `json:"redirect_uri" yaml:"redirect_uri"`
	AutoRedirect bool   `json:"auto_redirect" yaml:"auto_redirect"`
}

OIDCConfig holds OIDC provider settings.

type Option

type Option func(*authConfig)

Option configures an Authenticator, SessionVerifier, or APIKeyVerifier.

func WithAbsTimeout

func WithAbsTimeout(d time.Duration) Option

WithAbsTimeout sets the session absolute timeout duration.

func WithActivityThrottle

func WithActivityThrottle(d time.Duration) Option

WithActivityThrottle sets the minimum interval between session-activity writes for a given session within a SessionVerifier.

The default (d == 0) preserves write-on-every-authenticated-request behavior. When d > 0, the verifier records a session-activity write at most once per d per session hash, coalescing the high-frequency writes that would otherwise hit the store on every request. The write remains best-effort (errors are logged, never fatal).

d must be less than the configured idle timeout (ideally much less): because the persisted LastActivity is only refreshed once per window, it lags real activity by up to d, so a throttle >= the idle timeout can cause ValidateSession to expire sessions that are actively in use. The constructors reject such a configuration with an error.

func WithBypass

func WithBypass(fn func() bool) Option

WithBypass sets a function that reports whether authentication is bypassed. When the function returns true, all requests are treated as authenticated with a synthetic admin user.

func WithCookie

func WithCookie(cfg CookieConfig) Option

WithCookie sets the cookie configuration for session cookies.

func WithIdleTimeout

func WithIdleTimeout(d time.Duration) Option

WithIdleTimeout sets the session idle timeout duration.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets the logger for debug/warning output. If not provided, slog.Default() is used.

func WithLoginPath

func WithLoginPath(path string) Option

WithLoginPath sets the redirect target for unauthenticated browser requests. Defaults to "/login".

func WithTimeoutSource added in v2.1.0

func WithTimeoutSource(fn func() (idle, abs time.Duration)) Option

WithTimeoutSource sets a callback that resolves the session idle and absolute timeouts per verification, instead of the static values configured via WithIdleTimeout/WithAbsTimeout. It exists for consumers whose timeouts are hot-reloadable configuration: the default SessionVerifier consults the source on every Verify, so a changed value takes effect without reconstructing the verifier.

Validation is per-resolution (a construction-time check cannot bind dynamic values): a non-positive idle or absolute value returned by the source falls back to the corresponding static configured value, and the activity throttle (see WithActivityThrottle) is clamped to at most half the resolved idle timeout, so a source that shrinks the idle timeout below the configured throttle can never make the persisted LastActivity stale enough to expire a session that is actively in use.

func WithUnauthorizedResponse added in v2.1.0

func WithUnauthorizedResponse(fn func(w http.ResponseWriter, r *http.Request)) Option

WithUnauthorizedResponse sets the response written by Authenticator.RequireAuth when a request is not authenticated, replacing the default behavior (a 302 redirect to the login path for browser requests, a 401 JSON envelope otherwise). The hook owns the entire unauthorized response: it is called for browser and API requests alike, so a consumer that wants to keep the browser redirect applies IsBrowserRequest itself. A nil hook (the default) preserves the built-in behavior byte for byte.

func WithVerifiers

func WithVerifiers(vs []CredentialVerifier) Option

WithVerifiers sets an explicit, ordered credential-verifier chain for an Authenticator, replacing the default session + API-key chain. When the provided slice is empty (or this option is not used), the default chain is used. This lets consumers inject custom verifiers (e.g. a TOTP or app-specific session verifier) without copying Authenticate; a consumer that also needs its own unauthorized response pairs it with WithUnauthorizedResponse to keep RequireAuth.

type PasskeyCredential

type PasskeyCredential struct {
	CreatedAt       time.Time `json:"created_at"`
	AttestationType string    `json:"-"`
	Transport       string    `json:"transport,omitempty"`
	Name            string    `json:"name"`
	CredentialID    []byte    `json:"-"`
	PublicKey       []byte    `json:"-"`
	AAGUID          []byte    `json:"-"`
	RawAttestation  []byte    `json:"-"`
	ID              int64     `json:"id"`
	UserID          int64     `json:"user_id"`
	SignCount       uint32    `json:"-"`
	BackupEligible  bool      `json:"backup_eligible"`
	BackupState     bool      `json:"-"`
	UserPresent     bool      `json:"-"`
	UserVerified    bool      `json:"-"`
	CloneWarning    bool      `json:"-"`
}

PasskeyCredential represents a WebAuthn/FIDO2 credential registered to a user.

type PasskeyFlags

type PasskeyFlags struct {
	UserPresent    bool
	UserVerified   bool
	BackupEligible bool
	BackupState    bool
	CloneWarning   bool
}

PasskeyFlags holds the boolean authenticator flags for a credential update.

type Role

type Role string

Role is a typed string identifying a user's authorization level.

const (
	RoleAdmin Role = "admin"
	RoleUser  Role = "user"
)

User role constants.

type Session

type Session struct {
	CreatedAt    time.Time  `json:"created_at"`
	LastActivity time.Time  `json:"last_activity"`
	OIDCExpiry   *time.Time `json:"oidc_expiry,omitempty"`
	TokenHash    string     `json:"-"`
	AuthMethod   Method     `json:"auth_method"`
	IPAddress    string     `json:"ip_address"`
	UserID       int64      `json:"user_id"`
}

Session represents a server-side authenticated session.

type SessionActivityUpdater

type SessionActivityUpdater interface {
	UpdateSessionActivity(ctx context.Context, tokenHash string, now time.Time) error
}

SessionActivityUpdater updates the last activity timestamp for a session.

type SessionReader

type SessionReader interface {
	GetSessionByHash(ctx context.Context, tokenHash string) (*Session, error)
}

SessionReader finds session data by token hash.

type SessionStore

type SessionStore interface {
	SessionReader
	SessionWriter
}

SessionStore composes read + write for middleware that needs both.

type SessionVerifier

type SessionVerifier struct {
	// contains filtered or unexported fields
}

SessionVerifier authenticates requests via session cookie. Create with NewSessionVerifier.

func NewSessionVerifier

func NewSessionVerifier(store SessionVerifierStore, opts ...Option) (*SessionVerifier, error)

NewSessionVerifier creates a SessionVerifier with the given session store and options. It returns an error when the assembled configuration is unusable: for example a __Host- cookie posture combined with a Domain or a non-root Path (which browsers reject), or an activity throttle that is not less than the idle timeout. See CookieConfig.Validate. If no idle/absolute timeout is provided, defaults of 1h and 24h are applied.

func (*SessionVerifier) Verify

func (v *SessionVerifier) Verify(ctx context.Context, r *http.Request) (*User, string, error)

Verify checks the session cookie and returns the user if valid.

type SessionVerifierStore

type SessionVerifierStore interface {
	SessionReader
	SessionActivityUpdater
	UserReader
}

SessionVerifierStore is the minimal interface for session verification.

type SessionWriter

type SessionWriter interface {
	CreateSession(ctx context.Context, sess *Session) error
	UpdateSessionActivity(ctx context.Context, tokenHash string, now time.Time) error
	DeleteSession(ctx context.Context, tokenHash string) error
	DeleteUserSessions(ctx context.Context, userID int64, exceptHash string) error
}

SessionWriter persists and removes session data.

type User

type User struct {
	CreatedAt    time.Time `json:"created_at"`
	UpdatedAt    time.Time `json:"updated_at"`
	Username     string    `json:"username"`
	Email        string    `json:"email,omitempty"`
	DisplayName  string    `json:"display_name,omitempty"`
	PasswordHash string    `json:"-"`
	Role         Role      `json:"role"`
	OIDCSub      string    `json:"-"`
	OIDCIssuer   string    `json:"-"`
	ID           int64     `json:"id"`
	Enabled      bool      `json:"-"`
}

User represents an authenticated user account.

type UserReader

type UserReader interface {
	GetUserByID(ctx context.Context, id int64) (*User, error)
}

UserReader retrieves user records for authentication.

Directories

Path Synopsis
Package authtest provides an in-memory implementation of auth.AuthStore for use in consumer tests.
Package authtest provides an in-memory implementation of auth.AuthStore for use in consumer tests.
internal
capture
Package capture is an auth-internal test helper: a slog.Handler that records log records so tests can assert on auth's logging decisions, plus a helper to install it as the default logger.
Package capture is an auth-internal test helper: a slog.Handler that records log records so tests can assert on auth's logging decisions, plus a helper to install it as the default logger.
Package oidc wraps coreos/go-oidc to provide OIDC/OAuth2 authentication flows with PKCE.
Package oidc wraps coreos/go-oidc to provide OIDC/OAuth2 authentication flows with PKCE.
Package ratelimit implements a dual sliding-window rate limiter for authentication attempts (per-IP and per-account).
Package ratelimit implements a dual sliding-window rate limiter for authentication attempts (per-IP and per-account).
Package store defines the composite persistence contract for the authentication subsystem.
Package store defines the composite persistence contract for the authentication subsystem.
Package webauthn wraps go-webauthn/webauthn to provide WebAuthn/FIDO2 passkey ceremony helpers.
Package webauthn wraps go-webauthn/webauthn to provide WebAuthn/FIDO2 passkey ceremony helpers.

Jump to

Keyboard shortcuts

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