Id RFC 003 authentication service

RFC-003 — Authentication Service

  • *tatus:*Draft
  • *ate:*20260408
  • *uthor:*Koder Team
  • *epends on:*RFC001, 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 failed automatically
  • 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

  1. Auth service receives password from client
  2. Calls Identity service via gRPC to get the credential hash for the user
  3. Verifies password against Argon2id hash using golang.org/x/crypto/argon2
  4. On success: transitions flow to password_ok or completed
  5. 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

  1. Auth service calls Identity service to get the encrypted TOTP secret
  2. Generates the expected TOTP code for current time window
  3. Accepts codes from the current window ±1 (clock drift tolerance of 30 seconds)
  4. 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:

  1. *egin:*Auth service generates a challenge, retrieves allowed credential IDs from Identity service
  2. *lient:*Browser calls navigator.credentials.get() with the challenge
  3. *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)
  4. 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 singleuse 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.

Source: ../home/koder/dev/koder/meta/docs/stack/rfcs/id-RFC-003-authentication-service.md