defmodule Guardian.Token.Jwt do
@moduledoc """
Deals with things JWT. This module should not be used directly.
It is intended to be used by Guardian on behalf of your implementation
as its token module.
Token types are encoded in the `typ` field.
### Configuration
Configuration should be added to the implementation module
in either the configuration file or as options to `use Guardian`
#### Required
* `issuer` - The issuer of the token. Your application name/id
* `secret_key` - The secret key to use for the implementation module. This
may be any resolvable value for `Guardian.Config`
#### Optional
* `token_verify_module` - default `Guardian.Token.Jwt.Verify`. The module that verifies the claims
* `allowed_algos` - The allowed algos to use for encoding and decoding. See
JOSE for available. Default ["HS512"]
* `ttl` - The default time to live for all tokens. See the type in
Guardian.ttl. The default by `Guardian.Token.JWT` is `{4, :weeks}`
* `token_ttl` a map of `token_type` to `ttl`. Set specific ttls for specific types of tokens
* `allowed_drift` The drift that is allowed when decoding/verifying a token in milli seconds
* `verify_issuer` Verify that the token was issued by the configured issuer. Default false
* `secret_fetcher` A module used to fetch the secret. Default: `Guardian.Token.Jwt.SecretFetcher`
Options:
These options are available to encoding and decoding:
* `secret` The secret key to use for signing
* `headers` The Jose headers that should be used
* `allowed_algos` - A list of allowable algos
* `token_type` - Override the default token type. The default is "access"
* `ttl` - The time to live. See `Guardian.Token.ttl` type
#### Example
```elixir
# encode a simple token
{:ok, token, claims} =
MyApp.Tokens.encode_and_sign(resource)
# encode a token with custom claims
{:ok, token, claims} =
MyApp.Tokens.encode_and_sign(resource, %{some: "claim"})
# encode a token with a custom type
{:ok, token, claims} =
MyApp.Tokens.encode_and_sign(resource, %{}, token_type: "refresh")
# encode a token with custom options
{:ok, token, claims} =
MyApp.Tokens.encode_and_sign(
resource,
%{},
secret: {MyModule, :get_my_secret, ["some", "args"]},
ttl: {4, :weeks},
token_type: "refresh"
)
# decode a token
{:ok, claims} =
MyApp.Tokens.decode_and_verify(token)
# decode a token and check literal claims
{:ok, claims} =
MyApp.Tokens.decode_and_verify(token, %{"typ" => "refresh"})
# decode a token and check literal claims with options
{:ok, claims} =
MyApp.Tokens.decode_and_verify(token,
%{"typ" => "refresh"}, secret: {MyModule, :get_my_secret, ["some", "args"]})
# exchange a token
{:ok, {old_token, old_claims}, {new_token, new_claims}} =
MyApp.Tokens.exchange(old_token, ["access", "refresh"], "access")
# exchange a token with options
{:ok, {old_token, old_claims}, {new_token, new_claims}} =
MyApp.Tokens.exchange(old_token,
["access", "refresh"],
"access" secret: {MyModule, :get_my_secret, ["some", "args"]}, ttl: {1, :hour})
# refresh a token using defaults
{:ok, {old_token, old_claims}, {new_token, new_claims}} = MyApp.Tokens.refresh(old_token)
# refresh a token using options
{:ok, {old_token, old_claims}, {new_token, new_claims}} =
MyApp.Tokens.refresh(old_token, ttl: {1, :week}, secret: {MyMod, :get_secret, ["some", "args"})
```
### Token verify module
The token verify module by default is `Guardian.Token.Jwt.Verify`.
This module implements the `Guardian.Token.Verify` behaviour.
To customize your token validation you have 2 options.
1. Implement the `verify_claims` callback on your implementation
2. `use Guardian.Token.Verify` in your own module and use that.
To create your own verify module use `Guardian.Token.Verify` and configure
your implementation to use it either through config files or when you setup
your implementation.
```elixir
defmodule MyApp.Tokens do
use Guardian, otp_app: :my_app,
token_verify_module: MyVerifyModule
# ... snip
end
```
### SecretFetcher
When you need dynamic secret verification, you should use a custom
`Guardian.Token.Jwt.SecretFetcher` module.
This will allow you to use the header values to determine dynamically the
key that should be used.
```elixir
defmodule MyCustomSecretFetcher do
use Guardian.Token.Jwt.SecretFetcher
def fetch_signing_secret(impl_module, opts) do
# fetch the secret for signing
end
def fetch_verifying_secret(impl_module, token_headers, opts) do
# fetch the secret for verifying the token
end
end
```
If the signing secret contains a "kid" (https://tools.ietf.org/html/rfc7515#section-4.1.4)
it will be passed along to the signature to provide a hint about which secret was used.
This can be useful for specifying which public key to use during verification if you're using
a public/private key rotation strategy.
An example implementation of this can be found here: [https://gist.github.com/mpinkston/469009001b694d3ca162894d74c9bfe3](https://gist.github.com/mpinkston/469009001b694d3ca162894d74c9bfe3)
"""
@behaviour Guardian.Token
alias Guardian.Config
alias Guardian.Token.Jwt.SecretFetcher.SecretFetcherDefaultImpl
alias Guardian.Token.Jwt.Verify
alias JOSE.JWK
alias JOSE.JWS
alias JOSE.JWT
import Guardian, only: [stringify_keys: 1, ttl_to_seconds: 1]
@default_algos ["HS512"]
@default_token_type "access"
@type_key "typ"
@default_ttl {4, :weeks}
defmodule SecretFetcher do
@moduledoc """
Provides a behaviour that specifies how to fetch the secret for the token.
`use Guardian.Token.JWT.SecretFetcher` to provide default implementations of each function.
"""
@doc """
fetch_signing_secret fetches the secret to sign.
"""
@callback fetch_signing_secret(module, opts :: Guardian.options()) :: {:ok, term} | {:error, :secret_not_found}
@doc """
Fetches the secret to verify a token.
It is provided with the tokens headers in order to lookup the secret.
"""
@callback fetch_verifying_secret(module, token_headers :: map, opts :: Guardian.options()) ::
{:ok, term} | {:error, :secret_not_found}
defmacro __using__(_opts \\ []) do
quote do
alias Guardian.Token.Jwt.SecretFetcher.SecretFetcherDefaultImpl
def fetch_signing_secret(mod, opts) do
SecretFetcherDefaultImpl.fetch_signing_secret(mod, opts)
end
def fetch_verifying_secret(mod, token_headers, opts) do
SecretFetcherDefaultImpl.fetch_verifying_secret(mod, token_headers, opts)
end
defoverridable fetch_signing_secret: 2, fetch_verifying_secret: 3
end
end
end
defmodule SecretFetcher.SecretFetcherDefaultImpl do
@moduledoc false
use Guardian.Token.Jwt.SecretFetcher
def fetch_signing_secret(mod, opts) do
secret = Keyword.get(opts, :secret)
secret = Config.resolve_value(secret) || apply(mod, :config, [:secret_key])
case secret do
nil -> {:error, :secret_not_found}
val -> {:ok, val}
end
end
def fetch_verifying_secret(mod, _token_headers, opts) do
secret = Keyword.get(opts, :secret)
secret = Config.resolve_value(secret) || mod.config(:secret_key)
case secret do
nil -> {:error, :secret_not_found}
val -> {:ok, val}
end
end
end
@doc """
Inspect the JWT without any validation or signature checking.
Return an map with keys: `headers` and `claims`.
"""
def peek(_mod, nil), do: nil
def peek(_mod, token) do
%{headers: JWT.peek_protected(token).fields, claims: JWT.peek_payload(token).fields}
rescue
ArgumentError -> nil
end
@doc """
Generate unique token id.
"""
def token_id, do: Guardian.UUID.generate()
@doc """
Create a token. Uses the claims, encodes and signs the token.
The signing secret will be found first from the options.
If not specified the secret key from the configuration will be used.
Configuration:
* `secret_key` The secret key to use for signing
Options:
* `secret` The secret key to use for signing
* `headers` The Jose headers that should be used
* `allowed_algos`
The secret may be in the form of any resolved value from `Guardian.Config`.
"""
def create_token(mod, claims, options \\ []) do
with {:ok, secret_fetcher} <- fetch_secret_fetcher(mod),
{:ok, secret} <- secret_fetcher.fetch_signing_secret(mod, options) do
jose_jwk = jose_jwk(secret)
{_, token} =
jose_jwk
|> JWT.sign(jose_jws(mod, jose_jwk, options), claims)
|> JWS.compact()
{:ok, token}
end
end
@doc """
Builds the default claims for all JWT tokens.
Note:
* `aud` is set to the configured `issuer` unless `aud` is set
Options:
Options may override the defaults found in the configuration.
* `token_type` - Override the default token type
* `ttl` - The time to live. See `Guardian.Token.ttl` type
"""
# credo:disable-for-next-line /\.Warning\./
def build_claims(mod, _resource, sub, claims \\ %{}, options \\ []) do
claims =
claims
|> stringify_keys()
|> set_jti()
|> set_iat()
|> set_iss(mod, options)
|> set_aud(mod, options)
|> set_type(mod, options)
|> set_sub(mod, sub, options)
|> set_ttl(mod, options)
|> set_auth_time(mod, options)
{:ok, claims}
end
@doc """
Decodes the token and validates the signature.
Options:
* `secret` - Override the configured secret. `Guardian.Config.config_value` is valid
* `allowed_algos` - A list of allowable algos
"""
def decode_token(mod, token, options \\ []) do
with {:ok, secret_fetcher} <- fetch_secret_fetcher(mod),
%{headers: headers} <- peek(mod, token),
{:ok, raw_secret} <- secret_fetcher.fetch_verifying_secret(mod, headers, options) do
secret = jose_jwk(raw_secret)
algos = fetch_allowed_algos(mod, options)
verify_result = JWT.verify_strict(secret, algos, token)
case verify_result do
{true, jose_jwt, _} -> {:ok, jose_jwt.fields}
{false, _, _} -> {:error, :invalid_token}
end
else
_ -> {:error, :invalid_token}
end
end
@doc """
Verifies the claims.
Options:
* `token_verify_module` - the module to use to verify the claims. Default
`Guardian.Token.Jwt.Verify`
"""
def verify_claims(mod, claims, options) do
result =
mod
|> apply(:config, [:token_verify_module, Verify])
|> apply(:verify_claims, [mod, claims, options])
case result do
{:ok, claims} -> apply(mod, :verify_claims, [claims, options])
err -> err
end
end
@doc """
Revoking a JWT by default does not do anything.
You'll need to track the token in storage in some way
and revoke in your implementation callbacks.
See `GuardianDb` for an example.
"""
def revoke(_mod, claims, _token, _options), do: {:ok, claims}
@doc """
Refresh the token
Options:
* `secret` - Override the configured secret. `Guardian.Config.config_value` is valid
* `allowed_algos` - A list of allowable algos
* `ttl` - The time to live. See `Guardian.Token.ttl` type
"""
def refresh(mod, old_token, options) do
with {:ok, old_claims} <- apply(mod, :decode_and_verify, [old_token, %{}, options]),
{:ok, claims} <- refresh_claims(mod, old_claims, options),
{:ok, token} <- create_token(mod, claims, options) do
{:ok, {old_token, old_claims}, {token, claims}}
else
{:error, _} = err -> err
err -> {:error, err}
end
end
@doc """
Exchange a token of one type to another.
Type is encoded in the `typ` field.
Options:
* `secret` - Override the configured secret. `Guardian.Config.config_value` is valid
* `allowed_algos` - A list of allowable algos
* `ttl` - The time to live. See `Guardian.Token.ttl` type
"""
def exchange(mod, old_token, from_type, to_type, options) do
with {:ok, old_claims} <- apply(mod, :decode_and_verify, [old_token, %{}, options]),
{:ok, claims} <- exchange_claims(mod, old_claims, from_type, to_type, options),
{:ok, token} <- create_token(mod, claims, options) do
{:ok, {old_token, old_claims}, {token, claims}}
else
{:error, _} = err -> err
err -> {:error, err}
end
end
# If the JWK includes a "kid" add this to the signature to provide a hint
# about which key was used.
# See https://tools.ietf.org/html/rfc7515#section-4.1.4
defp jose_jws(mod, %JWK{fields: %{"kid" => kid}}, opts) do
header = %{"kid" => kid}
opts = Keyword.update(opts, :headers, header, &Map.merge(&1, header))
jose_jws(mod, opts)
end
defp jose_jws(mod, _, opts), do: jose_jws(mod, opts)
defp jose_jws(mod, opts) do
algos = fetch_allowed_algos(mod, opts) || @default_algos
headers = Keyword.get(opts, :headers, %{})
Map.merge(%{"alg" => hd(algos)}, headers)
end
defp jose_jwk(%JWK{} = the_secret), do: the_secret
defp jose_jwk(the_secret) when is_binary(the_secret), do: JWK.from_oct(the_secret)
defp jose_jwk(the_secret) when is_map(the_secret), do: JWK.from_map(the_secret)
defp jose_jwk(value), do: Config.resolve_value(value)
defp fetch_allowed_algos(mod, opts) do
opts
|> Keyword.get(:allowed_algos)
|> Config.resolve_value() || apply(mod, :config, [:allowed_algos, @default_algos])
end
defp set_type(%{"typ" => typ} = claims, _mod, _opts) when not is_nil(typ), do: claims
defp set_type(claims, mod, opts) do
defaults = apply(mod, :default_token_type, [])
typ = Keyword.get(opts, :token_type, defaults)
Map.put(claims, @type_key, to_string(typ || @default_token_type))
end
defp set_sub(claims, _mod, subject, _opts), do: Map.put(claims, "sub", subject)
defp set_iat(claims) do
ts = Guardian.timestamp()
claims |> Map.put("iat", ts) |> Map.put("nbf", ts - 1)
end
defp set_ttl(%{"exp" => exp} = claims, _mod, _opts) when not is_nil(exp), do: claims
defp set_ttl(claims, mod, opts) do
ttl = Keyword.get(opts, :ttl)
if ttl do
set_ttl(claims, ttl)
else
token_typ = claims |> Map.fetch!("typ") |> to_string()
token_ttl = apply(mod, :config, [:token_ttl, %{}])
fallback_ttl = apply(mod, :config, [:ttl, @default_ttl])
ttl = Map.get(token_ttl, token_typ, fallback_ttl)
set_ttl(claims, ttl)
end
end
defp set_ttl(the_claims, {num, period}) when is_binary(num),
do: set_ttl(the_claims, {String.to_integer(num), period})
defp set_ttl(the_claims, {num, period}) when is_binary(period),
do: set_ttl(the_claims, {num, String.to_existing_atom(period)})
defp set_ttl(%{"iat" => iat_v} = the_claims, requested_ttl),
do: assign_exp_from_ttl(the_claims, {iat_v, requested_ttl})
# catch all for when the issued at iat is not yet set
defp set_ttl(claims, requested_ttl), do: claims |> set_iat() |> set_ttl(requested_ttl)
defp set_auth_time(%{"auth_time" => auth_time} = claims, _mod, _opts) when not is_nil(auth_time), do: claims
defp set_auth_time(%{"iat" => iat} = claims, mod, opts) do
set_auth_time =
if opts[:auth_time] !== nil do
opts[:auth_time]
else
case mod.config(:auth_time) do
nil -> opts[:max_age] || mod.config(:max_age)
auth_time -> auth_time
end
end
if set_auth_time do
Map.put(claims, "auth_time", iat)
else
claims
end
end
defp assign_exp_from_ttl(the_claims, {iat_v, ttl}),
do: Map.put(the_claims, "exp", iat_v + ttl_to_seconds(ttl))
defp set_iss(claims, mod, _opts) do
issuer = mod |> apply(:config, [:issuer]) |> to_string()
Map.put(claims, "iss", issuer)
end
defp set_aud(%{"aud" => aud} = claims, _mod, _opts) when not is_nil(aud), do: claims
defp set_aud(claims, mod, _opts) do
issuer = mod |> apply(:config, [:issuer]) |> to_string()
Map.put(claims, "aud", issuer)
end
defp set_jti(claims), do: Map.put(claims, "jti", token_id())
defp refresh_claims(mod, claims, options), do: {:ok, reset_claims(mod, claims, options)}
defp exchange_claims(mod, old_claims, from_type, to_type, options) when is_list(from_type) do
from_type = Enum.map(from_type, &to_string(&1))
if Enum.member?(from_type, old_claims["typ"]) do
exchange_claims(mod, old_claims, old_claims["typ"], to_type, options)
else
{:error, :incorrect_token_type}
end
end
defp exchange_claims(mod, old_claims, from_type, to_type, options) do
if old_claims["typ"] == to_string(from_type) do
new_type = to_string(to_type)
# set the type first because the ttl can depend on the type
claims = Map.put(old_claims, "typ", new_type)
claims = reset_claims(mod, claims, options)
{:ok, claims}
else
{:error, :incorrect_token_type}
end
end
defp reset_claims(mod, claims, options) do
claims
|> Map.drop(["jti", "iss", "iat", "nbf", "exp"])
|> set_jti()
|> set_iat()
|> set_iss(mod, options)
|> set_ttl(mod, options)
end
defp fetch_secret_fetcher(mod) do
{:ok, mod.config(:secret_fetcher, SecretFetcherDefaultImpl)}
end
end