defmodule ExMicrosoftBot.TokenManager do @moduledoc """ This module is a GenServer that handles getting access token from Microsoft bot framework and also is responsible for refreshing the token before it expires. """ use ExMicrosoftBot.RefreshableAgent import ExMicrosoftBot.Client, only: [opts: 0] alias ExMicrosoftBot.{Client, Models} @auth_api_endpoint "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token" @scope Application.get_env(:ex_microsoftbot, :scope, "https://api.botframework.com/.default") # Public API @doc """ Get the token that can be used to authorize calls to Microsoft Bot Framework """ def get_token() do case get_state() do {:ok, %{token: token}} -> token _ -> nil end end # Refreshable Agent Callbacks @time_gap_to_refresh_in_seconds 500 @time_to_refresh_on_error_in_seconds 15 @doc """ Refresh the token by making a service call and then scheduling a message to this GenServer before token expires so that it can be refreshed """ def get_refreshed_state(%Models.AuthData{} = auth_data, _old_state) do :ex_microsoftbot |> Application.get_env(:using_bot_emulator) |> do_get_refreshed_state(auth_data) end @doc """ The time to refresh which is taken from the response of the token """ def time_to_refresh_after_in_seconds({:ok, %{expiry_in_seconds: expiry_in_seconds}}) do expiry_in_seconds - @time_gap_to_refresh_in_seconds end def time_to_refresh_after_in_seconds({:error, _, _}) do @time_to_refresh_on_error_in_seconds end # Private defp do_get_refreshed_state(_emulator? = true, %Models.AuthData{} = _auth_data) do {:ok, %{token: "TestToken", expiry_in_seconds: 36000}} end defp do_get_refreshed_state(__emulator?, %Models.AuthData{} = auth_data) do auth_data |> refresh_token() |> validate_token() end defp validate_token({:ok, %{token: token} = token_response}) do # TODO: See what other checks are needed to verify the JWT with jwt <- JOSE.JWT.peek_payload(token), true <- contains_valid_app_id_claim?(jwt) do {:ok, token_response} else result -> Logger.error("Error validating token. Result: #{inspect(result)}") {:error, result} end end defp validate_token({:error, _status_code, _body} = error) do Logger.error("Error validating token. Result: #{inspect(error)}") error end defp contains_valid_app_id_claim?(%JOSE.JWT{} = jwt) do contains_valid_app_id_claim?(Application.get_env(:ex_microsoftbot, :app_id), jwt) end defp contains_valid_app_id_claim?(expected_app_id, %JOSE.JWT{ fields: %{"appid" => expected_app_id} }), do: true defp contains_valid_app_id_claim?(_, %JOSE.JWT{}), do: false defp refresh_token(%Models.AuthData{} = auth_data) do case get_token_from_service(auth_data) do {:ok, token_response} -> {:ok, %{ token: Map.get(token_response, "access_token"), expiry_in_seconds: Map.get(token_response, "expires_in") }} {:error, _status_code, _body} = error -> Logger.error("Error refreshing token. Result: #{inspect(error)}") error end end defp get_token_from_service(%Models.AuthData{app_id: app_id, app_password: app_password}) do auth_api_endpoint = Application.get_env(:ex_microsoftbot, :auth_api_endpoint) || @auth_api_endpoint body = URI.encode_query(%{ grant_type: "client_credentials", client_id: app_id, client_secret: app_password, scope: @scope }) headers = ["Content-Type": "application/x-www-form-urlencoded"] auth_api_endpoint |> HTTPoison.post(body, headers, opts()) |> Client.deserialize_response(&Poison.decode!(&1, as: %{})) end end