Packages
joken
2.0.0
2.6.2
2.6.1
2.6.0
2.5.0
2.4.1
2.4.0
2.3.0
2.2.0
2.1.0
2.0.1
2.0.0
2.0.0-rc3
2.0.0-rc2
2.0.0-rc1
2.0.0-rc0
1.5.0
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.2
1.2.1
1.2.0
1.1.1
1.1.0
1.0.1
1.0.0
0.16.1
0.16.0
0.15.0
0.14.1
0.14.0
0.13.1
0.13.0
0.12.0
0.11.0
0.10.1
0.10.0
0.8.1
0.8.0
0.7.0
0.6.2
0.6.1
0.6.0
0.5.0
0.1.0
JWT (JSON Web Token) library for Elixir.
Current section
Files
Jump to
Current section
Files
lib/joken/hooks.ex
defmodule Joken.Hooks do
@moduledoc """
Behaviour for defining hooks into Joken's lifecycle.
Hooks are passed to `Joken` functions or added to `Joken.Config` through the
`Joken.Config.add_hook/2` macro. They can change the execution flow of a token configuration.
There are 2 kinds of hooks: before and after.
Both of them are executed in a reduce_while call and so must always return either:
- `{:halt, ...}` -> when you want to abort execution (other hooks won't be called)
- `{:cont, ...}` -> when you want to let other hooks execute
## Before hooks
A before hook receives as the first parameter its options and then a tuple with the input of
the function. For example, the `generate_claims` function receives the token configuration plus a
map of extra claims. Therefore, a `before_generate` hook receives:
- the hook options or `[]` if none are given;
- a tuple with two elements where the first is the token configuration and the second is the extra
claims map;
The return of a before hook is always the input of the next hook. Say you want to add an extra claim
with a hook. You could do so like in this example:
defmodule EnsureExtraClaimHook do
use Joken.Hooks
@impl true
def before_generate(_hook_options, {token_config, extra_claims}) do
{:cont, {token_config, Map.put(extra_claims, "must_exist", true)}}
end
end
You could also halt execution completely on a before hook. Just use the `:halt` return with an error
tuple:
defmodule StopTheWorldHook do
use Joken.Hooks
@impl true
def before_generate(_hook_options, _input) do
{:halt, {:error, :stop_the_world}}
end
end
## After hooks
After hooks work similar then before hooks. The difference is that it takes and returns the result of the
operation. So, instead of receiving 2 arguments it takes three:
- the hook options or `[]` if none are given;
- the result tuple which might be `{:error, reason}` or a tuple with `:ok` and its parameters;
- the input to the function call.
Let's see an example with `after_verify`. The verify function takes as argument the token and a signer. So,
an `after_verify` might look like this:
defmodule CheckVerifyError do
use Joken.Hooks
require Logger
@impl true
def after_verify(_hook_options, result, input) do
case result do
{:error, :invalid_signature} ->
Logger.error("Check signer!!!")
{:halt, result}
{:ok, _claims} ->
{:cont, result, input}
end
end
end
On this example we have conditional logic for different results.
## `Joken.Config`
When you create a module that has `use Joken.Config` it automatically implements
this behaviour with overridable functions. You can simply override a callback
implementation directly and it will be triggered when using any of the generated
functions. Example:
defmodule HookToken do
use Joken.Config
@impl Joken.Hooks
def before_generate(_options, input) do
IO.puts("Before generating claims")
{:cont, input}
end
end
Now if we call `HookToken.generate_claims/1` it will call our callback.
Also in `Joken.Config` there is an imported macro for adding hooks with options. Example:
defmodule ManyHooks do
use Joken.Config
add_hook(JokenJwks, jwks_url: "http://someserver.com/.well-known/certs")
end
"""
alias Joken.Signer
@type halt_tuple :: {:halt, tuple}
@type hook_options :: Keyword.t()
@type generate_input :: {Joken.token_config(), extra :: Joken.claims()}
@type sign_input :: {Joken.claims(), Signer.t()}
@type verify_input :: {Joken.bearer_token(), Signer.t()}
@type validate_input :: {Joken.token_config(), Joken.claims(), context :: map()}
@doc "Called before `Joken.generate_claims/3`"
@callback before_generate(hook_options, generate_input) :: {:cont, generate_input} | halt_tuple
@doc "Called before `Joken.encode_and_sign/3`"
@callback before_sign(hook_options, sign_input) :: {:cont, sign_input} | halt_tuple
@doc "Called before `Joken.verify/3`"
@callback before_verify(hook_options, verify_input) :: {:cont, verify_input} | halt_tuple
@doc "Called before `Joken.validate/4`"
@callback before_validate(hook_options, validate_input) :: {:cont, validate_input} | halt_tuple
@doc "Called after `Joken.generate_claims/3`"
@callback after_generate(hook_options, Joken.generate_result(), generate_input) ::
{:cont, Joken.generate_result(), generate_input} | halt_tuple
@doc "Called after `Joken.encode_and_sign/3`"
@callback after_sign(
hook_options,
Joken.sign_result(),
sign_input
) :: {:cont, Joken.sign_result(), sign_input} | halt_tuple
@doc "Called after `Joken.verify/3`"
@callback after_verify(
hook_options,
Joken.verify_result(),
verify_input
) :: {:cont, Joken.verify_result(), verify_input} | halt_tuple
@doc "Called after `Joken.validate/4`"
@callback after_validate(
hook_options,
Joken.validate_result(),
validate_input
) :: {:cont, Joken.validate_result(), validate_input} | halt_tuple
defmacro __using__(_opts) do
quote do
@behaviour Joken.Hooks
@impl true
def before_generate(_hook_options, input), do: {:cont, input}
@impl true
def before_sign(_hook_options, input), do: {:cont, input}
@impl true
def before_verify(_hook_options, input), do: {:cont, input}
@impl true
def before_validate(_hook_options, input), do: {:cont, input}
@impl true
def after_generate(_hook_options, result, input), do: {:cont, result, input}
@impl true
def after_sign(_hook_options, result, input), do: {:cont, result, input}
@impl true
def after_verify(_hook_options, result, input), do: {:cont, result, input}
@impl true
def after_validate(_hook_options, result, input), do: {:cont, result, input}
defoverridable before_generate: 2,
before_sign: 2,
before_verify: 2,
before_validate: 2,
after_generate: 3,
after_sign: 3,
after_verify: 3,
after_validate: 3
end
end
@before_hooks [:before_generate, :before_sign, :before_verify, :before_validate]
@after_hooks [:after_generate, :after_sign, :after_verify, :after_validate]
def run_before_hook(hooks, hook_function, input) when hook_function in @before_hooks do
hooks
|> Enum.reduce_while(input, fn hook, input ->
{hook, opts} = unwrap_hook(hook)
case apply(hook, hook_function, [opts, input]) do
{:cont, _next_input} = res -> res
{:halt, _reason} = res -> res
_ -> {:halt, {:error, :wrong_hook_return}}
end
end)
|> case do
{:error, _reason} = err -> err
res -> {:ok, res}
end
end
def run_after_hook(hooks, hook_function, result, input) when hook_function in @after_hooks do
hooks
|> Enum.reduce_while({result, input}, fn hook, {result, input} ->
{hook, opts} = unwrap_hook(hook)
case apply(hook, hook_function, [opts, result, input]) do
{:cont, result, next_input} -> {:cont, {result, next_input}}
{:halt, _reason} = res -> res
_ -> {:halt, {:error, :wrong_hook_return}}
end
end)
|> case do
{result, input} when is_tuple(input) -> result
res -> res
end
end
defp unwrap_hook({_hook_module, _opts} = hook), do: hook
defp unwrap_hook(hook) when is_atom(hook), do: {hook, []}
end