Skip to content

Commit 889709a

Browse files
committed
Test the bearer token fetching
1 parent b2cec13 commit 889709a

File tree

7 files changed

+175
-9
lines changed

7 files changed

+175
-9
lines changed

README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,31 @@ end
3232

3333
## Configuration
3434

35-
The configuration should _either_ define `storage_account_name` and `storage_account_key` _or_ `storage_account_connection_string`.
35+
For authentication, there is support for using both an account key or service principal.
36+
For configuration of the account key, use _either_ `storage_account_name` and `storage_account_key` _or_ `storage_account_connection_string`.
3637

3738
```elixir
3839
config :azurex, Azurex.Blob.Config,
3940
api_url: "https://sample.blob.core.windows.net", # Optional
4041
default_container: "defaultcontainer", # Optional
41-
storage_account_name: "name",
42+
storage_account_name: "sample",
4243
storage_account_key: "access key",
4344
storage_account_connection_string: "Storage=Account;Connection=String" # Required if storage account `name` and `key` not set
4445
```
4546

47+
For configuration of service principal, use `storage_client_id`, `storage_client_secret`, `storage_tenant_id`.
48+
49+
```elixir
50+
config :azurex, Azurex.Blob.Config,
51+
api_url: "https://sample.blob.core.windows.net", # Optional
52+
default_container: "defaultcontainer", # Optional
53+
storage_account_name: "sample",
54+
storage_client_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
55+
storage_client_secret: "secret"
56+
storage_tenant_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
57+
```
58+
Note that SAS token generation is currently not supported when using Service Principal.
59+
4660
## Documentation
4761

