Packages

AWS Signature Version 4 Signing Library - Community-maintained fork

Current section

Files

Jump to
ex_aws_auth lib aws_auth req.ex
Raw

lib/aws_auth/req.ex

defmodule AWSAuth.Req do
@moduledoc """
Req plugin for AWS Signature V4 authentication.
This module provides seamless integration between `ex_aws_auth` and the
`Req` HTTP client library. It automatically signs requests with AWS Signature V4
without requiring manual header manipulation.
## Usage
# Load credentials from environment
creds = AWSAuth.Credentials.from_env()
# Attach to a Req request
Req.new(url: url, method: :post, body: body)
|> AWSAuth.Req.attach(credentials: creds, service: "bedrock")
|> Req.request()
# Or create a reusable client
client =
Req.new(base_url: "https://bedrock-runtime.us-east-1.amazonaws.com")
|> AWSAuth.Req.attach(credentials: creds, service: "bedrock")
# Make multiple requests
Req.post!(client, url: "/model/my-model/invoke", json: params)
## Options
* `:credentials` - (required) `AWSAuth.Credentials` struct or keyword list with:
* `:access_key_id` - AWS Access Key ID
* `:secret_access_key` - AWS Secret Access Key
* `:session_token` - AWS Session Token (optional)
* `:region` - AWS region (optional, defaults to "us-east-1")
* `:service` - (required) AWS service name (e.g., "s3", "bedrock", "lambda")
* `:region` - AWS region (optional, overrides region from credentials)
## How it Works
The plugin adds a request step that:
1. Extracts the request method, URL, headers, and body
2. Normalizes Req's list-valued headers to string values for signing
3. Signs the request using AWS Signature V4
4. Returns headers in Req's expected format (`%{key => [value]}`)
## Examples
# Basic usage with environment credentials
creds = AWSAuth.Credentials.from_env()
response =
Req.new()
|> AWSAuth.Req.attach(credentials: creds, service: "s3")
|> Req.get!(url: "https://my-bucket.s3.amazonaws.com/object")
# With explicit credentials
creds = %AWSAuth.Credentials{
access_key_id: "AKIAIOSFODNN7EXAMPLE",
secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
region: "us-west-2"
}
response =
Req.new(url: url, method: :post, json: data)
|> AWSAuth.Req.attach(credentials: creds, service: "bedrock")
|> Req.request!()
# With session token (STS temporary credentials)
creds = %AWSAuth.Credentials{
access_key_id: "ASIAIOSFODNN7EXAMPLE",
secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
session_token: "FwoGZXIvYXdzEBYaDHhBTEMPLESessionToken123",
region: "us-east-1"
}
response =
Req.new()
|> AWSAuth.Req.attach(credentials: creds, service: "lambda")
|> Req.post!(url: lambda_url, json: payload)
# Override region
response =
Req.new()
|> AWSAuth.Req.attach(
credentials: creds,
service: "s3",
region: "eu-west-1"
)
|> Req.get!(url: "https://my-bucket.s3.eu-west-1.amazonaws.com/object")
"""
alias AWSAuth.Credentials
@doc """
Attaches AWS Signature V4 signing to a Req request.
## Parameters
* `request` - A `Req.Request` struct
* `opts` - Keyword list of options (see module docs for details)
## Returns
A `Req.Request` with the AWS signing step prepended.
## Examples
creds = AWSAuth.Credentials.from_env()
request =
Req.new(url: url)
|> AWSAuth.Req.attach(credentials: creds, service: "bedrock")
"""
@spec attach(Req.Request.t(), keyword()) :: Req.Request.t()
def attach(%Req.Request{} = request, opts) when is_list(opts) do
credentials = get_credentials!(opts)
service = get_service!(opts)
region = Keyword.get(opts, :region, credentials.region || "us-east-1")
request
|> Req.Request.prepend_request_steps(
aws_sigv4: fn req ->
sign_request(req, credentials, service, region)
end
)
end
# Sign a Req request with AWS Signature V4
defp sign_request(req, credentials, service, region) do
method = String.upcase(to_string(req.method))
url = URI.to_string(req.url)
# Normalize headers - Req uses %{key => [value]} format
# Convert to %{key => value} for signing
headers =
Map.new(req.headers, fn {k, v} ->
{k, if(is_list(v), do: List.first(v), else: v)}
end)
body = req.body || ""
# Sign using the credentials struct API with :req return format
signed_headers =
AWSAuth.sign_authorization_header(
credentials,
method,
url,
service,
headers: headers,
payload: body,
region: region,
return_format: :req
)
# Update request with signed headers
%{req | headers: signed_headers}
end
# Extract credentials from options, supporting both Credentials struct and keyword list
defp get_credentials!(opts) do
case Keyword.fetch(opts, :credentials) do
{:ok, %Credentials{} = creds} ->
creds
{:ok, creds} when is_list(creds) or is_map(creds) ->
Credentials.from_map(creds)
:error ->
raise ArgumentError, """
Missing required :credentials option.
Please provide credentials as either:
1. AWSAuth.Credentials struct:
AWSAuth.Req.attach(request, credentials: AWSAuth.Credentials.from_env(), service: "s3")
2. Keyword list or map:
AWSAuth.Req.attach(request,
credentials: [
access_key_id: "...",
secret_access_key: "...",
region: "us-east-1"
],
service: "s3"
)
"""
end
end
# Extract service from options
defp get_service!(opts) do
case Keyword.fetch(opts, :service) do
{:ok, service} when is_binary(service) ->
service
{:ok, other} ->
raise ArgumentError, """
Invalid :service option. Expected a string, got: #{inspect(other)}
Example:
AWSAuth.Req.attach(request, credentials: creds, service: "bedrock")
"""
:error ->
raise ArgumentError, """
Missing required :service option.
Please specify the AWS service name:
AWSAuth.Req.attach(request, credentials: creds, service: "bedrock")
Common services: "s3", "bedrock", "lambda", "dynamodb", "sns", "sqs"
"""
end
end
end