Skip to content

Commit

Permalink
Merge pull request #11 from RamonPage/v0.3.0
Browse files Browse the repository at this point in the history
v0.3.0
  • Loading branch information
RamonPage authored Jul 23, 2019
2 parents 3148847 + 4ba80b1 commit f09c095
Show file tree
Hide file tree
Showing 12 changed files with 450 additions and 21 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## v0.3.0

* Adding support for client assertion to connect to Azure (using Azure App Registration certificate).

## v0.2.3

* Updating dependencies.
Expand Down
30 changes: 29 additions & 1 deletion DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ by adding `ex_azure_key_vault` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:ex_azure_key_vault, "~> 0.2.3"}
{:ex_azure_key_vault, "~> 0.3.0"}
]
end
```
Expand Down Expand Up @@ -51,4 +51,32 @@ iex(1)> ExAzureKeyVault.Client.connect() |> ExAzureKeyVault.Client.create_secret
```elixir
iex(1)> ExAzureKeyVault.Client.connect() |> ExAzureKeyVault.Client.delete_secret("my-secret")
:ok
```

## Using client assertion to connect to Azure

For additional security, `ex_azure_key_vault` accepts client assertion instead of a client secret. To do so, first you need to upload a certificate to your Azure App Registration. Then pass the certificate SHA-1 thumbprint in base64 format and the private key in PEM format to `ex_azure_key_vault`.

```bash
$ export AZURE_CLIENT_ID="14e79d90-9abf..."
$ export AZURE_TENANT_ID="14e7a376-9abf..."
$ export AZURE_VAULT_NAME="my-vault"
$ export AZURE_CERT_BASE64_THUMBPRINT="Dss7v2YI3GgCGfl...",
$ export AZURE_CERT_PRIVATE_KEY_PEM="-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEF..."
```

```elixir
# Config.exs
config :ex_azure_key_vault,
azure_client_id: {:system, "AZURE_CLIENT_ID"},
azure_tenant_id: {:system, "AZURE_TENANT_ID"},
azure_vault_name: {:system, "AZURE_VAULT_NAME"},
azure_cert_base64_thumbprint: {:system, "AZURE_CERT_BASE64_THUMBPRINT"},
azure_cert_private_key_pem: {:system, "AZURE_CERT_PRIVATE_KEY_PEM"}
```

### Getting a secret
```elixir
iex(1)> ExAzureKeyVault.Client.certConnect() |> ExAzureKeyVault.Client.get_secret("my-secret")
{:ok, "my-value"}
```
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ by adding `ex_azure_key_vault` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:ex_azure_key_vault, "~> 0.2.3"}
{:ex_azure_key_vault, "~> 0.3.0"}
]
end
```
Expand Down Expand Up @@ -60,4 +60,32 @@ iex(1)> ExAzureKeyVault.Client.connect() |> ExAzureKeyVault.Client.delete_secret
:ok
```

## Using client assertion to connect to Azure

For additional security, `ex_azure_key_vault` accepts client assertion instead of a client secret. To do so, first you need to upload a certificate to your Azure App Registration. Then pass the certificate SHA-1 thumbprint in base64 format and the private key in PEM format to `ex_azure_key_vault`.

```bash
$ export AZURE_CLIENT_ID="14e79d90-9abf..."
$ export AZURE_TENANT_ID="14e7a376-9abf..."
$ export AZURE_VAULT_NAME="my-vault"
$ export AZURE_CERT_BASE64_THUMBPRINT="Dss7v2YI3GgCGfl...",
$ export AZURE_CERT_PRIVATE_KEY_PEM="-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEF..."
```

```elixir
# Config.exs
config :ex_azure_key_vault,
azure_client_id: {:system, "AZURE_CLIENT_ID"},
azure_tenant_id: {:system, "AZURE_TENANT_ID"},
azure_vault_name: {:system, "AZURE_VAULT_NAME"},
azure_cert_base64_thumbprint: {:system, "AZURE_CERT_BASE64_THUMBPRINT"},
azure_cert_private_key_pem: {:system, "AZURE_CERT_PRIVATE_KEY_PEM"}
```

### Getting a secret
```elixir
iex(1)> ExAzureKeyVault.Client.certConnect() |> ExAzureKeyVault.Client.get_secret("my-secret")
{:ok, "my-value"}
```

Based on [Ruby wrapper from stuartbarr](https://github.com/stuartbarr/azure-key-vault).
4 changes: 3 additions & 1 deletion config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ config :ex_azure_key_vault,
azure_client_id: {:system, "AZURE_CLIENT_ID"},
azure_client_secret: {:system, "AZURE_CLIENT_SECRET"},
azure_tenant_id: {:system, "AZURE_TENANT_ID"},
azure_vault_name: {:system, "AZURE_VAULT_NAME"}
azure_vault_name: {:system, "AZURE_VAULT_NAME"},
azure_cert_base64_thumbprint: {:system, "AZURE_CERT_BASE64_THUMBPRINT"},
azure_cert_private_key_pem: {:system, "AZURE_CERT_PRIVATE_KEY_PEM"}
4 changes: 3 additions & 1 deletion config/prod.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ config :ex_azure_key_vault,
azure_client_id: {:system, "AZURE_CLIENT_ID"},
azure_client_secret: {:system, "AZURE_CLIENT_SECRET"},
azure_tenant_id: {:system, "AZURE_TENANT_ID"},
azure_vault_name: {:system, "AZURE_VAULT_NAME"}
azure_vault_name: {:system, "AZURE_VAULT_NAME"},
azure_cert_base64_thumbprint: {:system, "AZURE_CERT_BASE64_THUMBPRINT"},
azure_cert_private_key_pem: {:system, "AZURE_CERT_PRIVATE_KEY_PEM"}
4 changes: 3 additions & 1 deletion config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ config :ex_azure_key_vault,
azure_client_id: nil,
azure_client_secret: nil,
azure_tenant_id: nil,
azure_vault_name: nil
azure_vault_name: nil,
azure_cert_base64_thumbprint: nil,
azure_cert_private_key_pem: nil
84 changes: 73 additions & 11 deletions lib/ex_azure_key_vault.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule ExAzureKeyVault.Client do
alias __MODULE__
alias ExAzureKeyVault.APIVersion
alias ExAzureKeyVault.Auth
alias ExAzureKeyVault.ClientAssertionAuth
alias ExAzureKeyVault.HTTPUtils
alias ExAzureKeyVault.Url

Expand Down Expand Up @@ -102,8 +103,69 @@ defmodule ExAzureKeyVault.Client do
if is_empty(client_id), do: raise ArgumentError, message: "Client ID is not present"
if is_empty(client_secret), do: raise ArgumentError, message: "Client secret is not present"
with %Auth{} = auth <- Auth.new(client_id, client_secret, tenant_id),
{:ok, bearer_token} <- auth |> Auth.get_bearer_token,
%Client{} = client <- bearer_token |> Client.new(vault_name, APIVersion.version()) do
{:ok, bearer_token} <- auth |> Auth.get_bearer_token,
%Client{} = client <- bearer_token |> Client.new(vault_name, APIVersion.version()) do
client
else
{:error, reason} -> {:error, reason}
end
end

@doc ~S"""
Connects to Azure Key Vault using client assertion with certificate.
## Examples
When defining environment variables and/or adding to configuration.
$ export AZURE_CLIENT_ID="14e79d90-9abf..."
$ export AZURE_TENANT_ID="14e7a376-9abf..."
$ export AZURE_VAULT_NAME="my-vault"
$ export AZURE_CERT_BASE64_THUMBPRINT="Dss7v2YI3GgCGfl..."
$ export AZURE_CERT_PRIVATE_KEY_PEM="-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEF..."
# Config.exs
config :ex_azure_key_vault,
azure_client_id: {:system, "AZURE_CLIENT_ID"},
azure_tenant_id: {:system, "AZURE_TENANT_ID"},
azure_vault_name: {:system, "AZURE_VAULT_NAME"}
azure_cert_base64_thumbprint: {:system, "AZURE_CERT_BASE64_THUMBPRINT"},
azure_cert_private_key_pem: {:system, "AZURE_CERT_PRIVATE_KEY_PEM"}
iex(1)> ExAzureKeyVault.Client.certConnect()
%ExAzureKeyVault.Client{
api_version: "2016-10-01",
bearer_token: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
vault_name: "my-vault"
}
Passing custom parameters.
iex(1)> ExAzureKeyVault.Client.certConnect("custom-vault", "14e7a376-9abf...", "14e79d90-9abf...", "Dss7v2YI3GgCGfl...", "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEF...")
%ExAzureKeyVault.Client{
api_version: "2016-10-01",
bearer_token: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
vault_name: "custom-vault"
}
"""
@spec certConnect() :: Client.t | {:error, any}
@spec certConnect(String.t | nil, String.t | nil, String.t | nil, String.t | nil, String.t | nil) :: Client.t | {:error, any}
def certConnect(vault_name \\ nil, tenant_id \\ nil, client_id \\ nil, cert_base64_thumbprint \\ nil, cert_private_key_pem \\ nil) do
vault_name = get_env(:azure_vault_name, vault_name)
tenant_id = get_env(:azure_tenant_id, tenant_id)
client_id = get_env(:azure_client_id, client_id)
cert_base64_thumbprint = get_env(:azure_cert_base64_thumbprint, cert_base64_thumbprint)
cert_private_key_pem = get_env(:azure_cert_private_key_pem, cert_private_key_pem)
if is_empty(vault_name), do: raise ArgumentError, message: "Vault name is not present"
if is_empty(tenant_id), do: raise ArgumentError, message: "Tenant ID is not present"
if is_empty(client_id), do: raise ArgumentError, message: "Client ID is not present"
if is_empty(cert_base64_thumbprint), do: raise ArgumentError, message: "Certificate base64 thumbprint is not present"
if is_empty(cert_private_key_pem), do: raise ArgumentError, message: "Certificate private key PEM is not present"
with cert_private_key_pem <- cert_private_key_pem |> String.replace("\\n", "\n"),
%ClientAssertionAuth{} = auth <- ClientAssertionAuth.new(client_id, tenant_id, cert_base64_thumbprint, cert_private_key_pem),
{:ok, bearer_token} <- auth |> ClientAssertionAuth.get_bearer_token,
%Client{} = client <- bearer_token |> Client.new(vault_name, APIVersion.version()) do
client
else
{:error, reason} -> {:error, reason}
Expand Down Expand Up @@ -133,9 +195,9 @@ defmodule ExAzureKeyVault.Client do
options = HTTPUtils.options_ssl
HTTPoison.get(url, headers, options)
|> handle_http_response(url, fn body ->
response = Poison.decode!(body)
{:ok, response["value"]}
end)
response = Poison.decode!(body)
{:ok, response["value"]}
end)
end

@doc """
Expand Down Expand Up @@ -215,9 +277,9 @@ defmodule ExAzureKeyVault.Client do
options = HTTPUtils.options_ssl
HTTPoison.get(url, headers, options)
|> handle_http_response(url, fn body ->
response = Poison.decode!(body)
{:ok, response}
end)
response = Poison.decode!(body)
{:ok, response}
end)
end

@doc """
Expand Down Expand Up @@ -257,9 +319,9 @@ defmodule ExAzureKeyVault.Client do
options = HTTPUtils.options_ssl
HTTPoison.get(next_link, headers, options)
|> handle_http_response(next_link, fn body ->
response = Poison.decode!(body)
{:ok, response}
end)
response = Poison.decode!(body)
{:ok, response}
end)
end

@doc """
Expand Down
115 changes: 115 additions & 0 deletions lib/ex_azure_key_vault/client_assertion_auth.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
defmodule ExAzureKeyVault.ClientAssertionAuth do
@moduledoc """
Internal module for getting authentication token for Azure connection using client assertion.
"""
alias __MODULE__
alias ExAzureKeyVault.HTTPUtils

@enforce_keys [:client_id, :tenant_id, :cert_base64_thumbprint, :cert_private_key_pem]
defstruct(
client_id: nil,
tenant_id: nil,
cert_base64_thumbprint: nil,
cert_private_key_pem: nil
)

@type t :: %__MODULE__{
client_id: String.t,
tenant_id: String.t,
cert_base64_thumbprint: String.t,
cert_private_key_pem: String.t
}

@doc ~S"""
Creates `%ExAzureKeyVault.ClientAssertionAuth{}` struct with account tokens and cert data.
## Examples
iex(1)> ExAzureKeyVault.ClientAssertionAuth.new("6f185f82-9909...", "6f1861e4-9909...", "Dss7v2YI3GgCGfl...", "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEF...")
%ExAzureKeyVault.ClientAssertionAuth{
client_id: "6f185f82-9909...",
tenant_id: "6f1861e4-9909...",
cert_base64_thumbprint: "Dss7v2YI3GgCGfl...",
cert_private_key_pem: "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEF..."
}
"""
@spec new(String.t, String.t, String.t, String.t) :: ClientAssertionAuth.t
def new(client_id, tenant_id, cert_base64_thumbprint, cert_private_key_pem) do
%ClientAssertionAuth{
client_id: client_id,
tenant_id: tenant_id,
cert_base64_thumbprint: cert_base64_thumbprint,
cert_private_key_pem: cert_private_key_pem
}
end

@doc ~S"""
Returns bearer token for Azure connection using client assertion.
## Examples
iex(1)> ExAzureKeyVault.ClientAssertionAuth.new("6f185f82-9909...", "6f1861e4-9909...", "Dss7v2YI3GgCGfl...", "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEF...")
...(1)> |> ExAzureKeyVault.ClientAssertionAuth.get_bearer_token()
{:ok, "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
"""
@spec get_bearer_token(ClientAssertionAuth.t) :: {:ok, String.t} | {:error, any}
def get_bearer_token(%ClientAssertionAuth{} = params) do
client_assertion = auth_client_assertion(
params.client_id,
params.tenant_id,
params.cert_base64_thumbprint,
params.cert_private_key_pem
)
url = auth_url(params.tenant_id)
body = auth_body(params.client_id, client_assertion)
headers = HTTPUtils.headers_form_urlencoded
options = HTTPUtils.options_ssl
case HTTPoison.post(url, body, headers, options) do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
response = Poison.decode!(body)
{:ok, "Bearer #{response["access_token"]}"}
{:ok, %HTTPoison.Response{status_code: status, body: ""}} ->
HTTPUtils.response_client_error_or_ok(status, url)
{:ok, %HTTPoison.Response{status_code: status, body: body}} ->
HTTPUtils.response_client_error_or_ok(status, url, body)
{:error, %HTTPoison.Error{reason: :nxdomain}} ->
HTTPUtils.response_server_error(:nxdomain, url)
{:error, %HTTPoison.Error{reason: reason}} ->
HTTPUtils.response_server_error(reason)
_ ->
{:error, "Something went wrong"}
end
end

@spec auth_url(String.t) :: String.t
defp auth_url(tenant_id) do
"https://login.microsoftonline.com/#{tenant_id}/oauth2/v2.0/token"
end

@spec auth_body(String.t, String.t) :: tuple
defp auth_body(client_id, client_assertion) do
{:form, [
grant_type: "client_credentials",
client_id: client_id,
client_assertion: client_assertion,
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
scope: "https://vault.azure.net/.default"
]}
end

@spec auth_client_assertion(String.t, String.t, String.t, String.t) :: String.t
defp auth_client_assertion(client_id, tenant_id, cert_base64_thumbprint, cert_private_key_pem) do
signer = Joken.Signer.create("RS256", %{"pem" => cert_private_key_pem}, %{"x5t" => cert_base64_thumbprint})
sub = client_id
iss = client_id
jti = Joken.generate_jti()
nbf = Joken.current_time()
exp = Joken.current_time() + 60 # in 1 minute
aud = "https://login.microsoftonline.com/#{tenant_id}/oauth2/v2.0/token"
{:ok, claims} = Joken.generate_claims(%{}, %{sub: sub, iss: iss, jti: jti, nbf: nbf, exp: exp, aud: aud})
{:ok, jwt, _} = Joken.encode_and_sign(claims, signer)
jwt
end
end
4 changes: 3 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule ExAzureKeyVault.MixProject do
use Mix.Project

@version "0.2.3"
@version "0.3.0"
@github_url "https://github.com/RamonPage/ex_azure_key_vault"

def project do
Expand Down Expand Up @@ -56,6 +56,8 @@ defmodule ExAzureKeyVault.MixProject do
[
{:httpoison, "~> 1.5.0"},
{:poison, "~> 4.0.0"},
{:joken, "~> 2.1.0"},
{:jason, "~> 1.1.2"},
{:dialyxir, "~> 0.5", only: :dev, runtime: false},
{:ex_doc, "~> 0.21.0", only: :dev, runtime: false},
{:mock, "~> 0.3.2", only: :test},
Expand Down
3 changes: 3 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
%{
"base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"},
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
"dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"},
"earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm"},
Expand All @@ -8,6 +9,8 @@
"httpoison": {:hex, :httpoison, "1.5.1", "0f55b5b673b03c5c327dac7015a67cb571b99b631acc0bc1b0b98dcd6b9f2104", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"joken": {:hex, :joken, "2.1.0", "bf21a73105d82649f617c5e59a7f8919aa47013d2519ebcc39d998d8d12adda9", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm"},
"jose": {:hex, :jose, "1.9.0", "4167c5f6d06ffaebffd15cdb8da61a108445ef5e85ab8f5a7ad926fdf3ada154", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"},
"makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
"meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm"},
Expand Down
Loading

0 comments on commit f09c095

Please sign in to comment.