| 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 |