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

auth

package
v0.30.0 Latest Latest
Warning

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

Go to latest
Published: Oct 19, 2026 License: MIT Imports: 23 Imported by: 1

Documentation

Overview

Package auth authenticates requests: guards (session for browsers, access tokens for APIs), user providers, middleware and password hashing (auth/hash).

A guard (SessionGuard, TokenGuard, or jwt.Guard in auth/jwt) loads users through a UserProvider such as ORMProvider. Routes opt in with the Required, Guest, Optional, Verified and Ability middleware, and handlers read the user with User. Apps scaffold the whole flow with `vukem install:auth`.

See https://github.com/AlexGalhardo/vukem/blob/main/docs/authentication.md.

Index

Examples

Constants

View Source
const IntendedKey = "url.intended"

IntendedKey is the session key holding the URL a guest tried to open; login handlers redirect there (web.SafeRedirect) after success.

Variables

View Source
var (
	// ErrUnauthenticated is 401 (WWW-Authenticate is added by middleware).
	ErrUnauthenticated = &errs.HTTPError{
		Status: http.StatusUnauthorized, Message: "Unauthenticated.", Key: "auth.unauthenticated",
	}
	// ErrUserNotFound is returned by providers for unknown users.
	ErrUserNotFound = errors.New("auth: user not found")
)

Errors returned by guards.

Functions

func Ability

func Ability(guard Checker, abilities ...string) func(http.Handler) http.Handler

Ability requires the request's access token (from guard) to grant every ability; 403 otherwise (problem+json when guard is an APIGuard or the route is under ProblemJSON). Place it after Required.

func BearerToken added in v0.28.0

func BearerToken(r *http.Request) (token string, ok bool)

BearerToken returns the credential of an "Authorization: Bearer <token>" header, trimmed. The scheme is matched case-insensitively (RFC 7235 §2.1), so "bearer" and "BEARER" work too; ok is false without a bearer header or with an empty token.

func Guest

func Guest(guards ...Checker) func(http.Handler) http.Handler

Guest sends authenticated users to the route named "home" (or "/"); use it on login and register pages.

func Optional added in v0.14.0

func Optional(guards ...Checker) func(http.Handler) http.Handler

Optional authenticates the request when a guard can (so handlers, policies and templates see the user) and never blocks it.

func Principal added in v0.24.0

func Principal(ctx context.Context) (guard, id string, ok bool)

Principal returns the guard and user id of the user most recently authenticated on this request by any guard, for code that must key data by caller without knowing the guard (api.Idempotency, ratelimit.ByUser). Combine the two unambiguously (length-prefixed or hashed): either may contain any character.

func ProblemJSON added in v0.27.0

func ProblemJSON(next http.Handler) http.Handler

ProblemJSON is route-group middleware for APIs whose authentication is not only [APIGuard]s: a session fallback next to a token guard, or a custom authenticator or scope check. Mount it first, before every middleware it should affect (g.Use(auth.ProblemJSON, auth.Required(api, web))): middleware that runs before it is not affected, so Required mounted ahead of it still redirects browsers. Below it, Required, Ability and Verified never redirect to the login or verification pages, and every 401 and 403 that middleware renders (the auth middleware, or custom middleware rejecting through web.Fail or web.ServicesFrom) is an RFC 9457 problem+json, whatever the Accept header says, like the built-in API guards. That covers any 401 or 403 from middleware below it, not only authentication: a CSRF or rate-limit 403 is problem+json too. Only the error path sees the forced Accept: other statuses and the handlers keep negotiating with the client's header. Handler errors go through the router's error handler as usual; an API handler that returns a 401 or 403 should answer JSON itself.

func Required

func Required(guards ...Checker) func(http.Handler) http.Handler

