summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHentioe <me@bluerain.io>2020-10-27 18:15:03 +0800
committerHentioe <me@bluerain.io>2020-10-27 18:15:03 +0800
commit32bfaf518f4eccb42170ad6bddfdcb113b14d439 (patch)
tree9cd08287042b80b31c3d5f4de2e46c42ad7abe2d
parentIndependent endpoint construction (diff)
Hosting token refresh
-rw-r--r--config/test.exs2
-rw-r--r--lib/azure_ex/application.ex18
-rw-r--r--lib/azure_ex/config.ex14
-rw-r--r--lib/azure_ex/request.ex6
-rw-r--r--lib/azure_ex/token_hosting.ex106
-rw-r--r--mix.exs1
6 files changed, 138 insertions, 9 deletions
diff --git a/config/test.exs b/config/test.exs
index 8b13789..b90d024 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -1 +1,3 @@
+use Mix.Config
+import_config "test.secret.exs"
diff --git a/lib/azure_ex/application.ex b/lib/azure_ex/application.ex
index 8b13789..e6be1d4 100644
--- a/lib/azure_ex/application.ex
+++ b/lib/azure_ex/application.ex
@@ -1 +1,19 @@
+defmodule AzureEx.Application do
+ @moduledoc false
+ use Application
+
+ def start(_tuple, _args) do
+ opts = [strategy: :one_for_one, name: AzureEx.Supervisor]
+
+ Supervisor.start_link([{AzureEx.TokenHosting, get_client_oauth_params()}], opts)
+ end
+
+ defp get_client_oauth_params do
+ tenant = Application.get_env(:azure_ex, :tenant)
+ client_id = Application.get_env(:azure_ex, :client_id)
+ client_secret = Application.get_env(:azure_ex, :client_secret)
+
+ [tenant: tenant, client_id: client_id, client_secret: client_secret]
+ end
+end
diff --git a/lib/azure_ex/config.ex b/lib/azure_ex/config.ex
index bbf6238..7c67ebf 100644
--- a/lib/azure_ex/config.ex
+++ b/lib/azure_ex/config.ex
@@ -4,13 +4,6 @@ defmodule AzureEx.Config do
@default_timeout 1000 * 15
@default_recv_timeout 1000 * 10
- # TODO: 通过 oauth2 API 获取并自动刷新 access_token 替代硬编码配置
- @spec access_token :: String.t() | nil
- def access_token, do: get(:access_token)
-
- @spec subscription_id :: String.t() | nil
- def subscription_id, do: get(:subscription_id)
-
@spec timeouts :: [timeout: integer(), recv_timeout: integer()]
def timeouts,
do: [
@@ -18,6 +11,13 @@ defmodule AzureEx.Config do
recv_timeout: get(:recv_timeout, @default_recv_timeout)
]
+ @spec tenant_id() :: binary
+ def tenant_id, do: get(:tenant)
+ @spec client_id() :: binary
+ def client_id, do: get(:client_id)
+ @spec client_secret() :: binary
+ def client_secret, do: get(:client_secret)
+
@spec get(atom(), any()) :: any()
defp get(key, default \\ nil) do
Application.get_env(:azure_ex, key, default)
diff --git a/lib/azure_ex/request.ex b/lib/azure_ex/request.ex
index 675ed84..592987f 100644
--- a/lib/azure_ex/request.ex
+++ b/lib/azure_ex/request.ex
@@ -3,7 +3,7 @@ defmodule AzureEx.Request do
HTTP request functions.
"""
- alias AzureEx.Config
+ alias AzureEx.{Config, TokenHosting}
@type params :: %{body: keyword | map}
@type result :: any
@@ -23,6 +23,8 @@ defmodule AzureEx.Request do
@spec send(:get, String.t(), params) :: httpoison_result
def send(:get, endpoint, %{}) do
- HTTPoison.get(endpoint, [Authorization: "Bearer #{Config.access_token()}"], Config.timeouts())
+ headers = [Authorization: "Bearer #{TokenHosting.get_token()}"]
+
+ HTTPoison.get(endpoint, headers, Config.timeouts())
end
end
diff --git a/lib/azure_ex/token_hosting.ex b/lib/azure_ex/token_hosting.ex
new file mode 100644
index 0000000..301ec4f
--- /dev/null
+++ b/lib/azure_ex/token_hosting.ex
@@ -0,0 +1,106 @@
+defmodule AzureEx.TokenHosting do
+ @moduledoc """
+ 托管令牌更新。
+ """
+
+ use GenServer
+
+ defmodule Token do
+ @moduledoc false
+
+ defstruct [:access_token, :expires_in, :created_at]
+
+ @type t :: %__MODULE__{
+ access_token: binary,
+ expires_in: integer,
+ created_at: NaiveDateTime.t()
+ }
+
+ def new(%{access_token: access_token, expires_in: expires_in}) do
+ created_at = NaiveDateTime.utc_now()
+
+ %__MODULE__{access_token: access_token, expires_in: expires_in, created_at: created_at}
+ end
+ end
+
+ defmodule Params do
+ @moduledoc false
+
+ defstruct [:tenant, :client_id, :client_secret]
+
+ @type t :: %__MODULE__{
+ tenant: binary,
+ client_id: binary,
+ client_secret: binary
+ }
+ end
+
+ def start_link(opts \\ []) do
+ name = Keyword.get(opts, :name, __MODULE__)
+ tenant = Keyword.get(opts, :tenant)
+ client_id = Keyword.get(opts, :client_id)
+ client_secret = Keyword.get(opts, :client_secret)
+ params = %Params{tenant: tenant, client_id: client_id, client_secret: client_secret}
+
+ {:ok, token} = apply_token(params)
+
+ GenServer.start_link(__MODULE__, %{params: params, token: token}, name: name)
+ end
+
+ @scope "https://management.azure.com//.default"
+ @grant_type "client_credentials"
+ @headers [{"Content-Type", "application/x-www-form-urlencoded"}]
+
+ @spec apply_token(Params.t()) :: {:ok, Token.t()} | {:error, binary}
+ defp apply_token(%{tenant: tt, client_id: ci, client_secret: cs}) do
+ endpoint = "https://login.microsoftonline.com/#{tt}/oauth2/v2.0/token"
+
+ form = [
+ client_id: ci,
+ scope: @scope,
+ client_secret: cs,
+ grant_type: @grant_type
+ ]
+
+ case HTTPoison.post(endpoint, {:form, form}, @headers) do
+ {:ok, %HTTPoison.Response{body: body}} ->
+ token = body |> Jason.decode!(keys: :atoms) |> Token.new()
+
+ {:ok, token}
+
+ {:error, e} ->
+ # TODO: 完善此处的错误模型
+ {:error, to_string(e)}
+ end
+ end
+
+ @impl true
+ def init(state) do
+ schedule_token_refresh()
+
+ {:ok, state}
+ end
+
+ def get_token do
+ GenServer.call(__MODULE__, :get)
+ end
+
+ @impl true
+ def handle_call(:get, _from, %{token: %{access_token: at}} = state) do
+ {:reply, at, state}
+ end
+
+ @impl true
+ def handle_info(:refresh, %{params: params} = state) do
+ {:ok, token} = apply_token(params)
+
+ schedule_token_refresh()
+ {:noreply, %{state | token: token}}
+ end
+
+ @interval 3500
+
+ defp schedule_token_refresh do
+ Process.send_after(__MODULE__, :refresh, @interval)
+ end
+end
diff --git a/mix.exs b/mix.exs
index 17d8138..6ffcc09 100644
--- a/mix.exs
+++ b/mix.exs
@@ -14,6 +14,7 @@ defmodule AzureEx.MixProject do
# Run "mix help compile.app" to learn about applications.
def application do
[
+ mod: {AzureEx.Application, []},
extra_applications: [:logger]
]
end