Current section
Files
Jump to
Current section
Files
lib/guardian/token/jwe.ex
defmodule Guardian.Token.Jwe do
@moduledoc """
Deals with all things JWE.
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.
The usage is exactly the same as JWTs, but JWE encryption is more strict about secret key sizes.
The `secret_key` bit sizes must be exact for `AnnnGCMKW` algorithms.
Specifically sized secrets can be generated by running `mix guardian.gen.secret <byte_size>`.
For secrets < 32 bytes, generate the secret and trim the result.
A128GCMKW - 128 bits/16 bytes
A192GCMKW - 192 bits/24 bytes
A256GCMKW - 256 bits/32 bytes
A512GCMKW - 512 bits/64 bytes
Supported algorithms:
A128GCMKW
A192GCMKW
A256GCMKW
A512GCMKW
PBES2-HS256+A128KW
PBES2-HS384+A192KW
PBES2-HS512+A256KW
"""
alias Guardian.{Config, Token.Jwt.Verify}
alias JOSE.{JWE, JWK}
import Guardian, only: [stringify_keys: 1]
@default_algos ["A256GCMKW"]
@default_token_type "access"
@type_key "typ"
@default_ttl {12, :hours}
@alg_to_enc %{
"A128GCMKW" => "A128GCM",
"A192GCMKW" => "A192GCM",
"A256GCMKW" => "A256GCM",
"PBES2-HS256+A128KW" => "A128GCM",
"PBES2-HS384+A192KW" => "A192GCM",
"PBES2-HS512+A256KW" => "A256GCM"
}
@doc """
Peek is not implemented for JWE as the claims are encrypted.
"""
def peek(_mod, _token) do
raise "Peek cannot be used with encrypted tokens"
end
@doc """
Generate unique token id
"""
def token_id, do: UUID.uuid4()
@doc """
Create a token. Uses the claims and encrypts 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`
`claims` must be a JSON-serializable structure.
"""
def create_token(mod, claims, opts) do
secret = fetch_secret(mod, opts)
jwk = jose_jwk(secret)
jws = jose_jws(mod, opts)
{_, token} =
jwk
|> JWE.block_encrypt(Poison.encode!(claims), jws)
|> JWE.compact()
{:ok, token}
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
* `token_ttl` - The time to live. See `Guardian.Token.ttl` type
"""
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, opts \\ []) do
jwk =
mod
|> fetch_secret(opts)
|> jose_jwk()
decrypted =
jwk
|> JWE.block_decrypt(token)
|> elem(0)
|> Poison.decode!()
{:ok, decrypted}
rescue
_ -> {:error, :invalid_token}
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, _opts), 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
* `token_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
* `token_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, %{})
alg = hd(algos)
enc = Map.get(@alg_to_enc, alg)
Map.merge(%{"alg" => alg, "enc" => enc}, 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 fetch_secret(mod, opts) do
secret = Keyword.get(opts, :secret)
secret = Config.resolve_value(secret) || apply(mod, :config, [:secret_key])
case secret do
nil -> raise "No secret key configured for JWE"
val -> val
end
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
end