JWT validation cookbook - verify CodeB tokens locally.
Verify a CodeB-issued JWT locally without a per-call network round-trip. Every snippet fetches JWKS once from /.well-known/jwks.json, caches it in memory, and verifies signature + iss + aud + exp + nbf. Works identically for European Digital Identity Wallet id_tokens.
Algorithm. Only RS256 is emitted. Reject any token whose header alg is not exactly RS256. Cited: RFC 7519 (JWT), RFC 7517 (JWK), RFC 7518 (JWA).
Language matrix
| Framework / language | Library | Recipe | Snippet |
|---|---|---|---|
| Node.js | jose (RemoteJWKSet) | Open | jwt-verify-nodejs.js |
| Python | PyJWT[crypto] + PyJWKClient | Open | jwt-verify-python.py |
| PHP | firebase/php-jwt + CachedKeySet | Open | jwt-verify-php.php |
| Java | nimbus-jose-jwt (JWKSourceBuilder) | Open | jwt-verify-java.java |
| Go | golang-jwt/jwt + MicahParks/keyfunc | Open | jwt-verify-go.go |
| .NET | .NET (ConfigurationManager | Open | jwt-verify-dotnet.cs |
| Ruby | jwt gem (JWKS proc) | Open | jwt-verify-ruby.rb |
Node.js Recipe
jose (RemoteJWKSet). Download raw .js
// Verify a CodeB JWT locally in Node.js.
// Signed with RS256 against the tenant JWKS. European Digital Identity
// Wallet id_tokens are validated the same way.
// [jwt-validation-cookbook 2026-07-23]
// npm install jose
const { createRemoteJWKSet, jwtVerify } = require('jose');
const ISSUER = 'https://<CODEB_TENANT_HOST>';
const AUDIENCE = '<YOUR_CLIENT_ID>';
const JWKS = createRemoteJWKSet(new URL(ISSUER + '/.well-known/jwks.json'));
async function verify(token){
// jose caches JWKS in memory (default 10 min); no per-call round-trip
// after the first fetch. It verifies signature + exp + nbf + iss + aud.
const { payload } = await jwtVerify(token, JWKS, {
issuer: ISSUER,
audience: AUDIENCE,
algorithms: ['RS256'],
clockTolerance: 5, // seconds
});
return payload; // { sub, role, scope, ... }
}
// Example
(async () => {
const claims = await verify(process.argv[2]);
console.log('sub =', claims.sub);
console.log('role =', claims.role || 'user');
console.log('scope=', claims.scope);
})();
/* Cited RFCs:
* - RFC 7519 (JWT) https://www.rfc-editor.org/rfc/rfc7519
* - RFC 7517 (JWK) https://www.rfc-editor.org/rfc/rfc7517
* - RFC 7518 (JWA) https://www.rfc-editor.org/rfc/rfc7518
*/
Python Recipe
PyJWT[crypto] + PyJWKClient. Download raw .python
# Verify a CodeB JWT locally in Python.
# Signed with RS256 against the tenant JWKS. European Digital Identity
# Wallet id_tokens are validated the same way.
# [jwt-validation-cookbook 2026-07-23]
# pip install "pyjwt[crypto]" requests
import jwt, requests, time
from jwt import PyJWKClient
ISSUER = "https://<CODEB_TENANT_HOST>"
AUDIENCE = "<YOUR_CLIENT_ID>"
JWKS_URL = ISSUER + "/.well-known/jwks.json"
# PyJWKClient caches keys in memory. First call fetches once; subsequent
# calls read from cache. Force refresh on kid-miss.
_jwks = PyJWKClient(JWKS_URL, cache_keys=True, lifespan=600)
def verify(token: str) -> dict:
signing_key = _jwks.get_signing_key_from_jwt(token).key
return jwt.decode(
token,
signing_key,
algorithms=["RS256"],
audience=AUDIENCE,
issuer=ISSUER,
leeway=5, # seconds
options={"require": ["exp", "iat", "iss", "aud", "sub"]},
)
if __name__ == "__main__":
import sys
claims = verify(sys.argv[1])
print("sub =", claims["sub"])
print("role =", claims.get("role", "user"))
print("scope=", claims.get("scope"))
# RFCs: 7519 (JWT), 7517 (JWK), 7518 (JWA).
PHP Recipe
firebase/php-jwt + CachedKeySet. Download raw .php
<?php
// Verify a CodeB JWT locally in PHP.
// Signed with RS256 against the tenant JWKS. European Digital Identity
// Wallet id_tokens are validated the same way.
// [jwt-validation-cookbook 2026-07-23]
// composer require firebase/php-jwt
require __DIR__ . '/vendor/autoload.php';
use Firebase\JWT\JWT;
use Firebase\JWT\JWK;
use Firebase\JWT\CachedKeySet;
use GuzzleHttp\Psr7\HttpFactory;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
const ISSUER = 'https://<CODEB_TENANT_HOST>';
const AUDIENCE = '<YOUR_CLIENT_ID>';
const JWKS_URL = ISSUER . '/.well-known/jwks.json';
// CachedKeySet fetches once, caches to PSR-6, honours HTTP cache headers.
$factory = new HttpFactory();
$cache = new FilesystemAdapter('codeb-jwks', 600, sys_get_temp_dir());
$keys = new CachedKeySet(JWKS_URL, new \GuzzleHttp\Client(),
$factory, $cache, 600, true);
function verify(string $token, $keys): array {
JWT::$leeway = 5;
$claims = (array) JWT::decode($token, $keys);
if (($claims['iss'] ?? '') !== ISSUER) throw new RuntimeException('bad iss');
if (($claims['aud'] ?? '') !== AUDIENCE) throw new RuntimeException('bad aud');
return $claims;
}
// Example
$claims = verify($argv[1], $keys);
printf("sub = %s\nrole = %s\n", $claims['sub'], $claims['role'] ?? 'user');
// RFCs: 7519 (JWT), 7517 (JWK), 7518 (JWA).
Java Recipe
nimbus-jose-jwt (JWKSourceBuilder). Download raw .java
// Verify a CodeB JWT locally in Java (nimbus-jose-jwt).
// Signed with RS256 against the tenant JWKS. European Digital Identity
// Wallet id_tokens are validated the same way.
// [jwt-validation-cookbook 2026-07-23]
//
// Maven:
// <dependency>
// <groupId>com.nimbusds</groupId>
// <artifactId>nimbus-jose-jwt</artifactId>
// <version>9.37.3</version>
// </dependency>
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.jwk.source.JWKSourceBuilder;
import com.nimbusds.jose.proc.JWSKeySelector;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import java.net.URL;
import java.util.Set;
public class JwtVerify {
private static final String ISSUER = "https://<CODEB_TENANT_HOST>";
private static final String AUDIENCE = "<YOUR_CLIENT_ID>";
public static JWTClaimsSet verify(String token) throws Exception {
// JWKSourceBuilder caches keys in memory (default 5 min TTL) and
// refreshes automatically on kid-miss.
JWKSource<SecurityContext> keys = JWKSourceBuilder
.create(new URL(ISSUER + "/.well-known/jwks.json"))
.build();
ConfigurableJWTProcessor<SecurityContext> p = new DefaultJWTProcessor<>();
JWSKeySelector<SecurityContext> sel =
new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, keys);
p.setJWSKeySelector(sel);
p.setJWTClaimsSetVerifier(new DefaultJWTClaimsVerifier<>(
AUDIENCE,
new JWTClaimsSet.Builder().issuer(ISSUER).build(),
Set.of("sub", "iat", "exp")
));
return p.process(token, null); // verifies sig + iss + aud + exp
}
public static void main(String[] args) throws Exception {
JWTClaimsSet c = verify(args[0]);
System.out.println("sub = " + c.getSubject());
System.out.println("role = " + c.getClaim("role"));
}
}
// RFCs: 7519 (JWT), 7517 (JWK), 7518 (JWA).
Go Recipe
golang-jwt/jwt + MicahParks/keyfunc. Download raw .go
// Verify a CodeB JWT locally in Go.
// Signed with RS256 against the tenant JWKS. European Digital Identity
// Wallet id_tokens are validated the same way.
// [jwt-validation-cookbook 2026-07-23]
//
// go get github.com/golang-jwt/jwt/v5
// go get github.com/MicahParks/keyfunc/v3
package main
import (
"context"
"fmt"
"os"
"time"
"github.com/MicahParks/keyfunc/v3"
"github.com/golang-jwt/jwt/v5"
)
const (
Issuer = "https://<CODEB_TENANT_HOST>"
Audience = "<YOUR_CLIENT_ID>"
)
func verify(token string) (jwt.MapClaims, error) {
// keyfunc fetches JWKS once, caches in memory, refreshes on kid-miss.
k, err := keyfunc.NewDefaultCtx(context.Background(),
[]string{Issuer + "/.well-known/jwks.json"})
if err != nil {
return nil, err
}
t, err := jwt.Parse(token, k.Keyfunc,
jwt.WithValidMethods([]string{"RS256"}),
jwt.WithIssuer(Issuer),
jwt.WithAudience(Audience),
jwt.WithLeeway(5*time.Second),
)
if err != nil {
return nil, err
}
return t.Claims.(jwt.MapClaims), nil
}
func main() {
c, err := verify(os.Args[1])
if err != nil {
fmt.Println("verify failed:", err)
os.Exit(1)
}
fmt.Printf("sub = %v\nrole = %v\n", c["sub"], c["role"])
}
// RFCs: 7519 (JWT), 7517 (JWK), 7518 (JWA).
.NET Recipe
.NET (ConfigurationManager
// Verify a CodeB JWT locally in .NET.
// Signed with RS256 against the tenant JWKS. European Digital Identity
// Wallet id_tokens are validated the same way.
// [jwt-validation-cookbook 2026-07-23]
// dotnet add package Microsoft.IdentityModel.Protocols.OpenIdConnect
// dotnet add package System.IdentityModel.Tokens.Jwt
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
const string Issuer = "https://<CODEB_TENANT_HOST>";
const string Audience = "<YOUR_CLIENT_ID>";
// ConfigurationManager caches the discovery + JWKS documents (default
// 1 day) and auto-refreshes on kid-miss.
var mgr = new ConfigurationManager<OpenIdConnectConfiguration>(
Issuer + "/.well-known/openid-configuration",
new OpenIdConnectConfigurationRetriever());
var oidc = await mgr.GetConfigurationAsync();
var handler = new JwtSecurityTokenHandler();
var parms = new TokenValidationParameters
{
ValidIssuer = Issuer,
ValidAudience = Audience,
IssuerSigningKeys = oidc.SigningKeys,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.FromSeconds(5),
};
var principal = handler.ValidateToken(args[0], parms, out _);
Console.WriteLine("sub = " + principal.FindFirst("sub")?.Value);
Console.WriteLine("role = " + (principal.FindFirst("role")?.Value ?? "user"));
// RFCs: 7519 (JWT), 7517 (JWK), 7518 (JWA).
Ruby Recipe
jwt gem (JWKS proc). Download raw .ruby
# Verify a CodeB JWT locally in Ruby.
# Signed with RS256 against the tenant JWKS. European Digital Identity
# Wallet id_tokens are validated the same way.
# [jwt-validation-cookbook 2026-07-23]
# gem install jwt
require 'jwt'
require 'net/http'
require 'json'
require 'uri'
ISSUER = 'https://<CODEB_TENANT_HOST>'
AUDIENCE = '<YOUR_CLIENT_ID>'
JWKS_URL = URI.parse("#{ISSUER}/.well-known/jwks.json")
# In-memory JWKS cache with 10-minute TTL, refreshed on kid-miss.
@jwks = nil
@jwks_fetched = nil
def fetch_jwks(force = false)
if force || @jwks.nil? || (Time.now - @jwks_fetched > 600)
@jwks = JSON.parse(Net::HTTP.get(JWKS_URL))
@jwks_fetched = Time.now
end
@jwks
end
def verify(token)
JWT.decode(token, nil, true, {
algorithms: ['RS256'],
iss: ISSUER, verify_iss: true,
aud: AUDIENCE, verify_aud: true,
leeway: 5,
jwks: proc { |opts| fetch_jwks(opts[:invalidate]) }
}).first
end
if $PROGRAM_NAME == __FILE__
claims = verify(ARGV[0])
puts "sub = #{claims['sub']}"
puts "role = #{claims['role'] || 'user'}"
end
# RFCs: 7519 (JWT), 7517 (JWK), 7518 (JWA).
Test it
Mint a test token via /oauth2/v1/token against a demo client, then paste it into any snippet as the CLI argument. A successful run prints sub, role, and scope.
FAQ
See structured answers in the schema block above.