Required lets the request through when any guard authenticates it. JSON and API clients get 401 (with WWW-Authenticate: Bearer when a token guard is involved); browsers are redirected to the route named "login", and the intended URL is remembered for GET requests. When every guard is an APIGuard (token and JWT guards), nobody is redirected: the 401 is always problem+json, whatever the Accept header or session cookie says; the same holds for any guards under ProblemJSON, which must be mounted before Required (Required mounted first never sees it and redirects).

Example
package main

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

	"github.com/AlexGalhardo/vukem/auth"
	"github.com/AlexGalhardo/vukem/router"
	"github.com/AlexGalhardo/vukem/web"
)

type exUser struct{ ID, Hash string }

func (u exUser) AuthID() string       { return u.ID }
func (u exUser) AuthPassword() string { return u.Hash }

// exHeaderGuard stands in for auth.NewSessionGuard or auth.NewTokenGuard,
// which need a database.
type exHeaderGuard struct{}

func (exHeaderGuard) Name() string { return "demo" }

func (exHeaderGuard) Check(c *web.Ctx) bool {
	id := c.Request().Header.Get("X-Demo-User")
	if id == "" {
		return false
	}
	auth.SetUser(c, "demo", exUser{ID: id})
	return true
}

func main() {
	guard := exHeaderGuard{}
	r := router.New(nil)
	r.Get("/me", func(c *web.Ctx) error {
		u, err := auth.User[exUser](c, guard.Name())
		if err != nil {
			return err
		}
		return c.String(http.StatusOK, "hello "+u.ID)
	}).Use(auth.Required(guard))

	anon := httptest.NewRequest(http.MethodGet, "/me", http.NoBody)
	anon.Header.Set("Accept", "application/json")
	rec := httptest.NewRecorder()
	r.ServeHTTP(rec, anon)
	fmt.Println(rec.Code)

	signedIn := httptest.NewRequest(http.MethodGet, "/me", http.NoBody)
	signedIn.Header.Set("X-Demo-User", "42")
	rec = httptest.NewRecorder()
	r.ServeHTTP(rec, signedIn)
	fmt.Println(rec.Code, rec.Body.String())
}
Output:
401
200 hello 42

func SessionValues added in v0.25.0

func SessionValues(guard string, user interface{ AuthID() string }) map[string]any

SessionValues returns the session values SessionGuard.Login stores for user on the guard named guard: the user id, the owner under web.SessionUserKey and, for a SessionVersioner, its session version. Tests use it to start a logged-in session (vukemtest Client.ActingAs).

func SetUser added in v0.18.0

func SetUser(c *web.Ctx, guard string, u any)

SetUser records u as the user guard authenticated for this request, for guards implemented outside this package (auth/jwt).

func TokenCan

func TokenCan(c *web.Ctx, guard, ability string) bool

TokenCan reports whether the request's token (any token guard) grants ability.

func TokensRevokedAtMigration added in v0.26.0

func TokensRevokedAtMigration(id string) schema.Migration

TokensRevokedAtMigration returns migration id adding the revoked_at column to an access_tokens table created before v0.26.0. It skips a table that already has it (the current install:auth scaffold), and its Down is a no-op, since the column may predate it.

func User

func User[U Authenticatable](c *web.Ctx, guard string) (U, error)

