[KongchangAI]
· 2 min read· 1,293 words

The Complete Guide to Handling JWT in Elixir: Joken, JOSE, and Production Best Practices

The Complete Guide to Handling JWT in Elixir: Joken, JOSE, and Production Best Practices

A complete guide to building production-grade JWT authentication in Elixir with Joken, JOSE, and OTP.

This article guides Elixir developers through the full JWT authentication stack — from token signing with Joken and JOSE, to building JWKS public key caches using OTP GenServers or ETS for third-party IdP integration, to centralizing auth logic in Phoenix Plug pipelines. It also covers key production practices: preferring RS256/ES256, strictly validating standard claims, injecting keys via secrets management, and using short-lived access tokens with refresh tokens to reduce exposure.

In modern web applications and microservice architectures, JWT (JSON Web Token) has become the de facto standard for authentication and authorization. For developers building high-concurrency systems with Elixir, correctly issuing, verifying, and validating JWTs is critical to both security and stability. This article walks through the core toolchain in the Elixir ecosystem, covering the complete path from basic signing to production best practices.

How to handle JWT in Elixir

The Role of JWT in the Elixir Ecosystem

The core value of JWT lies in stateless authentication — the server doesn't need to store session information and can confirm a token's authenticity and integrity through signature verification alone. A standard JWT consists of three parts: Header, Payload, and Signature, separated by dots and Base64URL-encoded.

Elixir runs on the BEAM virtual machine, which is naturally suited for handling large numbers of concurrent connections. In this environment, JWT's stateless nature pairs perfectly with Elixir's lightweight process model, enabling support for large-scale authentication requests. To achieve this in Elixir, two key libraries are indispensable: Joken and JOSE.

Joken and JOSE: The Core Toolchain

When working with JWTs in Elixir, Joken is the most popular choice. It provides a clean DSL for defining token configurations, encapsulates common logic for signing, verification, and claims validation, and lets developers describe token requirements in a declarative style.

Joken depends on JOSE (JSON Object Signing and Encryption) under the hood — a lower-level library that directly implements the JWS, JWE, and JWK specifications from the JOSE family of standards. When you need fine-grained control over signing algorithms (such as HS256, RS256, or ES256) or need to handle key format conversions, JOSE provides the necessary capabilities.

The Basic Sign-and-Verify Flow

A typical workflow has two steps: during issuance, claims are signed with a private key or symmetric key to generate a token; during verification, the corresponding public key or secret is used to validate the signature, followed by checking the validity of claims — such as expiration time (exp), issuer (iss), and audience (aud). Joken lets you define both generators and validators for each claim, decoupling business rules from token validation logic.

The JOSE family of specifications is defined across several RFCs: JWS (JSON Web Signature, RFC 7515) specifies how to digitally sign content; JWE (JSON Web Encryption, RFC 7516) specifies how to encrypt content; JWK (JSON Web Key, RFC 7517) defines a standard representation format for cryptographic keys; and JWT itself (RFC 7519) builds on top of JWS or JWE and defines the semantics of standard claims in the Payload. In practice, the vast majority of use cases rely on JWS mode — where token content is visible in plaintext but protected by a signature — rather than JWE's encrypted mode. This distinction matters: a JWT's Payload is only Base64URL-encoded by default, meaning anyone who holds the token can decode and read the claims. Sensitive data (such as passwords or payment information) should never be placed in the Payload; security comes from the signature, not from confidentiality.

JWKS and OTP-Based Caching

When an application needs to integrate with a third-party identity provider (such as Auth0, Okta, or any OIDC service), the public keys needed to verify tokens are typically fetched dynamically from a JWKS (JSON Web Key Set) endpoint. A JWKS is a collection of public keys published in a standard format; the server needs to match the appropriate key based on the kid (Key ID) in the token header.

