Packages

fnord

0.9.36

AI code archaeology

Current section

Files

Jump to
fnord lib mcp oauth2 bridge.ex
Raw

lib/mcp/oauth2/bridge.ex

defmodule MCP.OAuth2.Bridge do
@moduledoc """
Builds Authorization header for MCP transports.
If token is near expiry, attempts a refresh via `Client` and persists.
"""
alias MCP.OAuth2.{CredentialsStore, Client}
@default_refresh_margin 120
@spec authorization_header(String.t(), map(), keyword()) ::
{:ok, [{String.t(), String.t()}]} | {:error, term()}
def authorization_header(server, cfg, opts \\ []) do
margin = Keyword.get(opts, :refresh_margin, @default_refresh_margin)
with {:ok, toks} <- CredentialsStore.read(server),
{:ok, toks2} <- maybe_refresh(server, cfg, toks, margin),
at when is_binary(at) <- Map.get(toks2, "access_token") do
{:ok, [{"authorization", "Bearer " <> at}]}
else
{:error, reason} -> {:error, reason}
_ -> {:error, :no_credentials}
end
end
defp maybe_refresh(server, cfg, %{"expires_at" => exp} = toks, margin) when is_integer(exp) do
if exp - System.os_time(:second) <= margin do
refresh(server, cfg, toks)
else
{:ok, toks}
end
end
# If expires_at is missing or not an integer, do not attempt arithmetic - treat as no refresh needed here
defp maybe_refresh(_server, _cfg, toks, _margin), do: {:ok, toks}
defp refresh(server, cfg, %{"refresh_token" => rt}) when is_binary(rt) and rt != "" do
oauth = Map.get(cfg, "oauth", %{})
# Use configured redirect_uri, or build from redirect_port, or omit if neither present
redirect_uri =
cond do
is_binary(oauth["redirect_uri"]) ->
oauth["redirect_uri"]
is_integer(oauth["redirect_port"]) and oauth["redirect_port"] > 0 ->
"http://localhost:#{oauth["redirect_port"]}/callback"
true ->
nil
end
# Build OAuth config for refresh - need discovery_url, client_id, optional
# client_secret. The RFC 8707 resource indicator (the MCP server's own
# URL) rides along so refreshed tokens stay bound to the same resource
# the original grant was issued for.
oauth_cfg =
%{
discovery_url: oauth["discovery_url"],
client_id: oauth["client_id"],
client_secret: oauth["client_secret"],
scopes: Map.get(oauth, "scopes", []),
resource: Map.get(cfg, "base_url")
}
|> maybe_put_redirect(redirect_uri)
case Client.refresh_token(oauth_cfg, rt) do
{:ok, new} ->
:ok = CredentialsStore.write(server, normalize(new))
{:ok, normalize(new)}
{:error, e} ->
{:error, e}
end
end
defp refresh(_server, _cfg, _toks), do: {:error, :no_refresh_token}
defp maybe_put_redirect(map, nil), do: map
defp maybe_put_redirect(map, uri), do: Map.put(map, :redirect_uri, uri)
defp normalize(%{
access_token: at,
refresh_token: rt,
token_type: tt,
expires_at: exp,
scope: sc
}) do
%{
"access_token" => at,
"refresh_token" => rt,
"token_type" => tt,
"expires_at" => exp,
"scope" => sc
}
end
end