Skip to content

Authentication System

This page documents the authentication system implementation for contributors working in internal/auth/. If you’re an operator configuring authentication for a server, see the operator auth guide instead.

The authentication system spans three layers: a protocol handler in the gateway that calls core gRPC services, service implementations that enforce security invariants (timing-safe comparisons, argon2id hashing), and a repository layer that persists tokens and sessions to PostgreSQL. The sections below trace each layer from the wire inward.

HoloMUSH uses a three-layer authentication architecture:

Handles core authentication operations.

MethodPurposeSource
LoginAuthenticate player, create sessioninternal/auth/auth_service.go:81
LogoutInvalidate sessioninternal/auth/auth_service.go:197
ValidateSessionVerify token, update last-seeninternal/auth/auth_service.go:215
SelectCharacterBind character to sessioninternal/auth/auth_service.go:254

Manages character creation with validation.

MethodPurposeSource
CreateCreate character (default limit)internal/auth/character_service.go:56
CreateWithMaxCharactersCreate with custom limitinternal/auth/character_service.go:61

Character creation validates:

  • Name format (2-32 chars, letters and spaces, normalized to Initial Caps)
  • Name uniqueness (case-insensitive)
  • Player character limit (default: 5)
  • Starting location assignment

Handles password reset flow.

MethodPurposeSource
RequestResetGenerate reset token for emailinternal/auth/reset_service.go:80
ValidateTokenVerify token validity and expirationinternal/auth/reset_service.go:118
ResetPasswordUpdate password, invalidate tokensinternal/auth/reset_service.go:148

The auth service uses a dummy hash for non-existent users to prevent username enumeration:

Login(username, password):
1. Look up user by username
2. If not found -> use dummy hash (still verify to maintain constant time)
3. Verify password against hash (real or dummy)
4. Return same error for "user not found" and "wrong password"

See internal/auth/auth_service.go:67-76 for the dummy hash constant and internal/auth/auth_service.go:81-130 for the timing-safe implementation.

Passwords are hashed using argon2id with OWASP-recommended parameters:

ParameterValue
Time1 iteration
Memory64 MB
Parallelism4 threads
Salt16 bytes (random)
Output32 bytes

Implementation: internal/auth/hasher.go:30-36

Session token verification uses crypto/subtle.ConstantTimeCompare to prevent timing attacks that could leak token prefixes.

See internal/auth/session.go:115-125 for the implementation with security comments.

Sessions use opaque random tokens rather than signed JWTs. Key design choices:

AspectImplementation
Token size32 bytes (256 bits of entropy)
StorageSHA256 hash in database
ExpiryTTL after detach (per-role, default 24h)
RevocationInstant (delete database row)
Character bindMutable (update session row)

See ADR 0001 for the rationale behind choosing opaque tokens over signed JWTs.

The telnet handler calls through the gRPC CoreClient interface, not service methods directly:

CommandHandler MethodgRPC RPCSource
connecthandleConnectAuthenticatePlayerinternal/telnet/gateway_handler.go
createhandleCreateCreateCharacterinternal/telnet/gateway_handler.go
playhandlePlaySelectCharacterinternal/telnet/gateway_handler.go
quithandleQuitLogoutinternal/telnet/gateway_handler.go
internal/auth/player.go:128-151

Manages player account persistence including username lookups, email lookups, and password updates.

internal/auth/session.go:127-156

Manages session persistence including token hash lookups, last-seen updates, character binding, and expiration cleanup.

internal/auth/reset.go

Manages reset token persistence with token hash lookups and player-based cleanup.