A standalone, self-hosted, multi-tenant Identity Provider — EMC's drop-in replacement for Auth0.
Built with Go + Echo + PostgreSQL + Redis. Ships as a single binary that boots, migrates, seeds, and serves in under a second.
| Capability | Details |
|---|---|
| Multi-tenant | Tenant-scoped users, roles, permissions, and JWT secrets |
| JWT auth | HS256, per-tenant secret, 1-hour access + 30-day refresh |
| Refresh rotation | Atomic — old token revoked before new one issued; replay returns 401 |
| Password reset | SHA-256 hashed token, 15-min TTL, revokes all sessions on use |
| Rate limiting | 5 req/min/IP + 10 req/min/tenant on login |
| Admin API | Full tenant, user pool, role, and permission management |
| Audit logs | Every auth and admin event logged; tenant-scoped and system-wide views |
| Security headers | HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy |
| Swagger UI | Interactive docs at /swagger/index.html |
| Single binary | Migrations + seed embedded via embed.FS — no external tools needed |
| TOTP 2FA | AES-256-GCM encrypted secrets, 8 single-use backup codes, 2-step login flow |
| API Keys | emck_ prefix, SHA-256 hash stored, X-API-Key or Authorization: ApiKey header |
| Per-app rate limiting | X-App-ID header, app_rate_limits table, Redis-cached 60s TTL, 429+Retry-After |
| Cookie sessions | HttpOnly+SameSite=Lax emc_access_token+emc_refresh_token for browser/SPA |
| Per-tenant CORS | cors_origins[] on tenants, origin whitelist, OPTIONS preflight support |
| Prometheus metrics | GET /metrics, latency histogram, in-flight gauge, operation counters |
| Layer | Technology |
|---|---|
| Language | Go 1.23 |
| HTTP framework | Echo v4 |
| Database | PostgreSQL 16 (pgx v5 driver, pgxpool) |
| Cache / sessions | Redis 7 |
| Migrations | Goose v3 (embedded SQL) |
| Logging | Zerolog (structured JSON) |
| API docs | Swaggo / echo-swagger |
| Passwords | bcrypt cost 12 |
| Tokens | JWT HS256 (golang-jwt/jwt v5) |
| TOTP | github.com/pquerna/otp |
| Metrics | github.com/prometheus/client_golang v1.19 |
Client
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Echo HTTP Server │
│ │
│ ┌──────────────┐ ┌────────────────┐ ┌────────────────────┐ │
│ │ Request ID │─►│Security Headers│─►│ HTTPS Redirect │ │
│ │ (per-req) │ │ HSTS/CSP/etc. │ │ (production only) │ │
│ └──────────────┘ └────────────────┘ └─────────┬──────────┘ │
│ │ │
│ ┌────────────────────────────────────────────────▼──────────┐ │
│ │ Request Logger (zerolog JSON) │ │
│ └──────────────────────────┬─────────────────────────────┬─┘ │
│ │ │ │
│ ┌──────────────▼──────┐ ┌─────────────────▼──┐ │
│ │ Public Endpoints │ │ Protected Endpoints │ │
│ │ /auth/* /health │ │ /admin/* /auth/me │ │
│ │ │ │ │ │
│ │ Rate Limiter │ │ JWT Middleware │ │
│ │ (login: 5/min/IP │ │ (verify HS256, │ │
│ │ 10/min/tenant) │ │ extract claims) │ │
│ │ │ │ │ │
│ │ Tenant Resolution │ │ Permission Guard │ │
│ │ X-Tenant-Slug hdr │ │ (tenant:manage or │ │
│ │ → DB lookup → UUID │ │ admin:access) │ │
│ └──────────┬──────────┘ └──────────┬──────────┘ │
│ └──────────────┬───────────┘ │
│ │ │
│ ┌─────────▼──────────┐ │
│ │ Handler │ │
│ │ (auth / admin) │ │
│ └───────┬─────┬───────┘ │
│ │ │ │
│ ┌────────────▼──┐ ┌▼────────────┐ │
│ │ PostgreSQL │ │ Redis │ │
│ │ pgxpool(25) │ │ (sessions, │ │
│ │ Users/Roles/ │ │ rate lim) │ │
│ │ Audit/Tokens │ │ │ │
│ └───────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘
POST /auth/login
│
├─► Resolve tenant (X-Tenant-Slug → tenants table)
├─► Verify bcrypt hash (cost 12)
├─► Load user roles + permissions from DB
├─► Sign JWT (HS256, per-tenant secret, 1-hour TTL)
│ Payload: { user_id, tenant_id, email, role, permissions[], iss, aud, exp }
├─► Generate refresh token (32-byte crypto/rand → SHA-256 hash stored in Redis)
└─► Return { access_token, refresh_token, token_type, expires_in }
POST /auth/refresh
├─► Hash incoming token → lookup in Redis
├─► Atomically: delete old token, store new token
└─► Return new token pair (old token is immediately dead — replay → 401)
Every request resolves to one of three access tiers based on JWT claims:
┌──────────────────────────────────────────────────────────────────┐
│ Tier 1 — System Level (super_admin / tenant:manage) │
│ │
│ • Create / update / deactivate any tenant │
│ • View system-wide audit log (all tenants) │
│ • Assign super_admin role │
│ Scope: cross-tenant, system-wide │
├──────────────────────────────────────────────────────────────────┤
│ Tier 2 — Tenant Admin Level (admin:access) │
│ │
│ • Manage users within own tenant (create, update, delete) │
│ • Create / manage roles and permissions within own tenant │
│ • View tenant-scoped audit log (own tenant only) │
│ • Force password reset for any user in tenant │
│ Scope: tenant-isolated, never cross-tenant │
├──────────────────────────────────────────────────────────────────┤
│ Tier 3 — Authenticated User │
│ │
│ • Authenticate (login, refresh, logout) │
│ • View own profile (GET /auth/me) │
│ • Reset own password │
│ Scope: own user record only │
└──────────────────────────────────────────────────────────────────┘
Tier is determined from JWT claims at runtime — never from the request body. A token from tenant A cannot access tenant B's resources regardless of what the request contains.
Role (tenant-scoped)
└── has many Permissions (tenant-scoped, UNIQUE per tenant)
User
└── assigned one Role per tenant
JWT payload carries:
{ role: "super_admin", permissions: ["tenant:manage", "admin:access"] }
Seeded system permissions (emc tenant only):
tenant:manage → Tier 1 guard — super_admin operations
admin:access → Tier 2 guard — tenant admin operations
Tenant-created permissions (examples):
invoice:read, invoice:write, report:export, user:impersonate
(each tenant defines its own; names are isolated by tenant_id)
Permissions flow: tenant admin creates permissions → assigns to roles → assigns roles to users → JWT contains permission list → RequirePermission() middleware enforces at the route level.
Prerequisites: Docker Desktop running.
git clone https://github.com/Engineersmind/emc-auth-server.git
cd emc-auth-server
cp .env.example .env # review and adjust if needed
docker-compose up --buildThe server starts on port 8080. On first boot it automatically:
- Runs all database migrations
- Seeds the default
emctenant,super_adminrole, andadmin@emc.localuser
Verify:
curl http://localhost:8080/health
# {"status":"ok","service":"emc-auth-server"}Open the interactive API docs: http://localhost:8080/swagger/index.html
Prerequisites: Go 1.23+, PostgreSQL 16, Redis 7.
git clone https://github.com/Engineersmind/emc-auth-server.git
cd emc-auth-server
# Start only the dependencies (Postgres + Redis)
docker-compose up -d postgres redis
# Copy and configure environment
cp .env.example .env
# Download dependencies
go mod download
# Build
go build -o emc-auth-server ./cmd/server/
# Run
./emc-auth-serverCopy .env.example to .env and adjust as needed:
# Server
PORT=8080
ENV=development # set to "production" to enable HTTPS redirect
LOG_LEVEL=info
# PostgreSQL
DATABASE_URL=postgres://emc_auth:password@localhost:5432/emc_auth?sslmode=disable
# Redis
REDIS_URL=redis://localhost:6379/0
# JWT
JWT_ISSUER=https://auth.emc.local
# Password reset link base (prepended to /api/v1/auth/reset-password?token=...)
APP_BASE_URL=http://localhost:8080
# Seed admin password (change this in production!)
SEED_ADMIN_PASSWORD=ChangeMe123!
# TOTP — AES-256-GCM key for encrypting TOTP secrets at rest
# Generate: openssl rand -hex 32
# Required in production; dev defaults to zero-key (logs a warning)
TOTP_ENCRYPTION_KEY=
# SMTP — optional. In development the reset link is logged to console instead.
SMTP_HOST=
SMTP_PORT=587
SMTP_FROM=noreply@emc.local
SMTP_USERNAME=
SMTP_PASSWORD=After first boot the seed creates:
| Field | Value |
|---|---|
| Tenant slug | emc |
| Admin email | admin@emc.local |
| Admin password | value of SEED_ADMIN_PASSWORD (default: ChangeMe123!) |
| Admin role | super_admin |
| Admin permissions | tenant:manage, admin:access |
Change the default password before exposing the server to any network.
Connect any SQL client (DBeaver, pgAdmin, TablePlus) to the local dev database:
Host: localhost
Port: 5432
Database: emc_auth
Username: emc_auth
Password: password
All public auth endpoints require the
X-Tenant-Slugheader (e.g.X-Tenant-Slug: emc)
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/v1/auth/register |
— | Register a new user |
| POST | /api/v1/auth/login |
— | Login — returns JWT + refresh token |
| POST | /api/v1/auth/refresh |
— | Rotate refresh token |
| POST | /api/v1/auth/logout |
— | Revoke refresh token |
| GET | /api/v1/auth/me |
Bearer JWT | Get current user profile |
| POST | /api/v1/auth/forgot-password |
— | Request password reset link |
| POST | /api/v1/auth/reset-password |
— | Set new password with reset token |
| POST | /api/v1/auth/accept-invitation |
Invitation token | Set the password for an invited account |
| POST | /api/v1/auth/change-email |
Bearer JWT | Request an email change (confirmation goes to the new address) |
| GET | /api/v1/auth/confirm-email-change |
Change token | Apply a pending email change |
| GET | /api/v1/auth/unblock-account |
Unblock token | Lift an automatic failed-attempt lockout |
Requires
tenant:managepermission (super_admin only)
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/admin/tenants |
Create tenant |
| GET | /api/v1/admin/tenants |
List all tenants |
| PUT | /api/v1/admin/tenants/:id |
Update tenant name |
| DELETE | /api/v1/admin/tenants/:id |
Soft-deactivate tenant |
Requires
admin:accesspermission
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/admin/permissions |
Create permission (tenant-scoped) |
| GET | /api/v1/admin/permissions |
List permissions |
| DELETE | /api/v1/admin/permissions/:id |
Delete permission |
| POST | /api/v1/admin/roles |
Create role with permission assignments |
| GET | /api/v1/admin/roles |
List roles (with embedded permissions) |
| PUT | /api/v1/admin/roles/:id/permissions |
Replace role's permission set |
| DELETE | /api/v1/admin/roles/:id |
Delete role |
Requires
admin:accesspermission
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/admin/users |
List users (?search=&page=&limit=) |
| POST | /api/v1/admin/users |
Create user with optional role (send_invitation: true emails an invite instead of setting a password) |
| GET | /api/v1/admin/users/:id |
Get user details |
| PUT | /api/v1/admin/users/:id |
Update user profile |
| PUT | /api/v1/admin/users/:id/role |
Reassign role |
| DELETE | /api/v1/admin/users/:id |
Soft-delete user |
| POST | /api/v1/admin/users/:id/force-password-reset |
Send password reset email |
| POST | /api/v1/admin/users/:id/invite |
Resend the invitation link (supersedes any previous one) |
| Method | Endpoint | Guard | Description |
|---|---|---|---|
| GET | /api/v1/admin/audit-logs |
admin:access |
Tenant-scoped event log |
| GET | /api/v1/admin/audit-logs/system |
tenant:manage |
System-wide log (all tenants) |
Filters: ?action=auth.login_failed&user_id=<uuid>&from=2026-01-01T00:00:00Z&to=...&page=1&limit=50
Requires
Authorization: Bearer <token>
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/auth/otp/enroll |
Enroll TOTP — returns otpauth:// URI + 8 backup codes |
| POST | /api/v1/auth/otp/activate |
Activate TOTP with first valid code |
| DELETE | /api/v1/auth/otp |
Disable TOTP (requires valid TOTP code or backup code) |
| POST | /api/v1/auth/login/otp |
Complete TOTP-gated login (otp_session_token + code) |
Browser / SPA — no client-side token management needed
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/auth/session |
Login — sets emc_access_token + emc_refresh_token HttpOnly cookies |
| POST | /api/v1/auth/session/refresh |
Rotate tokens via refresh cookie |
| POST | /api/v1/auth/session/logout |
Revoke session + clear cookies |
Requires
admin:accesspermission
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/admin/api-keys |
Create API key — raw key returned once |
| GET | /api/v1/admin/api-keys |
List keys (name, created_at, last_used — no raw key) |
| DELETE | /api/v1/admin/api-keys/:id |
Revoke API key |
Requires
admin:accesspermission
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/admin/app-limits |
Create per-app rate limit config |
| GET | /api/v1/admin/app-limits |
List all configs (tenant-scoped) |
| PUT | /api/v1/admin/app-limits/:app_id |
Update limit (effective within 60s) |
| DELETE | /api/v1/admin/app-limits/:app_id |
Remove — app falls back to default (60 RPM) |
Requires
tenant:managepermission
| Method | Endpoint | Description |
|---|---|---|
| PUT | /api/v1/admin/tenants/:id/cors-origins |
Replace allowed origins list for a tenant |
| Method | Endpoint | Description |
|---|---|---|
| GET | /metrics |
Prometheus metrics endpoint (no auth — restrict via network policy) |
- Open http://localhost:8080/swagger/index.html
- Call
POST /api/v1/auth/loginwithX-Tenant-Slug: emcand admin credentials - Copy the
access_tokenfrom the response - Click Authorize (padlock icon, top right)
- Enter
Bearer <your_access_token>— include the word Bearer - All protected endpoints are now unlocked
Each tenant is fully isolated:
- JWT secret — per-tenant; tokens from tenant A cannot validate against tenant B
- User pool —
UNIQUE(tenant_id, email); same email can exist in different tenants - Roles and permissions — per-tenant; two tenants can both define
invoice:readindependently - Audit log — events are tagged with
tenant_id; admins only see their own tenant's events
Tenant resolution on public endpoints: X-Tenant-Slug header → DB lookup → tenant UUID.
Tenant on protected endpoints: tenant_id claim inside the verified JWT — never from request body.
Every security-relevant action is persisted to the audit_logs table:
| Action | Trigger |
|---|---|
auth.login |
Successful login |
auth.login_failed |
Wrong credentials |
auth.register |
New user registered |
auth.logout |
Logout |
auth.token_refresh |
Refresh token rotated |
auth.password_reset_requested |
Forgot-password called |
auth.password_reset_completed |
Password reset completed |
admin.tenant_created/updated/deactivated |
Tenant lifecycle |
admin.permission_created/deleted |
Permission management |
admin.role_created/permissions_updated/deleted |
Role management |
admin.user_created/updated/deleted |
User pool management |
admin.user_role_assigned |
Role reassigned |
admin.force_password_reset |
Admin-triggered reset |
admin.app_limit_created/updated/deleted |
Per-app rate limit config changed |
admin.cors_origins_updated |
Tenant CORS origins replaced |
Query the audit log via API (JSON) or directly in SQL:
-- Last 50 failed logins across all tenants
SELECT actor_email, ip_address, created_at
FROM audit_logs
WHERE action = 'auth.login_failed'
ORDER BY created_at DESC
LIMIT 50;
-- All admin actions on a specific user
SELECT action, actor_email, created_at
FROM audit_logs
WHERE resource_type = 'user'
AND resource_id = '<user-uuid>'
ORDER BY created_at DESC;| # | Decision | Choice | Rationale |
|---|---|---|---|
| ADR-01 | JWT signing algorithm | HS256 per-tenant secret | Simpler key management for self-hosted deployments; RS256 (asymmetric) deferred to v2 when external verifiers need public keys |
| ADR-02 | Multi-tenancy strategy | tenant_id column (row-level) |
Schema-per-tenant adds migration complexity and doesn't scale past ~100 tenants; row-level isolation is simpler and proven at scale |
| ADR-03 | Migration tool | Goose v3 (embedded SQL) | Plain SQL files version-controlled alongside code; embedded via embed.FS — zero external tooling at runtime |
| ADR-04 | ORM vs raw SQL | Raw SQL (pgx v5) | Auth queries are security-critical and latency-sensitive; full control over parameterization, no magic, no N+1 surprises |
| ADR-05 | Tenant resolution | X-Tenant-Slug header |
Human-readable slug over UUID; slug is stable and meaningful in logs and URLs |
| ADR-06 | Rate limiter storage | In-memory (sync.Map + x/time/rate) |
Sufficient for single-instance deployments; Redis-backed distributed limiting hooked in Phase 7 with explicit code comment |
| ADR-07 | Refresh token storage | SHA-256 hash in Redis | Raw token never stored; Redis TTL handles expiry automatically; hash prevents token extraction if cache is compromised |
| ADR-08 | Password hashing | bcrypt cost 12 | Industry standard; cost 12 gives ~300ms on modern hardware — strong against GPU brute-force while acceptable for login latency |
| ADR-09 | Audit log failure mode | Fire-and-forget | Audit failure must never block authentication; errors are logged via zerolog but never returned to caller |
| ADR-10 | Permission scope | Tenant-scoped (not system-global) | Permissions are business concepts owned by each tenant; system-global permissions would create cross-tenant coupling |
| ADR-11 | Admin UI delivery | React SPA embedded via embed.FS |
Single deployable artifact — no separate static file server or CDN required for self-hosted deployment (Phase 6) |
| ADR-12 | Email enumeration prevention | forgot-password always returns HTTP 200 |
Identical response body for registered and unregistered emails prevents user enumeration via timing or response diff |
| ADR-13 | TOTP encryption | AES-256-GCM with per-server key | Secret rotation deferred; dev uses zero-key with warning |
| ADR-14 | Cookie vs Bearer | Both supported | JWTRequired checks Bearer first, falls back to HttpOnly cookie |
| ADR-15 | Metrics auth | GET /metrics unauthenticated |
Intended for Prometheus; restrict via network policy in production |
| Control | Implementation |
|---|---|
| Password storage | bcrypt cost 12 — plaintext never persisted |
| SQL injection | Parameterized queries via pgx v5 positional args ($1, $2, ...) throughout |
| Token storage | Refresh and reset tokens stored as SHA-256 hashes — raw values never in DB |
| JWT integrity | HS256 signed with per-tenant secret; signature verified on every protected request |
| Session revocation | Logout and password reset atomically delete all Redis session keys for the user |
| Rate limiting | 5 req/min/IP + 10 req/min/tenant on /auth/login — returns 429 Too Many Requests |
| Security headers | HSTS (1 year), X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin |
| Replay protection | Refresh token rotation is atomic — consumed token is deleted before new token is issued |
| Soft delete | Users marked is_deleted=true — IDs preserved for audit trail integrity |
| Enumeration prevention | forgot-password returns identical HTTP 200 for both registered and unregistered emails |
| Tenant isolation | JWT tenant_id claim is authoritative — never trust request body for tenant resolution on protected routes |
| TOTP 2FA | AES-256-GCM encrypted secret at rest; backup codes SHA-256 hashed, single-use |
| API Key auth | emck_ prefix, 32-byte random, SHA-256 hash stored; raw key shown once at creation |
| Cookie sessions | HttpOnly + SameSite=Lax cookies; Secure flag auto-detected from TLS |
| Per-tenant CORS | Origin whitelist per tenant, DB-backed, Redis-cached (60s TTL) |
| Per-app rate limits | X-App-ID token bucket, 60s Redis cache, 429+Retry-After |
These run on every push and PR via .github/workflows/ci.yml:
gosec ./... # security-focused Go linter — SARIF output uploaded to GitHub Security
govulncheck ./... # known CVE scan across all dependencies
golangci-lint run # errcheck, staticcheck, unused, shadow, etc.
go test ./... # build + unit tests (coverage report artifact)
docker build . # smoke-tests the production image builds cleanly# 07-01: Test coverage gate (target ≥80% on internal/auth/ and internal/store/)
go test ./... -coverprofile=coverage.out
go tool cover -func=coverage.out
# 07-02: Security test suite (to be written)
# - Replay attack: re-submit consumed refresh token → assert 401
# - Brute-force: 6th login attempt in 1 min → assert 429
# - Email enumeration: diff forgot-password response for known vs unknown → assert identical
# - TOTP bypass: submit expired/wrong OTP → assert 401
# - SQL injection probes on search and filter params
# 07-03: Load test + distroless production image
k6 run --vus 500 --duration 60s load/login.js # p99 ≤ 200ms target# .github/workflows/ci.yml
on: [push, pull_request]
jobs:
quality:
steps:
- go test ./... -coverprofile=coverage.out # ≥80% gate
- gosec ./...
- govulncheck ./...
- golangci-lint run
build:
steps:
- docker build --target prod . # distroless final image
- docker-compose up -d && sleep 5
- curl -f http://localhost:8080/health| Signal | Implementation | Where to look |
|---|---|---|
| Structured request logs | Zerolog JSON — method, path, status, latency, request_id | stdout / log aggregator |
| Health check | GET /health → {"status":"ok","service":"emc-auth-server"} |
Uptime monitors |
| Audit trail | audit_logs table — 21 event types with tenant_id, user_id, IP |
PostgreSQL / API |
| Error logging | Zerolog Error() on all DB/Redis failures with context fields |
stdout |
GET /metrics — Prometheus-compatible endpoint (live, no auth required):
# HTTP layer
emc_auth_http_request_duration_seconds{method, path, status} histogram
emc_auth_http_requests_in_flight gauge
# Business operations
emc_auth_operations_total{operation, outcome} counter
# Rate limiting
emc_auth_rate_limit_hits_total{limiter} counter
Provisioned JSON at infra/grafana/dashboards/emc-auth.json — auto-loaded by the production compose stack.
Row 1 — Traffic Overview
├── Requests/sec (by endpoint)
├── Login success rate (success / total)
└── Rate limit hit rate
Row 2 — Auth Health
├── Failed logins by tenant (bar chart — spike = brute-force attempt)
├── Token refresh volume
└── Password reset requests
Row 3 — Performance
├── p50/p95/p99 request latency (heatmap)
├── DB query latency
└── Redis operation latency
Row 4 — Infrastructure
├── DB connection pool utilization
├── Go runtime memory (heap in use)
└── Goroutine count
Rules file: infra/prometheus/rules.yml
| Alert | Condition | Severity |
|---|---|---|
| Auth spike | Login failures > 50/min/tenant | Warning |
| Brute-force | Rate limit hits > 100/min/IP | Critical |
| DB saturation | Pool acquired > 90% for 5 min | Warning |
| High latency | p99 login > 500ms for 2 min | Warning |
| Server down | /health fails 3 consecutive checks |
Critical |
A React SPA embedded directly in the Go binary via embed.FS — no separate web server or CDN needed.
Planned screens:
| Screen | Capabilities |
|---|---|
| Login | Email/password + TOTP (if enabled); establishes admin session |
| Dashboard | Active user count, recent login events, auth success rate tile |
| User Pool | Paginated + searchable table; create, edit, assign role, soft-delete, force reset |
| Roles & Permissions | Create/edit roles with permission assignments; create custom permissions |
| Tenant Management | Super-admin only — create, rename, deactivate tenants (cross-tenant view) |
| API Keys | Create (one-time reveal), list with last_used, revoke |
| SAML Config | Per-tenant IdP entity ID, SSO URL, X.509 cert |
| Audit Log Viewer | Filterable table — action type, date range, user, IP |
Build pipeline: Vite + React + TypeScript → go generate embeds the ui/dist/ output into the binary → served at / by Echo static middleware.
Items tracked for v2 (post-Phase 7):
| Enhancement | Description | Priority |
|---|---|---|
| OAuth2 / OIDC server | Expose authorization_code + client_credentials flows so third-party apps can use EMC-Auth as a full OAuth2 provider | High |
| Social login | Google (#64 ✅) and GitHub (#66 ✅) shipped with account linking; Microsoft pending | Medium |
| Passwordless / magic link | Email-based one-click login for low-friction onboarding | Medium |
| RS256 JWT | Asymmetric signing — public key endpoint so external services can verify tokens without calling EMC-Auth | Medium |
| Webhooks | POST to a configured URL on user lifecycle events: user.created, user.deleted, auth.login_failed |
Medium |
| SDK libraries | Node.js and Python client libraries for JWT verification and token introspection | Medium |
| White-label login page | Per-tenant custom logo, colors, and domain on the hosted login UI | Low |
| Audit log retention | Configurable retention window + archive to S3/GCS after N days | Low |
| Login anomaly detection | Flag logins from new country/device; alert admin or require MFA step-up | Low |
| Distributed rate limiting | Replace in-memory rate limiter with Redis-backed solution for multi-instance deployments | Phase 7 |
| Token introspection endpoint | POST /oauth/introspect — RFC 7662 token introspection for resource servers |
v2 |
After adding or modifying handler annotations:
# Install swag if not already installed
go install github.com/swaggo/swag/cmd/swag@latest
swag init -g cmd/server/main.go --output docsPhase 1 Foundation ████████████████████ COMPLETE ✓ (3/3 plans)
Phase 2 Auth Engine ████████████████████ COMPLETE ✓ (4/4 plans)
Phase 3 TOTP + API Keys ████████████████████ COMPLETE ✓ (3/3 plans)
Phase 4 SAML 2.0 ░░░░░░░░░░░░░░░░░░░░ PLANNED (0/2 plans)
Phase 5 Admin API ████████████████████ COMPLETE ✓ (3/3 plans)
Phase 6 Admin UI ░░░░░░░░░░░░░░░░░░░░ PLANNED (0/4 plans)
Phase 7 Testing & Hardening ████████░░░░░░░░░░░░ PARTIAL (2/5 plans — 07-04 CI/CD ✓, 07-05 Prometheus ✓)
Phase 8 AI/Agent Security ████████░░░░░░░░░░░░ PARTIAL (1/4 plans — 08-02 Rate Limiting ✓)
+ Bonus Cookie Sessions ████████████████████ SHIPPED ✓
+ Bonus Per-tenant CORS ████████████████████ SHIPPED ✓
─────────────────────────────────────────────────────────────────
5+ of 8 phases complete · 15+ sub-plans done · ~65%
Phase 1 — Foundation (COMPLETE)
| Plan | Description | Status |
|---|---|---|
| 01-01 | Go module, Echo skeleton, logging, health, Dockerfile, docker-compose | ✓ Done |
| 01-02 | Goose migration files — all schema tables, FKs, indexes | ✓ Done |
| 01-03 | PostgreSQL + Redis pool, idempotent seed (tenant + super-admin) | ✓ Done |
Phase 2 — Auth Engine (COMPLETE)
| Plan | Description | Status |
|---|---|---|
| 02-01 | Register, login, bcrypt cost 12, JWT HS256, GET /auth/me |
✓ Done |
| 02-02 | Refresh rotation (Redis), logout, JWTRequired, RequirePermission middleware | ✓ Done |
| 02-03 | Rate limiting (5/min/IP, 10/min/tenant), HTTPS enforcement, HSTS headers | ✓ Done |
| 02-04 | Password reset — forgot-password, SHA-256 token, email dispatch, session revocation | ✓ Done |
Phase 3 — TOTP + API Keys (COMPLETE)
| Plan | Description | Status |
|---|---|---|
| 03-01 | TOTP enroll/verify/disable, AES-256-GCM secret encryption, backup codes | ✓ Done |
| 03-02 | TOTP step injected into login flow when TOTP is active | ✓ Done |
| 03-03 | API key CRUD (create/list/revoke) + APIKeyAuth middleware + last_used |
✓ Done |
Phase 4 — SAML 2.0 (branch: feat/phase-4-saml)
| Plan | Description | Status |
|---|---|---|
| 04-01 | Per-tenant SAML config model, GET /saml/metadata using crewjam/saml |
Planned |
| 04-02 | SP-initiated login, ACS handler, assertion validation, JIT provisioning | Planned |
Phase 5 — Admin API (COMPLETE)
| Plan | Description | Status |
|---|---|---|
| 05-01 | Tenant management endpoints with tenant:manage guard | ✓ Done |
| 05-02 | AdminService — user pool, RBAC, role/permission CRUD | ✓ Done |
| 05-03 | AdminHandler — all admin REST endpoints + audit query handlers | ✓ Done |
Phase 6 — Admin UI (branch: feat/phase-6-admin-ui)
| Plan | Description | Status |
|---|---|---|
| 06-01 | Vite + React + TypeScript scaffold, auth context, login page, protected routes | Planned |
| 06-02 | Tenant management views (super-admin) + user pool table (search, filter, paginate) | Planned |
| 06-03 | User detail page (edit, role assign, force reset) + roles/permissions management | Planned |
| 06-04 | API keys page, SAML config, dashboard metrics tile, embed.FS static serving |
Planned |
Phase 7 — Testing & Hardening (PARTIAL — branch: feat/phase-7-hardening)
| Plan | Description | Status |
|---|---|---|
| 07-01 | Unit + integration tests for internal/auth/ and internal/store/ (≥80% coverage) |
Planned |
| 07-02 | Security test suite — replay, rate limit, enumeration, TOTP bypass, SQL injection | Planned |
| 07-03 | Load test (k6, 500 VUs), production Dockerfile (distroless), deployment runbook | Planned |
| 07-04 | CI/CD — GitHub Actions: test + coverage gate, gosec, govulncheck, golangci-lint | ✓ Done |
| 07-05 | Observability — Prometheus /metrics, Grafana dashboard JSON, alerting rules |
✓ Done |
Phase 8 — AI/Agent Security (PARTIAL — branch: feat/phase-8-ai-agent-security)
| Plan | Description | Status |
|---|---|---|
| 08-01 | Agent registration (agent_registrations table), authenticate endpoint, agent JWT variant |
Planned |
| 08-02 | Per-application rate limiting — configurable limits per app_id, DB-backed, Redis-cached |
✓ Done |
| 08-03 | Agent audit trail — agent_id in audit_logs, ?agent_id= filter, GET /admin/agents list |
Planned |
| 08-04 | Security analysis engine — GET /admin/agents/analysis risk scoring, anomaly flags |
Planned |
master
├── feat/phase-4-saml ← SAML 2.0 enterprise SSO (planned)
├── feat/phase-6-admin-ui ← React admin dashboard (planned)
├── feat/phase-7-hardening ← remaining: tests, load test
└── feat/phase-8-ai-agent-security ← remaining: agent registration, audit trail, risk scoring
Naming convention: feat/, fix/, hotfix/, chore/, docs/, refactor/, test/ prefix required. Direct pushes to master are blocked by repository ruleset.
Merge policy: Merge commits only (--no-ff) — squash and rebase are disabled. This preserves per-feature commit history on master so git log --oneline reads as a narrative.
Branch lifecycle: Feature branches are automatically deleted after merge (delete_branch_on_merge: true).
Each merge to master is tagged with a semantic version (v1.x.0) and a GitHub Release with detailed notes.
Completion gate before merge:
□ All sub-plans committed and self-tested
□ go build ./... succeeds with zero warnings
□ go test ./... passes (≥80% coverage on auth core — Phase 7+)
□ gosec ./... clean (Phase 7+)
□ swag init updated if handlers changed
□ README phase tracking and ROADMAP.md updated
□ PR created — CODEOWNERS review approved, CI green, 2 reviewers signed off
| Control | Configuration | Status |
|---|---|---|
| PR required on master | Org ruleset: Protected Branch Standards | Active |
| 2 reviewer approvals | Org ruleset: 2-Approval Gate (main + production) |
Active — master scope pending admin:org |
| CODEOWNERS review required | .github/CODEOWNERS — auth / middleware / migrations / .github |
Active |
| CI must pass before merge | Repo ruleset: CI Gates (Test + Lint + Docker Build) | Active |
| Copilot code review | Org ruleset: Require Copilot Code Review | Active |
| Merge commit only — no squash/rebase | allow_squash_merge: false, allow_rebase_merge: false |
Active |
| Auto-delete merged branches | delete_branch_on_merge: true |
Active |
| No force-push to master | Org ruleset: non_fast_forward rule | Active |
| No branch deletion | Org ruleset: deletion rule | Active |
| Branch naming convention | Repo ruleset: regex ^(feat|fix|chore|hotfix|docs|refactor|test)/.+ |
Active |
EMC-Auth treats AI agents as first-class authenticated identities — not anonymous API consumers. Any LLM, orchestrator, or automated service can register, authenticate, and be governed by the same tenant-scoped permission and audit model as human users.
agent_registrations
├── id UUID PRIMARY KEY
├── tenant_id UUID (tenant-scoped — agent A cannot cross into tenant B)
├── name TEXT (human-readable label, e.g. "invoice-classifier-v2")
├── agent_type TEXT (llm | tool | orchestrator | service)
├── capabilities TEXT[] (declared scopes: ["invoice:read", "report:export"])
├── api_key_hash TEXT (SHA-256 of raw key — raw key shown once at registration)
├── is_active BOOL
└── created_at TIMESTAMPTZ
Agent JWT payload (same middleware, different claims):
{
"agent_id": "uuid",
"agent_type": "llm",
"tenant_id": "uuid",
"capabilities": ["invoice:read"],
"iss": "https://auth.emc.local",
"exp": 1234567890
}
Rate limits are configurable per app_id (header X-App-ID) without a server restart:
app_rate_limits table
├── app_id TEXT PRIMARY KEY
├── tenant_id UUID
├── requests_per_minute INT (default: 60)
├── burst INT (default: 10)
└── updated_at TIMESTAMPTZ
Enforcement:
1. Middleware reads limit from Redis (key: rate:app:{app_id})
2. Cache miss → query DB → write to Redis with 60s TTL
3. Admin updates limit → next DB read (within 60s) picks up change
4. Exceeds limit → 429 with Retry-After header
Admin API:
POST /api/v1/admin/apps Create app rate config
GET /api/v1/admin/apps List all app configs (tenant-scoped)
PUT /api/v1/admin/apps/:app_id Update limit (effective within 60s)
DELETE /api/v1/admin/apps/:app_id Remove custom limit (falls back to default)
GET /api/v1/admin/agents/analysis — requires admin:access
{
"window": "24h",
"agents": [
{
"agent_id": "uuid",
"name": "invoice-classifier-v2",
"agent_type": "llm",
"total_requests": 4821,
"rate_limit_hits": 12,
"unique_ips": 3,
"off_hours_access": false,
"risk_score": 14,
"flags": []
},
{
"agent_id": "uuid",
"name": "data-scraper-unknown",
"agent_type": "service",
"total_requests": 98200,
"rate_limit_hits": 447,
"unique_ips": 31,
"off_hours_access": true,
"risk_score": 87,
"flags": ["high_volume", "many_ips", "off_hours", "rate_limit_abuse"]
}
]
}Risk score signals (weighted):
| Signal | Weight | Threshold |
|---|---|---|
| Rate limit hit rate > 1% | 20 | hits/total > 0.01 |
| Unique IPs > 5 in 24h | 25 | unusual for a single agent |
| Off-hours access (outside 06–22 UTC) | 15 | any request in window |
| Request volume > 10k/24h | 20 | high automated load |
| New agent (< 7 days old) | 10 | elevated caution period |
| Capability mismatch (accessing non-declared routes) | 30 | critical |
Items tracked for v2 (post-Phase 8):
| Enhancement | Description | Priority |
|---|---|---|
| OAuth2 / OIDC server | Expose authorization_code + client_credentials flows so third-party apps can use EMC-Auth as a full OAuth2 provider | High |
| Social login | Google (#64 ✅) and GitHub (#66 ✅) shipped with account linking; Microsoft pending | Medium |
| Passwordless / magic link | Email-based one-click login for low-friction onboarding | Medium |
| RS256 JWT | Asymmetric signing — public key endpoint so external services can verify tokens without calling EMC-Auth | Medium |
| Webhooks | POST to a configured URL on user lifecycle events: user.created, user.deleted, auth.login_failed |
Medium |
| SDK libraries | Node.js and Python client libraries for JWT verification and token introspection | Medium |
| White-label login page | Per-tenant custom logo, colors, and domain on the hosted login UI | Low |
| Audit log retention | Configurable retention window + archive to S3/GCS after N days | Low |
| Login anomaly detection | Flag logins from new country/device; alert admin or require MFA step-up | Low |
| Distributed rate limiting | Replace in-memory rate limiter with Redis-backed solution for multi-instance deployments | Phase 7 |
| Token introspection endpoint | POST /oauth/introspect — RFC 7662 token introspection for resource servers |
v2 |
| Agent federation | Allow agents registered in tenant A to present credentials to tenant B with explicit trust grant | v2 |
| Agent behavior baseline | ML-based normal behavior model per agent; alert on deviation from baseline | v2 |
emc-auth-server/
├── cmd/server/main.go # entrypoint — wires everything and starts HTTP server
├── internal/
│ ├── admin/ # Admin service: tenant CRUD, user pool, role/permission CRUD
│ ├── api/
│ │ ├── handlers/ # HTTP handlers: auth.go, admin.go, health.go
│ │ └── middleware/ # jwt.go, permission.go, ratelimit.go, applimit.go,
│ │ # cors.go, prometheus.go, logger.go, apikey.go
│ ├── audit/ # Audit logger (write + paginated query)
│ ├── auth/ # service.go, jwt.go, reset.go, totp.go, apikey.go, applimit.go
│ ├── config/ # Environment variable loader
│ ├── mailer/ # DevMailer (console) + SMTPMailer
│ ├── metrics/ # metrics.go — Prometheus descriptor registration
│ └── store/ # DB pool, Redis client, migration runner, seed
├── migrations/ # Goose SQL files (00001–00018), embedded via embed.FS
├── infra/ # docker-compose.prod.yml, prometheus/, grafana/
├── .github/
│ ├── workflows/ # ci.yml (test+lint+build), release.yml (Docker publish on tag)
│ ├── ISSUE_TEMPLATE/ # bug.yml, feature.yml, config.yml
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── CODEOWNERS # path-based ownership for code review enforcement
├── Makefile
├── REVIEW.md # Human review checklist with known issues
├── docs/ # Swagger-generated files (auto-generated, do not edit)
├── .env.example # Template for local configuration
└── docker-compose.yml # Postgres 16 + Redis 7 + app service
After adding or modifying handler annotations:
# Install swag if not already installed
go install github.com/swaggo/swag/cmd/swag@latest
swag init -g cmd/server/main.go --output docsMIT