Help us shape the future of Go! Take the 2026 Go Developer Survey

password

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Oct 30, 2026 License: Apache-2.0 Imports: 31 Imported by: 0

Documentation

Overview

Package password is the password login provider: JSON endpoints over net/http for login by username or email, an optional TOTP second factor with backup codes, optional self-registration (off by default) and logout. Passwords are hashed with Argon2id; hashes of another scheme (bcrypt with VerifyBcrypt) verify through Options.LegacyVerifier and are upgraded to Argon2id at the next login.

Users live in the application's database behind Storage; MemoryStorage serves tests and examples. A successful login stores an auth.Identity in a session.Store session, which a session.Guard then authenticates; the Provider is also the session.Refresher that ends the sessions of deleted users.

Login answers 200 with the identity, or 202 {"challenge":"totp"} when the user has TOTP (verify the code at POST {prefix}/totp/verify) and 202 {"challenge":"totp_setup"} when TOTPOptions.Required makes a user without TOTP enroll first (POST {prefix}/totp/setup, then /totp/enable). Refusals are the JSON shape of auth.ErrorResponse with stable codes such as invalid_credentials and invalid_totp_code.

The provider never logs and never starts goroutines; it reports every request to the optional auth.Observer. It is safe for concurrent use.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrUserNotFound          = errors.New("user not found")
	ErrUserExists            = errors.New("user already exists")
	ErrTOTPAttemptsExhausted = errors.New("totp attempts exhausted")
)

Storage contract errors. Storage implementations return them; the provider branches on them with errors.Is.

View Source
var ErrInvalidHash = errors.New("invalid password hash")

ErrInvalidHash reports a stored hash that is not an Argon2id hash Verify accepts: another format, another Argon2 version, malformed parts or parameters outside the limits.

Functions

func Hash

func Hash(password string, parameters Parameters) (string, error)

Hash returns the Argon2id hash of password in the encoding "$argon2id$v=19$m=..,t=..,p=..$salt$hash" (salt and hash base64 without padding). The password must have 1..128 bytes. It is safe for concurrent use.

func Verify

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

Verify reports whether password matches encodedHash in constant time. Anything but an Argon2id hash within the limits (memory 128 MiB, 50 iterations, parallelism 8, salt and key 16..1024 bytes) is ErrInvalidHash. It is safe for concurrent use.

func VerifyBcrypt

func VerifyBcrypt(password, storedHash string) (bool, error)

VerifyBcrypt is a ready-made Options.LegacyVerifier for bcrypt hashes (PocketBase, Django, Rails and many others): it reports whether password matches storedHash. A malformed hash or an unsupported cost is an error, not a mismatch, so a broken import shows. bcrypt compares only the first 72 bytes of a password; the upgrade to Argon2id after a match keys the whole password, which is the one the user types. It is safe for concurrent use.

Types

type Challenge

type Challenge string

Challenge is the second-factor step a login answers with 202 {"challenge": ...}.

const (
	ChallengeTOTP      Challenge = "totp"       // verify a code at POST {prefix}/totp/verify
	ChallengeTOTPSetup Challenge = "totp_setup" // enroll at POST {prefix}/totp/setup and /totp/enable (TOTPOptions.Required)
)

The challenges.

type MemoryStorage

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

MemoryStorage is an in-memory Storage for tests and examples. It honours every Storage contract, matches usernames and emails case-insensitively and hands out copies, so callers never share its records. It is safe for concurrent use; the zero value is not usable, call NewMemoryStorage. ponytail: lookups by username and email scan every user; index them if it ever holds more than a test's worth.

func NewMemoryStorage

func NewMemoryStorage() *MemoryStorage

NewMemoryStorage returns an empty MemoryStorage.

func (*MemoryStorage) ClearTOTP

func (s *MemoryStorage) ClearTOTP(_ context.Context, id uuid.UUID) error

ClearTOTP disables TOTP and resets the attempt counter.

func (*MemoryStorage) ConsumeBackupCode

