Packages
ash_authentication
5.0.0-rc.11
5.0.0-rc.12
5.0.0-rc.11
5.0.0-rc.10
5.0.0-rc.9
5.0.0-rc.8
5.0.0-rc.7
5.0.0-rc.6
5.0.0-rc.5
5.0.0-rc.4
5.0.0-rc.3
5.0.0-rc.2
5.0.0-rc.1
5.0.0-rc.0
4.14.1
4.14.0
4.13.7
4.13.6
4.13.5
4.13.4
4.13.3
4.13.2
retired
4.13.1
retired
4.13.0
4.12.0
4.11.0
4.10.0
4.9.9
4.9.8
4.9.7
4.9.6
4.9.5
4.9.4
4.9.3
4.9.2
4.9.1
4.9.0
4.8.7
4.8.6
4.8.5
4.8.3
4.8.2
4.8.1
4.8.0
4.7.6
4.7.5
4.7.4
4.7.3
4.7.2
4.7.1
4.7.0
4.6.4
4.6.3
4.6.2
4.6.1
4.6.0
4.5.6
4.5.5
4.5.4
4.5.3
4.5.2
4.5.1
4.5.0
4.4.9
4.4.8
4.4.7
4.4.6
4.4.5
4.4.4
4.4.3
4.4.2
4.4.1
4.4.0
4.3.12
4.3.11
4.3.10
4.3.9
4.3.8
4.3.7
4.3.6
4.3.5
4.3.4
4.3.3
4.3.2
4.3.1
4.3.0
4.2.7
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.0
4.0.4
4.0.3
4.0.2
4.0.1
4.0.0
4.0.0-rc.6
4.0.0-rc.5
4.0.0-rc.3
4.0.0-rc.2
4.0.0-rc.1
4.0.0-rc.0
3.12.4
3.12.3
3.12.2
3.12.1
3.12.0
3.11.16
3.11.15
3.11.14
3.11.13
3.11.12
3.11.11
3.11.10
3.11.9
3.11.8
3.11.7
3.11.6
3.11.5
3.11.4
3.11.3
3.11.2
3.11.1
3.11.0
3.10.8
3.10.7
3.10.6
3.10.5
3.10.4
3.10.3
3.10.2
3.10.1
3.10.0
3.9.6
3.9.5
3.9.4
3.9.3
3.9.2
3.9.1
3.9.0
3.8.0
3.7.9
3.7.8
3.7.6
3.7.5
3.7.4
3.7.3
3.7.2
3.7.1
3.7.0
3.6.1
3.6.0
3.5.3
3.5.2
3.5.1
3.5.0
3.3.1
3.3.0
3.2.2
3.2.1
3.2.0
3.1.0
3.0.3
Authentication extension for the Ash Framework.
Current section
Files
Jump to
Current section
Files
lib/ash_authentication/strategies/oauth2/plug.ex
# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT
defmodule AshAuthentication.Strategy.OAuth2.Plug do
@moduledoc """
Handlers for incoming OAuth2 HTTP requests.
"""
alias Ash.Error.Framework.AssumptionFailed
alias AshAuthentication.{Errors, Info, Strategy, Strategy.OAuth2}
alias Assent.HTTPAdapter.Finch
alias Plug.Conn
import Ash.PlugHelpers, only: [get_actor: 1, get_tenant: 1, get_context: 1]
import AshAuthentication.Plug.Helpers, only: [store_authentication_result: 2]
import Plug.Conn
@raw_config_attrs [
:auth_method,
:client_authentication_method,
:id_token_signed_response_alg,
:id_token_ttl_seconds,
:openid_configuration_uri
]
@doc """
Perform the request phase of OAuth2.
Builds a redirection URL based on the provider configuration and redirects the
user to that endpoint.
"""
@spec request(Conn.t(), OAuth2.t()) :: Conn.t()
# sobelow_skip ["XSS.SendResp"]
def request(conn, strategy) do
with {:ok, config} <- config_for(strategy, %{conn: conn}),
{:ok, config} <- maybe_add_nonce(config, strategy, %{conn: conn}),
{:ok, session_key} <- session_key(strategy),
{:ok, %{session_params: session_params, url: url}} <-
strategy.assent_strategy.authorize_url(config) do
conn
|> put_session(session_key, session_params)
|> put_resp_header("location", url)
|> send_resp(:found, "Redirecting to #{strategy.name}")
else
{:error, reason} -> store_authentication_result(conn, {:error, reason})
end
end
@doc """
Perform the callback phase of OAuth2.
Responds to a user being redirected back from the remote authentication
provider, and validates the passed options, ultimately registering or
signing-in a user if the authentication was successful.
"""
@spec callback(Conn.t(), OAuth2.t()) :: Conn.t()
def callback(conn, strategy) do
with {:ok, session_key} <- session_key(strategy),
{:ok, config} <- config_for(strategy, %{conn: conn}),
session_params when is_map(session_params) <- get_session(conn, session_key),
conn <- delete_session(conn, session_key),
config <- Keyword.put(config, :session_params, session_params),
{:ok, %{user: user, token: token}} <-
strategy.assent_strategy.callback(config, conn.params),
action_opts <- action_opts(conn),
{:ok, user} <-
register_or_sign_in_user(
strategy,
%{user_info: user, oauth_tokens: token},
action_opts
) do
store_authentication_result(conn, {:ok, user})
else
nil ->
store_authentication_result(conn, {:error, nil})
{:error, reason} ->
store_authentication_result(conn, {:error, reason})
end
end
defp action_opts(conn) do
[actor: get_actor(conn), tenant: get_tenant(conn), context: get_context(conn) || %{}]
|> Enum.reject(&is_nil(elem(&1, 1)))
end
defp config_for(strategy, context) do
config =
strategy
|> Map.take(@raw_config_attrs)
with {:ok, config} <- add_secret_value(config, strategy, :base_url, context),
{:ok, config} <-
add_secret_value(config, strategy, :authorize_url, !!strategy.base_url, context),
{:ok, config} <-
add_secret_value(config, strategy, :client_id, !!strategy.base_url, context),
{:ok, config} <-
add_secret_value(config, strategy, :client_secret, !!strategy.base_url, context),
{:ok, config} <-
add_secret_value(config, strategy, :token_url, !!strategy.base_url, context),
{:ok, config} <-
add_secret_value(config, strategy, :code_verifier, !!strategy.code_verifier, context),
{:ok, config} <-
add_secret_value(
config,
strategy,
:authorization_params,
!!strategy.authorization_params,
context
),
{:ok, config} <-
add_secret_value(
config,
strategy,
:openid_configuration,
!strategy.openid_configuration,
context
),
{:ok, config} <-
add_secret_value(
config,
strategy,
:team_id,
strategy.assent_strategy != Assent.Strategy.Apple,
context
),
{:ok, config} <-
add_secret_value(
config,
strategy,
:private_key_id,
strategy.assent_strategy != Assent.Strategy.Apple,
context
),
{:ok, config} <-
add_secret_value(
config,
strategy,
:private_key_path,
strategy.assent_strategy != Assent.Strategy.Apple,
context
),
{:ok, config} <-
add_secret_value(config, strategy, :trusted_audiences, true, context),
{:ok, config} <- add_http_adapter(config),
{:ok, config} <-
add_secret_value(
config,
strategy,
:user_url,
!!strategy.authorize_url || !!strategy.base_url,
context
),
{:ok, redirect_uri} <- build_redirect_uri(strategy, context),
{:ok, jwt_algorithm} <-
Info.authentication_tokens_signing_algorithm(strategy.resource) do
config =
config
|> Map.put(:jwt_algorithm, jwt_algorithm)
|> Map.put(:redirect_uri, redirect_uri)
|> Map.update(:client_authentication_method, nil, &to_string/1)
|> Enum.reject(&is_nil(elem(&1, 1)))
{:ok, config}
end
end
defp register_or_sign_in_user(strategy, params, opts) when strategy.registration_enabled?,
do: Strategy.action(strategy, :register, params, opts)
defp register_or_sign_in_user(strategy, params, opts),
do: Strategy.action(strategy, :sign_in, params, opts)
# We need to temporarily store some information about the request in the
# session so that we can verify that there hasn't been a CSRF-related attack.
defp session_key(strategy) do
case Info.authentication_subject_name(strategy.resource) do
{:ok, subject_name} ->
{:ok, "#{subject_name}/#{strategy.name}"}
:error ->
{:error,
AssumptionFailed.exception(
message: "Resource `#{inspect(strategy.resource)}` has no subject name"
)}
end
end
# With OpenID Connect we can pass a "nonce" value into the assent strategy
# which is an additional way to ensure that the callback matches the request.
defp maybe_add_nonce(config, strategy, context) do
case fetch_secret(strategy, :nonce, context) do
{:ok, value} when is_binary(value) and byte_size(value) > 0 ->
{:ok, Keyword.put(config, :nonce, value)}
{:ok, false} ->
{:ok, config}
{:error, reason} ->
{:error, reason}
end
end
defp add_secret_value(config, strategy, secret_name, allow_nil? \\ false, context) do
case fetch_secret(strategy, secret_name, context) do
{:ok, nil} when allow_nil? ->
{:ok, config}
{:ok, nil} ->
path = [:authentication, :strategies, strategy.name, secret_name]
{:error, Errors.MissingSecret.exception(path: path, resource: strategy.resource)}
{:ok, value} when is_binary(value) and byte_size(value) > 0 ->
{:ok, Map.put(config, secret_name, value)}
{:ok, list} when is_list(list) ->
{:ok, Map.put(config, secret_name, list)}
{:ok, map} when is_map(map) ->
{:ok, Map.put(config, secret_name, map)}
{:ok, boolean} when is_boolean(boolean) ->
{:ok, Map.put(config, secret_name, boolean)}
{:error, reason} ->
{:error, reason}
end
end
defp fetch_secret(strategy, secret_name, context) do
path = [:authentication, :strategies, strategy.name, secret_name]
with {:ok, {secret_module, secret_opts}} <- Map.fetch(strategy, secret_name),
{:ok, secret} when is_binary(secret) and byte_size(secret) > 0 <-
AshAuthentication.Secret.secret_for(
secret_module,
path,
strategy.resource,
secret_opts,
context
) do
{:ok, secret}
else
{:ok, secret} ->
{:ok, secret}
_ ->
{:error, Errors.MissingSecret.exception(path: path, resource: strategy.resource)}
end
end
defp build_redirect_uri(strategy, context) do
with {:ok, subject_name} <- Info.authentication_subject_name(strategy.resource),
{:ok, redirect_uri} <- fetch_secret(strategy, :redirect_uri, context),
{:ok, %URI{} = uri} <- string_to_uri(redirect_uri) do
suffix = Path.join([to_string(subject_name), to_string(strategy.name), "callback"])
# Don't append the path if the secret ends with the path already
path =
if String.ends_with?(uri.path, suffix) do
uri.path
else
Path.join([uri.path, suffix])
end
{:ok, to_string(%URI{uri | path: path})}
else
:error ->
{:error,
AssumptionFailed.exception(
message: "Resource `#{inspect(strategy.resource)}` has no subject name"
)}
{:error, reason} ->
{:error, reason}
end
end
defp string_to_uri(input) when is_binary(input) do
case URI.new(input) do
{:ok, %URI{path: nil} = uri} -> {:ok, %URI{uri | path: "/"}}
{:ok, %URI{} = uri} -> {:ok, uri}
{:error, reason} -> {:error, reason}
end
end
defp add_http_adapter(config) do
http_adapter =
Application.get_env(
:ash_authentication,
:http_adapter,
{Finch, supervisor: AshAuthentication.Finch}
)
{:ok, Map.put(config, :http_adapter, http_adapter)}
end
end