Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authentication sample for Elixir #5

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions authentication_examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ In this repo you will find Fireblocks API authentication mechanism implementatio
- [Go](https://github.com/fireblocks/developers-hub/tree/main/authentication_examples/go)
- [C#](https://github.com/fireblocks/developers-hub/tree/main/authentication_examples/cs)
- [Ruby](https://github.com/fireblocks/developers-hub/tree/main/authentication_examples/ruby)
- [Elixir](https://github.com/fireblocks/developers-hub/tree/main/authentication_examples/elixir)
The examples include the access token generation method (JWT) in addition to a GET and POST call examples.

⚠️ **Note**
Expand Down
26 changes: 26 additions & 0 deletions authentication_examples/elixir/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
fireblocks_sdk_example-*.tar

# Temporary files, for example, from tests.
/tmp/
80 changes: 80 additions & 0 deletions authentication_examples/elixir/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
## Fireblocks SDK Example Application

This is a simple Elixir application demonstrating how to interact with the Fireblocks API. It shows how to authenticate using a private key and make HTTP requests to the Fireblocks API to retrieve vault accounts.

### Prerequisites

Before running this example, ensure you have the following prerequisites:

- **Elixir installed:** If you don't have Elixir installed, follow the official installation guide.
- **Fireblocks API credentials:**
- **API Key:** You'll need an API key from Fireblocks.
- **Private Key:** You need a `.pem` file containing your private key that is associated with your Fireblocks API key.

### Required dependencies:

The application uses the following dependencies:

- `httpoison` for making HTTP requests.
- `jason` for parsing JSON.
- `jose` for handling JWT token creation.

### Setup

1. **Clone the repository:**

```bash
cd developers-hub/authentication_examples/elixir
```

2. Then run the following command to install the dependencies:

```bash
mix deps.get
```

3. **Save your private key:**

Place your Fireblocks private key (in PEM format) in the location specified in the `@api_secret_path` in the code. The file should be similar to:

```vbnet
-----BEGIN PRIVATE KEY-----
YOUR_PRIVATE_KEY_HERE
-----END PRIVATE KEY-----
```

4. **Edit the API Key and Secret Path:**

In the `fireblocks_sdk_example.ex` file, ensure that your API key and the path to your private key are correctly set:

```elixir
@api_key "YOUR_API_KEY"
@api_secret_path "/path/to/your/private_key.pem"
```

### Running the Application

Once everything is set up, you can run the application using the Elixir interactive shell:

1. Start the interactive shell:

```bash
iex -S mix
```

2. **Run the example:**

In the interactive shell, run the following command to make the request to Fireblocks:

```elixir
Elixir.FireblocksSDKExample.run()
```

3. **Output:** If successful, the response from the Fireblocks API (vaults information) will be printed. If there is an error (e.g., an issue with authentication or network), an error message will be shown.

### Example Output

**Successful request:**

```elixir
%{"accounts" => [%{"assets" => [...]}] }
75 changes: 75 additions & 0 deletions authentication_examples/elixir/lib/fireblocks_http_client.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
defmodule FireblocksHttpClient do
@moduledoc """
Fireblocks HTTP Client for interacting with the Fireblocks API.
"""

defstruct [:api_key, :private_key, :base_url]

require Logger
alias Joken
alias HTTPoison

@headers [
{"Content-Type", "application/json; charset=utf-8"}
]

def request(client, method, path, data \\ nil) do
url = build_url(client.base_url, path)
jwt = sign_jwt(client, path, if(data, do: Jason.encode!(data), else: ""))

headers = build_headers(client.api_key, jwt)

case method do
:get -> HTTPoison.get(url, headers)
:post -> HTTPoison.post(url, Jason.encode!(data), headers)
end
|> handle_response()
end

def get(client, path), do: request(client, :get, path)

def post(client, path, data), do: request(client, :post, path, data)

defp build_url(base_url, path), do: "#{base_url}#{path}"

defp build_headers(api_key, jwt) do
[
{"X-API-Key", api_key},
{"Authorization", "Bearer " <> jwt} | @headers
]
end

defp sign_jwt(client, path, body) do
body_hash = hash_body(body)

claims = %{
"sub" => client.api_key,
"iat" => DateTime.utc_now() |> DateTime.to_unix(),
"exp" => DateTime.utc_now() |> DateTime.add(55) |> DateTime.to_unix(),
"nonce" => UUID.uuid4(),
"uri" => path,
"bodyHash" => body_hash
}

jwk = JOSE.JWK.from_pem(client.private_key)

{_, signed_token} =
jwk
|> JOSE.JWT.sign(%{"alg" => "RS256"}, claims)
|> JOSE.JWS.compact()

signed_token
end

defp hash_body(body) do
:crypto.hash(:sha256, body)
|> Base.encode16(case: :lower)
end

defp handle_response({:ok, %HTTPoison.Response{body: body}}), do: {:ok, Jason.decode!(body)}

defp handle_response({:error, %HTTPoison.Error{reason: reason}}) do
Logger.error("HTTP request failed: #{inspect(reason)}")
{:error, reason}
end
end
28 changes: 28 additions & 0 deletions authentication_examples/elixir/lib/fireblocks_sdk_example.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
defmodule FireblocksSDKExample do
@moduledoc """
Fireblocks SDK Example in Elixir.
"""

@api_key "API_KEY"
@base_url "https://api.fireblocks.io"
@api_secret_path "SECRET_KEY_PATH"

def run do
api_secret = read_private_key_file(@api_secret_path)

client = %FireblocksHttpClient{
api_key: @api_key,
private_key: api_secret,
base_url: @base_url
}

case FireblocksHttpClient.get(client, "/v1/vault/accounts_paged") do
{:ok, get_vaults_response} -> IO.inspect(get_vaults_response)
{:error, reason} -> IO.puts("Failed to fetch vaults: #{inspect(reason)}")
end
end

defp read_private_key_file(path) do
File.read!(path)
end
end
30 changes: 30 additions & 0 deletions authentication_examples/elixir/mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
defmodule FireblocksSdkExample.MixProject do
use Mix.Project

def project do
[
app: :fireblocks_sdk_example,
version: "0.1.0",
elixir: "~> 1.14",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end

# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end

# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:httpoison, "~> 1.8"},
{:jason, "~> 1.2"},
{:joken, "~> 2.5"},
{:uuid, "~> 1.1"}
]
end
end
15 changes: 15 additions & 0 deletions authentication_examples/elixir/mix.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
%{
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"},
"jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"},
}