Current section

Files

Jump to
joken lib joken.ex
Raw

lib/joken.ex

defmodule Joken do
alias :jsx, as: JSON
alias Joken.Utils
alias Joken.Claims
@type alg :: :HS256 | :HS384 | :HS512
@type status :: :ok | :error
@supported_algs %{ HS256: :sha256 , HS384: :sha384, HS512: :sha512 }
@moduledoc """
Encodes and decodes JSON Web Tokens.
iex(1)> Joken.encode(%{username: "johndoe"}, "secret")
{:ok,
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImpvaG5kb2UifQ.OFY_3SbHl2YaM7Y4Lj24eVMtcDaGEZU7KRzYCV4cqog"}
iex(2)> Joken.decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImpvaG5kb2UifQ.OFY_3SbHl2YaM7Y4Lj24eVMtcDaGEZU7KRzYCV4cqog", "secret")
{:ok, %{username: "johndoe"}}
iex(3)> Joken.encode(%{username: "johndoe"}, "secret", :HS384, %{ iss: "self"}) {:ok,
"eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzZWxmIiwidXNlcm5hbWUiOiJqb2huZG9lIn0.wG_LAQ7Z3uRl7B0TEuxvfHdqikU3boPorm5ldS6dutJ9r076i-LRCuascaxoNDw1"}
iex(4)> Joken.decode("eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzZWxmIiwidXNlcm5hbWUiOiJqb2huZG9lIn0.wG_LAQ7Z3uRl7B0TEuxvfHdqikU3boPorm5ldS6dutJ9r076i-LRCuascaxoNDw1", "secret", %{ iss: "not:self"})
{:error, "Invalid issuer"}
"""
@doc """
Encodes the payload into a JSON Web Token using the specified key and alg.
Adds the specified claims to the payload as well
alg must be either :HS256, :HS384, or :HS512
default alg is :HS256
default claims is an empty map
"""
@spec encode(map, binary, alg, map) :: { status, binary }
def encode(payload, key, alg \\ :HS256, claims \\ %{}) do
cond do
!Map.has_key?(@supported_algs, alg) ->
{:error, "Unsupported algorithm"}
true ->
headerJSON = JSON.encode(%{ alg: to_string(alg), "typ": "JWT"})
{status, payloadJSON} = try do {:ok, Map.merge(payload, claims) |> JSON.encode} rescue _ -> {:error, nil} end
case status do
:error ->
{:error, "Error encoding Map to JSON"}
:ok ->
header64 = Utils.base64url_encode(headerJSON)
payload64 = Utils.base64url_encode(payloadJSON)
signature = :crypto.hmac(@supported_algs[alg], key, "#{header64}.#{payload64}")
signature64 = Utils.base64url_encode(signature)
{:ok, "#{header64}.#{payload64}.#{signature64}"}
end
end
end
@doc """
Decodes the given jwt using the given key.
Also checks against aud, iss, and sub if given in the claims map.
claims defaults to an empty map.
"""
@spec decode(binary, binary, map) :: { status, map | binary }
def decode(jwt, key, claims \\ %{}) when
is_nil(jwt) == false and
byte_size(jwt) > 0 and
is_nil(key) == false and
byte_size(key) > 0 and
is_map(claims)
do
jwt
|> Utils.get_data
|> Claims.check_signature(@supported_algs, key)
|> Claims.check_exp
|> Claims.check_nbf
|> Claims.check_aud(Map.get(claims, :aud, nil))
|> Claims.check_iss(Map.get(claims, :iss, nil))
|> Claims.check_sub(Map.get(claims, :sub, nil))
|> Utils.to_map
end
end