identity

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Oct 5, 2026 License: MPL-2.0 Imports: 28 Imported by: 0

README

Identity Module

This optional package belongs to the v0.2 line in the existing Foundation Go module; immutable v0.1.0 has no Identity/Audit implementation. Descriptor.Version reports the actual packaged Foundation dependency through achrix.Version(). The existing ModuleVersion constant is retained only as a deprecated unpublished source-line label, not component/update/recovery identity. Public Go declarations own exact signatures. Identity owns product-local accounts, password credentials and opaque server-side sessions; products own profiles, roles, permissions and configuration. No central service, self-registration, recovery/email, MFA, OIDC or tenant model is supplied.

Composition and authorized operations

Use audit.Migrate(ctx, dsn) and identity.Migrate(ctx, dsn) as explicit installation actions against the same product-owned PostgreSQL 18 UTF-8 database. Each context needs a deadline. Startup verifies existing migration checksum/version/columns; it does not migrate. Construct both PostgreSQL Modules, then an achrix.Application with a product Policy; construct audit.NewService(app, auditModule) followed by identity.NewService(app, identityModule, auditService). The Service rejects mismatched database profiles. Identity requires Core authorization ABI 2 and Audit append ABI 1, ordering Audit before Identity on startup and after it on shutdown.

The product must explicitly permit only Authentication for PublicPrincipal to expose credential/session attempts. This principal is never the successfully authenticated account and conveys no product rights. Login/session operations traverse that Core admission decision before expensive/database work; each subsequent product operation must separately authorize the returned account principal.

CreateAccount, Account, LookupAccount, SetPassword, SetEnabled, RevokeAll and ChangePassword have separate capabilities. Account returns current status/revision without secrets; conditional credential/status mutations require that revision. Account/status/credential/revoke-all transitions serialize against issuance through an account row lock and revision fence, revoke every session and reject overflow. Even a repeated status intent advances revision and writes its accountable event. Self-change separately authorizes the authenticated principal against its own account, verifies the current password, then rechecks the initiating live token under the account fence before committing.

LookupAccount(ctx, actor, exactLogin) reconciles a create whose database or HTTP acknowledgement was lost and whose generated ID was not received. It validates the existing login syntax/case contract, separately authorizes AccountLookup against that exact login before any account query, and returns only Account metadata. The existing unique login index bounds this exact read; there is no account list, fuzzy search, credential/session join or normalization. Missing metadata returns ErrNotFound only after authorization. AccountRead, public authentication and successful login do not imply this permission. A product must explicitly grant this protected capability to a suitable operator and retain the original login before sending create; an absent result alone does not prove an earlier interrupted transaction can never commit, so do not automatically replay a create.

Five successful event classes are atomic-required: account create, administrative credential set, own credential change, account status set and revoke-all. Each prepares an authorized Audit record before domain mutation, then calls Audit-owned append SQL in the same transaction. Any append error rolls back domain and audit state. Duplicate-login/stale-revision outcomes are conflicts. Cancellation/provider failure is safe unavailability and never promises that replay is harmless: a connection failure while committing can leave an unknown result. Read the current account revision/history before retrying; there is no automatic retry. Ordinary login/session traffic and invalid/denied requests are operational behavior, not durable Audit records in this slice.

Passwords, sessions and time

Passwords use maintained github.com/alexedwards/argon2id v1.0.0 over x/crypto Argon2id. PHC records preserve algorithm/version/cost; default memory 19 MiB, iterations 2, parallelism 1 follows the reviewed OWASP minimum. Supported bounds are memory 19–64 MiB, iterations 2–4 and parallelism 1–2, with random 16-byte salts and 32-byte keys. Stored PHC length, version, cost and sizes are checked before the library allocates; compatible weaker parameters can deliberately rehash after successful verification without silently downgrading stronger records. New passwords require 15–256 Unicode code points and at most 1024 UTF-8 bytes, without truncation/normalization. Login handles are bounded lowercase ASCII identifiers; they are not email/profile semantics.

Nonqueued hash admission admits at most two synchronous computations. Argon2 cannot be forcibly interrupted; cancellation is checked before/after. Whole expensive Service operations hold Module work leases, so shutdown closes admission, cancels and waits for them before pool closure. Config.MaxOperations bounds active owned leases, including nested database work (zero defaults to 16; explicit minimum two). Config.MaxConns bounds the instance pool (zero defaults to four; explicit minimum one). Credential mutations and login can hold an outer work lease plus a nested database lease; the minimum preserves an isolated supported operation, without promising fairness for concurrent nested work. Products still stop ingress and drain their domain work before Core shutdown. Parsed pool DSN options cannot override typed effective limits or the other fixed resource policy. Operations owns aggregate budgets and native demand-based pool behavior. The pgx parser's standard connection configuration/environment semantics remain part of the driver; products supply and protect an explicit DSN/profile rather than relying on ambient defaults.

