88 lines
2.7 KiB
Elixir
88 lines
2.7 KiB
Elixir
defmodule Mv.Oidc.Discovery do
|
|
@moduledoc """
|
|
Fetches and caches the OIDC provider's discovery document
|
|
(`/.well-known/openid-configuration`).
|
|
|
|
Currently only `end_session_endpoint` is exposed — used by the logout flow to
|
|
trigger RP-initiated logout at the IdP so the user's SSO session is cleared
|
|
and they don't get auto-re-logged-in.
|
|
|
|
Cache lives in `:persistent_term`, keyed by base URL, for the lifetime of the
|
|
BEAM. Re-fetch on next call after `clear_cache/0`.
|
|
"""
|
|
|
|
require Logger
|
|
|
|
@persistent_term_key {__MODULE__, :discovery}
|
|
@request_timeout 5_000
|
|
|
|
@doc """
|
|
Returns the IdP's `end_session_endpoint` URL.
|
|
|
|
- `{:ok, url}` if discovery succeeds (and is cached for future calls)
|
|
- `{:error, reason}` if the IdP is unreachable, the document is malformed,
|
|
or the field is missing
|
|
"""
|
|
@spec end_session_endpoint(String.t()) :: {:ok, String.t()} | {:error, term()}
|
|
def end_session_endpoint(base_url) when is_binary(base_url) do
|
|
case fetch_cached(base_url) do
|
|
{:ok, %{"end_session_endpoint" => url}} when is_binary(url) -> {:ok, url}
|
|
{:ok, _config} -> {:error, :no_end_session_endpoint}
|
|
{:error, _} = err -> err
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Clears the cached discovery documents. Intended for tests.
|
|
"""
|
|
@spec clear_cache() :: :ok
|
|
def clear_cache do
|
|
:persistent_term.erase(@persistent_term_key)
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Seeds the cache with a fixed result for a base URL. Intended for tests so the
|
|
HTTP fetch is skipped.
|
|
"""
|
|
@spec put_cache(String.t(), {:ok, map()} | {:error, term()}) :: :ok
|
|
def put_cache(base_url, result) when is_binary(base_url) do
|
|
cache = :persistent_term.get(@persistent_term_key, %{})
|
|
:persistent_term.put(@persistent_term_key, Map.put(cache, base_url, result))
|
|
:ok
|
|
end
|
|
|
|
defp fetch_cached(base_url) do
|
|
cache = :persistent_term.get(@persistent_term_key, %{})
|
|
|
|
case Map.fetch(cache, base_url) do
|
|
{:ok, result} ->
|
|
result
|
|
|
|
:error ->
|
|
result = fetch(base_url)
|
|
:persistent_term.put(@persistent_term_key, Map.put(cache, base_url, result))
|
|
result
|
|
end
|
|
end
|
|
|
|
defp fetch(base_url) do
|
|
url = String.trim_trailing(base_url, "/") <> "/.well-known/openid-configuration"
|
|
|
|
case Req.get(url,
|
|
receive_timeout: @request_timeout,
|
|
connect_options: [timeout: @request_timeout]
|
|
) do
|
|
{:ok, %Req.Response{status: 200, body: body}} when is_map(body) ->
|
|
{:ok, body}
|
|
|
|
{:ok, %Req.Response{status: status}} ->
|
|
Logger.warning("OIDC discovery returned HTTP #{status} for #{url}")
|
|
{:error, {:http_status, status}}
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("OIDC discovery request failed for #{url}: #{inspect(reason)}")
|
|
{:error, reason}
|
|
end
|
|
end
|
|
end
|