4862
Documentation can be found at [https://hexdocs.pm/azurex](https://hexdocs.pm/azurex). Or generated using [ExDoc](https://github.com/elixir-lang/ex_doc)

lib/azurex/authorization/auth.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ defmodule Azurex.Authorization.Auth do
33
alias Azurex.Authorization.SharedKey
44
alias Azurex.Authorization.ServicePrincipal
55

6+
@doc """
7+
Adds authentication header to a given request based on the configured auth method.
8+
"""
9+
@spec authorize_request(HTTPoison.Request.t(), binary()) :: HTTPoison.Request.t()
610
def authorize_request(request, content_type \\ "") do
711
case Config.auth_method() do
812
{:account_key, storage_account_key} ->

lib/azurex/authorization/service_principal.ex

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
defmodule Azurex.Authorization.ServicePrincipal do
2+
require Logger
3+
alias Azurex.Blob.Config
4+
25
@cache_key "azurex_bearer_token"
36
@cache_expiry_margin_seconds 10
7+
8+
@doc """
9+
Fetches a bearer token and adds it to the request headers.
10+
In case fetching the token fails, it logs an error and returns "No token"
11+
which will fail the real request.
12+
"""
13+
@spec add_bearer_token(HTTPoison.Request.t(), binary(), binary(), binary()) ::
14+
HTTPoison.Request.t()
415
def add_bearer_token(%HTTPoison.Request{} = request, client_id, client_secret, tenant_id) do
516
bearer_token = fetch_bearer_token_cached(client_id, client_secret, tenant_id)
617
authorization = {"Authorization", "Bearer #{bearer_token}"}
@@ -20,7 +31,6 @@ defmodule Azurex.Authorization.ServicePrincipal do
2031
if expiry > System.os_time(:second) do
2132
token
2233
else
23-
IO.inspect("Token expired, refreshing")
2434
refresh_bearer_token_cache(client_id, client_secret, tenant_id)
2535
end
2636

@@ -45,14 +55,14 @@ defmodule Azurex.Authorization.ServicePrincipal do
4555
|> Map.get("exp")
4656
end
4757

48-
def fetch_bearer_token(client_id, client_secret, tenant_id) do
58+
defp fetch_bearer_token(client_id, client_secret, tenant_id) do
4959
body =
5060
"grant_type=client_credentials&client_id=#{client_id}&client_secret=#{client_secret}&scope=https://storage.azure.com/.default"
5161

5262
respone =
5363
%HTTPoison.Request{
5464
method: :post,
55-
url: "https://login.microsoftonline.com/#{tenant_id}/oauth2/v2.0/token",
65+
url: "#{Config.get_auth_url()}/#{tenant_id}/oauth2/v2.0/token",
5666
body: body,
5767
headers: [
5868
{"content-type", "application/x-www-form-urlencoded"}
@@ -62,10 +72,15 @@ defmodule Azurex.Authorization.ServicePrincipal do
6272

6373
case respone do
6474
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
65-
body |> Jason.decode!() |> Map.get("access_token")
75+
body |> Jason.decode!() |> Map.fetch!("access_token")
76+
77+
{:ok, %HTTPoison.Response{status_code: sc, body: body}} ->
78+
Logger.error("Failed to fetch bearer token. Reason: #{sc}: #{body}")
79+
"No token"
6680

67-
{:error, err} ->
68-
{:error, err}
81+
{:error, %HTTPoison.Error{reason: reason}} ->
82+
Logger.error("Failed to fetch bearer token. Reason: #{reason}")
83+
"No token"
6984
end
7085
end
7186
end

lib/azurex/blob/config.ex

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ defmodule Azurex.Blob.Config do
7878
defp try_service_principal(value), do: value
7979

8080
@doc """
81-
Investigate which authentication method is set.
81+
Investigate which authentication method is set and return the appropriate tuple
82+
or raise an error if miss configured.
8283
"""
8384
@spec auth_method() ::
8485
{:service_principal, binary(), binary(), binary()} | {:account_key, binary()}
@@ -138,4 +139,8 @@ defmodule Azurex.Blob.Config do
138139
|> parse_connection_string
139140
|> Map.get(key)
140141
end
142+
143+
def get_auth_url do
144+
Keyword.get(conf(), :auth_url) || "https://login.microsoftonline.com"
145+
end
141146
end

mix.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ defmodule Azurex.MixProject do
2828
[
2929
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
3030
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
31+
{:bypass, "~> 2.1", only: :test},
3132
{:httpoison, "~> 1.8 or ~> 2.2"},
3233
{:jason, "~> 1.4.4"}
3334
]

mix.lock

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
%{
2+
"bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"},
23
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
34
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
5+
"cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"},
6+
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
7+
"cowlib": {:hex, :cowlib, "2.14.0", "623791c56c1cc9df54a71a9c55147a401549917f00a2e48a6ae12b812c586ced", [:make, :rebar3], [], "hexpm", "0af652d1550c8411c3b58eed7a035a7fb088c0b86aff6bc504b0bc3b7f791aa2"},
48
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
59
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
610
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
@@ -15,10 +19,16 @@
1519
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
1620
"makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"},
1721
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
22+
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
1823
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
1924
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
2025
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
26+
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
27+
"plug_cowboy": {:hex, :plug_cowboy, "2.7.3", "1304d36752e8bdde213cea59ef424ca932910a91a07ef9f3874be709c4ddb94b", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "77c95524b2aa5364b247fa17089029e73b951ebc1adeef429361eab0bb55819d"},
28+
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
29+
"ranch": {:hex, :ranch, "1.8.1", "208169e65292ac5d333d6cdbad49388c1ae198136e4697ae2f474697140f201c", [:make, :rebar3], [], "hexpm", "aed58910f4e21deea992a67bf51632b6d60114895eb03bb392bb733064594dd0"},
2130
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
31+
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
2232
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
2333
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
2434
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
defmodule Azurex.Authorization.ServicePrincipalTest do
2+
use ExUnit.Case
3+
doctest Azurex.Authorization.ServicePrincipal
4+
5+
alias Azurex.Authorization.ServicePrincipal
6+
7+
setup do
8+
bypass = Bypass.open()
9+
10+
Application.put_env(:azurex, Azurex.Blob.Config, auth_url: "http://localhost:#{bypass.port}")
11+
12+
{:ok, bypass: bypass}
13+
end
14+
15+
defp generate_token(time) do
16+
token = %{exp: time} |> Jason.encode!() |> Base.encode64()
17+
"a.#{token}"
18+
end
19+
20+
defp generate_request do
21+
%HTTPoison.Request{
22+
method: :put,
23+
url: "https://example.com/sample-path",
24+
body: "sample body",
25+
headers: [
26+
{"x-ms-blob-type", "BlockBlob"}
27+
],
28+
options: [recv_timeout: :infinity]
29+
}
30+
end
31+
32+
defp prepare_auth_enpoint(bypass, token) do
33+
Bypass.expect_once(bypass, "POST", "/tenant_id/oauth2/v2.0/token", fn conn ->
34+
token_response = %{access_token: token} |> Jason.encode!()
35+
Plug.Conn.resp(conn, 200, token_response)
36+
end)
37+
end
38+
39+
describe "add_bearer_token/4" do
40+
test "Test bearer cache", %{bypass: bypass} do
41+
# Set token time so it expires in 100 seconds
42+
t = generate_token(:os.system_time(:second) + 100)
43+
# Expect one token request because the second will be cached
44+
prepare_auth_enpoint(bypass, t)
45+
input_request = generate_request()
46+
47+
for _ <- 1..2 do
48+
output_request =
49+
ServicePrincipal.add_bearer_token(
50+
input_request,
51+
"client_id",
52+
"client_secret",
53+
"tenant_id"
54+
)
55+
56+
assert output_request == %HTTPoison.Request{
57+
body: "sample body",
58+
headers: [
59+
{"Authorization", "Bearer #{t}"},
60+
{"x-ms-blob-type", "BlockBlob"}
61+
],
62+
method: :put,
63+
options: [recv_timeout: :infinity],
64+
params: %{},
65+
url: "https://example.com/sample-path"
66+
}
67+
end
68+
end
69+
70+
test "Test bearer cache refresh", %{bypass: bypass} do
71+
# Set token time so it expired 100 seconds ago
72+
t = generate_token(:os.system_time(:second) - 100)
73+
input_request = generate_request()
74+
75+
for _ <- 1..2 do
76+
# Now we expect two token requests because the token is expired
77+
prepare_auth_enpoint(bypass, t)
78+
79+
output_request =
80+
ServicePrincipal.add_bearer_token(
81+
input_request,
82+
"client_id",
83+
"client_secret",
84+
"tenant_id"
85+
)
86+
87+
assert output_request == %HTTPoison.Request{
88+
body: "sample body",
89+
headers: [
90+
{"Authorization", "Bearer #{t}"},
91+
{"x-ms-blob-type", "BlockBlob"}
92+
],
93+
method: :put,
94+
options: [recv_timeout: :infinity],
95+
params: %{},
96+
url: "https://example.com/sample-path"
97+
}
98+
end
99+
end
100+
101+
test "Failure", %{bypass: bypass} do
102+
Bypass.expect_once(bypass, "POST", "/tenant_id/oauth2/v2.0/token", fn conn ->
103+
Plug.Conn.resp(conn, 403, "Not authorized")
104+
end)
105+
106+
input_request = generate_request()
107+
108+
output_request =
109+
ServicePrincipal.add_bearer_token(
110+
input_request,
111+
"client_id",
112+
"client_secret",
113+
"tenant_id"
114+
)
115+
end
116+
end
117+
end

0 commit comments

Comments
 (0)