cover/Elixir.WalletAuth.Commands.VerifyOtpChallenge.html

1 defmodule WalletAuth.Commands.VerifyOtpChallenge do
2 @moduledoc """
3 OTP challenge verification command.
4
5 Per checklist Track C:
6 - Verifies code against stored challenge (timing-safe).
7 - Tracks attempt count and locks out after max_attempts.
8 - Emits `OtpChallengeVerified` on success.
9 - Emits `SuspiciousActivityDetected` on exhaustion.
10 """
11
12 alias WalletAuth.Otp.OtpStore
13 alias WalletAuth.Events.{OtpChallengeVerified, SuspiciousActivityDetected}
14 alias WalletObservability.AuditEvent
15
16 @type purpose :: WalletAuth.Otp.OtpStore.purpose()
17
18 @doc """
19 Verifies the OTP code for a user/purpose.
20
21 Returns `{:ok, :verified}` or
22 `{:error, :invalid_code | :exhausted | :expired | :not_found}`.
23 """
24 @spec execute(user_id :: String.t(), purpose(), code :: String.t(), keyword()) ::
25 {:ok, :verified} | {:error, :invalid_code | :exhausted | :expired | :not_found}
26
:-(
def execute(user_id, purpose, code, opts \\ []) do
27
:-(
correlation_id = Keyword.get(opts, :correlation_id, WalletSharedKernel.Correlation.new_correlation_id())
28
:-(
ip = Keyword.get(opts, :ip_address)
29
30
:-(
case OtpStore.verify_challenge(user_id, purpose, code) do
31 {:ok, :verified} ->
32
:-(
audit = AuditEvent.build(:auth, "otp_verified", "user", user_id, :success,
33 correlation_id: correlation_id,
34 metadata: %{purpose: purpose}
35 )
36
:-(
:telemetry.execute([:wallet_auth, :audit], %{}, audit)
37
38
:-(
event = OtpChallengeVerified.build(user_id, purpose, correlation_id: correlation_id)
39
:-(
emit_event(event)
40
41 {:ok, :verified}
42
43 {:error, :exhausted} = err ->
44
:-(
audit = AuditEvent.build(:auth, "otp_exhausted", "user", user_id, :failure,
45 correlation_id: correlation_id,
46 metadata: %{purpose: purpose, ip_address: ip}
47 )
48
:-(
:telemetry.execute([:wallet_auth, :audit], %{}, audit)
49
50
:-(
event = SuspiciousActivityDetected.build(user_id, :otp_exhausted,
51 correlation_id: correlation_id,
52 ip_address: ip
53 )
54
:-(
emit_event(event)
55
:-(
err
56
57 {:error, _reason} = err ->
58
:-(
err
59 end
60 end
61
62
:-(
defp emit_event(event) do
63
:-(
pubsub = Application.get_env(:wallet_auth, :pubsub, WalletWeb.PubSub)
64
:-(
apply(Phoenix.PubSub, :broadcast, [pubsub, "wallet_auth:events", {:domain_event, event}])
65 rescue
66
:-(
_ -> :ok
67 end
68 end
Line Hits Source