func (s *MemoryStorage) ConsumeBackupCode(_ context.Context, id uuid.UUID, hash string) (bool, error)

ConsumeBackupCode removes hash and reports whether it was there.

func (*MemoryStorage) ConsumeTOTPAttempt

func (s *MemoryStorage) ConsumeTOTPAttempt(_ context.Context, id uuid.UUID, maximum int) (int, error)

ConsumeTOTPAttempt reserves one attempt below maximum, else ErrTOTPAttemptsExhausted.

func (*MemoryStorage) ConsumeTOTPStep

func (s *MemoryStorage) ConsumeTOTPStep(_ context.Context, id uuid.UUID, step time.Time) (bool, error)

ConsumeTOTPStep records step when it is later than the last one and reports whether it was.

func (*MemoryStorage) CreateUser

func (s *MemoryStorage) CreateUser(_ context.Context, user *User) error

CreateUser stores a copy of user; a used ID, username or email is ErrUserExists.

func (*MemoryStorage) GetUserByEmail

func (s *MemoryStorage) GetUserByEmail(_ context.Context, email string) (*User, error)

GetUserByEmail returns a copy of the user with email, or ErrUserNotFound.

func (*MemoryStorage) GetUserByID

func (s *MemoryStorage) GetUserByID(_ context.Context, id uuid.UUID) (*User, error)

GetUserByID returns a copy of the user with id, or ErrUserNotFound.

func (*MemoryStorage) GetUserByUsername

func (s *MemoryStorage) GetUserByUsername(_ context.Context, username string) (*User, error)

GetUserByUsername returns a copy of the user with username, or ErrUserNotFound.

func (*MemoryStorage) ResetTOTPAttempts

func (s *MemoryStorage) ResetTOTPAttempts(_ context.Context, id uuid.UUID) error

ResetTOTPAttempts sets the attempt counter to zero.

func (*MemoryStorage) SetTOTP

func (s *MemoryStorage) SetTOTP(_ context.Context, id uuid.UUID, secret string, backupCodeHashes []string) (bool, error)

SetTOTP enables TOTP and resets the attempt counter while TOTP is disabled and reports whether it did.

func (*MemoryStorage) UpdatePasswordHash

func (s *MemoryStorage) UpdatePasswordHash(_ context.Context, id uuid.UUID, currentHash, newHash string) error

UpdatePasswordHash replaces the password hash while it still equals currentHash.

type Options

type Options struct {
	Sessions             *session.Store // required
	Storage              Storage        // required
	PathPrefix           string         // default "/auth/password"; segments of A-Z a-z 0-9 . _ ~ -, no trailing "/", no "." or ".." segment
	DisableUsernameLogin bool
	DisableEmailLogin    bool // both true: error
	EnableRegistration   bool // off by default: POST {prefix}/register is not even routed
	Policy               Policy
	TOTP                 TOTPOptions
	HashParameters       Parameters                                      // zero value: DefaultParameters(); at least 32 MiB, 1 iteration, salt and key 16 bytes
	LegacyVerifier       func(password, storedHash string) (bool, error) // optional, e.g. VerifyBcrypt; consulted for stored hashes that are not Argon2id
	OnLogin              auth.Hook                                       // must keep UserID, the Storage user ID that Refresh and the TOTP routes look users up by; a login whose hook replaces it fails (500)
	OnLogout             auth.LogoutHook
	Observer             auth.Observer
}

Options configure New.

type Parameters

type Parameters struct {
	Memory      uint32 // KiB
	Iterations  uint32
	Parallelism uint8
	SaltLength  uint32
	KeyLength   uint32
}

Parameters are Argon2id cost parameters.

func DefaultParameters

func DefaultParameters() Parameters

DefaultParameters returns m=64 MiB, t=3, p=2, salt 16, key 32.

type Policy

type Policy struct {
	MinimumLength    int // in characters (runes); default 12; maximum length is fixed at 128 bytes
	RequireUppercase bool
	RequireLowercase bool
	RequireDigit     bool
	RequireSpecial   bool // any rune that is neither letter nor digit
}

