authkit

package module
v2.0.0 Latest Latest
Warning

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

Go to latest
Published: Jul 19, 2026 License: MIT Imports: 38 Imported by: 0

README

CI CodeQL Coverage Status Go Report Card GitHub release (latest by date)

authkit

Batteries-included authentication and RBAC for Go HTTP services, built on net/http (Go 1.22+ routing) with storage-agnostic interfaces.

  • OAuth 2.0 via markbates/goth — GitHub, Google, GitLab, Bitbucket built in, 80+ more via GothProviders
  • Email/password authentication with a pluggable PasswordHasher (bcrypt default)
  • Revocable server-side sessions (optional) — opaque-ID cookie, idle + absolute timeouts, session fixation rotation, "log out everywhere"; falls back to encrypted cookie sessions
  • Two-factor auth (TOTP) — per-role enforcement, pending/confirmed enrollment, single-use recovery codes, optional anti-replay, "trust this device"
  • OAuth2 token layer for native clients — Authorization Code + PKCE, Ed25519-signed access JWTs with key rotation + JWKS, rotating opaque refresh tokens with reuse detection, cookie-free password grant
  • API keys — plug in any key store via a single-method interface
  • Device principals — machine clients (agents, kiosks, IoT) confined to a host-declared capability allow-list, isolated from human credential paths
  • Platform-operator axis — separate super-admin principals for multi-tenant SaaS, mandatory TOTP, audited break-glass single-tenant impersonation
  • Multi-tenant awareTenantID on every principal, tenant-scoped permission resolution, fail-closed tenant context helpers; single-tenant apps just leave it empty
  • Host-defined principal attributes — an Attrs map that round-trips through sessions and JWT claims for your own scoping (org unit, locale, plan tier)
  • RBAC — YAML file, layered YAML+database, or fully custom PolicyProvider; optional live per-request permission resolution with TTL cache; live policy reload
  • CSRF protection — signed double-submit middleware, optional trusted-origin check
  • Login throttling — pluggable per-account+IP rate limiting with Retry-After
  • Password reset — single-use hashed tokens, delivery-channel agnostic, no user enumeration
  • Audit sink — structured security events (login, logout, refresh, revoke, 2FA, reset, impersonate)
  • Structured logging via log/slog; secrets never logged
  • Uniform JSON errors — every error carries a stable machine-readable code; rendering is replaceable via ErrorWriter
  • JSON or form bodies on every endpoint
  • Ready-made Redis stores — the redisstore module implements sessions, throttling, trusted devices, and PKCE code claims

Migrating from v1? See MIGRATION.md.


Installation

go get github.com/tlmanz/authkit/v2
go get github.com/tlmanz/authkit/redisstore/v2   # optional Redis stores

Quick Start

1. Define your policy (policy.yaml)
roles:
  admin:
    permissions: ["*"]         # wildcard grants every permission
    members:
      - alice@company.com

  developer:
    permissions: ["view", "upload"]
    members:
      - bob@company.com

  viewer:
    permissions: ["view"]

# Fallback role for authenticated users not listed in any role.
# Omit to deny access to unlisted users entirely.
default_role: viewer
2. Construct
import authkit "github.com/tlmanz/authkit/v2"

