Developer cookbook

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 / languageLibraryRecipeSnippet
Node.jsjose (RemoteJWKSet)Openjwt-verify-nodejs.js
PythonPyJWT[crypto] + PyJWKClientOpenjwt-verify-python.py
PHPfirebase/php-jwt + CachedKeySetOpenjwt-verify-php.php
Javanimbus-jose-jwt (JWKSourceBuilder)Openjwt-verify-java.java
Gogolang-jwt/jwt + MicahParks/keyfuncOpenjwt-verify-go.go
.NET.NET (ConfigurationManager)Openjwt-verify-dotnet.cs
Rubyjwt gem (JWKS proc)Openjwt-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). Download raw .csharp

// 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.