On 2026-10-02, Go 1.27.1/Linux amd64 on the development server (two vCPU, AMD EPYC 7763, approximately 3.7 GiB RAM) measured three runs of three hashes per profile: default 21–25 ms/hash, approximately 19 MiB allocated; supported maximum 96–104 ms/hash, approximately 64 MiB allocated. These observations exclude constructor/dummy-hash setup and are not a production capacity claim or a reason to reduce the security floor. Reproduce with go test -run '^$' -bench '^BenchmarkPasswordHash' -benchmem -benchtime=3x -count=3 ./identity; a real deployment must measure and choose an intentional policy within the bounds.

Supplemental accepted raw benchmark evidence for unchanged password/hash code at candidate b505b1d18a7a7fb14c9b42ec5e06f256f0610dd3, using the same development environment and command above, measured default 22.75–33.16 ms/hash and supported maximum 173.84–186.03 ms/hash, with approximately 19/64 MiB allocated respectively. These later load-dependent samples supplement the earlier observations; neither range guarantees product latency/capacity or justifies lowering the security floor.

Session and CSRF tokens are independently generated 256-bit random opaque values. PostgreSQL stores SHA-256 hashes, never bearer/CSRF plaintext. General Session formatting, JSON secret fields and slog values are redacted; the deliberate web response includes a CSRF token but never the bearer. Login always issues a fresh token and revokes a supplied valid previous cookie in the same transaction, including account switches. Rotation is single-winner and preserves original absolute expiry; default lifetime is eight hours, configurable from one minute to 24 hours. This slice has no idle-timeout/sliding expiry. Each account holds at most 16 active session rows; expired rows are cleaned on issuance. Revocation, disabled status, changed revision and expiry fail closed. All persisted times/decisions use trusted concurrent-safe Config.Now, UTC and PostgreSQL microsecond precision; the time source must not move backwards for security decisions.

Same-origin HTTPS web boundary

NewWeb(service, exactHTTPSOrigin) provides relative /login, /csrf, /rotate, /logout, /password handlers and AuthenticateRequest for the product's own Application adapter. Actual TLS and exact Host are required; forwarded headers are not trusted. The product must terminate TLS here or supply a separately reviewed trusted proxy boundary. Mount one Web instance per authentication boundary so its shared budget covers every request. There is no CORS or cross-subdomain sharing.

The nonpersistent __Host-AChrix-Session cookie has Secure, HttpOnly, SameSite=Strict, Path=/ and no Domain. Session authentication is cookie-only; adapter requests with query strings are rejected. Mutations require a session-bound synchronizer X-CSRF-Token plus exact Origin (or single same-origin HTTPS Referer when Origin is absent). Every authentication endpoint additionally requires the non-simple X-Identity-Request: 1 header, including initial login. The same-origin /csrf endpoint refreshes the synchronizer token, invalidating the previous one; keep it in response/application memory, not URLs/cookies/persistent browser storage. These controls complement the reviewed OWASP session and CSRF guidance; they do not replace product XSS defenses or permissions.

Bodies are single strict JSON documents, 2 KiB maximum, with no unknown/trailing values. All responses are non-cacheable, nosniff, no-referrer and deny framing. The public login path uses a fixed 60/minute global budget plus 10/minute per socket-peer hash bucket in a fixed 256-entry table; collisions conservatively share a budget. No attacker-selected map/cardinality, queue or per-failure Audit/log is created. A 16-request admission limit and five-second Application operation contexts supplement the hash/pool limits. Strict JSON decoding additionally sets an actual two-second transport read deadline (or the earlier caller deadline) through net/http ResponseController; unsupported writer wrappers fail closed before decoding, and wrappers must expose Unwrap/deadline support. Context expiry alone does not bound Body.Read. Product servers also require bounded header reads, total reads/writes, idle duration and header size; the TLS proof composes those outer bounds explicitly. Real proxy/address topology and legitimate traffic policy need a supported deployment profile before tuning. Operational database failures expose only safe categories and emit at most one diagnostic per Module per second; products own slog threshold/correlation/handler policy under Operations.

Validation and retained state

