1defmodule Joken.Hooks do
2  @moduledoc """
3  Behaviour for defining hooks into Joken's lifecycle.
4
5  Hooks are passed to `Joken` functions or added to `Joken.Config` through the
6  `Joken.Config.add_hook/2` macro. They can change the execution flow of a token configuration.
7
8  There are 2 kinds of hooks: before and after.
9
10  Both of them are executed in a reduce_while call and so must always return either:
11    - `{:halt, ...}` -> when you want to abort execution (other hooks won't be called)
12    - `{:cont, ...}` -> when you want to let other hooks execute
13
14  ## Before hooks
15
16  A before hook receives as the first parameter its options and then a tuple with the input of
17  the function. For example, the `generate_claims` function receives the token configuration plus a
18  map of extra claims. Therefore, a `before_generate` hook receives:
19    - the hook options or `[]` if none are given;
20    - a tuple with two elements where the first is the token configuration and the second is the extra
21    claims map;
22
23  The return of a before hook is always the input of the next hook. Say you want to add an extra claim
24  with a hook. You could do so like in this example:
25
26      defmodule EnsureExtraClaimHook do
27        use Joken.Hooks
28
29        @impl true
30        def before_generate(_hook_options, {token_config, extra_claims}) do
31          {:cont, {token_config, Map.put(extra_claims, "must_exist", true)}}
32        end
33      end
34
35  You could also halt execution completely on a before hook. Just use the `:halt` return with an error
36  tuple:
37
38      defmodule StopTheWorldHook do
39        use Joken.Hooks
40
41        @impl true
42        def before_generate(_hook_options, _input) do
43          {:halt, {:error, :stop_the_world}}
44        end
45      end
46
47  ## After hooks
48
49  After hooks work similar then before hooks. The difference is that it takes and returns the result of the
50  operation. So, instead of receiving 2 arguments it takes three:
51    - the hook options or `[]` if none are given;
52    - the result tuple which might be `{:error, reason}` or a tuple with `:ok` and its parameters;
53    - the input to the function call.
54
55  Let's see an example with `after_verify`. The verify function takes as argument the token and a signer. So,
56  an `after_verify` might look like this:
57
58      defmodule CheckVerifyError do
59        use Joken.Hooks
60        require Logger
61
62        @impl true
63        def after_verify(_hook_options, result, input) do
64          case result do
65            {:error, :invalid_signature} ->
66              Logger.error("Check signer!!!")
67              {:halt, result}
68
69            {:ok, _claims} ->
70              {:cont, result, input}
71          end
72        end
73      end
74
75  On this example we have conditional logic for different results.
76
77  ## `Joken.Config`
78
79  When you create a module that has `use Joken.Config` it automatically implements
80  this behaviour with overridable functions. You can simply override a callback
81  implementation directly and it will be triggered when using any of the generated
82  functions. Example:
83
84      defmodule HookToken do
85        use Joken.Config
86
87        @impl Joken.Hooks
88        def before_generate(_options, input) do
89          IO.puts("Before generating claims")
90          {:cont, input}
91        end
92      end
93
94  Now if we call `HookToken.generate_claims/1` it will call our callback.
95
96  Also in `Joken.Config` there is an imported macro for adding hooks with options. Example:
97
98      defmodule ManyHooks do
99        use Joken.Config
100
101        add_hook(JokenJwks, jwks_url: "http://someserver.com/.well-known/certs")
102      end
103
104  For an implementation reference, please see the source code of `Joken.Hooks.RequiredClaims`
105  """
106  alias Joken.Signer
107
108  @type halt_tuple :: {:halt, tuple}
109  @type hook_options :: Keyword.t()
110  @type generate_input :: {Joken.token_config(), extra :: Joken.claims()}
111  @type sign_input :: {Joken.claims(), Signer.t()}
112  @type verify_input :: {Joken.bearer_token(), Signer.t()}
113  @type validate_input :: {Joken.token_config(), Joken.claims(), context :: map()}
114
115  @doc "Called before `Joken.generate_claims/3`"
116  @callback before_generate(hook_options, generate_input) :: {:cont, generate_input} | halt_tuple
117
118  @doc "Called before `Joken.encode_and_sign/3`"
119  @callback before_sign(hook_options, sign_input) :: {:cont, sign_input} | halt_tuple
120
121  @doc "Called before `Joken.verify/3`"
122  @callback before_verify(hook_options, verify_input) :: {:cont, verify_input} | halt_tuple
123
124  @doc "Called before `Joken.validate/4`"
125  @callback before_validate(hook_options, validate_input) :: {:cont, validate_input} | halt_tuple
126
127  @doc "Called after `Joken.generate_claims/3`"
128  @callback after_generate(hook_options, Joken.generate_result(), generate_input) ::
129              {:cont, Joken.generate_result(), generate_input} | halt_tuple
130
131  @doc "Called after `Joken.encode_and_sign/3`"
132  @callback after_sign(
133              hook_options,
134              Joken.sign_result(),
135              sign_input
136            ) :: {:cont, Joken.sign_result(), sign_input} | halt_tuple
137
138  @doc "Called after `Joken.verify/3`"
139  @callback after_verify(
140              hook_options,
141              Joken.verify_result(),
142              verify_input
143            ) :: {:cont, Joken.verify_result(), verify_input} | halt_tuple
144
145  @doc "Called after `Joken.validate/4`"
146  @callback after_validate(
147              hook_options,
148              Joken.validate_result(),
149              validate_input
150            ) :: {:cont, Joken.validate_result(), validate_input} | halt_tuple
151
152  defmacro __using__(_opts) do
153    quote do
154      @behaviour Joken.Hooks
155
156      @impl true
157      def before_generate(_hook_options, input), do: {:cont, input}
158
159      @impl true
160      def before_sign(_hook_options, input), do: {:cont, input}
161
162      @impl true
163      def before_verify(_hook_options, input), do: {:cont, input}
164
165      @impl true
166      def before_validate(_hook_options, input), do: {:cont, input}
167
168      @impl true
169      def after_generate(_hook_options, result, input), do: {:cont, result, input}
170
171      @impl true
172      def after_sign(_hook_options, result, input), do: {:cont, result, input}
173
174      @impl true
175      def after_verify(_hook_options, result, input), do: {:cont, result, input}
176
177      @impl true
178      def after_validate(_hook_options, result, input), do: {:cont, result, input}
179
180      defoverridable before_generate: 2,
181                     before_sign: 2,
182                     before_verify: 2,
183                     before_validate: 2,
184                     after_generate: 3,
185                     after_sign: 3,
186                     after_verify: 3,
187                     after_validate: 3
188    end
189  end
190
191  @before_hooks [:before_generate, :before_sign, :before_verify, :before_validate]
192  @after_hooks [:after_generate, :after_sign, :after_verify, :after_validate]
193
194  def run_before_hook(hooks, hook_function, input) when hook_function in @before_hooks do
195    hooks
196    |> Enum.reduce_while(input, fn hook, input ->
197      {hook, opts} = unwrap_hook(hook)
198
199      case apply(hook, hook_function, [opts, input]) do
200        {:cont, _next_input} = res -> res
201        {:halt, _reason} = res -> res
202        _ -> {:halt, {:error, :wrong_hook_return}}
203      end
204    end)
205    |> case do
206      {:error, _reason} = err -> err
207      res -> {:ok, res}
208    end
209  end
210
211  def run_after_hook(hooks, hook_function, result, input) when hook_function in @after_hooks do
212    hooks
213    |> Enum.reduce_while({result, input}, fn hook, {result, input} ->
214      {hook, opts} = unwrap_hook(hook)
215
216      case apply(hook, hook_function, [opts, result, input]) do
217        {:cont, result, next_input} -> {:cont, {result, next_input}}
218        {:halt, _reason} = res -> res
219        _ -> {:halt, {:error, :wrong_hook_return}}
220      end
221    end)
222    |> case do
223      {result, input} when is_tuple(input) -> result
224      res -> res
225    end
226  end
227
228  defp unwrap_hook({_hook_module, _opts} = hook), do: hook
229  defp unwrap_hook(hook) when is_atom(hook), do: {hook, []}
230end
231