Skip to main content
Version: v0.13

JWT Validation

The IDK provides a flexible JWT validation framework for validating access tokens from OAuth 2.0 authorization servers. The validation module supports JWKS-based signature verification, claims validation, and integrates with the IDK's key management system.

Modules

ModuleDescription
lib-oauth2-jwt-validation-apiValidation interfaces and models
lib-oauth2-jwt-validation-implDefault implementation with JWKS support
EDK Framework Integration

For Ktor and Spring Boot integrations, see the EDK documentation:

  • lib-oauth2-jwt-validation-ktor - Ktor server plugin
  • lib-oauth2-jwt-validation-spring - Spring Boot auto-configuration

Installation

dependencies {
implementation("com.sphereon.idk:lib-oauth2-jwt-validation-api:0.13.0")
implementation("com.sphereon.idk:lib-oauth2-jwt-validation-impl:0.13.0")
}

Core Concepts

JwtValidator

The main interface for validating JWTs:

import com.sphereon.oauth2.jwt.validation.JwtValidator
import com.sphereon.oauth2.jwt.validation.JwtValidationResult

val validator: JwtValidator = sessionComponent.jwtValidator

val result = validator.validate(accessToken)
when (result) {
is JwtValidationResult.Valid -> {
val claims = result.claims
val subject = claims.subject
val scopes = claims.getStringListClaim("scope")
// Token is valid, proceed with request
}
is JwtValidationResult.Invalid -> {
val error = result.error
// Token validation failed
}
}

Validation Options

Configure validation behavior:

data class JwtValidationOptions(
val issuer: String?, // Expected issuer (iss claim)
val audience: String?, // Expected audience (aud claim)
val clockSkewSeconds: Long = 60, // Tolerance for exp/nbf checks
val requiredClaims: List<String> = emptyList(), // Claims that must be present
val validateSignature: Boolean = true, // Verify cryptographic signature
val validateExpiration: Boolean = true, // Check exp claim
val validateNotBefore: Boolean = true // Check nbf claim
)

JWKS Integration

The validator fetches and caches JSON Web Key Sets from the authorization server:

import com.sphereon.oauth2.jwt.validation.JwksJwtValidator
import com.sphereon.oauth2.jwt.validation.JwksConfig

val jwksConfig = JwksConfig(
jwksUri = "https://auth.example.com/.well-known/jwks.json",
cacheDurationSeconds = 3600, // Cache keys for 1 hour
refreshBeforeExpiry = 300, // Refresh 5 minutes before expiry
maxRetries = 3,
retryDelayMs = 1000
)

val validator = JwksJwtValidator(
jwksConfig = jwksConfig,
httpClient = httpClient,
options = JwtValidationOptions(
issuer = "https://auth.example.com",
audience = "my-api"
)
)

Key Rotation

The JWKS validator automatically handles key rotation by:

  1. Caching fetched keys with configurable TTL
  2. Refreshing keys before cache expiration
  3. Retrying fetch on validation failure with unknown key ID
// When a token uses an unknown key ID, the validator will:
// 1. Attempt to refresh JWKS from the server
// 2. Retry validation with the new keys
// 3. Fail only if the key is still not found

Claims Access

Access validated claims with type-safe methods:

val claims = result.claims

// Standard claims
val issuer: String? = claims.issuer
val subject: String? = claims.subject
val audience: List<String>? = claims.audience
val expirationTime: Instant? = claims.expirationTime
val issuedAt: Instant? = claims.issuedAt
val notBefore: Instant? = claims.notBefore
val jwtId: String? = claims.jwtId

// Custom claims
val userId: String? = claims.getStringClaim("user_id")
val roles: List<String>? = claims.getStringListClaim("roles")
val metadata: Map<String, Any>? = claims.getObjectClaim("metadata")
val isAdmin: Boolean? = claims.getBooleanClaim("is_admin")
val score: Long? = claims.getLongClaim("score")

Validation Errors

Handle specific validation failures:

sealed class JwtValidationError {
object Expired : JwtValidationError()
object NotYetValid : JwtValidationError()
object InvalidSignature : JwtValidationError()
object InvalidIssuer : JwtValidationError()
object InvalidAudience : JwtValidationError()
object MalformedToken : JwtValidationError()
data class MissingClaim(val claim: String) : JwtValidationError()
data class KeyNotFound(val keyId: String) : JwtValidationError()
data class JwksFetchFailed(val cause: Throwable) : JwtValidationError()
}

when (val error = result.error) {
is JwtValidationError.Expired -> {
// Token has expired, client should refresh
respondUnauthorized("Token expired")
}
is JwtValidationError.InvalidSignature -> {
// Possible tampering or wrong issuer
respondUnauthorized("Invalid signature")
}
is JwtValidationError.MissingClaim -> {
// Required claim not present
respondUnauthorized("Missing claim: ${error.claim}")
}
else -> {
respondUnauthorized("Token validation failed")
}
}

Configuration

Configure JWT validation via settings:

import com.sphereon.conf.settings.SettingsBuilder

val settings = SettingsBuilder {
oauth2 {
jwt {
validation {
issuer = "https://auth.example.com"
audience = "my-api"
clockSkewSeconds = 60
requiredClaims = listOf("sub", "scope")
}
jwks {
uri = "https://auth.example.com/.well-known/jwks.json"
cacheDurationSeconds = 3600
refreshBeforeExpiry = 300
}
}
}
}

Best Practices

Always validate signatures. Never disable signature validation in production.

Configure appropriate clock skew. Allow 30-60 seconds to handle clock drift between systems.

Require essential claims. Specify claims that must be present for your authorization logic.

Handle JWKS fetch failures gracefully. Cache keys and implement fallback behavior when the auth server is unavailable.

Log validation failures. Monitor for patterns that might indicate attacks or misconfiguration.