User returns the user authenticated by guard for this request (after Required or a guard's Check ran).

func UserFrom added in v0.6.0

func UserFrom[U any](ctx context.Context, guard string) (U, error)

UserFrom returns the user authenticated by guard from a request context (authz gates, view functions).

func Verified

func Verified(guard Checker) func(http.Handler) http.Handler

Verified requires the user authenticated by guard to have a verified email. Signed-in browsers without one are redirected to the route named "verification.notice" when the app has it (the install:auth scaffold does); JSON clients, routes under ProblemJSON and apps without that route get 403. Place it after Required.

Types

type APIGuard added in v0.25.0

type APIGuard interface {
	Checker
	// APIOnly marks the guard; it is never called.
	APIOnly()
}

APIGuard is a guard for API clients that authenticate every request with a credential they carry (TokenGuard, jwt.Guard). Required and Ability answer problem+json and never redirect when their guards are APIGuards. A custom guard opts in by implementing APIOnly; for a route group that mixes in a session guard or custom middleware, use ProblemJSON.

type Authenticatable

type Authenticatable interface {
	// AuthID is the user's primary key as a string.
	AuthID() string
	// AuthPassword is the stored password hash.
	AuthPassword() string
}

Authenticatable is a user model.

type Checker

type Checker interface {
	// Name identifies the guard ("web", "api").
	Name() string
	// Check reports whether c is authenticated, caching the user in c.
	Check(c *web.Ctx) bool
}

Checker is the non-generic part of a guard, used by middleware.

type FailClosedChecker added in v0.28.0

type FailClosedChecker interface {
	Checker
	Authenticate(c *web.Ctx) error
}

FailClosedChecker is a guard that tells a missing or bad credential apart from a check that could not run. Authenticate returns nil when the request is authenticated, an error wrapping ErrUnauthenticated for a missing, wrong or expired credential, and any other error (a store or key-set outage, ideally an errs.HTTPError with status 503) when it could not decide. Required renders that error and stops: it neither tries the next guard nor treats the caller as a guest. Guards outside this package (jwt.Guard, custom guards) implement it to fail closed like SessionGuard and TokenGuard.

type LoginOption

type LoginOption func(*loginOpts)

LoginOption tunes Attempt and Login.

func Remember

func Remember(on bool) LoginOption

Remember issues a remember-me cookie on login.

type ORMProvider

type ORMProvider[U Authenticatable] struct {
	PasswordColumn string
	// contains filtered or unexported fields
}

ORMProvider loads users of model U with the ORM: ByID by primary key, ByCredentials by every credential except "password" (column names are validated against U). Its PasswordColumn (default "password") receives rehashed passwords.

func NewORMProvider

func NewORMProvider[U Authenticatable](ex database.Executor) *ORMProvider[U]

NewORMProvider returns the default provider for model U.

func (*ORMProvider[U]) ByCredentials

func (p *ORMProvider[U]) ByCredentials(ctx context.Context, creds map[string]string) (U, error)

ByCredentials implements UserProvider.

func (*ORMProvider[U]) ByID

func (p *ORMProvider[U]) ByID(ctx context.Context, id string) (U, error)

ByID implements UserProvider.

func (*ORMProvider[U]) UpdatePassword

func (p *ORMProvider[U]) UpdatePassword(ctx context.Context, u U, hash string) error

UpdatePassword implements PasswordUpdater.

type PasswordUpdater

type PasswordUpdater[U Authenticatable] interface {
	UpdatePassword(ctx context.Context, u U, hash string) error
}

PasswordUpdater is implemented by providers that can store a rehashed password after login (hash.Hasher.NeedsRehash).

type RememberStore

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

RememberStore keeps remember-me tokens: the cookie holds "<id>.<secret>" and only SHA-256(secret) is stored. Table (created by install:auth): remember_tokens(id TEXT PRIMARY KEY, user_id TEXT, hash TEXT, expires_at TIMESTAMP).

func NewRememberStore

func NewRememberStore(ex database.Executor) *RememberStore

NewRememberStore uses the remember_tokens table.

type SessionGuard

type SessionGuard[U Authenticatable] struct {

	// IdentifierField is where failed-login errors are reported ("email").
	IdentifierField string
	// RememberFor is the remember-me cookie lifetime (default 30 days).
	RememberFor time.Duration
	// InsecureCookies drops the Secure flag on the remember-me cookie
	// (plain-HTTP development without localhost only).
	InsecureCookies bool
	// contains filtered or unexported fields
}

SessionGuard authenticates browsers with the session: the user id is stored under "auth_<name>" after login.

func NewSessionGuard

func NewSessionGuard[U Authenticatable](name string, p UserProvider[U], h hash.Hasher,
	remember *RememberStore,
) *SessionGuard[U]

NewSessionGuard returns the "web" style guard. remember may be nil to disable remember-me. It panics when *U implements SessionVersioner but U does not (AuthSessionVersion on a pointer receiver while the guard holds values): the version would never be checked, so sessions could not be ended.

func (*SessionGuard[U]) Attempt

func (g *SessionGuard[U]) Attempt(c *web.Ctx, creds map[string]string, opts ...LoginOption) (U, error)

Attempt checks creds (identifier columns + "password") and logs the user in. It returns a 422 validation error on the identifier field when they do not match, taking the same time whether or not the user exists.

func (*SessionGuard[U]) Check

func (g *SessionGuard[U]) Check(c *web.Ctx) bool

Check implements Checker: session user, else a valid remember-me cookie.

func (*SessionGuard[U]) Login

func (g *SessionGuard[U]) Login(c *web.Ctx, u U, opts ...LoginOption) error

Login logs u in: the session id is regenerated (fixation) and the user id stored, also under web.SessionUserKey so server-side session stores can revoke the user's other sessions (session.Manager.RevokeOthers).

func (*SessionGuard[U]) Logout

func (g *SessionGuard[U]) Logout(c *web.Ctx) error

Logout clears the session (new id, no data) and the remember-me token.

func (*SessionGuard[U]) Name

func (g *SessionGuard[U]) Name() string

Name implements Checker.

func (*SessionGuard[U]) RefreshSession added in v0.25.0

func (g *SessionGuard[U]) RefreshSession(c *web.Ctx) error

RefreshSession reloads the logged-in user and records its current session version in this session, and issues a new remember-me token when the request carries one: call it after bumping the version when the current session must stay signed in (two-factor authentication turned on, password changed: the others end).

func (*SessionGuard[U]) SessionUserID added in v0.24.0

func (g *SessionGuard[U]) SessionUserID(id string) string

SessionUserID is the owner recorded with server-side sessions for the user id (web.SessionUserKey): "<guard>:<id>", so guards over different user tables never share an owner. Pass it to session.Manager.RevokeOthers.

func (*SessionGuard[U]) User

func (g *SessionGuard[U]) User(c *web.Ctx) (U, error)

User returns the logged-in user (cached for the request) or ErrUnauthenticated.

func (*SessionGuard[U]) Validate added in v0.10.0

func (g *SessionGuard[U]) Validate(c *web.Ctx, creds map[string]string) (U, error)

Validate checks creds like Attempt without logging in (API token issuance). Unknown users cost the same hash verification and get the same 422 error; a stale hash is upgraded after a successful check.

type SessionVersioner added in v0.25.0

type SessionVersioner interface {
	AuthSessionVersion() int64
}

SessionVersioner is implemented by users whose browser sessions can all be ended at once by changing a number kept with the user (a session_version column): SessionGuard records it on login and signs the session out when it no longer matches, on every session driver, encrypted cookies included. Bump it on a password reset or change; it costs nothing per request, since the guard loads the user anyway.

type Token

type Token struct {
	ID         string     `json:"id"`
	UserID     string     `json:"user_id"`
	Name       string     `json:"name"`
	Abilities  []string   `json:"abilities"`
	LastUsedAt *time.Time `json:"last_used_at"`
	ExpiresAt  *time.Time `json:"expires_at"`
	// RevokedAt is when the token was revoked; a revoked token is kept for
	// the audit trail (List shows it) until Prune deletes it.
	RevokedAt *time.Time `json:"revoked_at"`
}

Token describes an access token (never the secret).

func CurrentToken

func CurrentToken(c *web.Ctx, guard string) (Token, bool)

CurrentToken returns the token that authenticated c with guard.

func (Token) Can

func (t Token) Can(ability string) bool

Can reports whether the token grants ability ("*" grants everything).

type TokenGuard

type TokenGuard[U Authenticatable] struct {
	// contains filtered or unexported fields
}

TokenGuard authenticates "Authorization: Bearer <token>" requests, plus any header WithTokenHeaders names.

func NewTokenGuard

func NewTokenGuard[U Authenticatable](name string, p UserProvider[U], tokens *Tokens) *TokenGuard[U]

NewTokenGuard returns the "api" style guard. It reads the token from "Authorization: Bearer <token>" only; WithTokenHeaders adds more headers.

func (*TokenGuard[U]) APIOnly added in v0.25.0

func (*TokenGuard[U]) APIOnly()

APIOnly implements APIGuard.

func (*TokenGuard[U]) Check

func (g *TokenGuard[U]) Check(c *web.Ctx) bool

Check implements Checker.

func (*TokenGuard[U]) Name

func (g *TokenGuard[U]) Name() string

Name implements Checker.

func (*TokenGuard[U]) User

func (g *TokenGuard[U]) User(c *web.Ctx) (U, error)

User returns the token's user (cached for the request), ErrUnauthenticated for a missing, unknown, wrong, expired or revoked token, or a 503 error when the token or user lookup fails (database down, access_tokens without revoked_at), so an outage is not reported as bad credentials. The token comes from "Authorization: Bearer <token>" and from the headers of WithTokenHeaders, which the response's Vary then names.

func (*TokenGuard[U]) WithTokenHeaders added in v0.29.0

func (g *TokenGuard[U]) WithTokenHeaders(names ...string) *TokenGuard[U]

WithTokenHeaders also accepts the token from each named header, such as the "X-API-Key" convention, and returns g so it can be chained onto NewTokenGuard. Such a header carries the token alone, without a scheme.

Precedence: a bearer token in "Authorization" always wins, so opting in never changes what a request that already worked resolves to; the named headers are then tried in the order given, and one sent on more than one line is skipped rather than guessed at. Every named header is added to the response's Vary on every request the guard sees, carrying it or not, so a shared cache cannot serve one key's response to a request holding another. "Authorization" needs no Vary entry: RFC 9111 §3.5 already keeps shared caches from storing a response to a request that carries it.

No extra header is accepted without this call. It panics on a name that is not an HTTP field name, on "Authorization" (always read, and only as "Bearer <token>") and on "Cookie", since a token the browser attaches by itself would authenticate every cross-site request.

type Tokens

type Tokens struct {
	// Prefix starts every newly issued token ("vk_"), so secret scanners
	// (GitHub push protection, gitleaks) can match leaked tokens. It is up
	// to 32 letters, digits or underscores and ends with "_". Tokens issued
	// without it, or under an earlier prefix, stay valid.
	Prefix string
	// Now is the clock for issue, expiry and last-use times (default
	// time.Now). Set it to app.Now so a fake clock (vukem.WithClock,
	// vukemtest.Options.Clock) drives token expiry.
	Now func() time.Time
	// Log receives the one-time hint that access_tokens lacks revoked_at
	// (default slog.Default()); the token guard uses the request logger.
	Log *slog.Logger
	// contains filtered or unexported fields
}

Tokens issues and checks opaque access tokens "<prefix><id>.<secret>"; only SHA-256(secret) is stored. Table (created by install:auth / install:api): access_tokens(id TEXT PRIMARY KEY, user_id TEXT, name TEXT, hash TEXT, abilities TEXT, last_used_at TIMESTAMP NULL, expires_at TIMESTAMP NULL, revoked_at TIMESTAMP NULL, created_at TIMESTAMP). Tables created before revoked_at existed need TokensRevokedAtMigration; until then Find refuses every token.

func NewTokens

func NewTokens(ex database.Executor) *Tokens

NewTokens uses the access_tokens table.

func (*Tokens) Create

func (t *Tokens) Create(
	ctx context.Context,
	userID, name string,
	abilities []string,
	ttl time.Duration,
) (string, Token, error)

Create issues a token for userID and returns the plain value, shown to the client once. ttl 0 means the token never expires and stays valid until revoked; pass a ttl unless that is intended (the install:api scaffold issues 30-day tokens).

func (*Tokens) Find

func (t *Tokens) Find(ctx context.Context, plain string) (Token, error)

Find validates a plain token and returns it, or an error for an unknown, wrong, expired or revoked token (or a failed lookup). Every token pays for the same digest comparison before its state is checked. It updates last_used_at at most once a minute.

func (*Tokens) List added in v0.22.0

func (t *Tokens) List(ctx context.Context, userID string) ([]Token, error)

List returns userID's tokens, newest first, revoked ones included with RevokedAt set (never the secrets). Every Token carries the UserID stored on its row, like Find, so the owner does not depend on what the caller asked for.

func (*Tokens) Prune added in v0.26.0

func (t *Tokens) Prune(ctx context.Context, cutoff time.Time) (int64, error)

Prune deletes the tokens revoked or expired before cutoff and returns how many it deleted. Schedule it to bound the table while keeping recent revocations visible in List. Expiry is compared with the earlier of cutoff and now, so a cutoff in the future never deletes a live token.

func (*Tokens) Revoke

func (t *Tokens) Revoke(ctx context.Context, id string) error

Revoke marks a token revoked by id without checking who owns it; the row stays (List shows it) until Prune deletes it, and a second revoke keeps the first time. Use RevokeFor when the id comes from user input (a "revoke" button in a dashboard).

func (*Tokens) RevokeAll

func (t *Tokens) RevokeAll(ctx context.Context, userID string) error

RevokeAll revokes every live token of userID.

func (*Tokens) RevokeFor added in v0.22.0

func (t *Tokens) RevokeFor(ctx context.Context, userID, id string) (bool, error)

RevokeFor revokes token id only if it belongs to userID and reports whether a live token was revoked (false for another user's token, an unknown id or one already revoked), so a caller cannot revoke another user's token by guessing its id.

type UserProvider

type UserProvider[U Authenticatable] interface {
	ByID(ctx context.Context, id string) (U, error)
	ByCredentials(ctx context.Context, creds map[string]string) (U, error)
}

UserProvider loads users. ByCredentials never checks the password; the guard does, in constant time.

ByID must report a missing user as ErrUserNotFound (wrap it, or the orm.ErrNotFound / sql.ErrNoRows the lookup returned): the session guard forgets the session only then, and answers any other error with 503 while keeping the session, so returning a generic error for "not found" leaves a deleted user's session failing instead of logging out.

type Verifiable

type Verifiable interface {
	EmailVerified() bool
}

Verifiable is a user whose email address can be verified.

Directories

Path Synopsis
Package hash hashes and verifies passwords: argon2id by default (PHC string format), bcrypt for compatibility.
Package hash hashes and verifies passwords: argon2id by default (PHC string format), bcrypt for compatibility.
Package jwt issues and verifies JSON Web Tokens (RFC 7519) with the standard library: HS256, RS256, ES256 and EdDSA, an algorithm allowlist tied to each key (no algorithm confusion, never "none"), key ids with static or remote (JWKS) key sets that rotate, strict claim checks, a Guard for package auth, and rotating refresh tokens with reuse detection.
Package jwt issues and verifies JSON Web Tokens (RFC 7519) with the standard library: HS256, RS256, ES256 and EdDSA, an algorithm allowlist tied to each key (no algorithm confusion, never "none"), key ids with static or remote (JWKS) key sets that rotate, strict claim checks, a Guard for package auth, and rotating refresh tokens with reuse detection.
Package totp implements time-based one-time passwords (RFC 6238, the codes of authenticator apps) for two-factor authentication: secrets, otpauth:// enrolment URIs with an SVG QR code, validation with one step of clock skew and replay protection, and hashed single-use recovery codes.
Package totp implements time-based one-time passwords (RFC 6238, the codes of authenticator apps) for two-factor authentication: secrets, otpauth:// enrolment URIs with an SVG QR code, validation with one step of clock skew and replay protection, and hashed single-use recovery codes.

Jump to

Keyboard shortcuts

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