How to Monitor Joken with Vigilmon
Joken is the flexible JWT library for Elixir. It supports HS256/384/512, RS256/384/512, ES256, and Ed25519 signing algorithms, custom claim validators, and a clean composable API for generating and verifying tokens. Phoenix APIs, microservices, and inter-service communication all depend on Joken for authentication — which means a signing key rotation, a clock skew between services, or an algorithm mismatch silently breaks auth for every user while your HTTP health check still returns 200.
This tutorial wires up monitoring for a Joken-backed authentication layer:
- A token round-trip health check (generate → verify → validate claims)
- HTTP endpoint monitoring with Vigilmon
- Heartbeat monitoring for JWKS endpoint availability
- Signing key expiry alerting via a scheduled probe
- Slack notifications on auth failures
Why Monitor Joken?
| Signal | What it catches |
|---|---|
| Token generation | Signing key unavailable or misconfigured |
| Token verification | Algorithm mismatch, key rotation breakage |
| Claims validation | Expired exp, wrong iss/aud, clock skew |
| JWKS endpoint | Public key distribution failures for RS/ES keys |
| Signing key expiry | Key pair that will expire before the next rotation |
Auth failures typically present as HTTP 401s, not 5xxs — standard uptime monitors miss them entirely.
Step 1: Write a Joken Health Module
Create a module that exercises the full token lifecycle: generate a token with claims, verify the signature, and validate the standard claims.
# lib/my_app/auth/joken_health.ex
defmodule MyApp.Auth.JokenHealth do
use Joken.Config
@impl Joken.Config
def token_config do
default_claims(skip: [:nbf])
|> add_claim("iss", fn -> "my_app_health_check" end, &(&1 == "my_app_health_check"))
|> add_claim("aud", fn -> "health" end, &(&1 == "health"))
end
def check do
with {:ok, token, claims} <- generate_and_sign(%{}),
{:ok, verified_claims} <- verify_and_validate(token),
true <- Map.has_key?(verified_claims, "iss") do
{:ok, %{token_length: byte_size(token), iss: verified_claims["iss"]}}
else
{:error, reason} -> {:error, reason}
false -> {:error, :missing_claims}
end
end
end
Make sure your Joken signer is configured:
# config/runtime.exs
config :my_app, MyApp.Auth.JokenHealth,
default_signer: [
signer_alg: "HS256",
key_octet: System.fetch_env!("JWT_SECRET")
]
Test it in IEx:
iex> MyApp.Auth.JokenHealth.check()
{:ok, %{token_length: 147, iss: "my_app_health_check"}}
Step 2: Expose an HTTP Health Endpoint
Wrap the Joken health check in a Phoenix controller so Vigilmon has a plain HTTP target:
# lib/my_app_web/controllers/auth_health_controller.ex
defmodule MyAppWeb.AuthHealthController do
use MyAppWeb, :controller
def joken(conn, _params) do
case MyApp.Auth.JokenHealth.check() do
{:ok, info} ->
json(conn, Map.merge(info, %{status: "ok", algorithm: "HS256"}))
{:error, reason} ->
conn
|> put_status(503)
|> json(%{status: "error", reason: format_reason(reason)})
end
end
defp format_reason(reason) when is_atom(reason), do: Atom.to_string(reason)
defp format_reason(reason), do: inspect(reason)
end
# In your router — outside authentication pipelines
scope "/health", MyAppWeb do
pipe_through :api
get "/auth/joken", AuthHealthController, :joken
end
GET /health/auth/joken returns 200 with {"status":"ok"} when token generation and verification work, 503 on any failure.
Step 3: Add Vigilmon HTTP Monitoring
- Log in to vigilmon.online and click Add Monitor.
- Set Type to
HTTP / HTTPS. - Enter your endpoint:
https://api.yourapp.com/health/auth/joken. - Set Check interval to
1 minute. - Set Expected HTTP status to
200. - Under Advanced, add a Response body contains check:
"status":"ok". - Click Save.
Vigilmon will alert you within one minute if your JWT signing or verification breaks.
Step 4: Monitor JWKS Endpoint Availability
If you use RS256 or ES256, your public JWKS endpoint must stay available for token consumers. Monitor it with a dedicated Vigilmon check and a heartbeat probe:
# lib/my_app/auth/jwks_heartbeat.ex
defmodule MyApp.Auth.JwksHeartbeat do
use GenServer
require Logger
@interval :timer.minutes(5)
@jwks_url "https://api.yourapp.com/.well-known/jwks.json"
@vigilmon_heartbeat_url "https://vigilmon.online/api/heartbeat/YOUR_HEARTBEAT_ID"
def start_link(_), do: GenServer.start_link(__MODULE__, nil, name: __MODULE__)
def init(_), do: schedule() |> then(fn _ -> {:ok, nil} end)
def handle_info(:ping, state) do
case check_jwks() do
:ok ->
HTTPoison.get!(@vigilmon_heartbeat_url)
Logger.info("JWKS heartbeat: ok")
{:error, reason} ->
Logger.error("JWKS unavailable, heartbeat skipped: #{inspect(reason)}")
end
schedule()
{:noreply, state}
end
defp check_jwks do
case HTTPoison.get(@jwks_url) do
{:ok, %{status_code: 200, body: body}} ->
case Jason.decode(body) do
{:ok, %{"keys" => [_ | _]}} -> :ok
{:ok, %{"keys" => []}} -> {:error, :empty_keyset}
_ -> {:error, :invalid_jwks_format}
end
{:ok, %{status_code: code}} ->
{:error, {:http_error, code}}
{:error, reason} ->
{:error, reason}
end
end
defp schedule, do: Process.send_after(self(), :ping, @interval)
end
For the JWKS HTTP monitor in Vigilmon:
- Click Add Monitor → HTTP / HTTPS.
- Enter
https://api.yourapp.com/.well-known/jwks.json. - Add a Response body contains check:
"keys". - Set interval to
5 minutes.
Step 5: Alert on Signing Key Expiry
RSA and EC key pairs have a finite lifetime. Add a probe that reads the key's not-after field and alerts when it's within 30 days of expiry:
# lib/my_app/auth/key_expiry_checker.ex
defmodule MyApp.Auth.KeyExpiryChecker do
use GenServer
require Logger
@check_interval :timer.hours(24)
@expiry_warning_days 30
def start_link(_), do: GenServer.start_link(__MODULE__, nil, name: __MODULE__)
def init(_) do
check_expiry()
schedule()
{:ok, nil}
end
def handle_info(:check, state) do
check_expiry()
schedule()
{:noreply, state}
end
defp check_expiry do
pem = File.read!(Application.fetch_env!(:my_app, :jwt_private_key_path))
with [{:Certificate, der, _}] <- :public_key.pem_decode(pem),
cert <- :public_key.pkix_decode_cert(der, :otp),
{:Validity, _not_before, not_after} <- cert |> elem(1) |> elem(4) do
expiry_date = not_after |> asn1_time_to_date()
days_left = Date.diff(expiry_date, Date.utc_today())
if days_left <= @expiry_warning_days do
Logger.error(
"[Joken] Signing key expires in #{days_left} days (#{Date.to_iso8601(expiry_date)}). " <>
"Rotate immediately to avoid auth failures."
)
end
end
end
defp asn1_time_to_date({:utcTime, time}) do
<<y1, y2, m1, m2, d1, d2, _rest::binary>> = to_string(time)
year = 2000 + String.to_integer(<<y1, y2>>)
{:ok, date} = Date.new(year, String.to_integer(<<m1, m2>>), String.to_integer(<<d1, d2>>))
date
end
defp schedule, do: Process.send_after(self(), :check, @check_interval)
end
Step 6: Set Up Alerts
In Vigilmon, configure alert channels for your auth monitors:
- Go to Alert Channels → Add Channel.
- Add a Slack webhook or configure Email / PagerDuty.
- Set Alert after 2 consecutive failures on the Joken health monitor.
- Set Alert after 1 failure on the JWKS endpoint — key distribution failures are always urgent.
For teams with on-call rotations, route Joken failures to PagerDuty — a broken JWT layer means all authenticated users are locked out.
Key Metrics to Watch
| Metric | Vigilmon feature | Alert threshold |
|---|---|---|
| Token generation/verification | HTTP monitor on /health/auth/joken | Any non-200 |
| JWKS endpoint | HTTP monitor on /.well-known/jwks.json | Any non-200 |
| JWKS content | Body contains "keys" | Missing key |
| JWKS probe (includes format check) | Heartbeat monitor | Missing for > 10 min |
| SSL certificate on auth domain | Cert expiry monitor | Expires in < 14 days |
Conclusion
JWT authentication with Joken is invisible when it works and catastrophic when it breaks — every authenticated request fails simultaneously. Layering an HTTP health check that runs the full token lifecycle with a JWKS heartbeat and a signing key expiry probe gives you early warning before a routine key rotation or clock drift takes down your entire authenticated API.
Get started free at vigilmon.online.