Id RFC 003 authentication service
RFC-003 — Authentication Service
- *tatus:*Draft
- *ate:*2026
0408 - *uthor:*Koder Team
- *epends on:*RFC
001, RFC002
Summary
The Auth service handles multi-step authentication flows: email+password verification, TOTP validation, and WebAuthn/Passkeys ceremony. It does NOT store credentials (that's the Identity service) — it orchestrates the authentication process.
Authentication Flow
Overview
Authentication is a multi-step flow with a state machine:
┌─────────────┐
│ START │
└──────┬──────┘
│
┌──────▼──────┐
│ Password │
│ Step │
└──────┬──────┘
│
┌──────▼──────┐
┌─────│ MFA Check │─────┐
│ └─────────────┘ │
│ MFA enabled │ No MFA
▼ ▼
┌────────────────┐ ┌──────────────┐
│ MFA Required │ │ COMPLETED │
│ (TOTP or │ │ → Session │
│ WebAuthn) │ └──────────────┘
└───────┬────────┘
│
┌───────▼────────┐
│ MFA Step │
│ (verify code │
│ or assertion) │
└───────┬────────┘
│
┌───────▼────────┐
│ COMPLETED │
│ → Session │
└────────────────┘Flow States
| State | Description |
|---|---|
pending |
Flow created, awaiting password |
password_ok |
Password verified, checking MFA requirement |
mfa_required |
User has MFA enabled, awaiting second factor |
completed |
All steps passed, session will be created |
failed |
Authentication failed (wrong password, wrong code, timeout) |
Flow Timeout
- Each flow has a TTL of *0 minutes*from creation
- After timeout, the flow transitions to
failedautomatically - Client must start a new flow
API Endpoints (REST)
POST /v1/auth/flows
Start a new authentication flow.
*equest:*
{
"identifier": "user@example.com",
"tenant_id": "koder"
}*esponse:*
{
"flow_id": "01HXK...",
"status": "pending",
"next_step": "password",
"expires_at": "2026-04-08T12:10:00Z"
}POST /v1/auth/flows/{flow_id}/password
Submit password for verification.
*equest:*
{
"password": "..."
}*esponse (MFA required):*
{
"flow_id": "01HXK...",
"status": "mfa_required",
"next_step": "mfa",
"mfa_methods": ["totp", "webauthn"],
"expires_at": "2026-04-08T12:10:00Z"
}*esponse (no MFA, completed):*
{
"flow_id": "01HXK...",
"status": "completed",
"session": {
"access_token": "eyJhbGciOi...",
"refresh_token": "krt_...",
"token_type": "Bearer",
"expires_in": 900
}
}POST /v1/auth/flows/{flow_id}/totp
Submit TOTP code.
*equest:*
{
"code": "123456"
}POST /v1/auth/flows/{flow_id}/webauthn/begin
Begin WebAuthn assertion ceremony.
*esponse:*
{
"public_key": {
"challenge": "base64url...",
"timeout": 60000,
"rpId": "id.koder.dev",
"allowCredentials": [
{
"type": "public-key",
"id": "base64url..."
}
],
"userVerification": "preferred"
}
}POST /v1/auth/flows/{flow_id}/webauthn/finish
Complete WebAuthn assertion with authenticator response.
*equest:*
{
"id": "base64url...",
"rawId": "base64url...",
"response": {
"authenticatorData": "base64url...",
"clientDataJSON": "base64url...",
"signature": "base64url..."
},
"type": "public-key"
}gRPC Service Definition
syntax = "proto3";
package koder.id.auth.v1;
service AuthService {
// Start a new authentication flow
rpc CreateFlow(CreateFlowRequest) returns (Flow);
// Verify password step
rpc VerifyPassword(VerifyPasswordRequest) returns (Flow);
// Verify TOTP code
rpc VerifyTOTP(VerifyTOTPRequest) returns (Flow);
// Begin WebAuthn assertion
rpc BeginWebAuthn(BeginWebAuthnRequest) returns (WebAuthnChallenge);
// Finish WebAuthn assertion
rpc FinishWebAuthn(FinishWebAuthnRequest) returns (Flow);
// Get flow status
rpc GetFlow(GetFlowRequest) returns (Flow);
}Password Verification
- Auth service receives password from client
- Calls Identity service via gRPC to get the credential hash for the user
- Verifies password against Argon2id hash using
golang.org/x/crypto/argon2 - On success: transitions flow to
password_okorcompleted - On failure: increments
error_count, checks lockout threshold
Argon2id Parameters
*ote:*Password hashing (creation of new hashes) is the responsibility of the *dentity service*(see RFC-006). The Auth service only verifies passwords against existing hashes. The parameters below are used by Auth for verification and must match the parameters used by Identity when creating the hash.
const (
ArgonTime = 3 // iterations
ArgonMemory = 64 * 1024 // 64 MB
ArgonThreads = 4 // parallelism
ArgonKeyLen = 32 // output hash length
ArgonSaltLen = 16 // random salt length
)These parameters follow OWASP recommendations and can be tuned per tenant via settings (configured in the Identity service, propagated via the hash format string $argon2id$v=19$m=...,t=...,p=...$...).
Brute-Force Protection
| Threshold | Action |
|---|---|
| 5 failed attempts | Lock account for 5 minutes |
| 10 failed attempts | Lock account for 30 minutes |
| 20 failed attempts | Lock account for 24 hours |
| 50 failed attempts | Lock account permanently (admin unlock required) |
- Lockout is per user per tenant
- Failed attempts counter resets after successful login
- Lockout applies to the user, not the IP (to avoid locking legitimate users behind shared IPs)
- Rate limiting at the gateway level protects against distributed attacks
TOTP Verification
- Auth service calls Identity service to get the encrypted TOTP secret
- Generates the expected TOTP code for current time window
- Accepts codes from the current window ±1 (clock drift tolerance of 30 seconds)
- Rejects reused codes within the same time window (replay protection)
TOTP Parameters
Algorithm: SHA1 (RFC 6238 default, compatible with all authenticator apps)
Digits: 6
Period: 30 seconds
Drift: ±1 window (total acceptance: 90 seconds)WebAuthn Verification
Follows WebAuthn Level 2 specification:
- *egin:*Auth service generates a challenge, retrieves allowed credential IDs from Identity service
- *lient:*Browser calls
navigator.credentials.get()with the challenge - *inish:*Auth service verifies:
- Challenge matches
- Origin matches (
https://id.koder.dev) - Signature is valid against stored public key
- Sign counter is greater than stored value (cloning detection)
- Updates sign counter in Identity service
Relying Party Configuration
rpConfig := webauthn.Config{
RPDisplayName: "Koder ID",
RPID: "id.koder.dev", // configurable per tenant with custom domain
RPOrigins: []string{"https://id.koder.dev"},
}Interaction with Other Services
┌────────┐ gRPC ┌──────────┐
│ Auth │──────────────▶│ Identity │ Get credentials, MFA devices
│Service │ │ Service │ Update sign counter
└───┬────┘ └──────────┘
│
│ gRPC ┌──────────┐
└──────────────────▶│ Session │ Create session on completed flow
│ Service │
└──────────┘Security Considerations
- Password is transmitted over HTTPS, never logged
- Argon2id hash comparison uses constant-time comparison
- TOTP secret is encrypted at rest in Identity service
- WebAuthn challenge is single
use and timebound (60 seconds) - Auth flow state is server-side only — client gets an opaque
flow_id - All auth events are logged for audit (success and failure)
Error Responses
All auth errors return the same generic message to prevent user enumeration:
{
"error": "authentication_failed",
"message": "Invalid credentials"
}The specific reason (wrong password, user not found, account locked) is logged server-side in auth_events but never exposed to the client.