Policy is what a password chosen at registration must satisfy.

type Provider

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

Provider is the password login provider: JSON endpoints for login, an optional TOTP second factor, optional registration and logout, on users the application stores behind Storage. Its routes read the session themselves, so they need no Guard in front. It is safe for concurrent use.

func New

func New(options *Options) (*Provider, error)

New validates options, applies the defaults to a copy and precomputes the dummy hash used for unknown users (timing-safe lookups). It does not modify options, and changing them afterwards has no effect.

Example
package main

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"

	"github.com/google/uuid"

	"github.com/govengers/auth"
	"github.com/govengers/auth/password"
	"github.com/govengers/auth/session"
)

// newExampleSessions builds a session store on the in-process backend; production code shares a Redis
// backend between replicas and reads the key from its configuration.
func newExampleSessions() (*session.Store, error) {
	backend, err := session.NewMemoryBackend(nil)
	if err != nil {
		return nil, err
	}
	key, err := session.ParseKey("MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=")
	if err != nil {
		return nil, err
	}
	return session.NewStore(&session.StoreOptions{Backend: backend, EncryptionKey: key})
}

// newExampleStorage stands in for the application's user table with one user, ada.
func newExampleStorage() (*password.MemoryStorage, error) {
	storage := password.NewMemoryStorage()
	hash, err := password.Hash("correct horse battery staple", password.DefaultParameters())
	if err != nil {
		return nil, err
	}
	ada := &password.User{ID: uuid.MustParse("6f1c2a4e-8d3b-4f5a-9c7e-1b2d3e4f5a6b"), Username: new("ada"), PasswordHash: hash}
	return storage, storage.CreateUser(context.Background(), ada)
}

// send serves request on handler with the cookies an earlier response set, as a browser would.
func send(handler http.Handler, request *http.Request, earlier *httptest.ResponseRecorder) *httptest.ResponseRecorder {
	if earlier != nil {
		for _, line := range earlier.Header().Values("Set-Cookie") {
			if cookie, err := http.ParseSetCookie(line); err == nil {
				request.AddCookie(cookie)
			}
		}
	}
	recorder := httptest.NewRecorder()
	handler.ServeHTTP(recorder, request)
	return recorder
}

func main() {
	sessions, err := newExampleSessions()
	if err != nil {
		fmt.Println(err)
		return
	}
	storage, err := newExampleStorage()
	if err != nil {
		fmt.Println(err)
		return
	}
	provider, err := password.New(&password.Options{Sessions: sessions, Storage: storage})
	if err != nil {
		fmt.Println(err)
		return
	}
	// The provider is the Guard's Refresher, so deleting a user ends their sessions within a minute.
	guard, err := session.NewGuard(&session.GuardOptions{Store: sessions, Refreshers: []session.Refresher{provider}})
	if err != nil {
		fmt.Println(err)
		return
	}
	serveMux := http.NewServeMux()
	auth.RegisterRoutes(serveMux, provider.Routes())
	serveMux.Handle("GET /auth/me", guard.RequireAPI()(auth.IdentityHandler()))

	loginRequest := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/auth/password/login",
		strings.NewReader(`{"identifier":"ada","password":"correct horse battery staple"}`))
	loginRequest.Header.Set("Content-Type", "application/json")
	login := send(serveMux, loginRequest, nil)
	currentUser := send(serveMux, httptest.NewRequestWithContext(context.Background(), http.MethodGet, "/auth/me", nil), login)

	fmt.Print(login.Code, " ", login.Body.String())
	fmt.Print(currentUser.Code, " ", currentUser.Body.String())
}
Output:
200 {"provider":"password","subject":"6f1c2a4e-8d3b-4f5a-9c7e-1b2d3e4f5a6b","userId":"6f1c2a4e-8d3b-4f5a-9c7e-1b2d3e4f5a6b","username":"ada","active":true,"roles":[]}
200 {"provider":"password","subject":"6f1c2a4e-8d3b-4f5a-9c7e-1b2d3e4f5a6b","userId":"6f1c2a4e-8d3b-4f5a-9c7e-1b2d3e4f5a6b","username":"ada","active":true,"roles":[]}

