Packages

phoenix_kit

1.7.91
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit integrations oauth.ex
Raw

lib/phoenix_kit/integrations/oauth.ex

defmodule PhoenixKit.Integrations.OAuth do
@moduledoc """
Generic OAuth 2.0 flow for service integrations.
Handles authorization URL generation, code-to-token exchange,
token refresh, and userinfo fetching. Provider-specific details
(URLs, scopes, extra params) come from the provider definition
in `PhoenixKit.Integrations.Providers`.
"""
require Logger
@http_timeout 15_000
@doc """
Generate a random state token for CSRF protection in OAuth flows.
"""
@spec generate_state() :: String.t()
def generate_state do
:crypto.strong_rand_bytes(24) |> Base.url_encode64(padding: false)
end
@doc """
Build the OAuth authorization URL for a provider.
Requires `client_id` to be present in the integration data and
the provider to have `oauth_config` with an `auth_url`.
"""
@spec authorization_url(map(), map(), String.t(), String.t() | nil, String.t() | nil) ::
{:ok, String.t()} | {:error, atom()}
def authorization_url(
oauth_config,
integration_data,
redirect_uri,
extra_scopes \\ nil,
state \\ nil
) do
client_id = integration_data["client_id"]
if is_binary(client_id) and client_id != "" do
scopes =
extra_scopes || oauth_config[:default_scopes] || oauth_config["default_scopes"] || ""
params =
%{
"client_id" => client_id,
"redirect_uri" => redirect_uri,
"response_type" => "code",
"scope" => scopes
}
|> Map.merge(oauth_config[:auth_params] || oauth_config["auth_params"] || %{})
params = if state, do: Map.put(params, "state", state), else: params
url = "#{oauth_config[:auth_url] || oauth_config["auth_url"]}?#{URI.encode_query(params)}"
{:ok, url}
else
{:error, :client_id_not_configured}
end
end
@doc """
Exchange an authorization code for access and refresh tokens.
"""
@spec exchange_code(map(), map(), String.t(), String.t()) ::
{:ok, map()} | {:error, term()}
def exchange_code(oauth_config, integration_data, code, redirect_uri) do
with {:ok, client_id, client_secret} <- validate_client_credentials(integration_data) do
token_url = oauth_config[:token_url] || oauth_config["token_url"]
token_url
|> post_token_request(
code: code,
client_id: client_id,
client_secret: client_secret,
redirect_uri: redirect_uri,
grant_type: "authorization_code"
)
|> handle_token_response(integration_data)
end
end
@doc """
Refresh an expired access token using the refresh token.
"""
@spec refresh_access_token(map(), map()) :: {:ok, String.t(), map()} | {:error, term()}
def refresh_access_token(oauth_config, integration_data) do
refresh_token = integration_data["refresh_token"]
if is_binary(refresh_token) and refresh_token != "" do
with {:ok, client_id, client_secret} <- validate_client_credentials(integration_data) do
token_url = oauth_config[:token_url] || oauth_config["token_url"]
case post_token_request(token_url,
refresh_token: refresh_token,
client_id: client_id,
client_secret: client_secret,
grant_type: "refresh_token"
) do
{:ok, %{status: 200, body: %{"access_token" => new_token} = body}} ->
updated_fields =
%{
"access_token" => new_token,
"expires_at" => compute_expires_at(body["expires_in"]),
"token_obtained_at" => DateTime.utc_now() |> DateTime.to_iso8601()
}
|> maybe_put_refresh_token(body["refresh_token"])
{:ok, new_token, updated_fields}
{:ok, %{status: status}} ->
log_token_error("Token refresh failed", status)
{:error, {:refresh_failed, status}}
{:error, reason} ->
Logger.warning("[Integrations.OAuth] Token refresh error: #{inspect(reason)}")
{:error, reason}
end
end
else
{:error, :no_refresh_token}
end
end
@doc """
Fetch user info from the provider's userinfo endpoint.
Returns a map with at least `"email"` if available.
"""
@spec fetch_userinfo(map(), String.t()) :: {:ok, map()} | {:error, term()}
def fetch_userinfo(oauth_config, access_token) do
userinfo_url = oauth_config[:userinfo_url] || oauth_config["userinfo_url"]
if is_binary(userinfo_url) and userinfo_url != "" do
case Req.get(userinfo_url,
headers: [{"authorization", "Bearer #{access_token}"}],
receive_timeout: @http_timeout
) do
{:ok, %{status: 200, body: body}} when is_map(body) ->
{:ok, body}
{:ok, %{status: status}} ->
Logger.warning("[Integrations.OAuth] Userinfo request returned status #{status}")
{:error, {:userinfo_failed, status}}
{:error, reason} ->
Logger.warning("[Integrations.OAuth] Userinfo request failed: #{inspect(reason)}")
{:error, reason}
end
else
{:ok, %{}}
end
end
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
defp compute_expires_at(nil), do: nil
defp compute_expires_at(expires_in) when is_integer(expires_in) do
DateTime.utc_now()
|> DateTime.add(expires_in, :second)
|> DateTime.to_iso8601()
end
defp compute_expires_at(_), do: nil
defp validate_client_credentials(data) do
client_id = data["client_id"]
client_secret = data["client_secret"]
if is_binary(client_id) and client_id != "" and
is_binary(client_secret) and client_secret != "" do
{:ok, client_id, client_secret}
else
{:error, :client_credentials_not_configured}
end
end
defp post_token_request(url, form_params) do
Req.post(url, form: form_params, receive_timeout: @http_timeout)
end
defp handle_token_response(
{:ok, %{status: 200, body: %{"access_token" => _} = body}},
integration_data
) do
token_data = %{
"access_token" => body["access_token"],
"refresh_token" => body["refresh_token"] || integration_data["refresh_token"],
"token_type" => body["token_type"] || "Bearer",
"expires_at" => compute_expires_at(body["expires_in"]),
"token_obtained_at" => DateTime.utc_now() |> DateTime.to_iso8601()
}
{:ok, token_data}
end
defp handle_token_response({:ok, %{status: status}}, _integration_data) do
log_token_error("Token exchange failed", status)
{:error, {:token_exchange_failed, status}}
end
defp handle_token_response({:error, reason}, _integration_data) do
Logger.warning("[Integrations.OAuth] Token exchange error: #{inspect(reason)}")
{:error, reason}
end
defp maybe_put_refresh_token(fields, nil), do: fields
defp maybe_put_refresh_token(fields, ""), do: fields
defp maybe_put_refresh_token(fields, new_refresh_token),
do: Map.put(fields, "refresh_token", new_refresh_token)
defp log_token_error(message, status) do
Logger.warning("[Integrations.OAuth] #{message}: status=#{status}")
end
end