From 7a0a8e189de8aa9e60c52522cab3f3ba7956c443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tommy=20Tr=C3=B8en?= Date: Tue, 20 Aug 2024 10:44:39 +0200 Subject: [PATCH 1/2] feat: support custom TimeProvider when validating tokens * add verify function to OAuth2TokenProvider and use the TimeProvider if set - i.e. via overriding Nimbus DefaultJWTClaimsVerifier's currentTime function * refactor tests for simplicity --- .../mock/oauth2/introspect/Introspect.kt | 12 +- .../security/mock/oauth2/token/KeyProvider.kt | 14 +- .../mock/oauth2/token/OAuth2TokenProvider.kt | 32 ++++ .../security/mock/oauth2/userinfo/UserInfo.kt | 10 +- .../mock/oauth2/introspect/IntrospectTest.kt | 25 +++ .../token/OAuth2TokenProviderRSATest.kt | 168 ++++++++---------- .../mock/oauth2/userinfo/UserInfoTest.kt | 22 +++ 7 files changed, 171 insertions(+), 112 deletions(-) diff --git a/src/main/kotlin/no/nav/security/mock/oauth2/introspect/Introspect.kt b/src/main/kotlin/no/nav/security/mock/oauth2/introspect/Introspect.kt index 0a557ae0..f5ea6224 100644 --- a/src/main/kotlin/no/nav/security/mock/oauth2/introspect/Introspect.kt +++ b/src/main/kotlin/no/nav/security/mock/oauth2/introspect/Introspect.kt @@ -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 @@ -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 diff --git a/src/main/kotlin/no/nav/security/mock/oauth2/token/KeyProvider.kt b/src/main/kotlin/no/nav/security/mock/oauth2/token/KeyProvider.kt index 8157de17..fb01b1ec 100644 --- a/src/main/kotlin/no/nav/security/mock/oauth2/token/KeyProvider.kt +++ b/src/main/kotlin/no/nav/security/mock/oauth2/token/KeyProvider.kt @@ -3,9 +3,12 @@ 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 com.nimbusds.jose.jwk.source.JWKSource +import com.nimbusds.jose.proc.SecurityContext import no.nav.security.mock.oauth2.OAuth2Exception import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.LinkedBlockingDeque @@ -15,7 +18,7 @@ open class KeyProvider constructor( private val initialKeys: List = keysFromFile(INITIAL_KEYS_FILE), private val algorithm: String = JWSAlgorithm.RS256.name, - ) { + ) : JWKSource { private val signingKeys: ConcurrentHashMap = ConcurrentHashMap() private var generator: KeyGenerator = KeyGenerator(JWSAlgorithm.parse(algorithm)) @@ -35,9 +38,11 @@ open class KeyProvider 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}") } @@ -63,4 +68,11 @@ open class KeyProvider return emptyList() } } + + override fun get( + jwkSelector: JWKSelector?, + context: SecurityContext?, + ): MutableList { + return signingKeys.values.toMutableList() + } } diff --git a/src/main/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenProvider.kt b/src/main/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenProvider.kt index e391d901..4380e972 100644 --- a/src/main/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenProvider.kt +++ b/src/main/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenProvider.kt @@ -7,8 +7,13 @@ import com.nimbusds.jose.crypto.ECDSASigner import com.nimbusds.jose.crypto.RSASSASigner import com.nimbusds.jose.jwk.JWKSet import com.nimbusds.jose.jwk.KeyType +import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier +import com.nimbusds.jose.proc.JWSVerificationKeySelector +import com.nimbusds.jose.proc.SecurityContext import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT +import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier +import com.nimbusds.jwt.proc.DefaultJWTProcessor import com.nimbusds.oauth2.sdk.TokenRequest import no.nav.security.mock.oauth2.OAuth2Exception import no.nav.security.mock.oauth2.extensions.clientIdAsString @@ -106,6 +111,13 @@ class OAuth2TokenProvider builder.build() }.sign(issuerId, JOSEObjectType.JWT.type) + fun verify( + issuerUrl: HttpUrl, + token: String, + ): JWTClaimsSet { + return SignedJWT.parse(token).verify(issuerUrl) + } + private fun JWTClaimsSet.sign( issuerId: String, type: String, @@ -124,6 +136,7 @@ class OAuth2TokenProvider sign(RSASSASigner(key.toRSAKey().toPrivateKey())) } } + supported && keyType == KeyType.EC.value -> { SignedJWT( jwsHeader(key.keyID, type, algorithm), @@ -132,6 +145,7 @@ class OAuth2TokenProvider sign(ECDSASigner(key.toECKey().toECPrivateKey())) } } + else -> { throw OAuth2Exception("Unsupported algorithm: ${algorithm.name}") } @@ -178,4 +192,22 @@ class OAuth2TokenProvider } private fun Instant?.orNow(): Instant = this ?: Instant.now() + + private fun SignedJWT.verify(issuerUrl: HttpUrl): JWTClaimsSet { + val jwtProcessor = + DefaultJWTProcessor().apply { + jwsTypeVerifier = DefaultJOSEObjectTypeVerifier(JOSEObjectType("JWT")) + jwsKeySelector = JWSVerificationKeySelector(keyProvider.algorithm(), keyProvider) + jwtClaimsSetVerifier = + object : DefaultJWTClaimsVerifier( + JWTClaimsSet.Builder().issuer(issuerUrl.toString()).build(), + HashSet(listOf("iat", "exp")), + ) { + override fun currentTime(): Date { + return Date.from(timeProvider()) + } + } + } + return jwtProcessor.process(this, null) + } } diff --git a/src/main/kotlin/no/nav/security/mock/oauth2/userinfo/UserInfo.kt b/src/main/kotlin/no/nav/security/mock/oauth2/userinfo/UserInfo.kt index 65776726..716411a6 100644 --- a/src/main/kotlin/no/nav/security/mock/oauth2/userinfo/UserInfo.kt +++ b/src/main/kotlin/no/nav/security/mock/oauth2/userinfo/UserInfo.kt @@ -1,16 +1,12 @@ package no.nav.security.mock.oauth2.userinfo import com.nimbusds.jwt.JWTClaimsSet -import com.nimbusds.jwt.SignedJWT import com.nimbusds.oauth2.sdk.ErrorObject import com.nimbusds.oauth2.sdk.http.HTTPResponse -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.USER_INFO -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 @@ -27,12 +23,8 @@ internal fun Route.Builder.userInfo(tokenProvider: OAuth2TokenProvider) = } private fun OAuth2HttpRequest.verifyBearerToken(tokenProvider: OAuth2TokenProvider): JWTClaimsSet { - val tokenString = this.headers.bearerToken() - 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) + tokenProvider.verify(url.toIssuerUrl(), this.headers.bearerToken()) } catch (e: Exception) { throw invalidToken(e.message ?: "could not verify bearer token") } diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/introspect/IntrospectTest.kt b/src/test/kotlin/no/nav/security/mock/oauth2/introspect/IntrospectTest.kt index 47223019..390df092 100644 --- a/src/test/kotlin/no/nav/security/mock/oauth2/introspect/IntrospectTest.kt +++ b/src/test/kotlin/no/nav/security/mock/oauth2/introspect/IntrospectTest.kt @@ -19,6 +19,8 @@ import no.nav.security.mock.oauth2.token.OAuth2TokenProvider import okhttp3.Headers import okhttp3.HttpUrl.Companion.toHttpUrl import org.junit.jupiter.api.Test +import java.time.Instant +import java.time.temporal.ChronoUnit internal class IntrospectTest { private val rs384TokenProvider = OAuth2TokenProvider(keyProvider = KeyProvider(initialKeys = emptyList(), algorithm = JWSAlgorithm.RS384.name)) @@ -66,6 +68,29 @@ internal class IntrospectTest { } } + @Test + fun `introspect should return active and claims from token when using a custom timeProvider in the OAuth2TokenProvider`() { + val issuerUrl = "http://localhost/default" + val yesterday = Instant.now().minus(1, ChronoUnit.DAYS) + val tokenProvider = OAuth2TokenProvider(timeProvider = { yesterday }) + val claims = + mapOf( + "iss" to issuerUrl, + "client_id" to "yolo", + "token_type" to "token", + "sub" to "foo", + ) + val token = tokenProvider.jwt(claims) + val request = request("$issuerUrl$INTROSPECT", token.serialize()) + + routes { introspect(tokenProvider) }.invoke(request).asClue { + it.status shouldBe 200 + val response = it.parse>() + response shouldContainAll claims + response shouldContain ("active" to true) + } + } + @Test fun `introspect should return active false when token is missing`() { val url = "http://localhost/default$INTROSPECT" diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenProviderRSATest.kt b/src/test/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenProviderRSATest.kt index 5fdfd09c..8e5b2075 100644 --- a/src/test/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenProviderRSATest.kt +++ b/src/test/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenProviderRSATest.kt @@ -16,9 +16,7 @@ import okhttp3.HttpUrl.Companion.toHttpUrl import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource -import java.time.Clock import java.time.Instant -import java.time.ZoneId import java.time.temporal.ChronoUnit import java.util.Date @@ -53,33 +51,31 @@ internal class OAuth2TokenProviderRSATest { ), ) - tokenProvider - .exchangeAccessToken( - tokenRequest = - nimbusTokenRequest( - "myclient", - "grant_type" to GrantType.JWT_BEARER.value, - "scope" to "scope1", - "assertion" to initialToken.serialize(), - ), - issuerUrl = "http://default_if_not_overridden".toHttpUrl(), - claimsSet = initialToken.jwtClaimsSet, - oAuth2TokenCallback = - DefaultOAuth2TokenCallback( - claims = - mapOf( - "extraclaim" to "extra", - "iss" to "http://overrideissuer", - ), - ), - ).jwtClaimsSet - .asClue { - it.issuer shouldBe "http://overrideissuer" - it.subject shouldBe "initialsubject" - it.audience shouldBe listOf("scope1") - it.claims["initialclaim"] shouldBe "initialclaim" - it.claims["extraclaim"] shouldBe "extra" - } + tokenProvider.exchangeAccessToken( + tokenRequest = + nimbusTokenRequest( + "myclient", + "grant_type" to GrantType.JWT_BEARER.value, + "scope" to "scope1", + "assertion" to initialToken.serialize(), + ), + issuerUrl = "http://default_if_not_overridden".toHttpUrl(), + claimsSet = initialToken.jwtClaimsSet, + oAuth2TokenCallback = + DefaultOAuth2TokenCallback( + claims = + mapOf( + "extraclaim" to "extra", + "iss" to "http://overrideissuer", + ), + ), + ).jwtClaimsSet.asClue { + it.issuer shouldBe "http://overrideissuer" + it.subject shouldBe "initialsubject" + it.audience shouldBe listOf("scope1") + it.claims["initialclaim"] shouldBe "initialclaim" + it.claims["extraclaim"] shouldBe "extra" + } } @Test @@ -106,87 +102,71 @@ internal class OAuth2TokenProviderRSATest { val yesterday = Instant.now().minus(1, ChronoUnit.DAYS) val tokenProvider = OAuth2TokenProvider(systemTime = yesterday) - tokenProvider - .exchangeAccessToken( - tokenRequest = - nimbusTokenRequest( - "id", - "grant_type" to GrantType.CLIENT_CREDENTIALS.value, - "scope" to "scope1", - ), - issuerUrl = "http://default_if_not_overridden".toHttpUrl(), - claimsSet = tokenProvider.jwt(mapOf()).jwtClaimsSet, - oAuth2TokenCallback = DefaultOAuth2TokenCallback(), - ).asClue { - it.jwtClaimsSet.issueTime shouldBe Date.from(tokenProvider.systemTime) - println(it.serialize()) - } + tokenProvider.clientCredentialsToken("http://localhost/default").asClue { + it.jwtClaimsSet.issueTime shouldBe Date.from(tokenProvider.systemTime) + } + + val now = Instant.now() + OAuth2TokenProvider().clientCredentialsToken("http://localhost/default").asClue { + it.jwtClaimsSet.issueTime shouldBeAfter now + } } @Test fun `token should have issuedAt set dynamically according to timeProvider`() { - val clock = - object : Clock() { - private var clock = systemDefaultZone() - - override fun instant() = clock.instant() - - override fun withZone(zone: ZoneId) = clock.withZone(zone) + val timeProvider = + object : TimeProvider { + var time = Instant.now() - override fun getZone() = clock.zone - - fun fixed(instant: Instant) { - clock = fixed(instant, zone) - } + override fun invoke(): Instant = time } - val tokenProvider = OAuth2TokenProvider { clock.instant() } + val tokenProvider = OAuth2TokenProvider(timeProvider = timeProvider) val instant1 = Instant.parse("2000-12-03T10:15:30.00Z") val instant2 = Instant.parse("2020-01-21T00:00:00.00Z") - instant1 shouldNotBe instant2 - - run { - clock.fixed(instant1) - tokenProvider.systemTime shouldBe instant1 - - tokenProvider.exchangeAccessToken( - tokenRequest = - nimbusTokenRequest( - "id", - "grant_type" to GrantType.CLIENT_CREDENTIALS.value, - "scope" to "scope1", - ), - issuerUrl = "http://default_if_not_overridden".toHttpUrl(), - claimsSet = tokenProvider.jwt(mapOf()).jwtClaimsSet, - oAuth2TokenCallback = DefaultOAuth2TokenCallback(), - ) - }.asClue { + + timeProvider.time = instant1 + tokenProvider.systemTime shouldBe instant1 + + tokenProvider.clientCredentialsToken("http://localhost/default").asClue { it.jwtClaimsSet.issueTime shouldBe Date.from(instant1) - println(it.serialize()) } - run { - clock.fixed(instant2) - tokenProvider.systemTime shouldBe instant2 - - tokenProvider.exchangeAccessToken( - tokenRequest = - nimbusTokenRequest( - "id", - "grant_type" to GrantType.CLIENT_CREDENTIALS.value, - "scope" to "scope1", - ), - issuerUrl = "http://default_if_not_overridden".toHttpUrl(), - claimsSet = tokenProvider.jwt(mapOf()).jwtClaimsSet, - oAuth2TokenCallback = DefaultOAuth2TokenCallback(), - ) - }.asClue { + timeProvider.time = instant2 + tokenProvider.systemTime shouldBe instant2 + + tokenProvider.clientCredentialsToken("http://localhost/default").asClue { it.jwtClaimsSet.issueTime shouldBe Date.from(instant2) - println(it.serialize()) } } + @Test + fun `token with issueTime set to yesterday should be able to validate with the verify function using the same timeprovider`() { + val yesterday = Instant.now().minus(1, ChronoUnit.DAYS) + val tokenProvider = OAuth2TokenProvider(timeProvider = { yesterday }) + + val token = tokenProvider.clientCredentialsToken("http://localhost/default") + + token.jwtClaimsSet.issueTime shouldBe Date.from(tokenProvider.systemTime) + + tokenProvider.verify("http://localhost/default".toHttpUrl(), token.serialize()).toJSONObject().asClue { + it shouldBe token.jwtClaimsSet.toJSONObject() + } + } + + private fun OAuth2TokenProvider.clientCredentialsToken(issuerUrl: String): SignedJWT = + accessToken( + tokenRequest = + nimbusTokenRequest( + "client1", + "grant_type" to "client_credentials", + "scope" to "scope1", + ), + issuerUrl = issuerUrl.toHttpUrl(), + oAuth2TokenCallback = DefaultOAuth2TokenCallback(), + ) + private fun idToken(issuerUrl: String): SignedJWT = tokenProvider.idToken( tokenRequest = @@ -198,4 +178,6 @@ internal class OAuth2TokenProviderRSATest { issuerUrl = issuerUrl.toHttpUrl(), oAuth2TokenCallback = DefaultOAuth2TokenCallback(), ) + + private infix fun Date.shouldBeAfter(instant: Instant?) = this.after(Date.from(instant)) shouldBe true } diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/userinfo/UserInfoTest.kt b/src/test/kotlin/no/nav/security/mock/oauth2/userinfo/UserInfoTest.kt index 0c7e082f..3a3dae2d 100644 --- a/src/test/kotlin/no/nav/security/mock/oauth2/userinfo/UserInfoTest.kt +++ b/src/test/kotlin/no/nav/security/mock/oauth2/userinfo/UserInfoTest.kt @@ -17,6 +17,8 @@ import no.nav.security.mock.oauth2.token.OAuth2TokenProvider import okhttp3.Headers import okhttp3.HttpUrl.Companion.toHttpUrl import org.junit.jupiter.api.Test +import java.time.Instant +import java.time.temporal.ChronoUnit internal class UserInfoTest { @Test @@ -38,6 +40,26 @@ internal class UserInfoTest { } } + @Test + fun `userinfo should return claims from bearer token when using a custom timeProvider in OAuth2TokenProvider`() { + val issuerUrl = "http://localhost/default" + val yesterday = Instant.now().minus(1, ChronoUnit.DAYS) + val tokenProvider = OAuth2TokenProvider(timeProvider = { yesterday }) + val claims = + mapOf( + "iss" to issuerUrl, + "sub" to "foo", + "extra" to "bar", + ) + val bearerToken = tokenProvider.jwt(claims) + val request = request("$issuerUrl$USER_INFO", bearerToken.serialize()) + + routes { userInfo(tokenProvider) }.invoke(request).asClue { + it.status shouldBe 200 + it.parse>() shouldContainAll claims + } + } + @Test fun `userinfo should throw OAuth2Exception when algorithm does not match`() { val issuerUrl = "http://localhost/default" From 2e85e4efebb70ac7933d2c88b250de2555c9c483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tommy=20Tr=C3=B8en?= Date: Tue, 20 Aug 2024 12:14:42 +0200 Subject: [PATCH 2/2] fix: use jwkSelector when returning keys in KeyProvider * necessary to use jwkSelector to only get keys for supported algorithm * use Instant.now for currentTime when TimeProvider not set * formatting --- .../security/mock/oauth2/token/KeyProvider.kt | 5 +- .../mock/oauth2/token/OAuth2TokenProvider.kt | 8 +-- .../security/mock/oauth2/userinfo/UserInfo.kt | 5 +- .../token/OAuth2TokenProviderRSATest.kt | 54 ++++++++++--------- 4 files changed, 34 insertions(+), 38 deletions(-) diff --git a/src/main/kotlin/no/nav/security/mock/oauth2/token/KeyProvider.kt b/src/main/kotlin/no/nav/security/mock/oauth2/token/KeyProvider.kt index fb01b1ec..003acf7d 100644 --- a/src/main/kotlin/no/nav/security/mock/oauth2/token/KeyProvider.kt +++ b/src/main/kotlin/no/nav/security/mock/oauth2/token/KeyProvider.kt @@ -72,7 +72,6 @@ open class KeyProvider override fun get( jwkSelector: JWKSelector?, context: SecurityContext?, - ): MutableList { - return signingKeys.values.toMutableList() - } + ): MutableList = jwkSelector?.select(JWKSet(signingKeys.values.toList()).toPublicJWKSet()) ?: mutableListOf() + } diff --git a/src/main/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenProvider.kt b/src/main/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenProvider.kt index 4380e972..82a842e1 100644 --- a/src/main/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenProvider.kt +++ b/src/main/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenProvider.kt @@ -114,9 +114,7 @@ class OAuth2TokenProvider fun verify( issuerUrl: HttpUrl, token: String, - ): JWTClaimsSet { - return SignedJWT.parse(token).verify(issuerUrl) - } + ): JWTClaimsSet = SignedJWT.parse(token).verify(issuerUrl) private fun JWTClaimsSet.sign( issuerId: String, @@ -203,9 +201,7 @@ class OAuth2TokenProvider JWTClaimsSet.Builder().issuer(issuerUrl.toString()).build(), HashSet(listOf("iat", "exp")), ) { - override fun currentTime(): Date { - return Date.from(timeProvider()) - } + override fun currentTime(): Date = Date.from(timeProvider().orNow()) } } return jwtProcessor.process(this, null) diff --git a/src/main/kotlin/no/nav/security/mock/oauth2/userinfo/UserInfo.kt b/src/main/kotlin/no/nav/security/mock/oauth2/userinfo/UserInfo.kt index 716411a6..7a70c4a9 100644 --- a/src/main/kotlin/no/nav/security/mock/oauth2/userinfo/UserInfo.kt +++ b/src/main/kotlin/no/nav/security/mock/oauth2/userinfo/UserInfo.kt @@ -22,13 +22,12 @@ internal fun Route.Builder.userInfo(tokenProvider: OAuth2TokenProvider) = json(claims) } -private fun OAuth2HttpRequest.verifyBearerToken(tokenProvider: OAuth2TokenProvider): JWTClaimsSet { - return try { +private fun OAuth2HttpRequest.verifyBearerToken(tokenProvider: OAuth2TokenProvider): JWTClaimsSet = + try { tokenProvider.verify(url.toIssuerUrl(), this.headers.bearerToken()) } catch (e: Exception) { throw invalidToken(e.message ?: "could not verify bearer token") } -} private fun Headers.bearerToken(): String = this["Authorization"] diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenProviderRSATest.kt b/src/test/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenProviderRSATest.kt index 8e5b2075..fef071dc 100644 --- a/src/test/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenProviderRSATest.kt +++ b/src/test/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenProviderRSATest.kt @@ -51,31 +51,33 @@ internal class OAuth2TokenProviderRSATest { ), ) - tokenProvider.exchangeAccessToken( - tokenRequest = - nimbusTokenRequest( - "myclient", - "grant_type" to GrantType.JWT_BEARER.value, - "scope" to "scope1", - "assertion" to initialToken.serialize(), - ), - issuerUrl = "http://default_if_not_overridden".toHttpUrl(), - claimsSet = initialToken.jwtClaimsSet, - oAuth2TokenCallback = - DefaultOAuth2TokenCallback( - claims = - mapOf( - "extraclaim" to "extra", - "iss" to "http://overrideissuer", - ), - ), - ).jwtClaimsSet.asClue { - it.issuer shouldBe "http://overrideissuer" - it.subject shouldBe "initialsubject" - it.audience shouldBe listOf("scope1") - it.claims["initialclaim"] shouldBe "initialclaim" - it.claims["extraclaim"] shouldBe "extra" - } + tokenProvider + .exchangeAccessToken( + tokenRequest = + nimbusTokenRequest( + "myclient", + "grant_type" to GrantType.JWT_BEARER.value, + "scope" to "scope1", + "assertion" to initialToken.serialize(), + ), + issuerUrl = "http://default_if_not_overridden".toHttpUrl(), + claimsSet = initialToken.jwtClaimsSet, + oAuth2TokenCallback = + DefaultOAuth2TokenCallback( + claims = + mapOf( + "extraclaim" to "extra", + "iss" to "http://overrideissuer", + ), + ), + ).jwtClaimsSet + .asClue { + it.issuer shouldBe "http://overrideissuer" + it.subject shouldBe "initialsubject" + it.audience shouldBe listOf("scope1") + it.claims["initialclaim"] shouldBe "initialclaim" + it.claims["extraclaim"] shouldBe "extra" + } } @Test @@ -106,7 +108,7 @@ internal class OAuth2TokenProviderRSATest { it.jwtClaimsSet.issueTime shouldBe Date.from(tokenProvider.systemTime) } - val now = Instant.now() + val now = Instant.now().minus(1, ChronoUnit.SECONDS) OAuth2TokenProvider().clientCredentialsToken("http://localhost/default").asClue { it.jwtClaimsSet.issueTime shouldBeAfter now }