auth, err := authkit.New(authkit.Config{
    Mode:          authkit.AuthModeBoth, // OAuth + password (default: OAuth only)
    SessionSecret: os.Getenv("SESSION_SECRET"), // >= 32 random bytes
    SecureCookie:  true,                        // production (HTTPS)
    AfterLoginURL: "/dashboard",
    OAuth: authkit.OAuthConfig{
        Providers: []authkit.ProviderConfig{
            {Name: "github", ClientID: os.Getenv("GITHUB_CLIENT_ID"), ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET")},
        },
        CallbackBaseURL: "https://example.com",
    },
    UserStore: myUserStore, // implements authkit.UserStore (password mode)
    RBAC:      authkit.RBACConfig{FilePath: "policy.yaml"},
})
3. Wire up routes
mux := http.NewServeMux()

// OAuth routes (when OAuth is enabled)
mux.HandleFunc("GET /auth/{provider}",          auth.BeginAuth)
mux.HandleFunc("GET /auth/{provider}/callback", auth.Callback)

// Password routes (when password auth is enabled)
mux.HandleFunc("POST /auth/register", auth.Register)
mux.HandleFunc("POST /auth/login",    auth.Login)

// Common routes
mux.HandleFunc("POST /auth/logout", auth.Logout)
mux.HandleFunc("GET /auth/me",      auth.Me)

// Protected routes
mux.Handle("GET /api/reports",   auth.RequireAuth(http.HandlerFunc(reportsHandler)))
mux.Handle("POST /api/projects", auth.Require("projects:write")(http.HandlerFunc(createHandler)))

Every POST endpoint accepts either application/x-www-form-urlencoded or an application/json body with the same field names.


HTTP routes

Method Path Handler Feature
GET /auth/{provider} BeginAuth OAuth
GET /auth/{provider}/callback Callback OAuth
POST /auth/register Register Password
POST /auth/login Login Password
POST /auth/logout Logout All
POST /auth/logout/all LogoutEverywhere Server-side sessions
GET /auth/me Me All
POST /auth/password/change ChangePassword Password
POST /auth/password/first-change ChangeFirstPassword Must-change-password gate
POST /auth/password/forgot ForgotPassword Reset
POST /auth/password/reset ResetPassword Reset
POST /auth/2fa/enroll Enroll2FA TOTP
POST /auth/2fa/verify Verify2FA TOTP
POST /auth/2fa/confirm ConfirmTwoFactor TOTP (self-service)
POST /auth/2fa/disable DisableTwoFactor TOTP (self-service)
POST /auth/2fa/recovery/regenerate RegenerateRecoveryCodes TOTP
GET /auth/2fa/status TwoFactorStatus TOTP
GET /auth/csrf CSRFToken CSRF
GET /authorize Authorize Token layer (PKCE)
POST /token IssueToken Token layer
POST /token/refresh RefreshAccessToken Token layer
POST /oauth/token/password IssuePasswordToken Token layer (native password grant)
POST /oauth/token/2fa IssuePasswordToken2FA Token layer
GET /.well-known/jwks.json JWKS Token layer
POST /platform/login PlatformLogin Platform axis
POST /platform/2fa/enroll PlatformEnroll2FA Platform axis
POST /platform/2fa/verify PlatformVerify2FA Platform axis
POST /platform/logout PlatformLogout Platform axis
GET /platform/me PlatformMe Platform axis
POST /platform/password/forgot PlatformForgotPassword Reset
POST /platform/password/reset PlatformResetPassword Reset

Mount paths are suggestions — every handler is a plain http.HandlerFunc.


Middleware

Middleware Bearer (JWT / API key) Session Use for
RequireAuth(next) General protected routes
Require(perm)(next) Permission-gated routes
RequireSessionAuth(next) UI-only routes
RequireSession(perm)(next) Management routes that must not accept tokens
RequireDevice(cap)(next) device token only Device/agent routes
RequirePlatformAdmin(perm)(next) platform session Platform-operator routes
CSRF(next) exempt enforced State-changing cookie routes

Inside a handler:

u := authkit.UserFromCtx(r.Context())        // human principal (or nil)
tenantID, ok := authkit.TenantIDFromCtx(ctx) // fail closed when !ok
d := authkit.DeviceFromCtx(ctx)              // device principal (or nil)
p := authkit.PlatformAdminFromCtx(ctx)       // platform principal (or nil)

u.Can("projects:write") // permission check
u.Permissions()         // full resolved permission list (copy)
u.Attr("branch_id")     // host-defined attribute

Error responses

Every error authkit writes is a JSON envelope with a stable machine code:

{"error": "invalid_credentials", "error_description": "invalid email or password"}

Codes are the ErrCode* constants — unauthenticated, invalid_credentials, invalid_code, invalid_challenge, forbidden, csrf_invalid, invalid_request, password_policy, invalid_grant, conflict, rate_limited, not_enabled, server_error — and are append-only API. Clients branch and localize on the code, never the prose. To render errors differently (e.g. RFC 9457 problem+json with a trace id), set Config.ErrorWriter.

Step responses (not errors) are 200 JSON: {"status":"2fa_required", "action":"enroll"|"verify"} and {"status":"password_change_required"}.


Storage interfaces

authkit persists nothing itself. Implement only the interfaces for the features you enable:

Interface Enables Methods
UserStore password auth CreateUser, GetUserByEmail, UpdatePassword
SessionStore revocable sessions Create, Get, Touch, Revoke, RevokeAllForUser
PolicyProvider RBAC RoleFor, PermissionsForRole
LoginThrottler rate limiting Allow, RecordFailure, Reset
TOTPStore (+opt. TOTPManager, TOTPReplayGuard) 2FA Enroll, Confirm, Secret, ConsumeRecovery
TrustedDeviceStore "remember this device" Trust, IsTrusted, RevokeAllForUser
RefreshTokenStore token layer Create, Get, Rotate, RevokeChain, RevokeAllForUser
AuthCodeStore single-use PKCE codes ClaimAuthCode
APIKeyValidator API keys ValidateKey
DeviceTokenValidator device principals ValidateDeviceToken
PlatformAdminStore (+opt. PlatformTOTPReplayGuard) platform axis GetPlatformAdmin, UpdatePassword, EnrollPlatformTOTP, ConfirmPlatformTOTP, ConsumePlatformRecovery
PlatformPolicy platform axis PermissionsForPlatformRole
PasswordResetStore + ResetDelivery password reset CreateResetToken, ConsumeResetToken; SendPasswordReset
AuditSink audit log Emit
PasswordHasher custom KDF Hash, Verify

Contract details live on each interface's doc comment. Rules that matter: stores hash opaque tokens at rest; UserStore returns ErrUserExists / ErrUserNotFound sentinels; AuditSink.Emit must never block the request path.

Redis stores

The github.com/tlmanz/authkit/redisstore/v2 module (its own go.mod — the core stays Redis-free) ships production implementations of the four stores whose data is naturally self-expiring:

import (
    "github.com/redis/go-redis/v9"
    redisstore "github.com/tlmanz/authkit/redisstore/v2"
)

rc := redis.NewClient(&redis.Options{Addr: "localhost:6379"})

cfg.Sessions.Store            = redisstore.NewSessions(rc, 30*time.Minute, 24*time.Hour)
cfg.Throttler                 = redisstore.NewThrottler(rc, 5, time.Minute, 15*time.Minute, time.Hour)
cfg.TwoFactor.TrustedDevices  = redisstore.NewTrustedDevices(rc)
cfg.Tokens.AuthCodes          = redisstore.NewAuthCodes(rc)

All constructors take any redis.UniversalClient (single node, Sentinel, Cluster) and an optional redisstore.WithKeyPrefix("myapp:").


Multi-tenancy

Every principal carries an optional TenantID — the hard security boundary in a multi-tenant deployment. Your stores populate it (email is global, so the lookup determines the tenant); authkit puts it on the request context before resolving permissions:

tenantID, ok := authkit.TenantIDFromCtx(r.Context())
if !ok {
    http.Error(w, "no tenant", http.StatusForbidden) // fail closed
    return
}
// scope every query by tenantID (or set your per-transaction RLS variable)

A tenant-aware PolicyProvider reads the same context to resolve roles per tenant. Single-tenant applications leave TenantID empty everywhere and ignore all of this.

Principal attributes

User.Attrs / Session.Attrs / PasswordUser.Attrs / DeviceRecord.Attrs carry host-defined key/values (an org-unit scope, a plan tier). authkit round-trips them through sessions and the access-token attrs claim but never interprets them. Read with u.Attr("key"). Keep values small — they travel in the JWT.


Server-side sessions

Provide Sessions.Store for revocable sessions: the cookie carries only an opaque 256-bit ID and all state lives in your store.

  • Session-ID rotation on every login (fixation prevention)
  • Sliding idle renewal (Touch, throttled to once a minute) + absolute cap
  • __Host- cookie prefix when SecureCookie is true; all cookie names namespaced by CookiePrefix
  • auth.RevokeUserSessions(ctx, tenantID, email) — "log out everywhere"
  • auth.EstablishSession(ctx, w, r, u) — mint a first-class session from a host-driven login flow (SSO bridge, trial principal). The caller owns authentication; set the resolved permissions with u.SetPermissions(...).

When Sessions.Store is nil, sessions fall back to encrypted cookies (gorilla/sessions) — fine for small apps, no revocation.


Two-factor authentication (TOTP)

Users whose role is in TwoFactor.RequireForRoles must complete a TOTP challenge after the password step. Enrollment is two-phase (pending → confirmed on first successful verify) so an abandoned enrollment never locks a user out. Recovery codes are single-use and stored hashed. Optional:

  • TOTPManager on your store → self-service disable / recovery-code regeneration
  • TOTPReplayGuard on your store → each 30s time-step usable at most once
  • TwoFactor.TrustedDevices → "remember this device" skips the TOTP prompt (password still required); revoked by logout-everywhere, password change/reset, and disabling 2FA

Login responds {"status":"2fa_required","action":"enroll"|"verify"}; the client calls /auth/2fa/enroll (returns otpauthUrl, secret, recoveryCodes) and/or /auth/2fa/verify.


Token layer (native clients)

Ed25519-signed access JWTs + rotating opaque refresh tokens:

signing, _ := authkit.NewSigningKey("key-2026-01", seed) // 32 random bytes

cfg.Tokens = authkit.TokenConfig{
    Enable:       true,
    SigningKeys:  []authkit.SigningKey{signing}, // first signs; all verify (rotation)
    RefreshStore: myRefreshStore,
    AccessTTL:    15 * time.Minute,
    RefreshTTL:   30 * 24 * time.Hour,
    Issuer:       "https://api.example.com",
    ClientID:     "example-mobile",
    RedirectURIs: []string{"com.example.app://callback"},
    AuthCodes:    redisstore.NewAuthCodes(rc), // optional: single-use codes
}
  • PKCE flow (/authorize/token) for browser-mediated login; password grant (/oauth/token/password + /oauth/token/2fa) for first-party native screens — both share rotation, reuse detection, and JWKS.
  • Permissions are never in the JWT — resolved server-side per request, so role changes take effect within one access-token TTL.
  • Refresh rotation with reuse detection: replaying a spent refresh token revokes the whole chain and emits an audit event.
  • auth.MintAccessToken(u, ttl) mints a refresh-less access token for principals not backed by UserStore (ephemeral trials, service identities).

Platform-operator axis

For multi-tenant SaaS: platform admins operate the platform itself, across tenants, with no TenantID, on a separate cookie and login route. TOTP is mandatory with no role exemption.

cfg.Platform = authkit.PlatformConfig{
    Store:               myPlatformStore,
    Policy:              myPlatformPolicy, // small static capability catalog
    EnableImpersonation: true,
}

mux.Handle("GET /platform/tenants",
    auth.RequirePlatformAdmin("platform:tenants.read")(http.HandlerFunc(listTenants)))

Break-glass, audited, single-tenant support access:

ctx, err := auth.ImpersonationContext(r.Context(), admin, tenantID)
// ctx is now scoped to exactly one tenant; requires "platform:impersonate".

auth.EstablishPlatformSession exists for host-controlled flows (dev bypasses, test harnesses) — the caller takes on the authentication responsibility the built-in password+TOTP flow normally enforces.


Device principals

Machine clients (on-prem agents, kiosks, IoT) authenticate with opaque device tokens and are confined to a capability allow-list you declare in code:

cfg.Devices = authkit.DeviceConfig{
    Validator:    myDeviceValidator, // looks the token up by hash
    Capabilities: []string{"jobs:receive", "status:report"},
}

mux.Handle("GET /agent/jobs",
    auth.RequireDevice("jobs:receive")(http.HandlerFunc(jobsHandler)))

A device never resolves permissions from policy — it can never hold "*" or any role-granted capability, no matter what a role table says. RequireDevice panics at wire time on an undeclared capability. For non-HTTP channels (a WebSocket upgrade), call auth.AuthenticateDevice and bind the principal with authkit.WithDevice / authkit.WithTenant yourself.


CSRF, throttling, audit

  • CSRF: signed double-submit (CSRF.Enable), JS-readable cookie echoed in X-CSRF-Token, bearer requests exempt, optional CSRF.TrustedOrigins Origin allow-list. SPAs fetch the token from GET /auth/csrf.
  • Throttling: plug a LoginThrottler (see redisstore.NewThrottler); authkit calls it around password login, 2FA, platform login, and password reset, keyed per account+IP. Locked-out attempts get 429 + Retry-After. Client IP comes from RemoteAddr unless you set Config.ClientIP (do this behind a reverse proxy with a vetted header).
  • Audit: wire an AuditSink to receive login, logout, refresh, revoke, 2fa_*, password_*, role_change, permission_change, impersonate events. Emit must not block the request path.

RBAC options

  1. YAML fileRBAC: authkit.RBACConfig{FilePath: "policy.yaml"}; live reload with go auth.WatchRBAC(ctx, time.Minute).
  2. Layered — YAML baseline + per-user database overrides: authkit.NewLayeredProvider("policy.yaml", myUserRoleStore, authkit.WithLogger(slog.Default())).
  3. Custom PolicyProvider — anything (per-tenant role tables, an IdP). Both methods receive the tenant on ctx via authkit.TenantIDFromCtx.

Set LivePermissionResolution: true to re-resolve session permissions per request through a TTL cache (PermissionCacheTTL, default 30s), so role edits take effect without re-login. Bearer credentials always resolve live.


Configuration reference

authkit.Config{
    Mode:          authkit.AuthModeOAuth | AuthModePassword | AuthModeBoth,
    AppName:       "Acme",              // TOTP issuer etc. (default "App")
    SessionSecret: "...",               // required, >= 32 bytes
    SecureCookie:  true,                // production
    CookiePrefix:  "authkit",           // cookie namespace (default)
    AfterLoginURL: "/", AfterLogoutURL: "/",
    Logger:        slog.Default(),      // *slog.Logger (default)
    ClientIP:      nil,                 // func(*http.Request) string — proxy hook
    ErrorWriter:   nil,                 // replace the JSON error envelope

    OAuth:          authkit.OAuthConfig{...},
    UserStore:      myUserStore,
    PasswordHasher: nil,                       // default bcrypt cost 12
    PasswordPolicy: &authkit.PasswordPolicy{MinLength: 8, MaxLength: 72},
    RBAC:           authkit.RBACConfig{...},
    APIKeyValidator: myKeyStore,
    AuditSink:       myAuditSink,
    Throttler:       myThrottler,
    LivePermissionResolution: false,
    PermissionCacheTTL:       30 * time.Second,

    Sessions:  authkit.SessionConfig{Store, IdleTimeout, AbsoluteTimeout},
    CSRF:      authkit.CSRFConfig{Enable, TrustedOrigins},
    TwoFactor: authkit.TwoFactorConfig{Store, RequireForRoles, TrustedDevices, TrustedDeviceTTL},
    Tokens:    authkit.TokenConfig{Enable, SigningKeys, AccessTTL, RefreshTTL, RefreshStore, AuthCodes, Issuer, ClientID, RedirectURIs},
    Platform:  authkit.PlatformConfig{Store, Policy, EnableImpersonation},
    Reset:     authkit.ResetConfig{Store, Delivery, TTL},
    Devices:   authkit.DeviceConfig{Validator, Capabilities},
}

Other providers

authkit ships convenience wrappers for Bitbucket, GitHub, Google, and GitLab. For any of the 80+ other providers goth supports, construct the provider yourself and pass it via OAuth.GothProviders:

import "github.com/markbates/goth/providers/discord"

OAuth: authkit.OAuthConfig{
    GothProviders: []goth.Provider{
        discord.New(id, secret, "https://example.com/auth/discord/callback", "identify", "email"),
    },
    CallbackBaseURL: "https://example.com",
},

The callback URL pattern is always /auth/{providerName}/callback.


Security notes

  • Generate SessionSecret with openssl rand -hex 32; rotate deliberately.
  • Passwords: bcrypt cost 12 by default; constant-time unknown-user handling prevents timing-based enumeration; generic error messages prevent oracle responses; supply Argon2id via PasswordHasher if preferred.
  • Refresh tokens, device tokens, reset tokens, and session IDs are 256-bit opaque values; your stores must hash refresh/device/reset tokens at rest.
  • The forgot-password flow always answers 200 and rate-limits before any store work, so it leaks neither account existence nor delivery outcome.
  • Get an external security review before real financial or personal data flows through your deployment.

Documentation

Overview

Package authkit provides pluggable authentication and RBAC for Go HTTP services: OAuth (via markbates/goth), email/password with two-step TOTP, revocable server-side sessions, an OAuth2/PKCE token layer for native clients, API keys, device principals, a platform-operator axis for SaaS, and audit hooks. Storage is interface-driven — bring any database.

Quick start:

auth, err := authkit.New(authkit.Config{
    OAuth: authkit.OAuthConfig{
        Providers: []authkit.ProviderConfig{
            {Name: "github", ClientID: os.Getenv("GITHUB_CLIENT_ID"), ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET")},
        },
        CallbackBaseURL: "https://example.com",
    },
    SessionSecret: os.Getenv("SESSION_SECRET"),
    RBAC:          authkit.RBACConfig{FilePath: "policy.yaml"},
})

mux.Handle("GET /auth/{provider}",          http.HandlerFunc(auth.BeginAuth))
mux.Handle("GET /auth/{provider}/callback", http.HandlerFunc(auth.Callback))
mux.Handle("POST /auth/logout",             http.HandlerFunc(auth.Logout))
mux.Handle("GET /auth/me",                  http.HandlerFunc(auth.Me))

// Protected routes
mux.Handle("GET /api/reports", auth.RequireAuth(reportsHandler))
mux.Handle("POST /api/projects", auth.Require("projects:write")(createHandler))

Index

Constants

View Source
const (
	AuditLogin            = "login"
	AuditLogout           = "logout"
	AuditRefresh          = "refresh"
	AuditRevoke           = "revoke"
	Audit2FAEnroll        = "2fa_enroll"
	Audit2FAVerify        = "2fa_verify"
	Audit2FADisable       = "2fa_disable"
	Audit2FARecoveryRegen = "2fa_recovery_regenerate"
	AuditPasswordChange   = "password_change"
	AuditRoleChange       = "role_change"
	AuditPermissionChange = "permission_change"
	AuditImpersonate      = "impersonate"

	// Password reset: a recovery token was requested, and (later) a password was
	// actually changed via a consumed token.
	AuditPasswordResetRequest = "password_reset_request"
	AuditPasswordReset        = "password_reset"
)

Well-known audit event types.

View Source
const (
	ResetKindUser     = "user"
	ResetKindPlatform = "platform"
)

Reset kinds namespace a token to one principal axis.

View Source
const (
	ErrCodeUnauthenticated   = "unauthenticated"     // 401: no valid credential
	ErrCodeInvalidCredential = "invalid_credentials" // 401: wrong email/password
	ErrCodeInvalidCode       = "invalid_code"        // 401: wrong TOTP/recovery code
	ErrCodeInvalidChallenge  = "invalid_challenge"   // 401: missing/expired pending step
	ErrCodeForbidden         = "forbidden"           // 403: authenticated but not allowed
	ErrCodeCSRF              = "csrf_invalid"        // 403: CSRF check failed
	ErrCodeInvalidRequest    = "invalid_request"     // 400: malformed input
	ErrCodePasswordPolicy    = "password_policy"     // 400: password rejected by policy
	ErrCodeInvalidGrant      = "invalid_grant"       // 400/401: OAuth2 token-flow failures
	ErrCodeUnsupportedGrant  = "unsupported_grant_type"
	ErrCodeConflict          = "conflict"     // 409: already exists / already enrolled
	ErrCodeRateLimited       = "rate_limited" // 429: throttled; Retry-After is set
	ErrCodeNotEnabled        = "not_enabled"  // 404: feature not configured
	ErrCodeServerError       = "server_error" // 500: unexpected failure
)

Machine-readable error codes. Every error response authkit writes carries one of these in the "error" field of the JSON envelope, so clients (SPAs, mobile apps) can branch and localize without parsing English prose. The set is part of the public API: codes are only ever added, never renamed.

View Source
const (
	// PermAll grants every permission check. Use "*" in policy.yaml to assign
	// it to a role. All other permission strings are user-defined.
	PermAll = "*"
)

Variables

View Source
var (
	ErrUserExists   = errors.New("authkit: user already exists")
	ErrUserNotFound = errors.New("authkit: user not found")
)

Sentinel errors for UserStore implementations.

View Source
var (
	ErrImpersonationDisabled  = errors.New("authkit: impersonation is disabled")
	ErrImpersonationForbidden = errors.New("authkit: platform:impersonate capability required")
)

Sentinel errors for impersonation.

View Source
var ErrDeviceTokenInvalid = errors.New("authkit: invalid device token")

ErrDeviceTokenInvalid is returned by AuthenticateDevice when the presented token is missing, unknown, or revoked.

Functions

func CheckPassword

func CheckPassword(hashedPassword, password string) bool

CheckPassword compares a plaintext password against a bcrypt hash.

func HashPassword

func HashPassword(password string) (string, error)

HashPassword hashes a plaintext password with the default bcrypt hasher. Exported so consumers can use it in admin/seed tooling. Applications that configured a custom PasswordHasher should hash through that instead.

func TenantIDFromCtx

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

TenantIDFromCtx returns the tenant ID stored in ctx. ok is false when no tenant has been set (or it is empty), which callers MUST treat as fail-closed.

func WithDevice

func WithDevice(ctx context.Context, d *Device) context.Context

WithDevice returns a copy of ctx carrying the device principal. RequireDevice sets this; a host authenticating a non-HTTP channel (e.g. a WebSocket upgrade) sets it on the connection context after AuthenticateDevice.

func WithLogger

func WithLogger(l *slog.Logger) func(*LayeredPolicyProvider)

WithLogger configures a logger for LayeredPolicyProvider. When set, DB errors during role lookups are logged so operators can detect store outages.

Example:

provider, err := authkit.NewLayeredProvider("policy.yaml", store,
    authkit.WithLogger(slog.Default()),
)

func WithTenant

func WithTenant(ctx context.Context, tenantID string) context.Context

WithTenant returns a copy of ctx carrying the given tenant ID. authkit sets this from the authenticated principal before resolving permissions, so a tenant-aware PolicyProvider (and the host's per-transaction RLS GUC) can scope to the right tenant. It is the single owner of the tenant context key shared across the auth library and the host application.

Types

type APIKeyValidator

type APIKeyValidator interface {
	ValidateKey(ctx context.Context, rawKey string) (*User, error)
}

APIKeyValidator validates a raw API key string and returns the associated user. Implementations look up the key hash in a store and return a *User with Email, Name, Provider, and Role populated. Authkit then resolves the user's permissions from the RBAC policy based on the returned Role.

Return nil, nil when the key is not found, expired, or inactive. Return nil, err only for unexpected infrastructure failures.

type AuditEvent

type AuditEvent struct {
	Type     string
	TenantID string
	Actor    string
	Subject  string
	IP       string
	At       time.Time
	Meta     map[string]any
}

AuditEvent is a single auditable security event emitted by authkit. The host application wires an AuditSink to persist these to its audit log.

Type is one of the well-known constants below. TenantID is empty for platform principals and for pre-tenant events (e.g. a failed login before the user is resolved). Actor is who performed the action (email or admin id); Subject is who/what it acted on (often the same as Actor for self-service events). Meta carries event-specific detail (provider, session id, role names, etc.).

type AuditSink

type AuditSink interface {
	Emit(ctx context.Context, ev AuditEvent)
}

AuditSink receives audit events. Implementations MUST NOT block the request path on slow I/O — buffer or hand off as needed. Emit is best-effort from authkit's perspective; it never returns an error to the caller.

type Auth

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

Auth is the central object. Create one with New() and attach its methods as HTTP handlers and middleware.

func New

func New(cfg Config) (*Auth, error)

New validates the config, registers the OAuth providers with goth, loads the RBAC policy, and returns a ready-to-use Auth instance.

func (*Auth) AuthenticateDevice

func (a *Auth) AuthenticateDevice(ctx context.Context, rawToken string) (*Device, error)

AuthenticateDevice validates a raw device token and builds the principal. It is the credential path shared by RequireDevice (HTTP) and any host-driven channel authentication (e.g. a WebSocket upgrade), so both bind identically. It does NOT touch ctx — the caller binds (RequireDevice via WithDevice + WithTenant; a hub on its connection context).

func (*Auth) Authorize

func (a *Auth) Authorize(w http.ResponseWriter, r *http.Request)

Authorize is the PKCE authorization endpoint. It requires an authenticated session, validates the client + redirect + challenge, mints an auth code, and redirects back to the app. Mount on: GET /authorize

func (*Auth) BeginAuth

func (a *Auth) BeginAuth(w http.ResponseWriter, r *http.Request)

BeginAuth starts the OAuth flow for the provider named in the URL path. Mount this on: GET /auth/{provider}

The provider name is extracted from the Go 1.22+ path value {provider}.

func (*Auth) CSRF

func (a *Auth) CSRF(next http.Handler) http.Handler

CSRF is middleware enforcing the double-submit check on unsafe methods. On safe methods it ensures a valid token cookie is present (bootstrapping the SPA). It is a pass-through when CSRF.Enable is false or the request is token-authenticated.

func (*Auth) CSRFToken

func (a *Auth) CSRFToken(w http.ResponseWriter, r *http.Request)

CSRFToken issues (or returns the existing) CSRF token and writes it as JSON, for SPAs that fetch it explicitly. Mount on: GET /auth/csrf

func (*Auth) Callback

func (a *Auth) Callback(w http.ResponseWriter, r *http.Request)

Callback completes the OAuth handshake, resolves the user's role from the RBAC policy, stores the user in the session, then redirects to AfterLoginURL. Mount this on: GET /auth/{provider}/callback

func (*Auth) ChangeFirstPassword

func (a *Auth) ChangeFirstPassword(w http.ResponseWriter, r *http.Request)

ChangeFirstPassword replaces the password for a user in the first-login "must change password" pending state — the pending cookie set by Login proves the old (temporary) password was just verified. On success it clears the must-change flag (via UpdatePassword) and CONTINUES the login: it starts the TOTP step when the role needs 2FA, otherwise it mints the session directly. So the order is always Set password → 2FA, matching onboarding. Mount on: POST /auth/password/first-change. Fields: password.

func (*Auth) ChangePassword

func (a *Auth) ChangePassword(w http.ResponseWriter, r *http.Request)

ChangePassword lets a signed-in user rotate their own password. It verifies the current password, sets the new one, then revokes every session and mints a fresh one for THIS device — so other devices are logged out (a credential change must not leave old sessions live) while the user stays signed in here. Mount on: POST /auth/password/change. Fields: current_password, new_password.

func (*Auth) ConfirmTwoFactor

func (a *Auth) ConfirmTwoFactor(w http.ResponseWriter, r *http.Request)

ConfirmTwoFactor activates a pending TOTP secret for a user enrolling voluntarily from their account page (as opposed to the login-time flow, which uses Verify2FA + the pending cookie). It validates a code against the freshly provisioned secret and confirms it. Mount on: POST /auth/2fa/confirm. Fields: code (6-digit TOTP) or recovery_code.

func (*Auth) DisableTwoFactor

func (a *Auth) DisableTwoFactor(w http.ResponseWriter, r *http.Request)

DisableTwoFactor turns off the current user's 2FA. Refused when the user's role mandates 2FA (they would be forced to re-enroll at next login anyway). Mount on: POST /auth/2fa/disable.

func (*Auth) Enroll2FA

func (a *Auth) Enroll2FA(w http.ResponseWriter, r *http.Request)

Enroll2FA provisions a new TOTP secret + recovery codes for the user currently in the 2FA-pending state (or an authenticated session, for voluntary enrollment), and returns the otpauth URL + recovery codes to show once. Mount on: POST /auth/2fa/enroll

func (*Auth) EstablishPlatformSession

func (a *Auth) EstablishPlatformSession(ctx context.Context, w http.ResponseWriter, r *http.Request, rec *PlatformAdminRecord) error

EstablishPlatformSession mints a platform session for rec exactly as a completed platform login would. It exists for host-controlled flows that authenticate a platform admin outside the built-in password+TOTP handlers — a development bypass, a test harness, or a future SSO bridge.

SECURITY: the built-in flow requires password AND TOTP before ever reaching this point; a caller takes on that responsibility. Never expose a code path that reaches this from unauthenticated input in production.

func (*Auth) EstablishSession

func (a *Auth) EstablishSession(ctx context.Context, w http.ResponseWriter, r *http.Request, u *User) error

EstablishSession mints a session for u exactly as a successful login would: it rotates the session ID (fixation prevention), persists the record, and sets the cookie. It exists for host-driven login flows — an ephemeral trial principal, an SSO bridge, a development bypass — that authenticate outside authkit's built-in handlers but must produce a first-class session.

The caller is responsible for having authenticated the principal. u must carry Email, Role, TenantID (when multi-tenant), and — via SetPermissions — the resolved permission list; ctx must carry the user's tenant (WithTenant).

func (*Auth) ForgotPassword

func (a *Auth) ForgotPassword(w http.ResponseWriter, r *http.Request)

ForgotPassword starts self-service recovery for a user. It ALWAYS responds 200 with the same body whether or not the email exists, leaking nothing about which emails are registered. Mount on: POST /auth/password/forgot. Fields: email.

func (*Auth) ImpersonationContext

func (a *Auth) ImpersonationContext(ctx context.Context, admin *PlatformAdmin, tenantID string) (context.Context, error)

ImpersonationContext returns a single-tenant-scoped context for a platform admin to act within exactly one tenant (break-glass support access). It requires the `platform:impersonate` capability and is audited. Downstream tenant-scoped data access then runs under that one tenant's normal scoping — the admin is confined, never able to read across tenants.

func (*Auth) IsDeviceCapability

func (a *Auth) IsDeviceCapability(perm string) bool

IsDeviceCapability reports whether perm is one of the configured device capabilities. Used to reject a programming error where a non-device capability is required on a device route.

func (*Auth) IssuePasswordToken

func (a *Auth) IssuePasswordToken(w http.ResponseWriter, r *http.Request)

IssuePasswordToken exchanges email+password for a token pair. When the user's role requires 2FA, it instead returns a signed pending token that the client completes via IssuePasswordToken2FA. Cookie-free: everything travels in the request/response bodies so a native client needs no cookie jar.

Mount on: POST /oauth/token/password. Fields: email, password.

func (*Auth) IssuePasswordToken2FA

func (a *Auth) IssuePasswordToken2FA(w http.ResponseWriter, r *http.Request)

IssuePasswordToken2FA completes a pending native login by validating a TOTP (or recovery) code and returns the token pair. The pending token is the one returned by IssuePasswordToken; it is carried in the request body, not a cookie.

Mount on: POST /oauth/token/2fa. Fields: pending_token, code (or recovery_code).

func (*Auth) IssueResetToken

func (a *Auth) IssueResetToken(ctx context.Context, email, name, kind string) (string, error)

IssueResetToken mints, stores, and delivers a reset token for (email, kind). It is the shared core of the self-service forgot-password handlers and any admin-initiated reset (an operator resetting a staff member). It returns the raw token so an authenticated caller can surface the reset link directly; the public handlers discard it. A delivery error is returned (the caller decides); a store error is returned without attempting delivery.

func (*Auth) IssueToken

func (a *Auth) IssueToken(w http.ResponseWriter, r *http.Request)

IssueToken exchanges an authorization code (+ PKCE verifier) for an access + refresh token pair. Mount on: POST /token Fields: grant_type=authorization_code, code, code_verifier, redirect_uri.

func (*Auth) JWKS

func (a *Auth) JWKS(w http.ResponseWriter, r *http.Request)

JWKS serves the public verification keys. Mount on: GET /.well-known/jwks.json

func (*Auth) Login

func (a *Auth) Login(w http.ResponseWriter, r *http.Request)

Login authenticates a user with email and password. Mount this on: POST /auth/login

Expects fields (form or JSON): email and password.

func (*Auth) Logout

func (a *Auth) Logout(w http.ResponseWriter, r *http.Request)

Logout clears the session and redirects to AfterLogoutURL. Mount this on: POST /auth/logout

func (*Auth) LogoutEverywhere

func (a *Auth) LogoutEverywhere(w http.ResponseWriter, r *http.Request)

LogoutEverywhere revokes every session for the current user, including this one, and clears the cookie ("log out all devices"). The client should treat the response as a logout and route to login. Mount on: POST /auth/logout/all.

func (*Auth) Me

func (a *Auth) Me(w http.ResponseWriter, r *http.Request)

Me returns the currently authenticated user as JSON. Returns 401 if the request has no valid session. Mount this on: GET /auth/me

func (*Auth) MintAccessToken

func (a *Auth) MintAccessToken(u *User, ttl time.Duration) (string, error)

MintAccessToken signs and returns a single access-token JWT for u — no refresh token — with an explicit ttl. It exists for host-driven flows that issue tokens to principals not backed by UserStore (an ephemeral trial principal, a synthetic service identity): minting a refresh token for such a principal would hand out a credential that could never be redeemed, since the refresh flow re-resolves the principal through UserStore.

u.Provider should be set to whatever distinguishes this principal type — it round-trips through the token so origin guards see the same value as for a session carrying the same Provider. ttl must be positive; callers exist precisely because they need a deliberately-computed lifetime.

func (*Auth) PlatformEnroll2FA

func (a *Auth) PlatformEnroll2FA(w http.ResponseWriter, r *http.Request)

PlatformEnroll2FA provisions a PENDING TOTP secret + recovery codes for a platform admin who passed the password step but has not enrolled 2FA yet, and returns the otpauth URL + recovery codes to show once. The admin confirms by calling PlatformVerify2FA with a code. Mount on: POST /platform/2fa/enroll.

It refuses once 2FA is confirmed: a stolen password alone must never be able to re-enroll (and thus reset) a platform admin's authenticator. Re-enrollment of a confirmed admin only happens after another admin resets their 2FA.

func (*Auth) PlatformForgotPassword

func (a *Auth) PlatformForgotPassword(w http.ResponseWriter, r *http.Request)

PlatformForgotPassword starts self-service recovery for a platform admin. Same no-enumeration contract as ForgotPassword. Mount on: POST /platform/password/forgot. Fields: email.

func (*Auth) PlatformLogin

func (a *Auth) PlatformLogin(w http.ResponseWriter, r *http.Request)

PlatformLogin is step ONE of platform login: it verifies the email + password and, only on success, starts the mandatory TOTP challenge (no role exemption). It does NOT mint a session — the client must then call PlatformVerify2FA with the code. Mount on a separate route/subdomain: POST /platform/login. Expects fields: email, password.

func (*Auth) PlatformLogout

func (a *Auth) PlatformLogout(w http.ResponseWriter, r *http.Request)

PlatformLogout revokes the platform session and clears the cookie.

func (*Auth) PlatformMe

func (a *Auth) PlatformMe(w http.ResponseWriter, r *http.Request)

PlatformMe returns the currently signed-in platform admin (from the platform session cookie), or 401 when there is none. It is the platform counterpart to Me, letting a client confirm the platform session and show who is logged in. Mount on: GET /platform/me

func (*Auth) PlatformResetPassword

func (a *Auth) PlatformResetPassword(w http.ResponseWriter, r *http.Request)

PlatformResetPassword completes a platform admin's recovery: consume the token, set the new password, revoke platform sessions. 2FA enrollment is untouched — the admin still passes TOTP at next login. Mount on: POST /platform/password/reset. Fields: token, password.

func (*Auth) PlatformVerify2FA

func (a *Auth) PlatformVerify2FA(w http.ResponseWriter, r *http.Request)

PlatformVerify2FA is step TWO: it validates the TOTP code for the admin in the platform-pending state (password already verified) and, on success, establishes the platform session. Mount on: POST /platform/2fa/verify. Fields: code.

func (*Auth) RefreshAccessToken

func (a *Auth) RefreshAccessToken(w http.ResponseWriter, r *http.Request)

RefreshAccessToken rotates a refresh token: it issues a new access + refresh pair and invalidates the presented token. Reuse of a spent/revoked token revokes the entire chain. Mount on: POST /token/refresh Fields: refresh_token.

func (*Auth) RegenerateRecoveryCodes

func (a *Auth) RegenerateRecoveryCodes(w http.ResponseWriter, r *http.Request)

RegenerateRecoveryCodes issues a fresh set of recovery codes for a user whose 2FA is already confirmed, invalidating the old set, and returns the new codes to show once. Mount on: POST /auth/2fa/recovery/regenerate.

func (*Auth) Register

func (a *Auth) Register(w http.ResponseWriter, r *http.Request)

Register creates a new user account with email and password, then logs them in automatically. Mount this on: POST /auth/register

Expects fields (form or JSON): email, password, and optionally name.

func (*Auth) Require

func (a *Auth) Require(permission string) func(http.Handler) http.Handler

Require is middleware that enforces both a valid credential (bearer or session) AND that the authenticated user holds the given permission. Returns 401 when there is no credential, 403 when the user lacks the permission.

func (*Auth) RequireAuth

func (a *Auth) RequireAuth(next http.Handler) http.Handler

RequireAuth is middleware that enforces a valid credential — a bearer token (access JWT or API key) or a session cookie. Responds 401 when neither is present or valid. On success it injects the User into the request context.

func (*Auth) RequireDevice

func (a *Auth) RequireDevice(perm string) func(http.Handler) http.Handler

RequireDevice authenticates a device principal (opaque token via Authorization: Bearer / X-API-Key) and checks it holds the given device capability. It is a SEPARATE credential path from Require/RequireAuth — a device token never authenticates a human route, and a human credential never authenticates a device route, so a device can do nothing else in the API.

On success it sets both the device principal and the device's tenant on ctx (via WithTenant), so tenant-scoped data access runs under the right scope. It panics if perm is not a configured device capability — a wiring mistake caught at startup, not a runtime 403.

func (*Auth) RequirePlatformAdmin

func (a *Auth) RequirePlatformAdmin(perm string) func(http.Handler) http.Handler

RequirePlatformAdmin authenticates a platform principal and checks a platform capability. It NEVER sets a tenant on the context — platform routes operate on platform data only. 401 without a platform session, 403 without the capability.

func (*Auth) RequireSession

func (a *Auth) RequireSession(permission string) func(http.Handler) http.Handler

RequireSession is like Require but rejects bearer credentials. Use this for permission-gated management routes that must use sessions.

func (*Auth) RequireSessionAuth

func (a *Auth) RequireSessionAuth(next http.Handler) http.Handler

RequireSessionAuth is like RequireAuth but rejects bearer credentials. Use this for routes that must only be accessed via a browser session (e.g. /auth/me, UI-only management actions).

func (*Auth) ResetPassword

func (a *Auth) ResetPassword(w http.ResponseWriter, r *http.Request)

ResetPassword completes self-service recovery: consume a valid reset token, set the new password, and revoke all of the user's sessions. Mount on: POST /auth/password/reset. Fields: token, password.

func (*Auth) RevokeUserSessions

func (a *Auth) RevokeUserSessions(ctx context.Context, tenantID, email string) error

RevokeUserSessions revokes every server-side session for a user — the "log out everywhere" / offboarding path. No-op when no session Store is configured. ctx must carry the user's tenant.

func (*Auth) TwoFactorStatus

func (a *Auth) TwoFactorStatus(w http.ResponseWriter, r *http.Request)

TwoFactorStatus reports whether the current user has confirmed 2FA and whether their role mandates it. The client uses this to show enroll-vs-manage and to mark 2FA as required. Mount on: GET /auth/2fa/status.

func (*Auth) Verify2FA

func (a *Auth) Verify2FA(w http.ResponseWriter, r *http.Request)

Verify2FA completes a pending login by validating a TOTP code or a recovery code, then establishes the session. Mount on: POST /auth/2fa/verify Expects fields: code (6-digit TOTP) OR recovery_code.

func (*Auth) WatchRBAC

func (a *Auth) WatchRBAC(ctx context.Context, interval time.Duration)

WatchRBAC starts a background goroutine that reloads the RBAC policy file every interval. It stops when ctx is cancelled. This allows operators to update the policy without restarting the service.

Example:

go auth.WatchRBAC(ctx, 30*time.Second)

type AuthCodeStore

type AuthCodeStore interface {
	ClaimAuthCode(ctx context.Context, jti string, expiresAt time.Time) (ok bool, err error)
}

AuthCodeStore, when configured, makes PKCE authorization codes single-use. ClaimAuthCode atomically records jti as redeemed and returns ok=false if it was already redeemed (a replay). expiresAt lets the store self-expire the record (the code is only valid for authCodeTTL). A short-TTL store (Redis) fits best.

type AuthMode

type AuthMode string

AuthMode controls which authentication methods are enabled.

const (
	// AuthModeOAuth enables only OAuth providers (default).
	AuthModeOAuth AuthMode = "oauth"

	// AuthModePassword enables only email/password authentication.
	AuthModePassword AuthMode = "password"

	// AuthModeBoth enables both OAuth and email/password authentication.
	AuthModeBoth AuthMode = "both"
)

type BcryptHasher

type BcryptHasher struct {
	// Cost overrides the bcrypt cost. Zero means the default (12).
	Cost int
}

BcryptHasher is the default PasswordHasher (bcrypt, cost 12 — roughly 250ms per hash on modern hardware).

func (BcryptHasher) Hash

func (h BcryptHasher) Hash(password string) (string, error)

Hash implements PasswordHasher.

func (BcryptHasher) Verify

func (h BcryptHasher) Verify(hashedPassword, password string) bool

Verify implements PasswordHasher.

type CSRFConfig

type CSRFConfig struct {
	// Enable turns on the CSRF middleware (signed double-submit) for
	// cookie-authenticated, state-changing requests. Token-authenticated
	// requests are always exempt.
	Enable bool

	// TrustedOrigins, when non-empty, additionally requires the Origin header
	// (when present) of unsafe requests to match one of these origins
	// (scheme://host[:port]). Defense in depth on top of the signed
	// double-submit token.
	TrustedOrigins []string
}

CSRFConfig groups CSRF protection settings.

type Config

type Config struct {
	// Mode controls which authentication methods are enabled.
	// Defaults to AuthModeOAuth.
	Mode AuthMode

	// AppName is the product name shown to users where one is needed (e.g. the
	// issuer in authenticator apps for TOTP). Defaults to "App".
	AppName string

	// SessionSecret signs session cookies, CSRF tokens, and the short-lived
	// pending-step tokens. Must be at least 32 bytes of random data.
	SessionSecret string

	// SecureCookie controls the Secure flag (and __Host- prefix) on cookies.
	// Set to true in production (HTTPS only). Defaults to false.
	SecureCookie bool

	// CookiePrefix namespaces every cookie authkit sets (session ID, CSRF,
	// pending 2FA, platform, trusted device), so two authkit-based apps on one
	// host never collide. Defaults to "authkit".
	CookiePrefix string

	// AfterLoginURL is the URL the user is redirected to after a successful
	// browser login. Defaults to "/".
	AfterLoginURL string

	// AfterLogoutURL is the URL the user is redirected to after logout.
	// Defaults to "/".
	AfterLogoutURL string

	// Logger receives authkit's diagnostic output as structured logs. When
	// nil, slog.Default() is used. Secrets and tokens are never logged.
	Logger *slog.Logger

	// ClientIP resolves the client IP for throttling and audit events. When
	// nil, the host portion of RemoteAddr is used and forwarding headers are
	// deliberately NOT trusted — behind a reverse proxy, supply a function
	// that reads your vetted header.
	ClientIP func(*http.Request) string

	// ErrorWriter, when set, replaces authkit's JSON error envelope with the
	// host's own rendering. See the ErrCode* constants for the code catalog.
	ErrorWriter ErrorWriter

	// OAuth configures browser OAuth providers.
	// Required when Mode is AuthModeOAuth or AuthModeBoth.
	OAuth OAuthConfig

	// UserStore provides user persistence for password-based authentication.
	// Required when Mode is AuthModePassword or AuthModeBoth.
	UserStore UserStore

	// PasswordHasher hashes and verifies passwords. Defaults to bcrypt with
	// cost 12. Supply Argon2 or another KDF by implementing the interface.
	PasswordHasher PasswordHasher

	// PasswordPolicy configures password validation rules.
	// If nil, defaults are used (minimum 8 characters).
	PasswordPolicy *PasswordPolicy

	// RBAC configures the role policy. If FilePath is empty and Provider is
	// nil, all authenticated users receive an empty role with no permissions.
	RBAC RBACConfig

	// APIKeyValidator enables API key authentication alongside sessions.
	// When set, Require and RequireAuth middleware check the Authorization:
	// Bearer (or X-API-Key) header first. RequireSession and RequireSessionAuth
	// skip API key auth entirely (session-only routes).
	APIKeyValidator APIKeyValidator

	// AuditSink receives security audit events (login, logout, refresh, revoke,
	// 2fa_*, role_change, permission_change, impersonate). If nil, a
	// NopAuditSink is installed so authkit can emit unconditionally.
	AuditSink AuditSink

	// Throttler rate-limits password login and 2FA attempts (per account+IP).
	// When nil, no throttling is applied.
	Throttler LoginThrottler

	// LivePermissionResolution makes Require/RequireAuth re-resolve a session
	// user's permissions from the PolicyProvider on every request (through a
	// short TTL cache), so role and permission changes take effect within the
	// cache window rather than only on next login. Multi-tenant deploys want
	// this on; single-tenant deploys can leave it off (cheaper login-time
	// cache). API-key credentials always resolve live regardless of this flag.
	LivePermissionResolution bool

	// PermissionCacheTTL bounds how stale a live-resolved permission set may be.
	// Defaults to 30s when LivePermissionResolution is enabled.
	PermissionCacheTTL time.Duration

	// Sessions configures revocable server-side sessions.
	Sessions SessionConfig

	// CSRF configures CSRF protection for cookie-authenticated requests.
	CSRF CSRFConfig

	// TwoFactor configures TOTP two-step authentication.
	TwoFactor TwoFactorConfig

	// Tokens configures the OAuth2/PKCE token layer for native clients.
	Tokens TokenConfig

	// Platform configures the platform-operator (super-admin) axis.
	Platform PlatformConfig

	// Reset configures the self-service password-reset flow.
	Reset ResetConfig

	// Devices configures the device-principal axis.
	Devices DeviceConfig
}

Config holds all configuration needed to create an Auth instance.

type Device

type Device struct {
	AgentID  string
	Name     string
	TenantID string
	Attrs    map[string]string
	// contains filtered or unexported fields
}

Device is a device principal bound to exactly one tenant. It carries no human identity and resolves no permissions from a policy provider: its reach is the configured capability allow-list. Attrs carries host-defined scoping (e.g. a site or location id) used for routing.

func DeviceFromCtx

func DeviceFromCtx(ctx context.Context) *Device

DeviceFromCtx returns the device principal on ctx, or nil.

func (*Device) Attr

func (d *Device) Attr(key string) string

Attr returns the named host-defined attribute, or "" when absent.

func (*Device) Can

func (d *Device) Can(perm string) bool

Can reports whether the device holds a capability. Only the configured device capabilities ever pass — a device can never hold "*" or any policy-resolved permission.

type DeviceConfig

type DeviceConfig struct {
	// Validator enables the device principal axis. When set, RequireDevice and
	// AuthenticateDevice validate opaque device tokens against it. When nil,
	// device auth is disabled.
	Validator DeviceTokenValidator

	// Capabilities is the fixed allow-list of capability strings a device
	// principal may ever hold. It is declared here, in code, precisely so a
	// device can never acquire a capability through policy data. Required when
	// Validator is set.
	Capabilities []string
}

DeviceConfig groups the device-principal axis: headless machine clients (agents, kiosks, IoT devices) confined to a fixed capability allow-list.

type DeviceRecord

type DeviceRecord struct {
	AgentID  string
	Name     string
	TenantID string
	Attrs    map[string]string
}

DeviceRecord is what DeviceTokenValidator returns for a valid token. The store looks the token up by hash and returns only the binding the principal needs — never the token itself.

type DeviceTokenValidator

type DeviceTokenValidator interface {
	ValidateDeviceToken(ctx context.Context, rawToken string) (*DeviceRecord, error)
}

DeviceTokenValidator validates an opaque device token and returns the bound device. Implementations hash the token at rest and look it up before any tenant is known (a device authenticates with only the token). Return nil, nil when the token is unknown, revoked, or inactive; nil, err only on infrastructure failure.

type ErrorWriter

type ErrorWriter func(w http.ResponseWriter, r *http.Request, status int, code, desc string)

ErrorWriter lets the host replace authkit's error rendering (for example to emit RFC 9457 problem+json or add a trace id). status is the HTTP status, code one of the ErrCode* constants, desc a short human-readable default. When nil, authkit writes {"error": code, "error_description": desc}.

type LayeredPolicyProvider

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

LayeredPolicyProvider combines a YAML baseline with per-user database overrides. The YAML file defines roles and their initial members. A management UI can call SetOverride to change individual users without editing the file.

Lookup order per request:

  1. Database override for this email (UI-managed, takes precedence)
  2. YAML baseline (file-managed, fallback)

Implements PolicyProvider and PolicyReloader.

func NewLayeredProvider

func NewLayeredProvider(filePath string, store UserRoleStore, opts ...func(*LayeredPolicyProvider)) (*LayeredPolicyProvider, error)

NewLayeredProvider creates a LayeredPolicyProvider that reads the initial policy from filePath and looks up per-user overrides from store.

Example:

provider, err := authkit.NewLayeredProvider("policy.yaml", myDBStore,
    authkit.WithLogger(myLogger),
)
auth, err := authkit.New(authkit.Config{
    RBAC: authkit.RBACConfig{Provider: provider},
    ...
})
go auth.WatchRBAC(ctx, 30*time.Second) // reloads YAML baseline

func (*LayeredPolicyProvider) DeleteOverride

func (l *LayeredPolicyProvider) DeleteOverride(ctx context.Context, email string) error

DeleteOverride removes the role override for email, reverting that user to the YAML baseline on their next login.

func (*LayeredPolicyProvider) PermissionsForRole

func (l *LayeredPolicyProvider) PermissionsForRole(_ context.Context, role string) []string

PermissionsForRole implements PolicyProvider. Resolves permissions for a named role from the YAML baseline — role definitions are always file-managed. ctx is unused: the layered provider's role definitions are not per-tenant.

func (*LayeredPolicyProvider) Reload

func (l *LayeredPolicyProvider) Reload() error

Reload implements PolicyReloader. Re-reads the YAML baseline without affecting any database overrides. Called automatically by WatchRBAC.

func (*LayeredPolicyProvider) RoleFor

func (l *LayeredPolicyProvider) RoleFor(ctx context.Context, email string) (string, []string)

RoleFor implements PolicyProvider. Checks the DB override first, then falls back to the YAML baseline. If the DB returns an error the fallback is used and the error is logged (if a Logger was configured). This prevents a store outage from locking users out, but means a user whose DB override was a demotion will temporarily regain their YAML role. Monitor store errors.

func (*LayeredPolicyProvider) SetOverride

func (l *LayeredPolicyProvider) SetOverride(ctx context.Context, email, role string, permissions []string) error

SetOverride validates and stores a per-user role override. Returns an error if the email format is invalid, the role name is not defined in the current YAML policy, or any permission string is invalid.

NOTE: role changes take effect on the user's next login — existing sessions retain their current permissions until they expire or the user logs out and back in. There is no built-in session invalidation.

func (*LayeredPolicyProvider) Store

Store returns the raw UserRoleStore for operations not covered by SetOverride/DeleteOverride (e.g. listing all overrides in a management UI).

type LoginThrottler

type LoginThrottler interface {
	// Allow reports whether an attempt for key may proceed. When locked out, ok
	// is false and retryAfter is the remaining lockout duration.
	Allow(ctx context.Context, key string) (retryAfter time.Duration, ok bool)
	// RecordFailure registers a failed attempt (advancing backoff/lockout).
	RecordFailure(ctx context.Context, key string) error
	// Reset clears the failure state for key after a successful login.
	Reset(ctx context.Context, key string) error
}

LoginThrottler rate-limits authentication attempts to blunt credential stuffing and brute force. The host implements it (e.g. backed by Redis); authkit calls it around the password login, 2FA, and password-reset flows. Keys are per account+IP.

type NopAuditSink

type NopAuditSink struct{}

NopAuditSink discards every event. It is the default when Config.AuditSink is nil, so callers can emit unconditionally without nil checks.

func (NopAuditSink) Emit

Emit implements AuditSink and does nothing.

type OAuthConfig

type OAuthConfig struct {
	// Providers is the list of OAuth providers to enable using the built-in
	// convenience wrappers (bitbucket, github, google, gitlab).
	// Required when Mode is AuthModeOAuth or AuthModeBoth, unless GothProviders
	// is supplied instead.
	Providers []ProviderConfig

	// GothProviders is a list of pre-constructed goth.Provider values.
	// Use this to enable any of the 80+ providers that goth supports beyond the
	// built-in convenience wrappers. Import the provider package you need from
	// github.com/markbates/goth/providers/*, construct the provider, and pass
	// it here. It is merged with any providers built from Providers.
	GothProviders []goth.Provider

	// CallbackBaseURL is the externally-reachable base URL of the service
	// (e.g. "https://example.com"). The OAuth callback URLs are derived as
	// {CallbackBaseURL}/auth/{provider}/callback.
	// Required when Mode is AuthModeOAuth or AuthModeBoth.
	CallbackBaseURL string
}

OAuthConfig groups the OAuth provider settings.

type PasswordHasher

type PasswordHasher interface {
	Hash(password string) (string, error)
	Verify(hashedPassword, password string) bool
}

PasswordHasher hashes and verifies passwords. The default is bcrypt with cost 12; supply an implementation backed by Argon2id (or any KDF) to change the algorithm. Verify must return false — never panic or error — for a hash produced by a different algorithm, so deployments can migrate hashers gradually.

type PasswordPolicy

type PasswordPolicy struct {
	// MinLength is the minimum password length. Defaults to 8.
	MinLength int

	// MaxLength caps the password length in bytes. Defaults to 72, the hard
	// limit of the default bcrypt hasher (longer inputs would otherwise be
	// rejected by the KDF with an opaque error). Raise it when using a hasher
	// without that limit (e.g. Argon2).
	MaxLength int
}

PasswordPolicy configures password validation constraints.

type PasswordResetStore

type PasswordResetStore interface {
	// CreateResetToken stores a hashed token bound to (email, kind) with expiry.
	CreateResetToken(ctx context.Context, tokenHash, email, kind string, expiresAt time.Time) error
	// ConsumeResetToken atomically marks the token used (only if unused and
	// unexpired) and returns the email it binds. ok is false when the token is
	// unknown, already used, expired, or of the wrong kind.
	ConsumeResetToken(ctx context.Context, tokenHash, kind string) (email string, ok bool, err error)
}

PasswordResetStore persists single-use password-reset tokens. The raw token is delivered out-of-band (email, SMS, ...); only its hash is stored. Consume MUST be atomic and single-use.

type PasswordUser

type PasswordUser struct {
	Email          string
	Name           string
	HashedPassword string

	// TenantID binds this credential to one tenant (email is global, so the
	// lookup determines the tenant). Attrs carries host-defined principal
	// attributes. Both are copied onto the authenticated User.
	TenantID string
	Attrs    map[string]string

	// MustChangePassword is set when the credential is a temporary/onboarding
	// one the user must replace before doing anything else. When true, Login
	// stops before 2FA and asks the client to collect a new password (see
	// ChangeFirstPassword); the store clears it on the next UpdatePassword.
	MustChangePassword bool
}

PasswordUser is the record returned by UserStore.GetUserByEmail.

type PlatformAdmin

type PlatformAdmin struct {
	Email string
	Name  string
	Role  string
	// contains filtered or unexported fields
}

PlatformAdmin is a platform principal — an operator of the SaaS across tenants. It is a different axis from tenant RBAC and has NO TenantID. Its reach comes from application logic + explicit single-tenant impersonation, never from a database-level bypass.

func PlatformAdminFromCtx

func PlatformAdminFromCtx(ctx context.Context) *PlatformAdmin

PlatformAdminFromCtx returns the platform principal on ctx, or nil.

func (*PlatformAdmin) Can

func (p *PlatformAdmin) Can(perm string) bool

Can reports whether the admin holds a platform capability ("*" passes all).

type PlatformAdminRecord

type PlatformAdminRecord struct {
	Email          string
	Name           string
	HashedPassword string
	Role           string
	TOTPSecret     string
	TOTPConfirmed  bool
}

PlatformAdminRecord is what PlatformAdminStore returns for login. TOTPSecret is the decrypted TOTP secret (the store handles encryption at rest), and is empty for an admin who has not enrolled yet; TOTPConfirmed reports whether that secret has been activated by a first successful verification. 2FA is mandatory, but a newly created admin enrolls on first login (pending→confirmed), so the secret is not always present immediately.

type PlatformAdminStore

type PlatformAdminStore interface {
	GetPlatformAdmin(ctx context.Context, email string) (*PlatformAdminRecord, error)

	// UpdatePassword sets the hashed password for the platform admin with
	// this email. Called by the platform password-reset flow; it does NOT touch
	// the TOTP secret (2FA stays mandatory). Platform admins are not
	// tenant-scoped.
	UpdatePassword(ctx context.Context, email, hashedPassword string) error

	// EnrollPlatformTOTP stores (or replaces) a PENDING secret + recovery hashes
	// for an admin who has not confirmed 2FA yet (mirrors the user TOTPStore).
	EnrollPlatformTOTP(ctx context.Context, email, secret string, recoveryCodeHashes []string) error
	// ConfirmPlatformTOTP activates a pending secret on first successful verify.
	// Idempotent: a no-op once confirmed.
	ConfirmPlatformTOTP(ctx context.Context, email string) error
	// ConsumePlatformRecovery atomically marks a recovery code used (single-use)
	// and reports whether it matched an unused code.
	ConsumePlatformRecovery(ctx context.Context, email, codeHash string) (bool, error)
}

PlatformAdminStore looks up platform admins (separate from UserStore) and backs their TOTP enrollment. Admin lifecycle (create/list/remove) lives in the host app's own store methods; this interface is only what authkit's auth flow needs.

type PlatformConfig

type PlatformConfig struct {
	// Store + Policy enable the platform principal axis (separate from tenant
	// users). Platform login always requires TOTP.
	Store  PlatformAdminStore
	Policy PlatformPolicy

	// EnableImpersonation gates break-glass single-tenant access.
	EnableImpersonation bool
}

PlatformConfig groups the platform-operator (super-admin) axis — principals who operate the SaaS itself, across tenants, on a separate credential path.

type PlatformPolicy

type PlatformPolicy interface {
	PermissionsForPlatformRole(role string) []string
}

PlatformPolicy maps a platform role to its capabilities (a small, static catalog independent of the per-tenant PolicyProvider).

type PlatformTOTPReplayGuard

type PlatformTOTPReplayGuard interface {
	ClaimPlatformTOTPTimestep(ctx context.Context, email string, timestep int64) (ok bool, err error)
}

PlatformTOTPReplayGuard is the platform-admin equivalent (keyed by email only; platform admins have no tenant).

type Policy

type Policy struct {
	// Roles maps role names to their definition.
	Roles map[string]RolePolicy `yaml:"roles"`

	// DefaultRole is assigned to authenticated users whose email is not listed
	// under any role. Leave empty to deny access to unlisted users.
	DefaultRole string `yaml:"default_role"`
}

Policy is the top-level structure of an rbac.yaml file.

type PolicyProvider

type PolicyProvider interface {
	// RoleFor returns the role name and permission list for the given email.
	// Called on every login and API key auth. Return empty strings/nil when
	// the user has no assigned role.
	RoleFor(ctx context.Context, email string) (role string, permissions []string)

	// PermissionsForRole returns the permissions for a named role. The ctx
	// carries the tenant (via TenantIDFromCtx) so role definitions can be
	// per-tenant — a DB-backed provider scopes its lookup to that tenant.
	// Used to resolve permissions for API key users and for per-request live
	// resolution (LivePermissionResolution).
	PermissionsForRole(ctx context.Context, role string) []string
}

PolicyProvider resolves a user's role and permissions at login time. Implement this interface to back RBAC with any storage system. The built-in implementations are the YAML file provider (default) and LayeredPolicyProvider (YAML baseline + database overrides).

type PolicyReloader

type PolicyReloader interface {
	Reload() error
}

PolicyReloader is an optional interface that PolicyProvider implementations can satisfy to support live policy reloading via WatchRBAC.

type ProviderConfig

type ProviderConfig struct {
	// Name is the provider identifier for the built-in wrappers: "bitbucket", "github", "google", or "gitlab".
	// For any other provider, use Config.GothProviders instead.
	Name string

	// ClientID and ClientSecret are the OAuth application credentials.
	ClientID     string
	ClientSecret string

	// Scopes overrides the default scopes for the provider.
	// Leave nil to use the sensible defaults (email + profile).
	Scopes []string
}

ProviderConfig holds the OAuth credentials for a single provider.

type RBACConfig

type RBACConfig struct {
	// FilePath is the path to the rbac.yaml policy file.
	// Used when Provider is nil.
	FilePath string

	// Provider supplies a custom PolicyProvider implementation (e.g. a
	// database-backed or layered provider). When set, FilePath is ignored.
	Provider PolicyProvider
}

RBACConfig tells the Auth instance how to load the role policy.

type RefreshToken

type RefreshToken struct {
	ID        string
	UserEmail string
	TenantID  string
	ChainID   string
	ParentID  string
	IssuedAt  time.Time
	ExpiresAt time.Time
	UsedAt    *time.Time
	RevokedAt *time.Time
}

RefreshToken is one opaque refresh token in a rotation chain. A successful refresh marks the presented token used and issues a child (same ChainID, ParentID = the used token). Presenting an already-used or revoked token is treated as theft and revokes the whole chain.

ID is the raw token at the authkit boundary; the store MUST hash it at rest and hash lookups, so the database never holds a usable token.

type RefreshTokenStore

type RefreshTokenStore interface {
	// Create stores a new refresh token.
	Create(ctx context.Context, t *RefreshToken) error
	// Get returns the token for the raw value, or nil if not found.
	Get(ctx context.Context, rawToken string) (*RefreshToken, error)
	// Rotate atomically marks rawOld used and stores next (its child). It MUST
	// fail (and change nothing) if rawOld is already used or revoked.
	Rotate(ctx context.Context, rawOld string, next *RefreshToken) error
	// RevokeChain revokes every token sharing chainID (reuse response).
	RevokeChain(ctx context.Context, chainID string) error
	// RevokeAllForUser revokes every refresh token belonging to one user, so a
	// credential change or "log out everywhere" cuts off the bearer axis too,
	// not just cookie sessions. tenantID scopes the revoke; email identifies
	// the user.
	RevokeAllForUser(ctx context.Context, tenantID, email string) error
}

RefreshTokenStore persists refresh tokens with rotation lineage and reuse detection. Implementations hash ID at rest.

type ResetConfig

type ResetConfig struct {
	// Store persists single-use, hashed reset tokens; Delivery sends the raw
	// token out-of-band (email/SMS). Both are required to enable the
	// ForgotPassword/ResetPassword (and platform) handlers.
	Store    PasswordResetStore
	Delivery ResetDelivery

	// TTL bounds a token's validity (default 30m).
	TTL time.Duration
}

ResetConfig groups the self-service password-reset flow.

type ResetDelivery

type ResetDelivery interface {
	SendPasswordReset(ctx context.Context, req ResetRequest) error
}

ResetDelivery delivers a password-reset token to the principal. The host owns the channel (email, SMS, ...) — authkit is channel-agnostic. Sending is best-effort from the request's perspective: the "forgot" endpoint logs a delivery error but still returns 200, so it leaks neither which emails exist nor which channel succeeded.

type ResetRequest

type ResetRequest struct {
	Email string
	Name  string
	Kind  string
	Token string
	TTL   time.Duration
}

ResetRequest is handed to ResetDelivery to deliver a reset token. authkit stores only the hash; Token here is the raw secret that goes in the link/code. Kind selects the template/channel and which reset page the link points to.

type ResetToken

type ResetToken struct {
	TokenHash string
	Email     string
	Kind      string
	ExpiresAt time.Time
	UsedAt    *time.Time
}

ResetToken is a single-use, hashed password-reset token record. UsedAt is nil until the token is consumed.

type RolePolicy

type RolePolicy struct {
	// Permissions is the list of allowed permissions for this role.
	// Use "*" to grant all permissions (admin).
	Permissions []string `yaml:"permissions"`

	// Members is the list of email addresses assigned to this role.
	Members []string `yaml:"members"`
}

RolePolicy defines the permissions and member emails for a single role.

type Session

type Session struct {
	ID          string
	TenantID    string
	Email       string
	Name        string
	Provider    string
	Role        string
	Permissions []string

	// Attrs carries the principal's host-defined attributes (see User.Attrs).
	Attrs map[string]string

	// Platform marks a platform-operator session — no tenant. Tenant and
	// platform sessions use different cookies, so they never cross.
	Platform bool

	// CreatedAt anchors the absolute timeout; LastSeenAt anchors the idle
	// timeout (advanced by Touch on a sliding basis).
	CreatedAt  time.Time
	LastSeenAt time.Time
}

Session is a server-side session record. The cookie holds only the opaque ID; all identity state lives in the SessionStore, enabling instant revocation and "log out everywhere" — things a stateless cookie cannot do.

type SessionConfig

type SessionConfig struct {
	// Store enables revocable, server-side sessions. When set, the cookie
	// carries only an opaque session ID and all identity state lives in the
	// store, allowing instant revocation and "log out everywhere". When nil,
	// authkit falls back to the legacy encrypted-cookie session.
	Store SessionStore

	// IdleTimeout expires a session after inactivity (sliding). Defaults to 30m.
	IdleTimeout time.Duration

	// AbsoluteTimeout caps a session's total lifetime regardless of activity.
	// Defaults to 24h.
	AbsoluteTimeout time.Duration
}

SessionConfig groups the revocable server-side session settings.

type SessionStore

type SessionStore interface {
	// Create persists a new session. Returns an error only on infrastructure failure.
	Create(ctx context.Context, s *Session) error
	// Get returns the session for id, or nil when it does not exist.
	Get(ctx context.Context, id string) (*Session, error)
	// Touch advances LastSeenAt for sliding idle renewal.
	Touch(ctx context.Context, id string, lastSeen time.Time) error
	// Revoke deletes a single session (logout / fixation rotation).
	Revoke(ctx context.Context, id string) error
	// RevokeAllForUser deletes every session for a user ("log out everywhere").
	RevokeAllForUser(ctx context.Context, tenantID, email string) error
}

SessionStore is the revocable, server-side session backend (Redis, a database, ...). The host application implements it; authkit generates the opaque IDs and enforces the idle/absolute timeouts.

Get is called before the tenant is known (it resolves the session by its unguessable ID), so implementations MUST be able to read by ID without a tenant scope. Create/Touch/Revoke/RevokeAllForUser are always called with the session's tenant already on the context.

type SigningKey

type SigningKey struct {
	KID     string
	Private ed25519.PrivateKey
}

SigningKey is one Ed25519 key in the rotation ring. The current key (the first in Tokens.SigningKeys) signs new access tokens; all keys verify, so a key can be retired without invalidating tokens it already signed.

func NewSigningKey

func NewSigningKey(kid string, seed []byte) (SigningKey, error)

NewSigningKey builds a SigningKey from a 32-byte Ed25519 seed.

type TOTPManager

type TOTPManager interface {
	// Disable removes the user's TOTP secret and all recovery codes (turning 2FA
	// off). Idempotent: a no-op when none is stored.
	Disable(ctx context.Context, tenantID, email string) error
	// ReplaceRecoveryCodes deletes the user's existing recovery codes and stores
	// the given hashed set, without touching the confirmed TOTP secret.
	ReplaceRecoveryCodes(ctx context.Context, tenantID, email string, recoveryCodeHashes []string) error
}

TOTPManager is an OPTIONAL extension of TOTPStore for self-service 2FA management (disable, regenerate recovery codes). authkit type-asserts the configured TOTPStore to this — a store that does not implement it simply makes those endpoints return 501, leaving the base enroll/verify flow intact. Kept separate from TOTPStore so adding management never breaks existing implementers (e.g. a platform-admin store, whose 2FA is mandatory).

type TOTPReplayGuard

type TOTPReplayGuard interface {
	ClaimTOTPTimestep(ctx context.Context, tenantID, email string, timestep int64) (ok bool, err error)
}

TOTPReplayGuard is the optional capability. ClaimTOTPTimestep atomically records timestep as consumed for (tenant, email) and returns ok=false when it was already consumed (or a later step already was), i.e. a replay. The store keeps only the last-consumed step per user; it need not remember all of them.

type TOTPStore

type TOTPStore interface {
	// Enroll stores (or replaces) the user's TOTP secret and the hashed recovery
	// codes as PENDING — provisioned but NOT yet confirmed. The user is not
	// considered to have working 2FA until they prove possession of the
	// authenticator with a valid code (see Confirm). This two-phase model is what
	// lets a user who abandons enrollment (got the QR, never added it) be sent
	// back to enroll on the next login instead of being locked at the verify step.
	Enroll(ctx context.Context, tenantID, email, secret string, recoveryCodeHashes []string) error
	// Confirm marks a pending secret confirmed (activated). Called once, on the
	// user's first successful verification. MUST be idempotent: a no-op when the
	// secret is already confirmed or absent.
	Confirm(ctx context.Context, tenantID, email string) error
	// Secret returns the user's TOTP secret (empty when none is stored) and whether
	// it has been CONFIRMED. A non-empty secret with confirmed=false is a pending
	// enrollment: it can be validated (to confirm it) but does not by itself mean
	// the user has set up 2FA.
	Secret(ctx context.Context, tenantID, email string) (secret string, confirmed bool, err error)
	// ConsumeRecovery atomically marks a recovery code used (single-use) and
	// reports whether it matched an unused code.
	ConsumeRecovery(ctx context.Context, tenantID, email, codeHash string) (bool, error)
}

TOTPStore persists a user's TOTP secret and recovery codes. The host implementation is responsible for encrypting the secret at rest; authkit passes/receives the plaintext secret at this boundary. All methods are called with the user's tenant already on the context (TOTP access is tenant-scoped).

type TokenConfig

type TokenConfig struct {
	// Enable turns on the OAuth2/PKCE token endpoints and the bearer-JWT
	// verifier. Requires SigningKeys and RefreshStore.
	Enable bool

	// SigningKeys is the Ed25519 rotation ring; the first key signs new access
	// tokens, all keys verify (and are published via JWKS).
	SigningKeys []SigningKey

	// AccessTTL is the access-JWT lifetime (default 15m); RefreshTTL the
	// refresh-token lifetime (default 30d).
	AccessTTL  time.Duration
	RefreshTTL time.Duration

	// RefreshStore persists opaque refresh tokens (rotation + reuse detection).
	RefreshStore RefreshTokenStore

	// AuthCodes, when set, makes PKCE authorization codes single-use: the
	// code's jti is claimed at redemption so a code cannot be exchanged twice
	// within its short TTL. Optional (nil keeps the stateless,
	// replayable-within-TTL behavior); a short-TTL store is the natural backing.
	AuthCodes AuthCodeStore

	// Issuer is the JWT `iss` claim; ClientID the public native client id
	// (also the JWT audience); RedirectURIs the allowed PKCE redirect URIs.
	Issuer       string
	ClientID     string
	RedirectURIs []string
}

TokenConfig groups the OAuth2/PKCE token layer for native clients.

type TrustedDeviceStore

type TrustedDeviceStore interface {
	// Trust records a trusted device for (tenant, email) and returns the opaque
	// cookie token. ttl bounds its lifetime.
	Trust(ctx context.Context, tenantID, email string, ttl time.Duration) (token string, err error)
	// IsTrusted reports whether token is a live trusted device for (tenant, email).
	IsTrusted(ctx context.Context, tenantID, email, token string) (bool, error)
	// RevokeAllForUser drops every trusted device for a user.
	RevokeAllForUser(ctx context.Context, tenantID, email string) error
}

TrustedDeviceStore persists "remember this device" tokens so a user whose role requires 2FA can skip the TOTP step on a device they previously trusted. The token is opaque and server-side, so it is revocable: "log out everywhere", a password change/reset, and disabling 2FA all drop a user's trusted devices. Password is still required on every login; only the second factor is skipped. NOT used for platform admins (their 2FA is mandatory). All calls carry the user's tenant on the context.

type TwoFactorConfig

type TwoFactorConfig struct {
	// Store enables two-step auth (TOTP). When set, a user whose role is in
	// RequireForRoles must complete a TOTP challenge after the password step.
	// When nil, 2FA is disabled.
	Store TOTPStore

	// RequireForRoles lists the roles that must complete 2FA. Only consulted
	// when Store is set.
	RequireForRoles []string

	// TrustedDevices enables a "trust this device" option at the 2FA step: a
	// remembered device skips the TOTP prompt (the password is still required)
	// for TrustedDeviceTTL. The token is opaque + server-side, so it is
	// revocable (logout-everywhere, password change/reset, and disabling 2FA
	// all drop it). When nil, every 2FA login prompts for TOTP. NOT used for
	// platform admins (their 2FA is mandatory and never skipped).
	TrustedDevices TrustedDeviceStore

	// TrustedDeviceTTL bounds how long a trusted device skips 2FA. Defaults to 30d.
	TrustedDeviceTTL time.Duration
}

TwoFactorConfig groups TOTP two-step authentication settings.

type User

type User struct {
	Email     string `json:"email"`
	Name      string `json:"name"`
	AvatarURL string `json:"avatarUrl"`
	Provider  string `json:"provider"`
	Role      string `json:"role"`

	// TenantID binds the principal to one tenant — the hard security boundary
	// in a multi-tenant deployment. Populated in every credential path:
	// UserStore (password), the OAuth callback mapping, and APIKeyValidator.
	// Single-tenant applications simply leave it empty.
	TenantID string `json:"tenantId,omitempty"`

	// Attrs carries host-defined principal attributes (for example an
	// organizational sub-scope, locale, or plan tier). authkit round-trips it
	// through sessions and access-token claims but never interprets it: keys
	// and meaning belong to the host application. Keep values small — they
	// travel in the session record and the JWT.
	Attrs map[string]string `json:"attrs,omitempty"`
	// contains filtered or unexported fields
}

User represents an authenticated principal. It is injected into every request context after successful authentication, regardless of the credential used (OAuth session, password session, bearer JWT, or API key).

func UserFromCtx

func UserFromCtx(ctx context.Context) *User

UserFromCtx returns the authenticated User stored in ctx, or nil if the request has not passed through RequireAuth middleware.

func (*User) Attr

func (u *User) Attr(key string) string

Attr returns the named host-defined attribute, or "" when absent.

func (*User) Can

func (u *User) Can(permission string) bool

Can reports whether the user holds the given permission. A user with the "*" (PermAll) permission passes every check.

func (*User) Permissions

func (u *User) Permissions() []string

Permissions returns a copy of the user's resolved permission list. Hosts use it to enumerate capabilities (e.g. for a /me endpoint) instead of probing Can() against a parallel catalog.

func (*User) SetPermissions

func (u *User) SetPermissions(perms []string)

SetPermissions replaces the user's resolved permission list. It exists for host-driven login flows that mint sessions through EstablishSession with a permission set the host resolved itself (e.g. an ephemeral or synthetic principal outside the PolicyProvider). Normal logins never need it.

type UserRoleStore

type UserRoleStore interface {
	// GetOverride returns the role and permissions for email if a UI-managed
	// override exists. Return found=false when no override has been set for
	// this user — authkit will fall back to the YAML baseline.
	// The email argument is always lower-cased and trimmed before being passed.
	GetOverride(ctx context.Context, email string) (role string, permissions []string, found bool, err error)

	// SetOverride creates or replaces the role override for a user.
	// Prefer calling LayeredPolicyProvider.SetOverride instead — it validates
	// the role name and permission strings before writing to the store.
	SetOverride(ctx context.Context, email, role string, permissions []string) error

	// DeleteOverride removes the override for email, reverting that user to
	// whatever the YAML baseline assigns them.
	DeleteOverride(ctx context.Context, email string) error
}

UserRoleStore persists per-user role overrides to a database. Implement this interface against your preferred database (Postgres, SQLite, etc.) and pass it to NewLayeredProvider.

Only users whose roles have been changed via your UI need a row in the store. Everyone else falls through to the YAML baseline automatically.

type UserStore

type UserStore interface {
	// CreateUser persists a new user with the given email and pre-hashed
	// password. Implementations MUST return ErrUserExists if the email is
	// already taken.
	CreateUser(ctx context.Context, email, name, hashedPassword string) error

	// GetUserByEmail retrieves a user by email. Returns ErrUserNotFound if
	// no user matches.
	GetUserByEmail(ctx context.Context, email string) (*PasswordUser, error)

	// UpdatePassword sets the hashed password for the user with this (global)
	// email. Called by the password-reset and password-change flows. ctx
	// carries the user's tenant, so a tenant-scoped store can apply its own
	// row-level scoping.
	UpdatePassword(ctx context.Context, email, hashedPassword string) error
}

UserStore is the interface consumers implement to provide user persistence for password-based authentication. authkit is storage-agnostic — the consumer chooses the backing store (PostgreSQL, SQLite, in-memory, etc.).

Jump to

Keyboard shortcuts

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