Current section

Files

Jump to
ash_authentication lib ash_authentication secret.ex
Raw

lib/ash_authentication/secret.ex

# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT
defmodule AshAuthentication.Secret do
@moduledoc """
A module to implement retrieving of secrets.
Allows you to implement secrets access via your method or choice at runtime.
The context parameter is either a map with the `conn` key containing the Plug.Conn
if the secret is being retrieved in a plug, or the context of the ash action it is
called in
## Example
```elixir
defmodule MyApp.GetSecret do
use AshAuthentication.Secret
def secret_for([:authentication, :strategies, :oauth2, :client_id], MyApp.User, _opts, _context), do: Application.fetch_env(:my_app, :oauth_client_id)
end
defmodule MyApp.Accounts.User do
use Ash.Resource,
extensions: [AshAuthentication],
domain: MyApp.Accounts
authentication do
strategies do
oauth2 do
client_id MyApp.GetSecret
client_secret MyApp.GetSecret
end
end
end
end
```
You can also implement it directly as a function:
```elixir
defmodule MyApp.User do
use Ash.Resource,
extensions: [AshAuthentication],
domain: MyApp.Accounts
authentication do
strategies do
oauth2 do
client_id fn _secret, _resource ->
Application.fetch_env(:my_app, :oauth_client_id)
end
end
end
end
end
```
## Secret name
Because you may wish to reuse this module for a number of different providers
and resources, the first argument passed to the callback is the "secret name",
it contains the "path" to the option being set. The path is made up of a list
containing the DSL path to the secret.
"""
alias Ash.Resource
alias AshAuthentication.Errors.InvalidSecret
@doc deprecated: "Use AshAuthentication.Secret.secret_for/4 instead"
@callback secret_for(secret_name :: [atom], Resource.t(), keyword) :: {:ok, String.t()} | :error
@doc """
Secret retrieval callback.
This function will be called with the "secret name", see the module
documentation for more info.
The context paramter is either a map with the `conn` key containing the Plug.Conn
if the secret is being retrieved in a plug, or the context of the ash action it is
called in
"""
@callback secret_for(secret_name :: [atom], Resource.t(), keyword, context :: map()) ::
{:ok, String.t()} | :error
@optional_callbacks secret_for: 3,
secret_for: 4
@doc false
@spec __using__(any) :: Macro.t()
defmacro __using__(_) do
quote do
@behaviour AshAuthentication.Secret
@before_compile AshAuthentication.Secret
@after_verify AshAuthentication.Secret
end
end
@doc false
defmacro __before_compile__(_) do
quote do
if Module.defines?(__MODULE__, {:secret_for, 4}, :def) do
def __secret_for_arity__, do: 4
else
def __secret_for_arity__, do: 3
end
end
end
@doc false
def __after_verify__(module) do
Code.ensure_loaded!(module)
if function_exported?(module, :secret_for, 3) and
function_exported?(module, :secret_for, 4) do
raise "#{inspect(module)}: You should only implement `secret_for/3` or `secret_for/4`, not both."
end
if not function_exported?(module, :secret_for, 3) and
not function_exported?(module, :secret_for, 4) do
raise "#{inspect(module)}: You must implement either `secret_for/3` or `secret_for/4`."
end
if function_exported?(module, :secret_for, 3) do
IO.warn(
"#{inspect(module)}: The `secret_for/3` callback is deprecated, please implement `secret_for/4` instead."
)
end
end
@doc false
def secret_for(module, secret_name, resource, opts, context) do
result =
if module.__secret_for_arity__() == 4 do
module.secret_for(secret_name, resource, opts, context)
else
module.secret_for(secret_name, resource, opts)
end
case result do
{:ok, secret} ->
{:ok, secret}
:error ->
:error
other ->
path = secret_name
raise InvalidSecret.exception(
path: path,
resource: resource,
value: other
)
end
end
end