Zero-Dependency JWT Decoding in Kotlin
Kotlin has direct access to Java's java.util.Base64, so you can decode a JWT payload without any external library. On Android API 26+, java.util.Base64 is available; on older Android versions use android.util.Base64 with Base64.URL_SAFE or Base64.NO_PADDING.
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import java.util.Base64
// Decodes JWT payload without verifying. Inspection / logging only.
fun decodePayload(token: String): Map<String, JsonElement> {
val parts = token.split(".")
require(parts.size == 3) { "Not a JWT" }
val bytes = Base64.getUrlDecoder().decode(parts[1])
val jsonStr = String(bytes, Charsets.UTF_8)
return Json.parseToJsonElement(jsonStr).jsonObject
}
// Usage
fun main() {
val claims = decodePayload("eyJhbGciOi...")
println(claims["sub"])
println(claims["exp"])
}Using auth0/java-jwt — The Ktor Standard
Auth0's open-source JWT library. Fluent builder API, thread-safe verifiers, works seamlessly from Kotlin.
Installation (Gradle Kotlin DSL)
// build.gradle.kts
dependencies {
implementation("com.auth0:java-jwt:4.4.0")
implementation("com.auth0:jwks-rsa:0.22.1") // for JWKS support
}Verify with HS256 Secret
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import com.auth0.jwt.exceptions.JWTVerificationException
import com.auth0.jwt.exceptions.TokenExpiredException
import com.auth0.jwt.interfaces.DecodedJWT
object JwtService {
private val secret = System.getenv("JWT_SECRET")
?: error("JWT_SECRET missing")
private val algorithm = Algorithm.HMAC256(secret)
private val verifier = JWT.require(algorithm)
.withIssuer("https://auth.example.com")
.withAudience("https://api.example.com")
.acceptLeeway(30) // 30-second clock drift tolerance
.build()
fun verify(token: String): Result<DecodedJWT> = try {
Result.success(verifier.verify(token))
} catch (e: TokenExpiredException) {
Result.failure(RuntimeException("Token expired"))
} catch (e: JWTVerificationException) {
Result.failure(RuntimeException("Invalid token: ${e.message}"))
}
}Verify with RS256 (Public Key)
import java.security.KeyFactory
import java.security.interfaces.RSAPublicKey
import java.security.spec.X509EncodedKeySpec
import java.util.Base64
fun loadPublicKey(pemContent: String): RSAPublicKey {
val cleaned = pemContent
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replace("\s".toRegex(), "")
val bytes = Base64.getDecoder().decode(cleaned)
return KeyFactory.getInstance("RSA")
.generatePublic(X509EncodedKeySpec(bytes)) as RSAPublicKey
}
val publicKey = loadPublicKey(File("public.pem").readText())
val algorithm = Algorithm.RSA256(publicKey, null) // null = verify only
val verifier = JWT.require(algorithm)
.withIssuer("https://auth.example.com")
.withAudience("https://api.example.com")
.build()
val jwt = verifier.verify(token)Ktor JWT Authentication
// build.gradle.kts
// implementation("io.ktor:ktor-server-auth-jwt:2.3.12")
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Application.configureSecurity() {
val secret = environment.config.property("jwt.secret").getString()
val issuer = environment.config.property("jwt.issuer").getString()
val audience = environment.config.property("jwt.audience").getString()
install(Authentication) {
jwt("auth-jwt") {
realm = "PromptSpace API"
verifier(
JWT.require(Algorithm.HMAC256(secret))
.withIssuer(issuer)
.withAudience(audience)
.build()
)
validate { credential ->
if (credential.payload.getClaim("sub").asString().isNotEmpty()) {
JWTPrincipal(credential.payload)
} else null
}
challenge { _, _ ->
call.respond(HttpStatusCode.Unauthorized, "Token invalid or expired")
}
}
}
routing {
authenticate("auth-jwt") {
get("/me") {
val principal = call.principal<JWTPrincipal>()
val userId = principal!!.payload.getClaim("sub").asString()
call.respondText("Hello, $userId")
}
}
}
}Spring Boot Resource Server (Kotlin)
# application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: https://auth.example.com/.well-known/jwks.json
issuer-uri: https://auth.example.com// SecurityConfig.kt
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.SecurityFilterChain
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain = http
.authorizeHttpRequests { it
.requestMatchers("/actuator/health", "/public/**").permitAll()
.anyRequest().authenticated()
}
.oauth2ResourceServer { it.jwt {} }
.csrf { it.disable() }
.build()
}
// UserController.kt — access the JWT via @AuthenticationPrincipal
@RestController
class UserController {
@GetMapping("/me")
fun me(@AuthenticationPrincipal jwt: Jwt): Map<String, Any?> =
mapOf("sub" to jwt.subject, "email" to jwt.getClaim("email"))
}Android JWT Decode (Client-Side Expiration Check)
import android.util.Base64
import org.json.JSONObject
// Client-side ONLY — never trust for authorization.
// Use to skip a wasted API call when we know the token is expired.
fun isTokenExpired(token: String): Boolean {
return try {
val parts = token.split(".")
if (parts.size != 3) return true
val payloadBytes = Base64.decode(
parts[1],
Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP
)
val payload = JSONObject(String(payloadBytes, Charsets.UTF_8))
val exp = payload.optLong("exp", 0L)
val nowSec = System.currentTimeMillis() / 1000
exp < nowSec + 30 // 30-second buffer
} catch (e: Exception) {
true // fail closed — treat as expired
}
}Verify with JWKS (Auth0 / Cognito / Azure AD)
import com.auth0.jwk.JwkProviderBuilder
import java.net.URL
import java.util.concurrent.TimeUnit
val provider = JwkProviderBuilder(URL("https://auth.example.com/.well-known/jwks.json"))
.cached(10, 24, TimeUnit.HOURS) // Cache 10 keys for 24 hours
.rateLimited(10, 1, TimeUnit.MINUTES) // Max 10 requests per minute if cache miss
.build()
fun verifyJwks(token: String): DecodedJWT {
val decoded = JWT.decode(token)
val jwk = provider.get(decoded.keyId)
val algorithm = Algorithm.RSA256(jwk.publicKey as RSAPublicKey, null)
return JWT.require(algorithm)
.withIssuer("https://auth.example.com")
.withAudience("https://api.example.com")
.build()
.verify(token)
}Common Kotlin/Java-JWT Errors
- TokenExpiredException — exp claim is in the past. Client must refresh.
- IncorrectClaimException — a specific claim (iss, aud, or a withClaim) does not match.
- SignatureVerificationException — signature does not match your key. Wrong secret or tampered token.
- InvalidClaimException — a claim is missing or wrong type.
- AlgorithmMismatchException — token alg header does not match the verifier's algorithm. Alg confusion defence.
- JWTDecodeException — token is malformed (not three segments, invalid base64, invalid JSON).
Security Best Practices for Kotlin JWT
- Always pin the algorithm at verifier construction:
Algorithm.HMAC256(secret)orAlgorithm.RSA256(pubKey, null). Never accept the token's alg header without pinning. - Use
.acceptLeeway(30)for clock drift — do not exceed 60 seconds. - HS256 secret must be at least 32 random bytes. Store in env, use Kotlin's
System.getenvor Spring's@Value. - For production JWKS, cache with
.cached(10, 24, TimeUnit.HOURS)and rate-limit refreshes. - On Android, never verify JWTs on the client — the secret would be in your APK. Use JWT only for the client-side expiration heuristic.
- Spring Security 6 auto-configures everything from
jwk-set-uri— do not add manual code that duplicates it.
Key Facts
- Ktor lib:
- com.auth0:java-jwt + ktor-server-auth-jwt
- Spring lib:
- spring-boot-starter-oauth2-resource-server (built-in)
- Android:
- android.util.Base64 (pre-26) or java.util.Base64 (API 26+)
- JWKS:
- com.auth0:jwks-rsa with .cached(10, 24, TimeUnit.HOURS)
- Kotlin version:
- 1.9+ for coroutines with Ktor 2.3+
Related JWT Tools
- JWT Decoder Online — decode any token instantly
- JWT Decoder Java — jjwt and Spring Boot patterns
- JWT Decoder Node.js — jsonwebtoken and jose
- JWT Decoder Python — PyJWT patterns
- JWT Decoder Go — golang-jwt/jwt v5