Skip to content

Commit

Permalink
feat: support custom TimeProvider when validating tokens (introspect,…
Browse files Browse the repository at this point in the history
… userinfo)

* add verify function to OAuth2TokenProvider and use the TimeProvider if set - i.e. via overriding Nimbus DefaultJWTClaimsVerifier's currentTime function
* refactor tests for simplicity
  • Loading branch information
tommytroen committed Aug 20, 2024
1 parent 4ad60a6 commit fe55b15
Show file tree
Hide file tree
Showing 7 changed files with 343 additions and 287 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,11 @@ package no.nav.security.mock.oauth2.introspect
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonProperty
import com.nimbusds.jwt.JWTClaimsSet
import com.nimbusds.jwt.SignedJWT
import com.nimbusds.oauth2.sdk.OAuth2Error
import com.nimbusds.oauth2.sdk.id.Issuer
import mu.KotlinLogging
import no.nav.security.mock.oauth2.OAuth2Exception
import no.nav.security.mock.oauth2.extensions.OAuth2Endpoints.INTROSPECT
import no.nav.security.mock.oauth2.extensions.issuerId
import no.nav.security.mock.oauth2.extensions.toIssuerUrl
import no.nav.security.mock.oauth2.extensions.verifySignatureAndIssuer
import no.nav.security.mock.oauth2.http.OAuth2HttpRequest
import no.nav.security.mock.oauth2.http.Route
import no.nav.security.mock.oauth2.http.json
Expand Down Expand Up @@ -51,12 +47,10 @@ internal fun Route.Builder.introspect(tokenProvider: OAuth2TokenProvider) =
}

private fun OAuth2HttpRequest.verifyToken(tokenProvider: OAuth2TokenProvider): JWTClaimsSet? {
val tokenString = this.formParameters.get("token")
val issuer = url.toIssuerUrl()
val jwkSet = tokenProvider.publicJwkSet(issuer.issuerId())
val algorithm = tokenProvider.getAlgorithm()
return try {
SignedJWT.parse(tokenString).verifySignatureAndIssuer(Issuer(issuer.toString()), jwkSet, algorithm)
this.formParameters.get("token")?.let {
tokenProvider.verify(url.toIssuerUrl(), it)
}
} catch (e: Exception) {
log.debug("token_introspection: failed signature validation")
return null
Expand Down
89 changes: 49 additions & 40 deletions src/main/kotlin/no/nav/security/mock/oauth2/token/KeyProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,64 +3,73 @@ package no.nav.security.mock.oauth2.token
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.jwk.ECKey
import com.nimbusds.jose.jwk.JWK
import com.nimbusds.jose.jwk.JWKSelector
import com.nimbusds.jose.jwk.JWKSet
import com.nimbusds.jose.jwk.KeyType
import com.nimbusds.jose.jwk.RSAKey
import no.nav.security.mock.oauth2.OAuth2Exception
import com.nimbusds.jose.jwk.source.JWKSource
import com.nimbusds.jose.proc.SecurityContext
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.LinkedBlockingDeque
import no.nav.security.mock.oauth2.OAuth2Exception

open class KeyProvider
@JvmOverloads
constructor(
private val initialKeys: List<JWK> = keysFromFile(INITIAL_KEYS_FILE),
private val algorithm: String = JWSAlgorithm.RS256.name,
) {
private val signingKeys: ConcurrentHashMap<String, JWK> = ConcurrentHashMap()
@JvmOverloads
constructor(
private val initialKeys: List<JWK> = keysFromFile(INITIAL_KEYS_FILE),
private val algorithm: String = JWSAlgorithm.RS256.name,
) : JWKSource<SecurityContext> {
private val signingKeys: ConcurrentHashMap<String, JWK> = ConcurrentHashMap()

private var generator: KeyGenerator = KeyGenerator(JWSAlgorithm.parse(algorithm))
private var generator: KeyGenerator = KeyGenerator(JWSAlgorithm.parse(algorithm))

private val keyDeque =
LinkedBlockingDeque<JWK>().apply {
initialKeys.forEach {
put(it)
}
private val keyDeque =
LinkedBlockingDeque<JWK>().apply {
initialKeys.forEach {
put(it)
}
}

fun signingKey(keyId: String): JWK = signingKeys.computeIfAbsent(keyId) { keyFromDequeOrNew(keyId) }

private fun keyFromDequeOrNew(keyId: String): JWK =
keyDeque.poll()?.let { polledJwk ->
when (polledJwk.keyType.value) {
KeyType.RSA.value -> {
RSAKey.Builder(polledJwk.toRSAKey()).keyID(keyId).build()
}

fun signingKey(keyId: String): JWK = signingKeys.computeIfAbsent(keyId) { keyFromDequeOrNew(keyId) }
KeyType.EC.value -> {
ECKey.Builder(polledJwk.toECKey()).keyID(keyId).build()
}

private fun keyFromDequeOrNew(keyId: String): JWK =
keyDeque.poll()?.let { polledJwk ->
when (polledJwk.keyType.value) {
KeyType.RSA.value -> {
RSAKey.Builder(polledJwk.toRSAKey()).keyID(keyId).build()
}
KeyType.EC.value -> {
ECKey.Builder(polledJwk.toECKey()).keyID(keyId).build()
}
else -> {
throw OAuth2Exception("Unsupported key type: ${polledJwk.keyType.value}")
}
else -> {
throw OAuth2Exception("Unsupported key type: ${polledJwk.keyType.value}")
}
} ?: generator.generateKey(keyId)
}
} ?: generator.generateKey(keyId)

fun algorithm(): JWSAlgorithm = JWSAlgorithm.parse(algorithm)
fun algorithm(): JWSAlgorithm = JWSAlgorithm.parse(algorithm)

fun keyType(): String = generator.keyGenerator.algorithm
fun keyType(): String = generator.keyGenerator.algorithm

fun generate(algorithm: String) {
generator = KeyGenerator(JWSAlgorithm.parse(algorithm))
}
fun generate(algorithm: String) {
generator = KeyGenerator(JWSAlgorithm.parse(algorithm))
}

companion object {
const val INITIAL_KEYS_FILE = "/mock-oauth2-server-keys.json"
companion object {
const val INITIAL_KEYS_FILE = "/mock-oauth2-server-keys.json"

fun keysFromFile(filename: String): List<JWK> {
val keysFromFile = KeyProvider::class.java.getResource(filename)
if (keysFromFile != null) {
return JWKSet.parse(keysFromFile.readText()).keys.map { it as JWK }
}
return emptyList()
fun keysFromFile(filename: String): List<JWK> {
val keysFromFile = KeyProvider::class.java.getResource(filename)
if (keysFromFile != null) {
return JWKSet.parse(keysFromFile.readText()).keys.map { it as JWK }
}
return emptyList()
}
}

override fun get(jwkSelector: JWKSelector?, context: SecurityContext?): MutableList<JWK> {
return signingKeys.values.toMutableList()
}
}
Loading

0 comments on commit fe55b15

Please sign in to comment.