# wallet_auth

**OTP app:** `:wallet_auth`
**Module namespace:** `WalletAuth.*`
**Owner:** Security Team

## Responsibilities

Implements all authentication and session security for the wallet platform.
This app is the DMZ security gateway per **ADR 0001**: it owns AuthN/AuthZ decisions
and must be fully operational before any financial API is exposed.

Per **ADR 0006**: short-lived JWTs with rotating keys, one-time-use refresh tokens,
OTP/MFA with anti-abuse controls, and externalized secrets are the mandatory baseline.

## Public API

### `WalletAuth.Commands.LoginWithPassword`
Password-based login: rate-limited, credential-validated, session-creating.
```elixir
lookup_fn = fn identifier -> {:ok, {"user_123", hashed_password}} end

{:ok, %{access_token: at, refresh_token: rt, session_id: sid, user_id: uid}} =
  WalletAuth.Commands.LoginWithPassword.execute("user@example.com", "password", lookup_fn,
    device_id: "dev_xyz",
    ip_address: "1.2.3.4"
  )
```

### `WalletAuth.Commands.RefreshTokens`
Rotates refresh token (one-time-use) and issues a new access token.
```elixir
{:ok, %{access_token: new_at, refresh_token: new_rt}} =
  WalletAuth.Commands.RefreshTokens.execute(old_refresh_token)
```
- `{:error, :consumed}` — replay detected (emits SuspiciousActivityDetected event).
- `{:error, :session_revoked}` — session was revoked before rotation.

### `WalletAuth.Commands.Logout`
Revokes session and all associated refresh tokens.
```elixir
:ok = WalletAuth.Commands.Logout.execute(session_id, revoked_by_user_id)
```

### `WalletAuth.Commands.StartOtpChallenge`
Starts an OTP challenge for a user/purpose pair.
```elixir
{:ok, code} = WalletAuth.Commands.StartOtpChallenge.execute("user_123", :login_mfa)
# code is delivered to the user via wallet_notifications — wallet_auth does not dispatch
```

### `WalletAuth.Commands.VerifyOtpChallenge`
Verifies the returned OTP code.
```elixir
{:ok, :verified} = WalletAuth.Commands.VerifyOtpChallenge.execute("user_123", :login_mfa, "123456")
# {:error, :exhausted} — emits SuspiciousActivityDetected on max attempts
```

### `WalletAuth.Commands.RegisterDevice`
Registers a device as trusted (call after OTP verification).
```elixir
{:ok, device} = WalletAuth.Commands.RegisterDevice.execute("user_123",
  name: "iPhone 14",
  user_agent: "WalletApp/3.0",
  ip_address: "1.2.3.4"
)
```

### `WalletAuth.Commands.RevokeDevice`
Revokes a trusted device.
```elixir
:ok = WalletAuth.Commands.RevokeDevice.execute(device_id, revoked_by_user_id)
```

### `WalletAuth.Token.AccessToken`
JWT issuance and validation.
```elixir
{:ok, token, claims} = WalletAuth.Token.AccessToken.issue("user_123",
  roles: ["wallet_user"],
  scopes: ["transfers:write"]
)

{:ok, claims} = WalletAuth.Token.AccessToken.validate(token)
# {:error, :expired | :invalid_signature | :invalid_iss | :kid_not_found | ...}
```

### `WalletAuth.Plugs.VerifyAccessToken`
Plug for protecting wallet_web pipelines. Sets `:current_claims` and `:current_user_id`.
```elixir
# In wallet_web router.ex:
pipeline :authenticated do
  plug WalletAuth.Plugs.VerifyAccessToken
end
```
Returns `401 UNAUTHORIZED` with canonical error envelope on failure.

### `WalletAuth.Plugs.RequireMfa`
Plug for high-risk routes requiring step-up MFA.
```elixir
pipeline :high_risk do
  plug WalletAuth.Plugs.VerifyAccessToken
  plug WalletAuth.Plugs.RequireMfa
end
```
Returns `401 OTP_REQUIRED` if `mfa_verified` claim is absent or false.

### `WalletAuth.RateLimiter`
ETS-backed rate limiter for login and OTP endpoints.
```elixir
:ok | {:error, :rate_limited} = WalletAuth.RateLimiter.check_and_increment(:login, identifier)
WalletAuth.RateLimiter.reset(:login, identifier)
```

