Current section
Files
Jump to
Current section
Files
lib/jwt_lite.ex
defmodule JwtLite do
@moduledoc """
Lightweight JWT decoder for Elixir. Decodes and inspects JWTs without
performing signature verification. Useful for inspecting token claims
in development, logging, and debugging.
## Usage
iex> token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
iex> {:ok, %{header: h, payload: p}} = JwtLite.decode(token)
iex> h["alg"]
"HS256"
iex> p["name"]
"John Doe"
## Tip Jar
If this library saves you time, tips are welcome:
ETH/Polygon/Base: `0x7463903430D5294DcfBa09Abb9B8B30A42BdFe36`
"""
@doc """
Decodes a JWT string into its constituent parts.
Returns `{:ok, %{header: map, payload: map, signature: binary}}` on success,
or `{:error, reason}` if the token is malformed.
## Options
* `:validate_exp` - When `true`, returns `{:error, :expired}` if the `exp`
claim is in the past. Defaults to `false`.
## Examples
iex> JwtLite.decode("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.abc")
{:ok, %{header: %{"alg" => "HS256", "typ" => "JWT"}, payload: %{"sub" => "1234567890"}, signature: "abc"}}
"""
@spec decode(String.t(), keyword()) :: {:ok, map()} | {:error, atom()}
def decode(token, opts \\\\ []) when is_binary(token) do
case String.split(token, ".") do
[header_enc, payload_enc, signature] ->
with {:ok, header} <- decode_part(header_enc),
{:ok, payload} <- decode_part(payload_enc) do
result = %{header: header, payload: payload, signature: signature}
if Keyword.get(opts, :validate_exp, false) do
validate_expiry(result)
else
{:ok, result}
end
end
_ ->
{:error, :invalid_format}
end
end
@doc """
Like `decode/2` but raises on error.
"""
@spec decode!(String.t(), keyword()) :: map()
def decode!(token, opts \\\\ []) do
case decode(token, opts) do
{:ok, result} -> result
{:error, reason} -> raise ArgumentError, "Failed to decode JWT: #{reason}"
end
end
@doc """
Returns `true` if the token is expired (has an `exp` claim in the past).
Returns `false` if valid or if there is no `exp` claim.
"""
@spec expired?(String.t()) :: boolean()
def expired?(token) when is_binary(token) do
case decode(token, validate_exp: true) do
{:error, :expired} -> true
_ -> false
end
end
@doc """
Returns the remaining seconds until the token expires, or `nil` if no `exp` claim.
Returns a negative number if already expired.
"""
@spec ttl(String.t()) :: integer() | nil
def ttl(token) when is_binary(token) do
case decode(token) do
{:ok, %{payload: %{"exp" => exp}}} ->
exp - System.system_time(:second)
_ ->
nil
end
end
# Private
defp decode_part(encoded) do
padded = pad_base64(encoded)
case Base.decode64(padded, padding: true) do
{:ok, json} ->
case Jason.decode(json) do
{:ok, map} -> {:ok, map}
_ ->
# Try without Jason (manual JSON would be needed, fallback to raw)
{:ok, %{"_raw" => json}}
end
:error ->
{:error, :invalid_base64}
end
rescue
_ -> {:error, :decode_failed}
end
defp pad_base64(str) do
str = String.replace(str, "-", "+") |> String.replace("_", "/")
case rem(String.length(str), 4) do
0 -> str
2 -> str <> "=="
3 -> str <> "="
_ -> str
end
end
defp validate_expiry({:ok, %{payload: payload} = result}) do
now = System.system_time(:second)
case Map.get(payload, "exp") do
nil -> {:ok, result}
exp when is_integer(exp) and exp < now -> {:error, :expired}
_ -> {:ok, result}
end
end
defp validate_expiry(error), do: error
end