Fetching the JWKS endpoint on every verification is clearly unacceptable — it adds latency and puts pressure on upstream services. This is where OTP (Open Telecom Platform) shines. Using a GenServer or an ETS table, the fetched key set can be cached in memory with a sensible refresh interval. When key rotation occurs, the caching layer can re-fetch on demand, balancing performance and security.

This OTP-based caching design is a classic Elixir pattern for distributed authentication: a long-running supervised process manages the lifecycle of keys, while business processes simply read from the cache quickly without worrying about the underlying network interactions.

Key Rotation refers to the practice of identity providers periodically replacing their signing private keys in order to limit the blast radius of a single key compromise. When rotation occurs, the JWKS endpoint publishes both the old and new public keys simultaneously (distinguished by different kid values), allowing verifiers to still validate existing tokens signed with the old key during the transition period. A common robustness technique in OTP cache implementations is: when a kid carried by a token is not found in the cache, trigger a forced refresh rather than immediately rejecting the request — this handles the race window where key rotation has just completed but the cache hasn't been updated yet. Combining this "lazy refresh" strategy with periodic proactive refreshes covers most rotation scenarios without introducing complex distributed coordination.

Plug Integration in Phoenix

In the Phoenix framework, authentication logic is typically injected into the request pipeline as a Plug. By writing a custom Plug, you can uniformly handle token extraction, verification, and user identity injection before a request reaches the controller.

A typical authentication Plug performs the following actions: extracts the Bearer token from the Authorization request header, calls Joken to verify the signature and validate claims, halts the pipeline and returns a 401 on failure, and writes the parsed user information into conn.assigns on success. This design centralizes authentication concerns at the pipeline layer and keeps controller code clean.

Combined with the JWKS cache described earlier, verification at the Plug layer can be performed with near-zero network overhead, maintaining stable response times even under high QPS.

A Plug is the foundational abstraction for request processing pipelines in Phoenix (and the underlying Cowboy/Bandit servers). It is essentially a function or module that receives a %Plug.Conn{} struct and returns a struct of the same type. Phoenix Router allows multiple Plugs to be composed into Pipelines, and different route groups can be mounted to different Pipelines — enabling fine-grained authentication policies, such as skipping JWT validation for public API routes while enforcing valid tokens on protected routes. conn.assigns is the standard mechanism for passing data across Plugs and Controllers within a single request lifecycle: an authentication Plug writes the parsed user ID or permission set into assigns, and downstream Controllers read it directly without re-parsing the token — both efficient and avoiding scattered logic.

Production Best Practices

When deploying JWT authentication to production, there are several critical points to keep in mind:

Prefer asymmetric algorithms. For cross-service or third-party integration scenarios, asymmetric algorithms like RS256 and ES256 are safer than HS256 — the issuer holds the private key while verifiers only need the public key, eliminating the risk of sharing a secret across multiple parties.

Strictly validate standard claims. Don't just verify the signature while ignoring exp, nbf, iss, aud, and other claims. A valid signature doesn't mean a token should be accepted — expired tokens or those with a mismatched audience must be rejected.

Manage keys and rotations carefully. Keys should be injected via environment variables or a dedicated secrets management service — never hardcoded. Key rotation must be supported, paired with JWKS caching to enable smooth transitions.

Control token lifetimes. Access tokens should have short expiration periods; long-lived sessions should be maintained via a refresh token mechanism, minimizing the exposure window if a token is compromised.

Summary

Elixir's ability to handle JWT is built on the ergonomic abstractions of Joken and the low-level implementation of JOSE, with OTP's process model solving distributed challenges like JWKS caching, all elegantly integrated into the request pipeline through Phoenix's Plug mechanism. Mastering this toolchain along with the accompanying production practices enables developers to build authentication systems that are both secure and high-performance.

#Elixir JWT#Joken#JOSE#Phoenix authentication#JWKS caching#OTP GenServer#JWT verification#RS256 Elixir#Phoenix Plug authentication#Elixir security
Share:

Related articles