cover/Elixir.WalletAuth.Plugs.VerifyAccessToken.html

1 defmodule WalletAuth.Plugs.VerifyAccessToken do
2 @moduledoc """
3 Plug for JWT access token validation in wallet_web pipelines.
4
5 Per checklist Track D: enforces auth checks in wallet_web pipelines.
6 Validates Bearer token from Authorization header against ADR 0006 rules.
7
8 On success:
9 - Sets `:current_claims` in `conn.assigns`.
10 - Sets `:current_user_id` in `conn.assigns`.
11
12 On failure:
13 - Returns 401 with canonical error envelope (ADR 0005).
14 - Halts the pipeline.
15
16 ## Usage in router.ex
17 ```elixir
18 pipeline :authenticated do
19 plug WalletAuth.Plugs.VerifyAccessToken
20 end
21 ```
22 """
23
24 import Plug.Conn
25
26 alias WalletAuth.Token.AccessToken
27 alias WalletApiContracts.ErrorEnvelope
28 alias WalletApiContracts.ErrorCodes
29
30
:-(
def init(opts), do: opts
31
32 def call(conn, _opts) do
33 5 with {:ok, token} <- extract_token(conn),
34 4 {:ok, claims} <- AccessToken.validate(token) do
35 conn
36 |> assign(:current_claims, claims)
37 1 |> assign(:current_user_id, claims["sub"])
38 else
39 {:error, reason} ->
40 conn
41 |> put_resp_content_type("application/json")
42 |> put_resp_header("x-request-id", get_or_generate_request_id(conn))
43 |> send_resp(401, build_error(reason))
44 4 |> halt()
45 end
46 end
47
48 defp extract_token(conn) do
49 5 case get_req_header(conn, "authorization") do
50 4 ["Bearer " <> token | _] ->
51 {:ok, String.trim(token)}
52
53 1 _ ->
54 {:error, :missing_token}
55 end
56 end
57
58 defp build_error(reason) do
59 4 {code, message} = error_for(reason)
60
61 ErrorEnvelope.build(code, message, :auth, false)
62 4 |> Jason.encode!()
63 end
64
65 1 defp error_for(:expired), do: {ErrorCodes.unauthorized(), "Access token has expired"}
66 1 defp error_for(:invalid_signature), do: {ErrorCodes.unauthorized(), "Token signature invalid"}
67
:-(
defp error_for(:invalid_iss), do: {ErrorCodes.unauthorized(), "Token issuer not accepted"}
68
:-(
defp error_for(:invalid_aud), do: {ErrorCodes.unauthorized(), "Token audience not accepted"}
69
:-(
defp error_for(:missing_claims), do: {ErrorCodes.unauthorized(), "Token missing required claims"}
70
:-(
defp error_for(:kid_not_found), do: {ErrorCodes.unauthorized(), "Token signing key not found"}
71
:-(
defp error_for(:key_revoked), do: {ErrorCodes.unauthorized(), "Token signing key revoked"}
72 1 defp error_for(:malformed), do: {ErrorCodes.unauthorized(), "Token is malformed"}
73 1 defp error_for(:missing_token), do: {ErrorCodes.unauthorized(), "Authorization header required"}
74
:-(
defp error_for(_), do: {ErrorCodes.unauthorized(), "Authentication required"}
75
76 defp get_or_generate_request_id(conn) do
77 4 case get_req_header(conn, "x-request-id") do
78
:-(
[id | _] -> id
79 4 [] -> WalletSharedKernel.Correlation.new_request_id()
80 end
81 end
82 end
Line Hits Source