func (*Provider) Name

func (p *Provider) Name() string

Name returns "password", the Identity.Provider of every identity this provider creates.

func (*Provider) Refresh

func (p *Provider) Refresh(ctx context.Context, _ session.Session, identity *auth.Identity) (*auth.Identity, error)

Refresh implements session.Refresher: it re-reads the user by the identity's UserID (the Storage user ID, which OnLogin must keep) and updates Username and Email, keeping every other field. A deleted user is ErrUnauthenticated, which ends the session.

func (*Provider) Routes

func (p *Provider) Routes() []auth.Route

Routes returns the provider's endpoints below PathPrefix: login and logout always, register with EnableRegistration, the TOTP endpoints with TOTP.Enabled. Register them with auth.RegisterRoutes or ginadapter.RegisterRoutes.

Example
package main

import (
	"fmt"

	"github.com/govengers/auth/password"
	"github.com/govengers/auth/session"
)

// newExampleSessions builds a session store on the in-process backend; production code shares a Redis
// backend between replicas and reads the key from its configuration.
func newExampleSessions() (*session.Store, error) {
	backend, err := session.NewMemoryBackend(nil)
	if err != nil {
		return nil, err
	}
	key, err := session.ParseKey("MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=")
	if err != nil {
		return nil, err
	}
	return session.NewStore(&session.StoreOptions{Backend: backend, EncryptionKey: key})
}

func main() {
	sessions, err := newExampleSessions()
	if err != nil {
		fmt.Println(err)
		return
	}
	provider, err := password.New(&password.Options{
		Sessions:           sessions,
		Storage:            password.NewMemoryStorage(),
		EnableRegistration: true,
		TOTP:               password.TOTPOptions{Enabled: true, Issuer: "Example"},
	})
	if err != nil {
		fmt.Println(err)
		return
	}
	for _, route := range provider.Routes() {
		fmt.Println(route.Method, route.Path)
	}
}
Output:
POST /auth/password/login
POST /auth/password/logout
POST /auth/password/register
POST /auth/password/totp/verify
GET /auth/password/totp
POST /auth/password/totp/setup
POST /auth/password/totp/enable
POST /auth/password/totp/disable

type Storage

