Blog >>> Techtalk_

API Token Cache via Elixir GenServer

Im Zuge der Entwicklung einer Sharepoint-Integration, müssen Schnittstellen von Microsoft eingebunden werden. Die Authentifizierung der Schnittstelle haben wir in einen separaten Prozess ausgelagert, um immer einen aktuellen Token zur Verfügung zu haben.
30.09.2021
Tristan Damm
Tristan Damm
Entwickler
API Token Cache via Elixir GenServer

Token Lifetime

Für das Projekt »Inspire« müssen wir dauerhaft auf die Graph-API von Microsoft zugreifen können. Dazu benötigt man einen Authentifizierungstoken von Microsoft. Da der Token nur eine Stunde lang gültig ist, muss dieser bei jedem API-Request auf Gültigkeit überprüft und ggf. erneuert werden.

GenServer

Dadurch, dass Elixir vollen Zugriff auf das Erlang-Ökosystem hat, bietet es auch gleichzeitig die perfekte Lösung für unser Problem. Um den API-Token auf Gültigkeit zu überprüfen, bei Ungültigkeit einen neuen Token anzufordern und diesen dann zwischenzuspeichern, verwenden wir einen generischen Serverprozess (GenServer).

Ein GenServer ist ein selbstständiger Elixir-Prozess, der dazu verwendet werden kann, den Zustand (State) einer Anwendung zu halten oder asynchrone Operationen auszuführen. Im Gegensatz zu einem System-Prozess ist ein Elixir-Prozess extrem leichtgewichtig, in Bezug auf Speicher und CPU-Ausnutzung. Ein wichtiger Vorteil eines GenServers ist, dass sich der Prozess bei Fehlern oder Abstürzen selbst neustarten kann. Zudem lassen sich mit GenServern Mehrkernprozessoren besser auslasten, wenn mehrere GenServer gestartet werden.

Damit sich mit der Graph-API von Microsoft authentifiziert werden kann, wird ein Token vom Authentifizierungsserver angefordert. Der Server liefert uns den Token im folgenden Format:

{
    access_token: "eyJ0eXAiOiJKV1QiLCJub25jZSI6Ii1v...", 
    expires_in: 3599
}

Diese Informationen werden in ein Struct des AccessToken-Moduls überführt. Das Modul ist ebenfalls für das Abrufen des Tokens und dessen Validierung zuständig. Wir gehen davon aus das unser Token gültig ist, wenn die Zeit des Tokens, mit einem Puffer von 60 Sekunden (DateTime.add(access_token.expires_at, -60)), größer ist (:gt), als die aktuelle Zeit (DateTime.utc_now()).

defmodule Inspire.Azure.AccessToken do
  @moduledoc false

  @enforce_keys [:token, :expires_at]
  defstruct token: nil, expires_at: nil

  def get do
    case fetch_token(client()) do
      {:ok, response} -> new(response)
      _ -> :error
    end
  end

  def valid?(%__MODULE__{} = access_token) do
    DateTime.compare(DateTime.add(access_token.expires_at, -60), DateTime.utc_now()) == :gt
  end

  defp new(%{status: 200, body: %{"access_token" => token, "expires_in" => expires}}) do
    expires_at = DateTime.add(DateTime.utc_now(), expires)

    {:ok, %__MODULE__{token: token, expires_at: expires_at}}
  end

  defp new(_), do: :error

  def fetch_token(auth_client) do
    # ...fetch token form microsoft
  end

  def client do
    # ... setup for api client
  end
end

Um den State des Tokens zu halten, erstellen wir ein AccessTokenCache-Modul, worin ein GenServer implementiert wird. Der AccessTokenCache wird mit nil als Wert für den Token initialisiert. Beim ersten Aufruf wird get_access_token ausgeführt, um den Token zu erhalten. Daraufhin wird der Token im State des GenServers gespeichert. Bei folgenden Aufrufen wird überprüft, ob der aktuelle Token noch gültig ist. Wenn ja, wird er zurückgegeben, andernfalls wird ein neuer Token angefragt und gespeichert.

defmodule Inspire.Azure.AccessTokenCache do
  @moduledoc false

  use GenServer
  alias Inspire.Azure.AccessToken

  def start_link(opts) do
    GenServer.start_link(__MODULE__, :ok, opts)
  end

  def get do
    GenServer.call(__MODULE__, :get)
  end

  @impl true
  def init(:ok) do
    {:ok, nil}
  end

  @impl true
  def handle_call(:get, _from, nil) do
    get_access_token()
  end

  def handle_call(:get, _from, %AccessToken{} = access_token) do
    if AccessToken.valid?(access_token) do
      {:reply, access_token, access_token}
    else
      get_access_token()
    end
  end

  defp get_access_token do
    case AccessToken.get() do
      {:ok, access_token} ->
        {:reply, access_token, access_token}

      _ ->
        {:reply, :error, nil}
    end
  end
end

Fazit

Durch die Verwendung eines GenServers konnten wir ein konkretes Problem, unabhängig von der eigentlichen Anwendung, lösen. Dieser separate Prozess sorgt dafür, dass wir immer einen aktuellen Token für den Zugriff auf die Microsoft Graph-API haben. Sollte innerhalb des Prozesses etwas schiefgehen, weil beispielsweise die Microsoft API nicht verfügbar ist, wird der Prozess einfach neugestartet.

Teilen @