From 4e9e5a37bf87dea7a125411f26daa9157293f227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tommy=20Tr=C3=B8en?= Date: Thu, 18 Apr 2024 09:38:23 +0200 Subject: [PATCH] feat(tokenprovider): support setting a static systemtime (#668) * tokens will be issued with this time if set --- README.md | 28 ++++++++++++++++++- .../nav/security/mock/oauth2/OAuth2Config.kt | 8 ++++++ .../mock/oauth2/StandaloneMockOAuth2Server.kt | 8 +++--- .../mock/oauth2/token/OAuth2TokenProvider.kt | 9 ++++-- .../security/mock/oauth2/OAuth2ConfigTest.kt | 15 ++++++++++ .../oauth2/token/OAuth2TokenCallbackTest.kt | 1 - .../token/OAuth2TokenProviderRSATest.kt | 24 ++++++++++++++++ 7 files changed, 84 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 71e90db2..541622ac 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,7 @@ fun loginWithIdTokenForAcrClaimEqualsLevel4() { } ~~~ + ##### Testing an API requiring access_token (e.g. a signed JWT) ```kotlin @@ -183,6 +184,20 @@ val token: SignedJWT = oAuth2Server.issueToken(issuerId, "someclientid", Default val request = // .... request.addHeader("Authorization", "Bearer ${token.serialize()}") ``` +If you for some reason need to manipulate the system time/clock you can configure the OAuth2TokenProvider to use a specific time, resulting in the `iat` claim being set to that time: + +```kotlin +@Test +fun testWithSpecificTime() { + val server = MockOAuth2Server( + config = OAuth2Config( + tokenProvider = OAuth2TokenProvider(systemTime = Instant.parse("2020-01-21T00:00:00Z") + ) + ) + val token = server.issueToken(issuerId = "issuer1") + // do whatever token testing you need to do here and assert the token has iat=2020-01-21T00:00:00Z +} +``` ##### More examples @@ -285,6 +300,17 @@ add this to your config with preferred `JWS algorithm`: } ``` +A token provider can also support a static "systemTime", i.e. the time for when the token is issued (`iat` claim) if you have tests that require a specific time. +The following configuration will set the system time to `2020-01-21T00:00:00Z`: + +```json +{ + "tokenProvider" : { + "systemTime" : "2020-01-21T00:00:00Z" + } +} +``` + | Property | Description | |----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `interactiveLogin` | `true` or `false`, enables login screen when redirecting to server `/authorize` endpoint | @@ -294,7 +320,7 @@ add this to your config with preferred `JWS algorithm`: | `httpServer` | A string identifying the httpserver to use. Must match one of the following enum values: `MockWebServerWrapper` or `NettyWrapper` | | `tokenCallbacks` | A list of [`RequestMappingTokenCallback`](src/main/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenCallback.kt) that lets you specify which token claims to return when a token request matches the specified condition. | -*From the JSON example above:* +*From the first JSON example above:* A token request to `http://localhost:8080/issuer1/token` with parameter `scope` equal to `scope1` will match the first `tokenCallback`: diff --git a/src/main/kotlin/no/nav/security/mock/oauth2/OAuth2Config.kt b/src/main/kotlin/no/nav/security/mock/oauth2/OAuth2Config.kt index 84e9c8ed..7cab5ac5 100644 --- a/src/main/kotlin/no/nav/security/mock/oauth2/OAuth2Config.kt +++ b/src/main/kotlin/no/nav/security/mock/oauth2/OAuth2Config.kt @@ -19,6 +19,7 @@ import no.nav.security.mock.oauth2.token.OAuth2TokenCallback import no.nav.security.mock.oauth2.token.OAuth2TokenProvider import no.nav.security.mock.oauth2.token.RequestMappingTokenCallback import java.io.File +import java.time.Instant data class OAuth2Config @JvmOverloads @@ -37,6 +38,7 @@ data class OAuth2Config class OAuth2TokenProviderDeserializer : JsonDeserializer() { data class ProviderConfig( val keyProvider: KeyProviderConfig?, + val systemTime: String?, ) data class KeyProviderConfig( @@ -60,11 +62,17 @@ data class OAuth2Config listOf(JWK.parse(it)) } ?: emptyList() + val systemTime = + config.systemTime?.let { + Instant.parse(it) + } + return OAuth2TokenProvider( KeyProvider( jwks, config.keyProvider?.algorithm ?: JWSAlgorithm.RS256.name, ), + systemTime, ) } } diff --git a/src/main/kotlin/no/nav/security/mock/oauth2/StandaloneMockOAuth2Server.kt b/src/main/kotlin/no/nav/security/mock/oauth2/StandaloneMockOAuth2Server.kt index 8f89e076..4f46796a 100644 --- a/src/main/kotlin/no/nav/security/mock/oauth2/StandaloneMockOAuth2Server.kt +++ b/src/main/kotlin/no/nav/security/mock/oauth2/StandaloneMockOAuth2Server.kt @@ -1,16 +1,16 @@ package no.nav.security.mock.oauth2 import ch.qos.logback.classic.ClassicConstants -import java.io.File -import java.io.FileNotFoundException -import java.net.InetAddress -import java.net.InetSocketAddress import no.nav.security.mock.oauth2.StandaloneConfig.hostname import no.nav.security.mock.oauth2.StandaloneConfig.oauth2Config import no.nav.security.mock.oauth2.StandaloneConfig.port import no.nav.security.mock.oauth2.http.NettyWrapper import no.nav.security.mock.oauth2.http.OAuth2HttpResponse import no.nav.security.mock.oauth2.http.route +import java.io.File +import java.io.FileNotFoundException +import java.net.InetAddress +import java.net.InetSocketAddress object StandaloneConfig { const val JSON_CONFIG = "JSON_CONFIG" 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 d3de86bf..a8f12daf 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 @@ -23,6 +23,7 @@ class OAuth2TokenProvider @JvmOverloads constructor( private val keyProvider: KeyProvider = KeyProvider(), + val systemTime: Instant? = null, ) { @JvmOverloads fun publicJwkSet(issuerId: String = "default"): JWKSet { @@ -66,7 +67,7 @@ class OAuth2TokenProvider issuerUrl: HttpUrl, claimsSet: JWTClaimsSet, oAuth2TokenCallback: OAuth2TokenCallback, - ) = Instant.now().let { now -> + ) = systemTime.orNow().let { now -> JWTClaimsSet.Builder(claimsSet) .issuer(issuerUrl.toString()) .expirationTime(Date.from(now.plusSeconds(oAuth2TokenCallback.tokenExpiry()))) @@ -86,7 +87,7 @@ class OAuth2TokenProvider issuerId: String = "default", ): SignedJWT = JWTClaimsSet.Builder().let { builder -> - val now = Instant.now() + val now = systemTime.orNow() builder .issueTime(Date.from(now)) .notBeforeTime(Date.from(now)) @@ -150,7 +151,7 @@ class OAuth2TokenProvider additionalClaims: Map, expiry: Long, ) = JWTClaimsSet.Builder().let { builder -> - val now = Instant.now() + val now = systemTime.orNow() builder.subject(subject) .audience(audience) .issuer(issuerUrl.toString()) @@ -163,4 +164,6 @@ class OAuth2TokenProvider builder.addClaims(additionalClaims) builder.build() } + + private fun Instant?.orNow(): Instant = this ?: Instant.now() } diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/OAuth2ConfigTest.kt b/src/test/kotlin/no/nav/security/mock/oauth2/OAuth2ConfigTest.kt index 27355090..60e9f620 100644 --- a/src/test/kotlin/no/nav/security/mock/oauth2/OAuth2ConfigTest.kt +++ b/src/test/kotlin/no/nav/security/mock/oauth2/OAuth2ConfigTest.kt @@ -74,6 +74,21 @@ internal class OAuth2ConfigTest { }.message shouldContain "Unsupported algorithm: EdDSA" } + @Test + fun `create config from json with tokenprovider system time set`() { + val config = + OAuth2Config.fromJson( + """ + { + "tokenProvider" : { + "systemTime" : "2020-01-21T00:00:00Z" + } + } + """.trimIndent(), + ) + config.tokenProvider.systemTime shouldBe Instant.parse("2020-01-21T00:00:00Z") + } + @Test fun `create NettyWrapper with https enabled and provided keystore`() { val server = OAuth2Config.fromJson(nettyWithProvidedKeystore).httpServer as NettyWrapper diff --git a/src/test/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenCallbackTest.kt b/src/test/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenCallbackTest.kt index 00581e69..368c83fc 100644 --- a/src/test/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenCallbackTest.kt +++ b/src/test/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenCallbackTest.kt @@ -161,7 +161,6 @@ internal class OAuth2TokenCallbackTest { it.addClaims(tokenRequest) shouldContainAll mapOf("tid" to "test-tid") } } - } private fun authCodeRequest(vararg formParams: Pair) = 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 a1b1c873..8094023e 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,6 +16,9 @@ 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.Instant +import java.time.temporal.ChronoUnit +import java.util.Date internal class OAuth2TokenProviderRSATest { private val tokenProvider = OAuth2TokenProvider() @@ -94,6 +97,27 @@ internal class OAuth2TokenProviderRSATest { } } + @Test + fun `token should have issuedAt set to systemTime if set, otherwise use now()`() { + 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()) + } + } + private fun idToken(issuerUrl: String): SignedJWT = tokenProvider.idToken( tokenRequest =