When writing backend services that validate JWT access tokens, you run into a frustrating problem: you need a real identity provider just to test your authentication logic. With Cognito, Auth0, or other OpenID Connect providers, spinning up an authentication service for local development or CI pipelines adds unnecessary complexity. You need valid signatures and correct claims, not the provider itself. That’s where JWKServe comes in.
The Annoying Reality of JWT Validation
When implementing JWT validation in your backend, you’re typically dealing with the full JWKS flow per RFC 7519 and RFC 7517. Your service needs to:
- Verify the token contains two period separators
- Split the token into
Header.Payload.Signaturevalues - Base64 decode each component
- Use
kidin theHeaderobject to identify the needed key - Use
issin thePayloadto fetch the Discovery Endpoint at/.well-known/openid-configuration - Parse the JSON structure and use
jwks_uriproperty as the location for public keys - Fetch the provided JWKS Endpoint (usually at
/.well-known/jwks.json) - Parse the JSON structure and retrieve the key with
kidfrom the JWTHeader
That’s a lot of moving parts, and for integration tests, you don’t need a real identity provider. You need tokens with valid signatures and the correct claims structure. The server itself doesn’t matter—what matters is that your validation logic works correctly with properly formatted tokens.
Flexible Testing Scenarios
JWKServe acts as a token vending machine that signs any payload as a valid JWT access token. This makes it perfect for testing complex scenarios with different custom claims, various expiration times, or edge cases around token validation. You can generate tokens on the fly for any test scenario without worrying about provider configuration or rate limits.
Installation
You can install JWKServe in two ways:
# Install jwkserve binary
cargo install jwkserve
# OR: Download jwkserve container
docker pull sbstjn/jwkserve:latest
Using the CLI: Serve Command
The serve command starts the JWKServe server. By default, it generates a new temporary RSA-2048 key on startup, which takes about 2 seconds. That’s fine for occasional use, but if you’re restarting frequently during development, instant startup helps.
# Use local binary
jwkserve serve
# Use local binary with existing key
jwkserve serve --key key.pem
The output shows what’s happening:
INFO Starting jwkserve
INFO Generating new RSA-2048 key
INFO RSA key size: 2048 bits
INFO Server listening on 0.0.0.0:3000 for issuer http://localhost:3000
INFO Supported algorithms: [RS256]
The server is now ready to generate tokens and serve JWKS endpoints. You can generate a token by posting claims to the /sign endpoint:
curl -X POST http://localhost:3000/sign \
-H "Content-Type: application/json" \
-d '{
"aud": "my-app",
"exp": 1735689600,
"iat": 1704067200,
"nbf": 1704067200,
"sub": "user-12345"
}'
The response includes a complete, valid JWT token:
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJteS1hcHAiLCJleHAiOjE3MzU2ODk2MDAsImlhdCI6MTcwNDA2NzIwMCwibmJmIjoxNzA0MDY3MjAwLCJzdWIiOiJ1c2VyLTEyMzQ1In0.signature_here"
}
Custom Algorithms
When serving JWKS files, you can configure RS256, RS384, and RS512 as supported algorithms:
# Use only RS256 by default
jwkserve serve
# Use RS256 and RS512
jwkserve serve --algorithm RS256 --algorithm RS512
The /sign endpoint for generating tokens supports signing algorithms as well. For flexible usage, signing is always possible using all three algorithms, regardless of what you’ve configured for the JWKS endpoint:
curl -X POST http://localhost:3000/sign/RS384 \
-H "Content-Type: application/json" \
-d '{
"aud": "my-app",
"exp": 1735689600,
"iat": 1704067200,
"nbf": 1704067200,
"sub": "user-12345"
}'
Using the CLI: Keygen Command
For faster startup during development, you can generate a key once and reuse it:
# Generate key once, also available as key size 3072 and 4096
jwkserve keygen --size 2048 --output key.pem
# Use pre-generated key (instant startup)
jwkserve serve --key key.pem
With a pre-generated key, the server starts instantly without the 2-second delay for key generation.
JWKS Flow Endpoints
To enable the full JWKS flow, JWKServe serves two endpoints you’ll need:
OpenID Discovery Endpoint
curl http://localhost:3000/.well-known/openid-configuration
The response includes the standard OpenID Connect configuration:
{
"issuer": "http://localhost:3000",
"authorization_endpoint": "http://localhost:3000/authorize",
"token_endpoint": "http://localhost:3000/token",
"jwks_uri": "http://localhost:3000/.well-known/jwks.json",
"response_types_supported": ["id_token", "token", "code"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256"],
"scopes_supported": ["openid", "profile", "email"],
"claims_supported": ["sub", "iss", "aud", "exp", "iat", "name", "email"]
}
JWKS Key Endpoint
curl http://localhost:3000/.well-known/jwks.json
This returns the public key in JWKS format:
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "vB_ZfJ5y5E5PPMBUyaZxoPcmKxgaclK6ImLI-YkheEs-RS256",
"alg": "RS256",
"n": "2x2LkXrzc2DLo7tytA0ZfBq4KWpctpe67SWL7gcfDfG7mlKXTd6Rg05Hts8i7gLPCKb-iFKpm57n...",
"e": "AQAB"
}
]
}
Your backend can now fetch this endpoint and validate tokens using the standard JWKS flow. If you’re building Rust applications, JWTiny provides a minimal validator that works seamlessly with JWKServe’s endpoints, making local development and testing straightforward.
Docker Usage
Running JWKServe in a container is straightforward:
docker run -it \
-p 3000:3000 \
sbstjn/jwkserve:latest
For development environments, Docker Compose makes it even easier:
services:
jwkserve:
image: sbstjn/jwkserve
container_name: jwkserve
ports:
- "3000:3000"
command: ["jwkserve", "serve", "--bind", "0.0.0.0"]
With this setup, you can include JWKServe in your local development stack and have it available for all your services that need JWT validation.
Testing Pipelines
In CI/CD pipelines, JWKServe shines because it eliminates the need for external dependencies. You can spin up a container, generate tokens with specific claims for your test scenarios, and tear it down when tests complete. No API keys, no rate limits, no network dependencies—just a simple service that generates valid tokens.
For example, in a GitHub Actions workflow, you might start JWKServe as a service container and use it to generate tokens for integration tests:
services:
jwkserve:
image: sbstjn/jwkserve:latest
ports:
- 3000:3000
Your tests can then generate tokens on demand and validate them against the JWKS endpoint, ensuring your authentication logic works correctly without any external dependencies.
That’s It! 🎉
JWKServe solves a real problem when writing backends that validate JWT tokens. Instead of dealing with identity provider configuration, API keys, and rate limits, you get a simple service that generates valid tokens and serves the JWKS endpoints your code needs. The server itself doesn’t matter—what matters is that your validation logic works correctly with properly formatted tokens, and JWKServe gives you exactly that.
You can find the project on GitHub and install it via cargo install jwkserve or pull the Docker image from sbstjn/jwkserve:latest. That’s it! 🎉