After successfully verifying a Verifiable Presentation, VCVerifier generates a signed JWT token containing the credential data. This JWT can be used for authentication and authorization in downstream services.
Overview
The JWT generation process:
- Verify credentials: Validate cryptographic proofs and trust chains
- Extract claims: Pull relevant data from verified credentials
- Build JWT: Construct token with standard and custom claims
- Sign token: Create cryptographic signature with configured key
- Return token: Provide signed JWT to the client
Source reference: verifier/verifier.go:1135-1159 and common/tokenSigner.go
JWT Structure
{
"alg": "ES256",
"kid": "WHFu4HZ59SM853C7eN0OvlKGrMeerDCpHOURoTQwHw",
"typ": "JWT"
}
Signing algorithm: RS256 (RSA) or ES256 (ECDSA)
Key ID for signature verification, available via JWKS endpoint
Payload
{
"iss": "https://verifier.example.com",
"sub": "did:key:z6MksQu8W3TRLqYdJhnQSw5XKZy5QqT4yF4UqPMkVfTQ",
"aud": ["https://api.example.com"],
"exp": 1735689600,
"iat": 1735686000,
"verifiableCredential": {
"type": ["VerifiableCredential", "CustomerCredential"],
"credentialSubject": {
"id": "did:key:z6MksQu8W3TRLqYdJhnQSw5XKZy5QqT4yF4UqPMkVfTQ",
"customerType": "enterprise",
"region": "EU"
}
}
}
Standard Claims
Issuer: The VCVerifier host URL
Subject: The holder DID from the Verifiable Presentation
Audience: The service(s) that should accept this token
Expiration time: Unix timestamp when the token expires
Issued at: Unix timestamp when the token was created
Signing Configuration
Configure the JWT signing key and algorithm in server.yaml:
verifier:
# Algorithm for JWT signatures
keyAlgorithm: "ES256" # RS256 or ES256
# Generate ephemeral key (not persisted)
generateKey: true
# Or provide a key file (PEM format)
keyPath: "/path/to/private-key.pem"
# JWT expiration in minutes
jwtExpiration: 60
# Client identification for key ID
clientIdentification:
id: "did:web:verifier.example.com"
kid: "key-2024-01"
Supported Algorithms
Elliptic Curve Digital Signature Algorithm (ECDSA)
- Curve: P-256 (secp256r1)
- Key size: 256 bits
- Performance: Fast signing and verification
- Key size: Small keys and signatures
Recommended: Use ES256 for modern applicationsverifier:
keyAlgorithm: "ES256"
generateKey: true
RSA Signature with SHA-256
- Algorithm: RSASSA-PKCS1-v1_5
- Key size: Typically 2048 or 4096 bits
- Performance: Slower than ES256
- Compatibility: Widely supported
verifier:
keyAlgorithm: "RS256"
keyPath: "/keys/rsa-private.pem"
Key Management
Generated Keys (Ephemeral)
For development and testing, generate keys on startup:
verifier:
generateKey: true
keyAlgorithm: "ES256"
Generated keys are not persisted and change on restart. Existing JWTs will become invalid. Not suitable for production.
Provided Keys (Persistent)
For production, provide a persistent private key:
verifier:
generateKey: false
keyPath: "/etc/vcverifier/keys/signing-key.pem"
keyAlgorithm: "ES256"
Generate an ES256 key:
# Generate private key
openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem
# Extract public key
openssl ec -in private-key.pem -pubout -out public-key.pem
Generate an RS256 key:
# Generate private key
openssl genrsa -out private-key.pem 2048
# Extract public key
openssl rsa -in private-key.pem -pubout -out public-key.pem
Key ID (kid)
The Key ID helps clients identify which key signed the token:
verifier:
clientIdentification:
# Used as kid if certificate doesn't include it
kid: "2024-q1-signing-key"
# Fallback: client ID used if kid not set
id: "did:web:verifier.example.com"
Priority order:
kid value in the key itself (if present in JWK)
clientIdentification.kid from config
clientIdentification.id from config
JWT Token Signer
The token signer interface provides signing capabilities:
Source reference: common/tokenSigner.go:7-15
type TokenSigner interface {
Sign(t jwt.Token, options ...jwt.SignOption) ([]byte, error)
}
type JwtTokenSigner struct{}
func (JwtTokenSigner) Sign(t jwt.Token, options ...jwt.SignOption) ([]byte, error) {
return jwt.Sign(t, options...)
}
Signing Process
When generating a JWT:
Source reference: verifier/verifier.go:493-506
var signatureAlgorithm jwa.SignatureAlgorithm
switch v.signingAlgorithm {
case "RS256":
signatureAlgorithm = jwa.RS256()
case "ES256":
signatureAlgorithm = jwa.ES256()
}
jwtBytes, err := v.tokenSigner.Sign(
tokenSession.token,
jwt.WithKey(signatureAlgorithm, v.signingKey),
)
Credential Inclusion
VCVerifier supports different methods for including credential data in the JWT.
Single Credential
If one credential is verified, it’s included as verifiableCredential:
{
"iss": "https://verifier.example.com",
"sub": "did:key:z6Mks...",
"exp": 1735689600,
"verifiableCredential": {
"type": ["VerifiableCredential", "CustomerCredential"],
"credentialSubject": {
"id": "did:key:z6Mks...",
"role": "admin"
}
}
}
Multiple Credentials
If multiple credentials are verified, they’re included as verifiablePresentation:
{
"iss": "https://verifier.example.com",
"sub": "did:key:z6Mks...",
"exp": 1735689600,
"verifiablePresentation": [
{
"type": ["VerifiableCredential", "CustomerCredential"],
"credentialSubject": {/* ... */}
},
{
"type": ["VerifiableCredential", "RoleCredential"],
"credentialSubject": {/* ... */}
}
]
}
Flat Claims Mode
For simpler downstream processing, enable flat claims mode:
configRepo:
services:
- id: myService
oidcScopes:
default:
flatClaims: true
With flat claims, credential data is flattened to top-level JWT claims:
{
"iss": "https://verifier.example.com",
"sub": "did:key:z6Mks...",
"exp": 1735689600,
"customerType": "enterprise",
"region": "EU",
"role": "admin"
}
Flat claims mode makes it easier to use standard JWT libraries for authorization decisions.
Token Expiration
Configure token lifetime:
verifier:
# JWT expires after 60 minutes (default)
jwtExpiration: 60
The expiration timestamp is calculated:
Source reference: verifier/verifier.go:1138
jwtBuilder := jwt.NewBuilder().
Issuer(v.GetHost()).
Audience([]string{audience}).
Expiration(v.clock.Now().Add(v.jwtExpiration))
Token expiration is independent of session expiry. Sessions expire during the authentication flow, while JWTs expire after issuance.
JWKS Endpoint
Clients verify JWT signatures using the public key from the JWKS endpoint:
Endpoint: GET /.well-known/jwks
Response:
{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
"use": "sig",
"kid": "2024-q1-signing-key"
}
]
}
Source reference: verifier/verifier.go:522-527
func (v *CredentialVerifier) GetJWKS() jwk.Set {
jwks := jwk.NewSet()
publicKey, _ := v.signingKey.PublicKey()
jwks.AddKey(publicKey)
return jwks
}
Token Retrieval
After successful authentication, retrieve the JWT via the token endpoint:
Request:
curl -X POST https://verifier.example.com/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=authorization_code' \
-d 'code=IwMTgvY3JlZGVudGlhbHMv' \
-d 'redirect_uri=https://app.example.com/callback'
Response:
{
"token_type": "Bearer",
"expires_in": 3600,
"access_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6Ik..."
}
Single retrieval: Each authorization code can only be used once. The token is deleted from cache after retrieval to prevent replay attacks.
Token Verification
Downstream services verify the JWT:
1. Fetch JWKS
curl https://verifier.example.com/.well-known/jwks
2. Verify Signature
Use a JWT library to verify the signature:
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const client = jwksClient({
jwksUri: 'https://verifier.example.com/.well-known/jwks'
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}
jwt.verify(token, getKey, {
issuer: 'https://verifier.example.com',
algorithms: ['ES256', 'RS256']
}, (err, decoded) => {
if (err) {
console.error('Invalid token:', err);
} else {
console.log('Valid token:', decoded);
}
});
3. Check Claims
Verify standard claims:
- iss: Token issued by expected verifier
- aud: Token intended for your service
- exp: Token not expired
- sub: Subject matches expected user
Security Best Practices
-
Use ES256 for new deployments: Better performance and smaller keys than RS256
-
Persist signing keys: Don’t use
generateKey: true in production
-
Rotate keys regularly: Implement key rotation with overlapping validity
-
Set appropriate expiration: Balance security (shorter) vs. usability (longer)
-
Validate audience claim: Ensure tokens are used by intended services
-
Monitor JWKS access: Track which services fetch your public keys
-
Use HTTPS only: Never expose JWKS or tokens over unencrypted connections
Complete Configuration Example
server:
host: "https://verifier.example.com"
port: 8080
verifier:
did: "did:web:verifier.example.com"
# JWT signing configuration
keyAlgorithm: "ES256"
generateKey: false
keyPath: "/etc/vcverifier/keys/es256-private.pem"
jwtExpiration: 60
# Client identification
clientIdentification:
id: "did:web:verifier.example.com"
kid: "2024-q1-signing-key"
# Session configuration
sessionExpiry: 300
validationMode: "combined"
# Request modes
supportedModes:
- "byReference"
- "byValue"
configRepo:
services:
- id: api-service
defaultOidcScope: "default"
oidcScopes:
default:
flatClaims: false
credentials:
- type: CustomerCredential
jwtInclusion:
enabled: true
fullInclusion: true