scripts/validate.sh full runs real PostgreSQL account/session/concurrency/migration/atomic Audit failure tests, a cold pinned public Notes composition with real HTTPS authentication followed by product authorization, and native logical capture/restore of the retained joint dataset. Tests use dedicated private databases with guarded names. Source/configuration and both migration identities must remain available to interpret credentials and history. Backups contain sensitive password/session hashes and require access protection. Partial Identity-only/Audit-only restore is unsupported; after actual recovery, reconcile changed credentials/revocations and revoke restored sessions before reopening ingress. This proof is integration-only, not a production CMS or activation of paused #19.

Documentation

Overview

SPDX-License-Identifier: MPL-2.0

SPDX-License-Identifier: MPL-2.0

SPDX-License-Identifier: MPL-2.0 Package identity owns product-local accounts, password credentials and opaque server-side sessions. An authenticated principal conveys identity, never rights.

SPDX-License-Identifier: MPL-2.0

SPDX-License-Identifier: MPL-2.0

SPDX-License-Identifier: MPL-2.0

Index

Constants

View Source
const (
	AccountCreate     = "achrix.identity.account.create"
	AccountRead       = "achrix.identity.account.read"
	AccountLookup     = "achrix.identity.account.lookup"
	CredentialSet     = "achrix.identity.credential.set"
	AccountSetEnabled = "achrix.identity.account.set-enabled"
	SessionRevokeAll  = "achrix.identity.session.revoke-all"
	Authentication    = "achrix.identity.authenticate"
	PasswordChange    = "achrix.identity.credential.change"
)
View Source
const CookieName = "__Host-AChrix-Session"
View Source
const ModuleVersion = "0.2.0-development"

ModuleVersion is the unpublished source-line label, not the packaged build version. Deprecated: use Module.Descriptor().Version for component/update/recovery identity.

View Source
const PublicPrincipal achrix.Principal = "achrix.identity.public"

PublicPrincipal is only the principal for attempting credential/session authentication. A product must explicitly permit Authentication for this principal; it conveys no account identity or product permission.

Variables

View Source
var (
	ErrAuthentication = errors.New("authentication failed")
	ErrInvalid        = errors.New("invalid identity input")
	ErrNotFound       = errors.New("identity account not found")
	ErrConflict       = errors.New("identity precondition conflict")
	ErrUnavailable    = errors.New("identity unavailable")
	ErrLimited        = errors.New("identity admission limited")
	ErrConfiguration  = errors.New("invalid identity configuration")
)

Functions

func Migrate

func Migrate(ctx context.Context, dsn string) error

Migrate is an explicit product installation action, never part of Start. The schema and migration identity commit together. A repeated installation verifies the immutable checksum rather than reinterpreting existing data.

Types

type Account

type Account struct {
	ID, Login string
	Enabled   bool
	Revision  int64
}

Account exposes only the immutable opaque ID, login identifier and status. Login is a product-local authentication handle, never an email/profile contract.

type Config

type Config struct {
	// MaxConns bounds this instance's pool. Zero uses four; negatives are invalid.
	MaxConns int32
	// MaxOperations bounds active owned leases, including nested database work.
	// Zero uses 16; explicit values must be at least two.
	MaxOperations   int
	Password        PasswordPolicy
	HashConcurrency int
	SessionLifetime time.Duration
	Now             func() time.Time
}

Config is instance-owned. No environment/global configuration is read by Identity. Now controls security time decisions and must be trusted, monotonic in use and concurrency-safe. Zero fields use explicit bounded defaults.

type Module

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

Module owns its pool and schema. Products explicitly compose it, install the immutable migration, and wire a Service after constructing their Application. Stop closes admission, cancels/drains owned DB work, then closes the pool.

func NewPostgres

func NewPostgres(dsn string, config Config, logger *slog.Logger) (*Module, error)

NewPostgres validates configuration without network/database side effects. Runtime DSN sources remain product-owned. Unix/loopback is the measured dev profile; a remote DSN requires certificate-verified TLS and earns no support claim merely by passing this defensive preflight.

func (*Module) Descriptor

func (m *Module) Descriptor() achrix.Descriptor

func (*Module) FailureCount

func (m *Module) FailureCount() uint64

FailureCount is fixed-cardinality operational evidence, not durable Audit. Expected denial/authentication/invalid/limited traffic does not emit logs.

func (*Module) Ready

func (m *Module) Ready(parent context.Context) error

func (*Module) Start

func (m *Module) Start(ctx context.Context) error

func (*Module) Stop

func (m *Module) Stop(ctx context.Context) error

type PasswordPolicy

