Packages

JWT authentication library for Reckon microservices ecosystem

Current section

Files

Jump to
reckon_jwt lib reckon_jwt.ex
Raw

lib/reckon_jwt.ex

defmodule ReckonJwt do
@moduledoc """
JWT authentication library for Reckon microservices.
Provides a simple, consistent API for JWT token operations across
all Reckon services including token generation, validation, and
refresh functionality.
## Configuration
config :reckon_jwt, ReckonJwt.Guardian,
issuer: "reckon_identity",
secret_key: "your-secret-key",
ttl: {4, :hours}
## Usage
# Generate session tokens
{:ok, tokens} = ReckonJwt.generate_session_tokens("account_123", "session_456")
# Validate tokens
{:ok, claims} = ReckonJwt.validate_token(token)
# Use in Phoenix pipelines
plug ReckonJwt.Middleware, required_scopes: ["read"]
"""
alias ReckonJwt.Guardian
# =============================================================================
# Token Generation
# =============================================================================
@doc """
Generate session tokens (access + refresh) for authentication.
Returns both access and refresh tokens with session information.
## Examples
iex> {:ok, tokens} = ReckonJwt.generate_session_tokens("acc_123", "sess_456")
iex> tokens.account_id
"acc_123"
iex> tokens.session_id
"sess_456"
"""
def generate_session_tokens(account_id, session_id, device_info \\ %{}) do
with {:ok, access_token, access_claims} <-
Guardian.generate_session_token(account_id, session_id, device_info),
{:ok, refresh_token, _refresh_claims} <-
Guardian.generate_refresh_token(account_id, session_id) do
{:ok,
%{
access_token: access_token,
refresh_token: refresh_token,
expires_at: access_claims["exp"],
token_type: "Bearer",
account_id: account_id,
session_id: session_id
}}
else
{:error, reason} -> {:error, reason}
end
end
@doc """
Generate a simple access token without session context.
Useful for service-to-service authentication.
"""
def generate_access_token(account_id, custom_claims \\ %{}) do
case Guardian.generate_token(account_id, custom_claims) do
{:ok, token, claims} ->
{:ok,
%{
access_token: token,
expires_at: claims["exp"],
token_type: "Bearer",
account_id: account_id
}}
{:error, reason} ->
{:error, reason}
end
end
# =============================================================================
# Token Validation
# =============================================================================
@doc """
Validate a JWT token and extract authentication information.
Returns account and session information from the token.
## Examples
iex> {:ok, tokens} = ReckonJwt.generate_session_tokens("acc_123", "sess_456")
iex> {:ok, result} = ReckonJwt.validate_token(tokens.access_token)
iex> result.account_id
"acc_123"
"""
def validate_token(token) when is_binary(token) do
case Guardian.decode_and_verify(token) do
{:ok, claims} ->
account_id = claims["sub"]
session_id = claims["session_id"]
if account_id do
result = %{
account_id: account_id,
claims: claims,
token_type: Guardian.token_type(claims),
expires_at: claims["exp"]
}
# Add session info if available
result = if session_id, do: Map.put(result, :session_id, session_id), else: result
{:ok, result}
else
{:error, :invalid_token}
end
{:error, reason} ->
{:error, reason}
end
end
def validate_token(_), do: {:error, :invalid_token_format}
@doc """
Validate a session token specifically (requires session_id in claims).
"""
def validate_session_token(token) when is_binary(token) do
case Guardian.validate_session_token(token) do
{:ok, account_id, session_id, claims} ->
device_info = extract_device_info(claims)
{:ok,
%{
account_id: account_id,
session_id: session_id,
device_info: device_info,
claims: claims,
token_type: Guardian.token_type(claims),
expires_at: claims["exp"]
}}
{:error, reason} ->
{:error, reason}
end
end
def validate_session_token(_), do: {:error, :invalid_token_format}
# =============================================================================
# Token Refresh
# =============================================================================
@doc """
Refresh tokens using a valid refresh token.
Generates new access token while maintaining session context.
"""
def refresh_session_tokens(refresh_token) when is_binary(refresh_token) do
with {:ok, claims} <- Guardian.decode_and_verify(refresh_token),
"refresh" <- Guardian.token_type(claims),
false <- Guardian.token_expired?(claims),
account_id when is_binary(account_id) <- claims["sub"],
session_id when is_binary(session_id) <- claims["session_id"] do
# Generate new access token
device_info = extract_device_info(claims)
case Guardian.generate_session_token(account_id, session_id, device_info) do
{:ok, new_access_token, new_claims} ->
{:ok,
%{
access_token: new_access_token,
expires_at: new_claims["exp"],
token_type: "Bearer",
account_id: account_id,
session_id: session_id
}}
{:error, reason} ->
{:error, reason}
end
else
{:error, reason} -> {:error, reason}
true -> {:error, :refresh_token_expired}
nil -> {:error, :invalid_refresh_token}
_ -> {:error, :invalid_refresh_token}
end
end
def refresh_session_tokens(_), do: {:error, :invalid_token_format}
# =============================================================================
# Utility Functions
# =============================================================================
@doc """
Check if a token is expired.
"""
def token_expired?(token) when is_binary(token) do
case Guardian.decode_and_verify(token) do
{:ok, claims} -> Guardian.token_expired?(claims)
{:error, _} -> true
end
end
def token_expired?(_), do: true
@doc """
Extract account ID from token (for debugging/logging).
"""
def extract_account_id(token) when is_binary(token) do
case Guardian.decode_and_verify(token) do
{:ok, claims} -> claims["sub"]
{:error, _} -> nil
end
end
def extract_account_id(_), do: nil
@doc """
Get configuration value with fallback.
"""
def config(key, default \\ nil) do
Application.get_env(:reckon_jwt, key, default)
end
# =============================================================================
# Private Helper Functions
# =============================================================================
defp extract_device_info(claims) when is_map(claims) do
%{
fingerprint: claims["device_fingerprint"],
type: claims["device_type"],
ip_address: claims["ip_address"],
user_agent: claims["user_agent"]
}
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|> Map.new()
end
defp extract_device_info(_), do: %{}
end