A Go command-line authentication project using PostgreSQL, Redis, Docker, database-backed sessions, account lockout, and optional TOTP two-factor authentication.
This is a terminal application, not an HTTP API. You use commands such as register, login, and whoami.
- Registration and bcrypt password hashing
- Password login with generic invalid-credential responses
- Redis-backed failed-attempt counting and temporary lockout
- Optional TOTP two-factor authentication
- Random session tokens with only SHA-256 hashes stored in PostgreSQL
- Multiple independent sessions for the same user
- Per-session logout and absolute session expiration
- State-aware tab completion and command-name-only history
- Embedded PostgreSQL migrations
- Persistent PostgreSQL, Redis, and CLI-history Docker volumes
The easiest way to run the project is with Docker.
For Docker:
- Git
- Docker Desktop on Windows or macOS, or Docker Engine on Linux
- Docker Compose v2
For a native run:
- Go 1.26 or newer
- PostgreSQL and Redis, which may still run through Docker
Check Docker:
docker --version
docker compose version
Run all commands from the project directory containing docker-compose.yml.
The application requires a Base64-encoded 32-byte key to encrypt TOTP secrets.
You may use this public key to check the assignment locally:
TOTP_ENCRYPTION_KEY_BASE64=tt+dC5vhHj4ghzUFiP62J31NevdTWqB7qP0merFdzu8=This key is intentionally public. Do not use it in production. Anyone with this key and the database could decrypt stored TOTP secrets.
Generate a private key on Windows PowerShell:
$keyBytes = New-Object byte[] 32
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
$rng.GetBytes($keyBytes)
$rng.Dispose()
[Convert]::ToBase64String($keyBytes)Generate a private key on macOS or Linux:
openssl rand -base64 32Open PowerShell in the project directory:
cd C:\path\to\go-secure-login-cliCreate the Docker environment file:
Copy-Item .env.docker.example .env.dockerBuild the application and start PostgreSQL and Redis:
docker compose build app
docker compose up -d postgres redisOpen the CLI:
docker compose run --rm appOpen a terminal in the project directory:
cd /path/to/go-secure-login-cliCreate the Docker environment file:
cp .env.docker.example .env.dockerBuild the application and start PostgreSQL and Redis:
docker compose build app
docker compose up -d postgres redisOpen the CLI:
docker compose run --rm appBoth operating systems should now show:
Authentication CLI ready. Run `help` to see available commands.
auth-cli>
Show available commands:
help
Create a user:
register
Username rules:
- 3 to 50 characters by default
- Converted to lowercase and trimmed
- Starts with a lowercase letter or number
- Remaining characters may include lowercase letters, numbers, dots, underscores, and hyphens
Password rules:
- 8 to 72 UTF-8 bytes by default
- Not trimmed or converted to lowercase
- Confirmation must match exactly
ABcd1234is valid with the default settings
Password input is hidden.
Log in:
login
After login, try:
whoami
enable-2fa
logout
Press Tab at an empty prompt to see commands available in the current login state.
Logged out:
| Command | Purpose |
|---|---|
help |
Show available commands |
register |
Create an account |
login |
Log in with password and TOTP when enabled |
exit |
Close the CLI |
Logged in:
| Command | Purpose |
|---|---|
help |
Show available commands |
whoami |
Validate the session and show user information |
enable-2fa |
Set up TOTP authentication |
disable-2fa |
Disable TOTP after password and code verification |
logout |
Revoke only this terminal's session |
exit |
Close the CLI and clear its local session token |
Commands take no command-line arguments. The CLI prompts separately for usernames, passwords, and codes, so secrets do not enter command history.
Install Google Authenticator, Microsoft Authenticator, Authy, or another TOTP-compatible app.
- Register and log in.
- Run
enable-2fa. - Scan the QR code or use the displayed provisioning URI.
- Enter the current six-digit authenticator code.
- Run
logout. - Run
loginagain. - Enter the password and then the current TOTP code.
The encryption key is not the six-digit authenticator code. It encrypts the saved TOTP secret.
Keep the same TOTP_ENCRYPTION_KEY_BASE64 after enabling 2FA. Changing or losing it makes existing encrypted TOTP secrets unreadable.
Each docker compose run command creates an independent CLI process. All processes share PostgreSQL and Redis but keep separate raw session tokens and authentication state.
Start shared services:
docker compose up -d postgres redis
Open terminal 1:
docker compose run --rm app
Open terminal 2:
docker compose run --rm app
Suggested check:
- Register in terminal 1.
- Log in as the same user in terminals 1 and 2.
- Run
whoamiin both. - Log out in terminal 1.
- Run
whoamiin terminal 2; it should remain logged in. - Cause failed logins in one terminal and confirm Redis enforces the block in another.
Separate history paths stop command histories from mixing. They do not create sessions. Every successful login creates its own token and PostgreSQL session row.
Shared between terminals:
- PostgreSQL users and hashed sessions
- Redis failure counters and block state
- Docker volumes
Not shared between terminals:
- Raw session tokens
- Process login state and tab completion
- Instance-specific history
- Pending TOTP setup and login challenges
Create both environment files:
Copy-Item .env.docker.example .env.docker
Copy-Item .env.local.example .env.localStart the databases and run Go:
docker compose up -d postgres redis
go run ./cmd/cliFor independent histories, use separate PowerShell windows:
$env:HISTORY_PATH = "data/.auth-cli-history-1"
go run ./cmd/cliUse .auth-cli-history-2 in the second window.
Create both environment files:
cp .env.docker.example .env.docker
cp .env.local.example .env.localStart the databases and run Go:
docker compose up -d postgres redis
go run ./cmd/cliFor independent histories:
HISTORY_PATH=data/.auth-cli-history-1 go run ./cmd/cliUse .auth-cli-history-2 in the second terminal.
Inside Docker, Compose services use service names:
DATABASE_URL=postgres://auth_cli:change-this-postgres-password@postgres:5432/auth_cli?sslmode=disable
REDIS_URL=redis://redis:6379/0A Go process running directly on your computer uses localhost:
DATABASE_URL=postgres://auth_cli:change-this-postgres-password@localhost:5432/auth_cli?sslmode=disable
REDIS_URL=redis://localhost:6379/0Use postgres and redis in .env.docker. Use localhost in .env.local. The PostgreSQL username, password, and database name must match in both files.
.env.dockeris loaded by Docker Compose..env.localis loaded by a natively executed Go application.- Real environment files are ignored by Git.
- Committed
.examplefiles are safe templates.
| Variable | Example | Purpose |
|---|---|---|
DATABASE_URL |
PostgreSQL URL | Database connection |
REDIS_URL |
Redis URL | Shared lockout state |
HISTORY_PATH |
/app/data/.auth-cli-history |
Command-name history |
MIN_USERNAME_LENGTH |
3 |
Minimum username length |
MAX_USERNAME_LENGTH |
50 |
Maximum username length |
MIN_PASSWORD_LENGTH |
8 |
Minimum password byte length |
MAX_PASSWORD_LENGTH |
72 |
Maximum bcrypt-safe byte length |
BCRYPT_COST |
12 |
bcrypt work factor |
MAX_LOGIN_ATTEMPTS |
5 |
Failures before lockout |
ACCOUNT_LOCKOUT_DURATION |
15m |
Lockout duration |
SESSION_TIMEOUT |
30m |
Absolute session lifetime |
TOTP_ENCRYPTION_KEY_BASE64 |
Required | TOTP encryption key |
All configuration is validated at startup.
A successful login stores the raw token only in that CLI process:
AUTH_CLI_SESSION_TOKEN
PostgreSQL stores only its lowercase SHA-256 hash. Every protected command hashes and validates the process token. Logout, expiration, authorization failure, or shell exit removes the local token.
Passwords are stored as bcrypt hashes. TOTP secrets are encrypted with AES-256-GCM. Redis stores failed-attempt counters and temporary blocks.
Stop services while keeping all Docker volume data:
docker compose down
Restart later:
docker compose up -d postgres redis
docker compose run --rm app
Permanently delete users, sessions, Redis data, and histories:
docker compose down -v
Warning: docker compose down -v cannot be undone.
Clear only Redis lockouts during local testing:
docker compose exec redis redis-cli FLUSHDB
Repository tests use go-sqlmock, so they do not connect to PostgreSQL. Redis repository tests use generated client mocks and do not connect to Redis.
go generate ./...
go test ./...
go vet ./...
go build ./cmd/cli
docker build .
Current measured coverage: 13.2% of statements (measured on July 22, 2026).
This is overall application coverage excluding generated mock packages. The current tests focus on PostgreSQL repositories, Redis lockout behavior, transactions, and migrations; handlers and services are not yet covered.
Generated mocks packages are excluded from the coverage report because generated code is not application logic and including it would distort the useful percentage.
On Windows PowerShell, run these commands from the project directory:
$packages = go list ./... | Where-Object { $_ -notmatch '/mocks$' }
go test $packages "-coverprofile=coverage.out"
go tool cover "-func=coverage.out"
go tool cover "-html=coverage.out" "-o=coverage.html"
Start-Process .\coverage.htmlOn macOS or Linux:
packages=$(go list ./... | grep -v '/mocks$')
go test $packages -coverprofile=coverage.out
go tool cover -func=coverage.out
go tool cover -html=coverage.out -o coverage.htmlOpen the HTML report with:
# macOS
open coverage.html
# Linux
xdg-open coverage.htmlThe commands create:
coverage.out: machine-readable Go coverage data, ignored by Git.coverage.html: a line-by-line browser report. Green statements were executed; red statements were not.
For a quick package-by-package percentage without creating files, including generated mock packages:
go test ./... -cover
Interactive CLI -> Handlers -> Services -> Repository interfaces -> PostgreSQL/Redis
internal/
handler/
auth/
session/
shared/
totp/
service/
auth/
session/
totp/
repository/
user/
session/
transaction/
loginsecurity/
database/
postgres/
Handlers own terminal input and output. Services own authentication rules. Repositories own PostgreSQL and Redis access.
Copy the demonstration value into the environment file used by your run:
TOTP_ENCRYPTION_KEY_BASE64=tt+dC5vhHj4ghzUFiP62J31NevdTWqB7qP0merFdzu8=Restart the CLI.
Check MAX_LOGIN_ATTEMPTS. With 5, the fifth failure starts the temporary block. Existing Redis blocks remain until their TTL expires.
For local testing only:
docker compose exec redis redis-cli FLUSHDB
Check services:
docker compose ps
Native runs use localhost in .env.local. Docker runs use postgres and redis in .env.docker.
Stop the existing PostgreSQL or Redis service, or change the published Compose port and the matching URL in .env.local.
docker compose down -v
docker compose up -d postgres redis
docker compose run --rm app