cover/Elixir.DaProductAppWeb.Plugs.PartnerAuth.html

1 defmodule DaProductAppWeb.Plugs.PartnerAuth do
2 @moduledoc """
3 Partner API authentication plug.
4
5 Supports two authentication methods:
6 1. Bearer token: `Authorization: Bearer <api_key>`
7 2. API Key: `Authorization: ApiKey <api_key>`
8
9 Also validates:
10 - Partner status (must be ACTIVE)
11 - API key expiration
12 - IP whitelisting (if configured)
13 """
14
15 import Plug.Conn
16 alias DaProductApp.Partners
17
18 1 def init(opts), do: opts
19
20 def call(conn, _opts) do
21 1 case extract_auth_token(conn) do
22
:-(
{:ok, api_key} -> validate_partner_auth(conn, api_key)
23 1 {:error, reason} -> unauthorized(conn, reason)
24 end
25 end
26
27 # Extract authentication token from headers
28 defp extract_auth_token(conn) do
29 1 case get_req_header(conn, "authorization") do
30
:-(
["Bearer " <> token] when byte_size(token) > 0 -> {:ok, String.trim(token)}
31
:-(
["ApiKey " <> token] when byte_size(token) > 0 -> {:ok, String.trim(token)}
32
:-(
["API-Key " <> token] when byte_size(token) > 0 -> {:ok, String.trim(token)}
33 1 [] -> {:error, "Missing Authorization header"}
34
:-(
[_] -> {:error, "Invalid Authorization header format"}
35
:-(
_ -> {:error, "Multiple Authorization headers"}
36 end
37 end
38
39 # Validate partner authentication and set context
40 defp validate_partner_auth(conn, api_key) do
41
:-(
client_ip = get_client_ip(conn)
42
43
:-(
with {:ok, partner} <- Partners.authenticate_partner(api_key),
44
:-(
:ok <- Partners.check_ip_whitelist(partner, client_ip),
45
:-(
:ok <- Partners.check_rate_limit(partner.id, partner.rate_limit_per_minute) do
46
47 conn
48 |> assign(:current_partner, partner)
49 |> assign(:auth_method, :api_key)
50
:-(
|> assign(:partner_id, partner.id)
51
:-(
|> put_private(:partner_authenticated, true)
52
53 else
54 {:error, :invalid_api_key} ->
55
:-(
unauthorized(conn, "Invalid or expired API key")
56
57 {:error, :ip_not_whitelisted} ->
58
:-(
unauthorized(conn, "IP address not whitelisted")
59
60 {:error, :rate_limit_exceeded} ->
61
:-(
rate_limited(conn)
62 end
63 end
64
65 # Get client IP address
66 defp get_client_ip(conn) do
67
:-(
case get_req_header(conn, "x-forwarded-for") do
68
:-(
[ip | _] -> ip |> String.split(",") |> List.first() |> String.trim()
69 [] ->
70
:-(
case get_req_header(conn, "x-real-ip") do
71
:-(
[ip] -> String.trim(ip)
72
:-(
[] -> conn.remote_ip |> Tuple.to_list() |> Enum.join(".")
73 end
74 end
75 end
76
77 # Send unauthorized response
78 defp unauthorized(conn, reason) do
79 conn
80 |> put_status(:unauthorized)
81 |> put_resp_content_type("application/json")
82 |> send_resp(401, Jason.encode!(%{
83 success: false,
84 error: "Unauthorized",
85 message: reason,
86 code: "AUTH_REQUIRED",
87 timestamp: DateTime.utc_now() |> DateTime.to_iso8601()
88 }))
89 1 |> halt()
90 end
91
92 # Send rate limit exceeded response
93 defp rate_limited(conn) do
94 conn
95 |> put_status(:too_many_requests)
96 |> put_resp_content_type("application/json")
97 |> put_resp_header("retry-after", "60")
98 |> send_resp(429, Jason.encode!(%{
99 success: false,
100 error: "Rate limit exceeded",
101 message: "Too many requests. Please try again after 1 minute.",
102 code: "RATE_LIMIT_EXCEEDED",
103 retry_after: 60,
104 timestamp: DateTime.utc_now() |> DateTime.to_iso8601()
105 }))
106
:-(
|> halt()
107 end
108 end
Line Hits Source