type PasswordPolicy struct {
	Memory, Iterations uint32
	Parallelism        uint8
}

PasswordPolicy uses KiB of memory. Defaults follow the reviewed OWASP Argon2id minimum; products must measure before changing the supported bounds.

func DefaultPasswordPolicy

func DefaultPasswordPolicy() PasswordPolicy

type Service

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

Service routes authorized account management through the normal Application. Public login/session authentication is credential-based; it never grants any product capability. Products use Principal with their own authorized Service.

func NewService

func NewService(app *achrix.Application, module *Module, accountability *audit.Service) (*Service, error)

func (*Service) Account

func (s *Service) Account(parent context.Context, actor achrix.Principal, id string) (Account, error)

Account is an authorized status/revision read for subsequent conditional management operations. It never returns credential or session material.

func (*Service) Authenticate

func (s *Service) Authenticate(parent context.Context, value string) (achrix.Principal, error)

func (*Service) ChangePassword

func (s *Service) ChangePassword(parent context.Context, value, current, next string) error

ChangePassword reauthenticates the currently enabled session account, atomically replaces the expected credential and revokes all sessions. A new login is needed.

func (*Service) CreateAccount

func (s *Service) CreateAccount(parent context.Context, actor achrix.Principal, login, password string) (Account, error)

func (*Service) Login

func (s *Service) Login(parent context.Context, login, password, previousToken string) (Session, error)

Login never adopts a client token. A supplied previous cookie is revoked in the same transaction as successful fresh issuance, including cross-account login. Unknown/disabled accounts undergo the bounded dummy hash; failures stay generic.

func (*Service) Logout

func (s *Service) Logout(parent context.Context, value string) error

func (*Service) LookupAccount

func (s *Service) LookupAccount(parent context.Context, actor achrix.Principal, login string) (Account, error)

LookupAccount reconciles one exact, validated login after an unknown create outcome. Its distinct permission targets that login, never an opaque ID or wildcard. Authentication and AccountRead do not implicitly grant this read. The result contains no credential/session material; no listing is provided.

func (*Service) RefreshCSRF

func (s *Service) RefreshCSRF(parent context.Context, value string) (string, error)

RefreshCSRF serves a freshly generated synchronizer token through an authenticated same-origin response. Only its hash is persisted; adapters must prevent cross-origin reads and must not put it in URL/cookie/Web Storage.

func (*Service) RevokeAll

func (s *Service) RevokeAll(parent context.Context, actor achrix.Principal, id string) error

func (*Service) Rotate

func (s *Service) Rotate(parent context.Context, value string) (Session, error)

func (*Service) SetEnabled

func (s *Service) SetEnabled(parent context.Context, actor achrix.Principal, id string, expectedRevision int64, enabled bool) error

func (*Service) SetPassword

func (s *Service) SetPassword(parent context.Context, actor achrix.Principal, id string, expectedRevision int64, password string) error

func (*Service) ValidateCSRF

func (s *Service) ValidateCSRF(parent context.Context, value, csrf string) (achrix.Principal, error)

type Session

type Session struct {
	Principal achrix.Principal
	Token     string `json:"-"`
	CSRF      string `json:"-"`
	ExpiresAt time.Time
}

Session secrets are returned only to the trusted adapter on issue/rotation. Default formatting deliberately redacts secret fields.

func (Session) GoString

func (s Session) GoString() string

func (Session) LogValue

func (s Session) LogValue() slog.Value

func (Session) String

func (s Session) String() string

type Web

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

Web owns a deliberately same-origin HTTPS browser boundary. It does not trust Forwarded/X-Forwarded-* or implement CORS. A product must terminate TLS here or compose a separately reviewed trusted proxy boundary before invoking this API.

func NewWeb

func NewWeb(service *Service, origin string) (*Web, error)

func (*Web) AuthenticateRequest

func (w *Web) AuthenticateRequest(r *http.Request, mutation bool) (achrix.Principal, error)

AuthenticateRequest validates cookie authentication and, for mutations, the synchronizer token and exact Origin/Referer. It returns no authority: the owning product Application operation must still authorize this principal.

func (*Web) Handler

func (w *Web) Handler() http.Handler

Handler serves /login, /csrf, /rotate, /logout and /password relative to its mount. All browser responses are non-cacheable, with no reflected diagnostics.

Directories

Path Synopsis
SPDX-License-Identifier: MPL-2.0 Package admin owns Identity's account-management presentation.
SPDX-License-Identifier: MPL-2.0 Package admin owns Identity's account-management presentation.

Jump to

Keyboard shortcuts

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