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