Packages
guardian
1.1.1
2.4.0
2.3.2
2.3.1
2.3.0
2.2.4
2.2.3
2.2.2
2.2.1
2.2.0
2.1.2
2.1.1
2.0.0
1.2.1
1.2.0
retired
1.1.1
1.1.0
1.0.1
1.0.0
1.0.0-beta.1
1.0.0-beta.0
0.14.6
0.14.5
0.14.4
0.14.3
retired
0.14.2
0.14.1
0.14.0
0.13.0
0.12.0
0.11.1
0.10.1
0.10.0
0.9.1
0.9.0
0.8.1
0.8.0
0.7.4
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.0
0.4.1
0.4.0
0.3.1
0.3.0
0.2.0
0.1.1
0.1.0
Elixir Authentication framework
Current section
Files
Jump to
Current section
Files
lib/guardian/token/jwt.ex
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 it's 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
* `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`
* `token_type` - Override the default token type
* `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.
```
defmodule MyCustomSecretFetcher do
use Guardian.Token.Jwt.SecretFetcher
def fetch_signing_secret(impl_module, opts) do
# fetch the secret for sigining
end
def fetch_verifying_secret(impl_module, token_headers, opts) do
# fetch the secret for verifying the token
end
end
```
"""
@behaviour Guardian.Token
alias Guardian.{Config, Token.Jwt.Verify, Token.Jwt.SecretFetcher.SecretFetcherDefaultImpl}
alias JOSE.{JWT, JWS, JWK}
import Guardian, only: [stringify_keys: 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_siginig_secret fetches the secret to sign.
"""
@callback fetch_signing_secret(module, opts :: Guardian.options()) ::
{:ok, term} | {:error, :secret_not_found}
@doc """
fetch_verifying_secret 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, as: DI
def fetch_signing_secret(mod, opts), do: DI.fetch_signing_secret(mod, opts)
def fetch_verifying_secret(mod, token_headers, opts),
do: DI.fetch_verifying_secret(mod, token_headers, opts)
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}
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
{_, token} =
secret
|> jose_jwk()
|> JWT.sign(jose_jws(mod, 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)
{: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),
secret <- jose_jwk(raw_secret),
algos = fetch_allowed_algos(mod, options) do
verify_result = JWT.verify_strict(secret, algos, token)
case verify_result do
{true, jose_jwt, _} -> {:ok, jose_jwt.fields}
{false, _, _} -> {:error, :invalid_token}
end
end
end
@doc """
Verifies the claims.
Configuration:
* `token_verify_module` Default `Guardian.Token.Jwt.Verify` the module to use to verify the claims
"""
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
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(%{"typ" => token_typ} = claims, mod, opts) do
ttl = Keyword.get(opts, :ttl)
if ttl do
set_ttl(claims, ttl)
else
token_typ = to_string(token_typ)
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 assign_exp_from_ttl(the_claims, {iat_v, {seconds, unit}}) when unit in [:second, :seconds],
do: Map.put(the_claims, "exp", iat_v + seconds)
defp assign_exp_from_ttl(the_claims, {iat_v, {minutes, unit}}) when unit in [:minute, :minutes],
do: Map.put(the_claims, "exp", iat_v + minutes * 60)
defp assign_exp_from_ttl(the_claims, {iat_v, {hours, unit}}) when unit in [:hour, :hours],
do: Map.put(the_claims, "exp", iat_v + hours * 60 * 60)
defp assign_exp_from_ttl(the_claims, {iat_v, {days, unit}}) when unit in [:day, :days],
do: Map.put(the_claims, "exp", iat_v + days * 24 * 60 * 60)
defp assign_exp_from_ttl(the_claims, {iat_v, {weeks, unit}}) when unit in [:week, :weeks],
do: Map.put(the_claims, "exp", iat_v + weeks * 7 * 24 * 60 * 60)
defp assign_exp_from_ttl(_, {_iat_v, {_, units}}), do: raise("Unknown Units: #{units}")
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