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
| Module | Description |
|---|---|
lib-oauth2-jwt-validation-api | Validation interfaces and models |
lib-oauth2-jwt-validation-impl | Default implementation with JWKS support |
For Ktor and Spring Boot integrations, see the EDK documentation:
lib-oauth2-jwt-validation-ktor- Ktor server pluginlib-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:
- Android/Kotlin
- iOS/Swift
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
}
}
import SphereonIDK
let validator = sessionComponent.jwtValidator
let result = try await validator.validate(accessToken: accessToken)
switch result {
case .valid(let claims):
let subject = claims.subject
let scopes = claims.getStringListClaim(name: "scope")
// Token is valid, proceed with request
case .invalid(let error):
// Token validation failed
break
}
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:
- Caching fetched keys with configurable TTL
- Refreshing keys before cache expiration
- 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:
- Android/Kotlin
- iOS/Swift
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")
let claims = result.claims
// Standard claims
let issuer = claims.issuer
let subject = claims.subject
let audience = claims.audience
let expirationTime = claims.expirationTime
let issuedAt = claims.issuedAt
// Custom claims
let userId = claims.getStringClaim(name: "user_id")
let roles = claims.getStringListClaim(name: "roles")
let isAdmin = claims.getBooleanClaim(name: "is_admin")
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.