Packages
ash_authentication
4.8.3
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.
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/ash_authentication/plug/helpers.ex
defmodule AshAuthentication.Plug.Helpers do
@moduledoc """
Authentication helpers for use in your router, etc.
"""
alias Ash.{PlugHelpers, Resource}
alias AshAuthentication.{Info, Jwt, TokenResource}
alias Plug.Conn
@doc """
Store the user in the connections' session.
"""
@spec store_in_session(Conn.t(), Resource.record()) :: Conn.t()
def store_in_session(conn, user) when is_struct(user) do
subject_name = Info.authentication_subject_name!(user.__struct__)
if Info.authentication_tokens_require_token_presence_for_authentication?(user.__struct__) do
Conn.put_session(conn, "#{subject_name}_token", user.__metadata__.token)
else
subject = AshAuthentication.user_to_subject(user)
Conn.put_session(conn, subject_name, subject)
end
end
def store_in_session(conn, _), do: conn
@doc """
Given a list of subjects, turn as many as possible into users.
Opts are forwarded to `AshAuthentication.subject_to_user/2`
"""
@spec load_subjects([AshAuthentication.subject()], module, opts :: Keyword.t()) :: map
def load_subjects(subjects, otp_app, opts \\ []) when is_list(subjects) do
resources =
otp_app
|> AshAuthentication.authenticated_resources()
|> Stream.map(&{to_string(Info.authentication_subject_name!(&1)), &1})
|> Map.new()
subjects
|> Enum.reduce(%{}, fn subject, result ->
subject = URI.parse(subject)
with {:ok, resource} <- Map.fetch(resources, subject.path),
{:ok, user} <-
AshAuthentication.subject_to_user(subject, resource, opts),
{:ok, subject_name} <- Info.authentication_subject_name(resource) do
current_subject_name = current_subject_name(subject_name)
Map.put(result, current_subject_name, user)
else
_ -> result
end
end)
end
@doc """
Attempt to retrieve all users from the connections' session.
Iterates through all configured authentication resources for `otp_app` and
retrieves any users stored in the session, loads them and stores them in the
assigns under their subject name (with the prefix `current_`).
If there is no user present for a resource then the assign is set to `nil`.
"""
@spec retrieve_from_session(Conn.t(), module, keyword) :: Conn.t()
def retrieve_from_session(conn, otp_app, opts \\ []) do
opts =
opts
|> Keyword.put_new(:tenant, Ash.PlugHelpers.get_tenant(conn))
|> Keyword.put_new(:context, Ash.PlugHelpers.get_context(conn) || %{})
otp_app
|> AshAuthentication.authenticated_resources()
|> Stream.map(
&{&1, Info.authentication_options(&1),
Info.authentication_tokens_require_token_presence_for_authentication?(&1)}
)
|> Enum.reduce(conn, fn
{resource, options, true}, conn ->
current_subject_name = current_subject_name(options.subject_name)
token_resource = Info.authentication_tokens_token_resource!(resource)
session_key = "#{options.subject_name}_token"
with token when is_binary(token) <-
Conn.get_session(conn, session_key),
{:ok, %{"sub" => subject, "jti" => jti} = claims, _}
when not is_map_key(claims, "act") <- Jwt.verify(token, otp_app, opts),
{:ok, [_]} <-
TokenResource.Actions.get_token(
token_resource,
%{
"jti" => jti,
"purpose" => "user"
},
opts
),
{:ok, user} <-
AshAuthentication.subject_to_user(
subject,
resource,
opts
) do
Conn.assign(conn, current_subject_name, user)
else
_ ->
conn
|> Conn.assign(current_subject_name, nil)
|> Conn.delete_session(session_key)
end
{resource, options, false}, conn ->
current_subject_name = current_subject_name(options.subject_name)
with subject when is_binary(subject) <- Conn.get_session(conn, options.subject_name),
{:ok, user} <-
AshAuthentication.subject_to_user(
subject,
resource,
opts
) do
Conn.assign(conn, current_subject_name, user)
else
_ ->
conn
|> Conn.assign(current_subject_name, nil)
|> Conn.delete_session(options.subject_name)
end
end)
end
@doc """
Assigns all subjects from their equivalent sessions, if they are not already assigned.
This is meant to used via `AshAuthenticationPhoenix` for nested liveviews.
See `AshAuthenticationPhoenix.LiveSession.assign_new_resources/3` for more.
"""
def assign_new_resources(socket, session, assign_new, opts) do
opts = Keyword.put_new(opts, :tenant, session["tenant"])
{otp_app, opts} = Keyword.pop(opts, :otp_app)
otp_app
|> AshAuthentication.authenticated_resources()
|> Stream.map(
&{&1, Info.authentication_options(&1),
Info.authentication_tokens_require_token_presence_for_authentication?(&1)}
)
|> Enum.reduce(socket, fn
{resource, options, true}, socket ->
current_subject_name = current_subject_name(options.subject_name)
assign_new.(socket, current_subject_name, fn ->
with token when is_binary(token) <-
Map.get(session, "#{options.subject_name}_token"),
{:ok, %{"sub" => subject} = claims, _}
when not is_map_key(claims, "act") <- Jwt.verify(token, otp_app, opts),
{:ok, user} <-
AshAuthentication.subject_to_user(
subject,
resource,
opts
) do
user
else
_ -> nil
end
end)
{resource, options, false}, socket ->
current_subject_name = current_subject_name(options.subject_name)
assign_new.(socket, current_subject_name, fn ->
with subject when is_binary(subject) <- session[to_string(options.subject_name)],
{:ok, user} <-
AshAuthentication.subject_to_user(
subject,
resource,
opts
) do
user
else
_ ->
nil
end
end)
end)
end
@doc """
Validate authorization header(s).
Assumes that your clients are sending a bearer-style authorization header with
your request. If a valid bearer token is present then the subject is loaded
into the assigns under their subject name (with the prefix `current_`).
If the authentication token is required to be present in the database, it is
loaded into the assigns using `current_\#{subject_name}_token_record`
If there is no user present for a resource then the assign is set to `nil`.
"""
@spec retrieve_from_bearer(Conn.t(), module, keyword) :: Conn.t()
def retrieve_from_bearer(conn, otp_app, opts \\ []) do
opts =
opts
|> Keyword.put_new(:tenant, Ash.PlugHelpers.get_tenant(conn))
|> Keyword.put_new(:context, Ash.PlugHelpers.get_context(conn) || %{})
conn
|> Conn.get_req_header("authorization")
|> Stream.filter(&String.starts_with?(&1, "Bearer "))
|> Stream.map(&String.replace_leading(&1, "Bearer ", ""))
|> Enum.reduce(conn, fn token, conn ->
with {:ok, %{"sub" => subject, "jti" => jti} = claims, resource}
when not is_map_key(claims, "act") <- Jwt.verify(token, otp_app, opts),
{:ok, token_record} <-
validate_token(resource, jti),
{:ok, user} <-
AshAuthentication.subject_to_user(
subject,
resource,
opts
),
{:ok, subject_name} <- Info.authentication_subject_name(resource),
current_subject_name <- current_subject_name(subject_name) do
conn
|> Conn.assign(current_subject_name, user)
|> maybe_assign_token_record(token_record, subject_name)
else
_ -> conn
end
end)
end
defp maybe_assign_token_record(conn, _token_record, subject_name) when is_nil(subject_name) do
conn
end
defp maybe_assign_token_record(conn, token_record, subject_name) do
conn
|> Conn.assign(
current_subject_token_record_name(subject_name),
token_record
)
end
defp validate_token(resource, jti) do
if Info.authentication_tokens_require_token_presence_for_authentication?(resource) do
with {:ok, token_resource} <-
Info.authentication_tokens_token_resource(resource),
{:ok, [token_record]} <-
TokenResource.Actions.get_token(token_resource, %{
"jti" => jti,
"purpose" => "user"
}) do
{:ok, token_record}
else
_ -> :error
end
else
{:ok, nil}
end
end
@doc """
Revoke all authorization header(s).
Any bearer-style authorization headers will have their tokens revoked.
"""
@spec revoke_bearer_tokens(Conn.t(), module) :: Conn.t()
def revoke_bearer_tokens(conn, otp_app) do
conn
|> Conn.get_req_header("authorization")
|> Stream.filter(&String.starts_with?(&1, "Bearer "))
|> Stream.map(&String.replace_leading(&1, "Bearer ", ""))
|> Enum.reduce(conn, fn token, conn ->
with {:ok, resource} <- Jwt.token_to_resource(token, otp_app),
{:ok, token_resource} <- Info.authentication_tokens_token_resource(resource),
:ok <- TokenResource.Actions.revoke(token_resource, token) do
conn
else
_ -> conn
end
end)
end
@doc """
Set a subject as the request actor.
Presumes that you have already loaded your user resource(s) into the
connection's assigns.
Uses `Ash.PlugHelpers` to streamline integration with `AshGraphql` and
`AshJsonApi`.
## Examples
Setting the actor for a AshGraphql API using `Plug.Router`.
```elixir
defmodule MyApp.ApiRouter do
use Plug.Router
import MyApp.AuthPlug
plug :match
plug :retrieve_from_bearer
plug :set_actor, :user
plug :dispatch
forward "/gql",
to: Absinthe.Plug,
init_opts: [schema: MyApp.Schema]
end
```
"""
@spec set_actor(Conn.t(), subject_name :: atom) :: Conn.t()
def set_actor(conn, subject_name) do
current_subject_name =
subject_name
|> current_subject_name()
actor =
conn
|> Map.get(:assigns, %{})
|> Map.get(current_subject_name)
conn
|> PlugHelpers.set_actor(actor)
end
@doc """
Store result in private.
This is used by authentication plug handlers to store their result for passing
back to the dispatcher.
"""
@spec store_authentication_result(
Conn.t(),
:ok | {:ok, Resource.record()} | :error | {:error, any}
) ::
Conn.t()
def store_authentication_result(conn, :ok),
do: Conn.put_private(conn, :authentication_result, {:ok, nil})
def store_authentication_result(conn, {:ok, record}),
do: Conn.put_private(conn, :authentication_result, {:ok, record})
def store_authentication_result(conn, :error),
do: Conn.put_private(conn, :authentication_result, :error)
def store_authentication_result(conn, {:error, reason}),
do: Conn.put_private(conn, :authentication_result, {:error, reason})
def get_authentication_result(%{private: %{authentication_result: result}} = conn),
do: {conn, result}
def get_authentication_result(conn), do: conn
# Dynamically generated atoms are generally frowned upon, but in this case
# the `subject_name` is a statically configured atom, so should be fine.
# sobelow_skip ["DOS.StringToAtom"]
defp current_subject_name(subject_name) when is_atom(subject_name),
do: String.to_atom("current_#{subject_name}")
# sobelow_skip ["DOS.StringToAtom"]
defp current_subject_token_record_name(subject_name) when is_atom(subject_name),
do: String.to_atom("current_#{subject_name}_token_record")
end