### `WalletAuth.Credentials.CredentialPolicy`
Password hash and verify using PBKDF2-SHA256.
```elixir
hash = WalletAuth.Credentials.CredentialPolicy.hash("my_password")
true = WalletAuth.Credentials.CredentialPolicy.verify("my_password", hash)
```

### `WalletAuth.Secrets.SecretProvider`
Behaviour for runtime secret retrieval. Default: `EnvSecretProvider`.
```elixir
{:ok, key} = WalletAuth.Secrets.SecretProvider.get("jwt_signing_key_key_v1")
```
Configure via:
```elixir
config :wallet_auth, :secrets, %{
  "jwt_signing_key_key_v1" => System.get_env("JWT_SIGNING_KEY_V1")
}
```

### `WalletAuth.Jwks.KeySet`
Key rotation management baseline.
```elixir
{kid, key_bytes} = WalletAuth.Jwks.KeySet.current_signing_key()
{:ok, key_bytes} = WalletAuth.Jwks.KeySet.find_validation_key("key_v1")
:ok = WalletAuth.Jwks.KeySet.rotate_to("key_v2")
```

## Domain Events

All events implement `WalletEvents.DomainEvent` and are broadcast on the `"wallet_auth:events"` PubSub topic:

| Event | Trigger |
|---|---|
| `WalletAuth.Events.UserAuthenticated` | Successful login |
| `WalletAuth.Events.LoginFailed` | Failed login attempt |
| `WalletAuth.Events.AuthSessionStarted` | Session created |
| `WalletAuth.Events.AuthSessionRevoked` | Session revoked (logout/admin) |
| `WalletAuth.Events.TokenRefreshed` | Refresh rotation succeeded |
| `WalletAuth.Events.OtpChallengeStarted` | OTP challenge initiated |
| `WalletAuth.Events.OtpChallengeVerified` | OTP verified |
| `WalletAuth.Events.SuspiciousActivityDetected` | Rate limit/replay/exhaustion |
| `WalletAuth.Events.DeviceRegistered` | Device enrolled as trusted |
| `WalletAuth.Events.DeviceRevoked` | Trusted device revoked |

## Security Baseline (ADR 0006)

1. Access tokens: HS256 by default (RS256 by swapping KeySet signer). TTL: 10 min (configurable).
2. Refresh tokens: opaque, one-time-use, session-bound, ETS-backed (swap for DB in production).
3. OTP: 6-digit CSPRNG codes, 5 min TTL, 3-attempt lockout (configurable).
4. Rate limiting: sliding-window per identifier; 5 login/15 min, 5 OTP/5 min (configurable).
5. Secrets: never plaintext; sourced from `SecretProvider` (swap `EnvSecretProvider` for Vault).
6. Audit: all auth events emit `WalletObservability.AuditEvent` with correlation IDs.
7. Suspicious activity signals forwarded to `wallet_risk` via `SuspiciousActivityDetected` events.

## Configuration

```elixir
config :wallet_auth,
  active_kid: "key_v1",
  access_token_ttl_seconds: 600,
  secret_provider: WalletAuth.Secrets.EnvSecretProvider,
  secrets: %{
    "jwt_signing_key_key_v1" => System.get_env("JWT_SIGNING_KEY")
  },
  jwks_keys: [
    %{kid: "key_v1", algorithm: :HS256, status: :active, activated_at: ~U[2026-03-11 00:00:00Z]}
  ],
  rate_limit_policy: %{
    login: %{max_attempts: 5, window_seconds: 900},
    otp: %{max_attempts: 5, window_seconds: 300}
  }
```

## Allowed Dependencies

- `wallet_shared_kernel` — typed IDs, Money, Correlation primitives.
- `wallet_api_contracts` — error envelope and error code constants (ADR 0005).
- `wallet_observability` — AuditEvent schema and Telemetry helpers (ADR 0007).
- `wallet_state` — IdempotencyKey contract (Phase 2+, for future command dedup).
- `wallet_events` — DomainEvent behaviour for auth events (ADR 0002).
- `plug` — VerifyAccessToken and RequireMfa plugs, Plug.Crypto for secure_compare.
- `jason` — JSON encoding for JWT and error responses.
