Current section
Files
Jump to
Current section
Files
lib/ueberauth/strategy/indie_auth.ex
defmodule Ueberauth.Strategy.IndieAuth do
use Ueberauth.Strategy,
uid_field: :me,
default_scope: "read",
oauth2_module: Ueberauth.Strategy.IndieAuth.OAuth2
alias Ueberauth.Auth.Info
alias Ueberauth.Auth.Credentials
alias Ueberauth.Auth.Extra
require Logger
@spec handle_request!(Plug.Conn.t()) :: Plug.Conn.t()
def handle_request!(conn) do
with(
{:ok, client_id} <- get_client_id(conn),
{:ok, me} <- get_user(conn),
{:ok, state} <- get_state(conn),
{:ok, scopes} <- get_scope(conn)
) do
opts = [
scope: scopes,
redirect_uri: callback_url(conn),
client_id: client_id,
me: me,
state: state
]
module = option(conn, :oauth2_module)
case apply(module, :authorize_url!, [opts]) do
nil ->
send_error(conn, :no_authorization_endpoint)
{:error, error} ->
Logger.info("Couldn't communicate with the site requesting login.",
me: me,
error: inspect(error)
)
send_error(conn, :me_not_responding)
authorize_url ->
cache_key = state_cache_key(opts[:state])
if :ok = IndieWeb.Cache.set("indieauth_me:state:#{cache_key}", me) do
Logger.debug("Stored state to map to request.", state: opts[:state], me: me)
conn
|> redirect!(authorize_url)
conn
else
conn
|> send_error(:me_not_found)
end
end
else
{:error, error_code} when is_atom(error_code) ->
send_error(conn, error_code)
end
end
@spec handle_callback!(Plug.Conn.t()) :: Plug.Conn.t()
def handle_callback!(%Plug.Conn{params: %{"code" => code}} = conn) do
module = option(conn, :oauth2_module)
with(
{:ok, client_id} <- get_client_id(conn),
{:ok, me} <- get_user(conn),
{:ok, _} <- IndieWeb.Http.get(me)
) do
Logger.debug("Attempting to resolve callback request.", code: code, client_id: client_id)
resp =
apply(module, :get_token!, [
[
code: code,
redirect_uri: callback_url(conn),
client_id: client_id,
scope: "read",
me: me
]
])
case resp do
%OAuth2.AccessToken{access_token: token} ->
Logger.info("Extracted authentication information from request.",
me: me,
resp: inspect(resp)
)
state = conn.params["state"]
IndieWeb.Cache.delete("indieauth_me:state:#{state_cache_key(state)}")
conn
|> Plug.Conn.put_private(:indieauth_token, resp)
|> Plug.Conn.put_private(:indieauth_user, me)
|> Plug.Conn.put_private(:indieauth_access_token, token)
{:error, :no_user_specified} ->
Logger.warn("No user was found by the authentication endpoint.")
conn
|> send_error(:no_user_found)
{:error, error} ->
Logger.error("Failed to login.", error: inspect(error))
conn
|> set_errors!([
error(
Keyword.get(error, :reason, "unknown"),
Keyword.get(error, :description, "An unspecified error occurred")
)
])
end
else
{:error, code} when is_atom(code) ->
send_error(conn, code)
error ->
Logger.warn("Failed to resolve information about the user.", error: inspect(error))
send_error(conn, :site_not_responding)
end
end
def handle_callback!(conn) do
send_error(conn, :missing_code)
end
def handle_cleanup!(conn) do
conn
|> put_private(:indieauth_me, nil)
|> put_private(:indieauth_token, nil)
end
def credentials(conn) do
token = conn.private[:indieauth_token]
scope_string = token.other_params["scope"] || ""
scopes = String.split(scope_string, ",")
%Credentials{
token: token.access_token,
refresh_token: token.refresh_token,
expires_at: token.expires_at,
token_type: token.token_type,
expires: !!token.expires_at,
scopes: scopes
}
end
def info(conn) do
user = conn.private.indieauth_user
case IndieWeb.HCard.resolve(user) do
{:ok, hcard} ->
%Info{
name: IndieWeb.MF2.get_value!(hcard, "name") |> List.first(),
description: IndieWeb.MF2.get_value!(hcard, "note") |> List.first(),
nickname: IndieWeb.MF2.get_value!(hcard, "nickname") |> List.first(),
email: IndieWeb.MF2.get_value!(hcard, "email") |> List.first(),
image: IndieWeb.MF2.get_value!(hcard, "photo") |> List.first(),
urls: %{
url: user
}
}
_ ->
nil
end
end
def extra(conn) do
%Extra{
raw_info: %{
token: conn.private.indieauth_access_token,
user: conn.private.indieauth_user
}
}
end
def option(conn, key) do
Keyword.get(options(conn), key, Keyword.get(default_options(), key))
end
@doc "Obtains the specified state for this `Plug.Conn`."
@spec get_state(Plug.Conn.t()) :: {:ok, binary()} | {:error, :no_state_provided}
def get_state(conn) do
state = Map.get(conn.params, "state", nil)
if is_binary(state) do
{:ok, state}
else
{:error, :no_state_provided}
end
end
@doc "Obtains the client specified by this `Plug.Conn` (or uses the system-defined one if not found)."
@spec get_client_id(Plug.Conn.t()) :: {:ok, binary()} | {:error, :no_client_id}
def get_client_id(conn) do
case option(conn, :client_id) do
client_id when is_binary(client_id) ->
{:ok, client_id}
{:from_environment, app, key} ->
get_client_id(app, key)
_ ->
{:error, :no_client_id}
end
end
@doc "Provides the application-configured client ID."
@spec get_client_id(atom(), atom()) :: {:ok, binary()} | {:error, :client_id_not_specified}
def get_client_id(app, key) do
case Application.get_env(app, key, nil) do
client_id when is_binary(client_id) -> {:ok, client_id}
_ -> {:error, :no_client_id}
end
end
@doc "Obtains the scopes used in this `Plug.Conn`."
@spec get_scope(Plug.Conn.t()) :: {:ok, binary()} | {:error, :no_scope_specified}
def get_scope(conn) do
case Map.get(conn.params, "scope", option(conn, :default_scope)) do
scope when is_binary(scope) -> {:ok, scope}
nil -> {:error, :no_scope_specified}
end
end
@doc "Sets the specified error information into this `Plug.Conn`"
@spec send_error(Plug.Conn.t(), atom()) :: Plug.Conn.t()
def send_error(conn, error_code) do
case error_code do
:no_me_value ->
set_errors!(
conn,
[error("me_not_found", "No referencing user for this login request could be found")]
)
:no_client_id ->
set_errors!(
conn,
[error("client_misconfigured", "No client ID was configured by this site")]
)
:site_not_responding ->
set_errors!(conn, [
error("site_not_responding", "Your site doesn't seem to be responding")
])
:no_authorization_endpoint ->
set_errors!(
conn,
[
error(
"no_authorization_endpoint",
"The authorization endpoint could not be resolved"
)
]
)
:no_user_found ->
set_errors!(conn, [
error("no_user_found", "The session was cleaned")
])
:missing_code ->
set_errors!(conn, [error("missing_code", "No code received")])
:missing_state ->
set_errors!(conn, [error("missing_state", "No state provided")])
:no_scope_specified ->
set_errors!(conn, [error("no_scope_specified", "No scope was provided")])
unknown_code ->
set_errors!(conn, [error("unknown", "Experienced an unexpected error: #{unknown_code}")])
end
end
@doc "Gets the user to be specified by this user."
@spec get_user(Plug.Conn.t()) :: {:ok, binary()} | {:error, :no_me_value}
def get_user(conn) do
state_key = state_cache_key(Map.get(conn.params, "state", ""))
values = [
conn.params["me"],
conn.private[:indieauth_me],
IndieWeb.Cache.get("indieauth_me:state:#{state_key}")
]
value =
values
|> Enum.reject(&is_nil/1)
|> List.first()
Logger.info("Resolved user from values.", me: value, values: inspect(values))
case value do
nil -> {:error, :no_me_value}
me -> {:ok, me}
end
end
defp state_cache_key(state) do
Base.hex_encode32(state)
end
end