tutorial

How to Monitor Joken with Vigilmon

Joken handles JWT generation and validation in Elixir — but expired signing keys, clock skew, and algorithm mismatches cause silent auth failures that never show up in uptime dashboards. Here's how to monitor your Joken-based authentication with Vigilmon.

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

  1. Log in to vigilmon.online and click Add Monitor.
  2. Set Type to HTTP / HTTPS.
  3. Enter your endpoint: https://api.yourapp.com/health/auth/joken.
  4. Set Check interval to 1 minute.
  5. Set Expected HTTP status to 200.
  6. Under Advanced, add a Response body contains check: "status":"ok".
  7. 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:

  1. Click Add MonitorHTTP / HTTPS.
  2. Enter https://api.yourapp.com/.well-known/jwks.json.
  3. Add a Response body contains check: "keys".
  4. 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:

  1. Go to Alert ChannelsAdd Channel.
  2. Add a Slack webhook or configure Email / PagerDuty.
  3. Set Alert after 2 consecutive failures on the Joken health monitor.
  4. 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.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →