From 1cafeb741cfc4ab08ba33f960ba96350e3d71d19 Mon Sep 17 00:00:00 2001 From: Vladislav Kislitsyn Date: Tue, 22 Oct 2024 09:58:53 +0300 Subject: [PATCH] Added authentication sample for Elixir --- authentication_examples/README.md | 1 + authentication_examples/elixir/.gitignore | 26 ++++++ authentication_examples/elixir/README.md | 80 +++++++++++++++++++ .../elixir/lib/fireblocks_http_client.ex | 75 +++++++++++++++++ .../elixir/lib/fireblocks_sdk_example.ex | 28 +++++++ authentication_examples/elixir/mix.exs | 30 +++++++ authentication_examples/elixir/mix.lock | 15 ++++ 7 files changed, 255 insertions(+) create mode 100644 authentication_examples/elixir/.gitignore create mode 100644 authentication_examples/elixir/README.md create mode 100644 authentication_examples/elixir/lib/fireblocks_http_client.ex create mode 100644 authentication_examples/elixir/lib/fireblocks_sdk_example.ex create mode 100644 authentication_examples/elixir/mix.exs create mode 100644 authentication_examples/elixir/mix.lock diff --git a/authentication_examples/README.md b/authentication_examples/README.md index eed4ff8..1487363 100644 --- a/authentication_examples/README.md +++ b/authentication_examples/README.md @@ -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** diff --git a/authentication_examples/elixir/.gitignore b/authentication_examples/elixir/.gitignore new file mode 100644 index 0000000..147940d --- /dev/null +++ b/authentication_examples/elixir/.gitignore @@ -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/ diff --git a/authentication_examples/elixir/README.md b/authentication_examples/elixir/README.md new file mode 100644 index 0000000..785cecd --- /dev/null +++ b/authentication_examples/elixir/README.md @@ -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" => [...]}] } diff --git a/authentication_examples/elixir/lib/fireblocks_http_client.ex b/authentication_examples/elixir/lib/fireblocks_http_client.ex new file mode 100644 index 0000000..c75e34c --- /dev/null +++ b/authentication_examples/elixir/lib/fireblocks_http_client.ex @@ -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 diff --git a/authentication_examples/elixir/lib/fireblocks_sdk_example.ex b/authentication_examples/elixir/lib/fireblocks_sdk_example.ex new file mode 100644 index 0000000..366cd60 --- /dev/null +++ b/authentication_examples/elixir/lib/fireblocks_sdk_example.ex @@ -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 diff --git a/authentication_examples/elixir/mix.exs b/authentication_examples/elixir/mix.exs new file mode 100644 index 0000000..6433faf --- /dev/null +++ b/authentication_examples/elixir/mix.exs @@ -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 diff --git a/authentication_examples/elixir/mix.lock b/authentication_examples/elixir/mix.lock new file mode 100644 index 0000000..34c82f0 --- /dev/null +++ b/authentication_examples/elixir/mix.lock @@ -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"}, +}