diff options
author | Hentioe <me@bluerain.io> | 2020-10-27 18:15:03 +0800 |
---|---|---|
committer | Hentioe <me@bluerain.io> | 2020-10-27 18:15:03 +0800 |
commit | 32bfaf518f4eccb42170ad6bddfdcb113b14d439 (patch) | |
tree | 9cd08287042b80b31c3d5f4de2e46c42ad7abe2d | |
parent | Independent endpoint construction (diff) |
Hosting token refresh
-rw-r--r-- | config/test.exs | 2 | ||||
-rw-r--r-- | lib/azure_ex/application.ex | 18 | ||||
-rw-r--r-- | lib/azure_ex/config.ex | 14 | ||||
-rw-r--r-- | lib/azure_ex/request.ex | 6 | ||||
-rw-r--r-- | lib/azure_ex/token_hosting.ex | 106 | ||||
-rw-r--r-- | mix.exs | 1 |
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 @@ -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 |