type Storage interface {
	// CreateUser stores a new user. A username or email another user already has, ignoring case, is
	// ErrUserExists; let a unique constraint decide it (e.g. on lower(email)), not a lookup before the insert.
	CreateUser(ctx context.Context, user *User) error
	// GetUserByUsername returns the user with username, ignoring case (e.g. WHERE lower(username) = lower($1)),
	// or ErrUserNotFound. The provider passes identifiers as typed.
	GetUserByUsername(ctx context.Context, username string) (*User, error)
	// GetUserByEmail returns the user with email, ignoring case, or ErrUserNotFound.
	GetUserByEmail(ctx context.Context, email string) (*User, error)
	// GetUserByID returns the user with id, or ErrUserNotFound.
	GetUserByID(ctx context.Context, id uuid.UUID) (*User, error)
	// UpdatePasswordHash replaces the password hash only while the stored one still equals currentHash
	// (compare-and-swap, e.g. UPDATE ... WHERE id = $1 AND password_hash = $2); a mismatch is not an error, so a
	// concurrent password change is never overwritten.
	UpdatePasswordHash(ctx context.Context, id uuid.UUID, currentHash, newHash string) error
	// ConsumeTOTPAttempt reserves one second-factor attempt before a code is checked: below maximum it
	// increments the counter and returns the attempts left after this one (never negative); otherwise it changes
	// nothing and returns ErrTOTPAttemptsExhausted. Check and increment are one atomic step, e.g.
	// UPDATE ... SET attempts = attempts + 1 WHERE id = $1 AND attempts < $2 RETURNING $2 - attempts.
	ConsumeTOTPAttempt(ctx context.Context, id uuid.UUID, maximum int) (remaining int, err error)
	// ResetTOTPAttempts sets the attempt counter to zero. The provider calls it after a verified second factor;
	// the application calls it to unlock a user whose attempts are exhausted.
	ResetTOTPAttempts(ctx context.Context, id uuid.UUID) error
	// ConsumeTOTPStep marks the TOTP time step starting at step as the last one the user used: when step is later
	// than the step it holds (or it holds none), it records step and returns true; otherwise it changes nothing
	// and returns false, so a code is accepted once and never after a later one. Check and write are one atomic
	// step, e.g. UPDATE ... SET totp_step = $2 WHERE id = $1 AND (totp_step IS NULL OR totp_step < $2).
	// SetTOTP and ClearTOTP keep it.
	ConsumeTOTPStep(ctx context.Context, id uuid.UUID, step time.Time) (bool, error)
	// ConsumeBackupCode removes hash from BackupCodeHashes and reports whether it was there; of two concurrent
	// calls with the same hash at most one returns true.
	ConsumeBackupCode(ctx context.Context, id uuid.UUID, hash string) (bool, error)
	// SetTOTP enables TOTP with secret and backupCodeHashes and resets the attempt counter, but only while TOTP is
	// disabled: it reports whether it did, and changes nothing for a user who has TOTP enabled, so a concurrent
	// enrollment never replaces the second factor another one just set. Check and write are one atomic step, e.g.
	// UPDATE ... SET totp_enabled = true, ... WHERE id = $1 AND NOT totp_enabled.
	SetTOTP(ctx context.Context, id uuid.UUID, secret string, backupCodeHashes []string) (bool, error)
	// ClearTOTP disables TOTP, clears the secret and the backup codes and resets the attempt counter.
	ClearTOTP(ctx context.Context, id uuid.UUID) error
}

Storage persists users; the application implements it. Every method is atomic on its own and safe for concurrent use. The TOTP state (TOTPEnabled, TOTPSecret, BackupCodeHashes, plus a failed-attempt counter and the last used time step the storage keeps) changes only through the TOTP methods below.

type TOTPOptions

type TOTPOptions struct {
	Enabled            bool          // routes the TOTP endpoints and challenges users who enabled TOTP; while off, their logins fail (500)
	Required           bool          // users without TOTP enroll during login; needs Enabled
	Issuer             string        // required when Enabled (otpauth issuer)
	Period             uint          // default 30 s; at most 3600
	Digits             uint          // 6 or 8; default 6
	Skew               uint          // periods accepted before and after the current one; default 1; at most 10
	MaximumAttempts    int           // default 5 wrong codes in a row, across logins; then the second factor stays locked until Storage.ResetTOTPAttempts
	PendingTTL         time.Duration // default 5 min (password step to code step)
	DisableBackupCodes bool
	BackupCodeCount    int    // default 10; at most 20
	BackupCodeLength   int    // default 10; at most 128 bytes per code and at least a million distinct codes
	BackupCodeAlphabet string // default "0123456789"; at least 2 distinct runes, no white space; stored lower-case, repeated runes count once
}

TOTPOptions configure the second factor.

type User

type User struct {
	ID               uuid.UUID
	Username         *string // nil when the user has none; at least one of Username and Email is set
	Email            *string
	PasswordHash     string   // Argon2id (Hash), or a legacy hash Options.LegacyVerifier understands
	TOTPEnabled      bool     // set by SetTOTP, cleared by ClearTOTP
	TOTPSecret       *string  // base32 TOTP secret while TOTPEnabled
	BackupCodeHashes []string // Argon2id hashes of the unused backup codes
}

User is the record the provider reads from Storage. The library never deletes users and never writes profile fields; keep everything else about a user (creation time, roles, flags) in the application's own tables.

Jump to

Keyboard shortcuts

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