Packages
hibana_plugins
0.1.0
35 built-in plugins for the Hibana web framework: JWT, OAuth, CORS, rate limiting, GraphQL, admin dashboard, i18n, and more.
Current section
Files
Jump to
Current section
Files
lib/hibana/plugins/jwt.ex
defmodule Hibana.Plugins.JWT do
@moduledoc """
JWT (JSON Web Token) authentication plugin.
## Features
- Token verification with configurable algorithms
- Automatic token expiration checking
- Claims extraction and assignment to conn
- Token generation utilities
## Usage
# Basic usage
plug Hibana.Plugins.JWT, secret: "my_secret_key"
# With custom options
plug Hibana.Plugins.JWT,
secret: "my_secret_key",
algorithm: :HS256,
header_name: "authorization",
scheme: "Bearer"
## Options
- `:secret` - Secret key for token verification (required)
- `:algorithm` - JWT algorithm (`:HS256`, `:HS384`, `:HS512`) (default: `:HS256`)
- `:claims` - Expected claims configuration (default: includes exp/iat)
- `:header_name` - Header to extract token from (default: `"authorization"`)
- `:scheme` - Auth scheme (default: `"Bearer"`)
## Conn Assignments
After successful verification, these assignments are made:
- `conn.assigns.jwt_claims` - Full JWT claims map
- `conn.assigns.current_user` - Value of "sub" claim
## Module Functions
### sign/2-3
Generate a new JWT token:
claims = %{"sub" => "user123", "name" => "John"}
token = Hibana.Plugins.JWT.sign(claims, "secret", exp: 3600)
### verify/2-3
Verify and decode a token:
{:ok, claims} = Hibana.Plugins.JWT.verify(token, "secret")
### decode/1
Decode without verification (for debugging):
{:ok, claims} = Hibana.Plugins.JWT.decode(token)
"""
use Hibana.Plugin
import Plug.Conn
@impl true
def init(opts) do
secret = Keyword.get(opts, :secret)
unless secret do
raise ArgumentError, "Hibana.Plugins.JWT requires a :secret option"
end
%{
secret: secret,
algorithm: Keyword.get(opts, :algorithm, :HS256),
claims: Keyword.get(opts, :claims, %{"exp" => true, "iat" => true}),
header_name: Keyword.get(opts, :header_name, "authorization"),
scheme: Keyword.get(opts, :scheme, "Bearer")
}
end
@impl true
def call(conn, %{secret: secret, algorithm: algorithm, header_name: header_name, scheme: scheme}) do
case get_req_header(conn, header_name) do
[token | _] ->
case extract_token(token, scheme) do
nil ->
unauthorized(conn)
token_value ->
case verify_token(token_value, secret, algorithm) do
{:ok, claims} ->
conn
|> assign(:jwt_claims, claims)
|> assign(:current_user, claims["sub"])
{:error, _reason} ->
unauthorized(conn)
end
end
_ ->
unauthorized(conn)
end
end
defp extract_token(header, scheme) do
case String.split(header, " ", parts: 2) do
[^scheme, token] -> token
_ -> nil
end
end
defp verify_token(token, secret, algorithm) do
try do
alg_str = algorithm_to_string(algorithm)
jwk = JOSE.JWK.from_oct(secret)
case JOSE.JWS.verify_strict(jwk, [alg_str], token) do
{true, payload, _jws} ->
claims = Jason.decode!(payload)
case claims do
%{"exp" => exp} when is_number(exp) ->
now = DateTime.to_unix(DateTime.utc_now())
if now > exp do
{:error, :token_expired}
else
{:ok, claims}
end
_ ->
{:ok, claims}
end
_ ->
{:error, :invalid_signature}
end
rescue
_ -> {:error, :invalid_token}
end
end
defp unauthorized(conn) do
conn
|> put_resp_content_type("application/json")
|> send_resp(
401,
Jason.encode!(%{error: "Unauthorized", message: "Invalid or missing token"})
)
|> halt()
end
@doc """
Generate a new JWT token.
"""
def sign(claims, secret, opts \\ []) do
algorithm = Keyword.get(opts, :algorithm, :HS256)
claims = add_expiry(claims, Keyword.get(opts, :exp, 3600))
jws = %{"alg" => algorithm_to_string(algorithm)}
jwk = JOSE.JWK.from_oct(secret)
jwt = %JOSE.JWT{fields: claims}
{_, signed} = JOSE.JWT.sign(jwk, jws, jwt) |> JOSE.JWS.compact()
signed
end
@doc """
Verify and decode a JWT token.
"""
def verify(token, secret, _opts \\ []) do
verify_token(token, secret, :HS256)
end
@doc """
Extract claims from a token without verification (for debugging).
"""
def decode(token) do
try do
%JOSE.JWT{fields: fields} = JOSE.JWT.peek(token)
{:ok, fields}
rescue
_ -> {:error, :invalid_token}
end
end
defp add_expiry(claims, exp) when is_integer(exp) do
now = DateTime.to_unix(DateTime.utc_now())
Map.put(claims, "exp", now + exp)
|> Map.put("iat", now)
end
defp add_expiry(claims, _), do: claims
defp algorithm_to_string(:HS256), do: "HS256"
defp algorithm_to_string(:HS384), do: "HS384"
defp algorithm_to_string(:HS512), do: "HS512"
end