1import io.jsonwebtoken.Claims
2import io.jsonwebtoken.JwsHeader
3import io.jsonwebtoken.Jwts
4import io.jsonwebtoken.LocatorAdapter
5import io.jsonwebtoken.security.Jwks
6import io.jsonwebtoken.security.PublicJwk
7import java.net.URI
8import java.net.http.HttpClient
9import java.net.http.HttpRequest
10import java.net.http.HttpResponse
11import java.security.Key
12
13class JwksKeyLocator(private val jwksUri: URI) : LocatorAdapter<Key>() {
14 private val http = HttpClient.newHttpClient()
15 @Volatile private var keys: Map<String, Key> = emptyMap()
16
17 private fun fetch(): Map<String, Key> {
18 val body = http.send(HttpRequest.newBuilder(jwksUri).build(), HttpResponse.BodyHandlers.ofString()).body()
19 return Jwks.setParser().build().parse(body).getKeys() // JwkSet is a Map too: .keys would be its member names
20 .filterIsInstance<PublicJwk<*>>() // public keys only, with a kid, not marked "enc"
21 .filter { it.id != null && it.publicKeyUse != "enc" }
22 .associate { it.id to it.toKey() }
23 }
24
25 // Called by JJWT for every token: the key whose kid the header names
26 override fun locate(header: JwsHeader): Key? {
27 val kid = header.keyId ?: return null
28 // One refetch for an unknown kid (rotation); production code rate-limits it
29 return keys[kid] ?: fetch().also { keys = it }[kid]
30 }
31}
32
33val parser = Jwts.parser()
34 .keyLocator(JwksKeyLocator(URI("https://issuer.example.com/.well-known/jwks.json")))
35 .sig().clear().add(Jwts.SIG.ES256).and() // only ES256 is accepted
36 .requireIssuer("https://issuer.example.com")
37 .requireAudience("api://orders")
38 .build()
39
40fun verifyAccessToken(token: String): Claims = parser.parseSignedClaims(token).payload