Skip to content

Commit

Permalink
feat(tokenprovider): support setting a static systemtime (#668)
Browse files Browse the repository at this point in the history
* tokens will be issued with this time if set
  • Loading branch information
tommytroen authored Apr 18, 2024
1 parent 68664e8 commit 4e9e5a3
Show file tree
Hide file tree
Showing 7 changed files with 84 additions and 9 deletions.
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ fun loginWithIdTokenForAcrClaimEqualsLevel4() {
}
~~~


##### Testing an API requiring access_token (e.g. a signed JWT)

```kotlin
Expand All @@ -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

Expand Down Expand Up @@ -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 |
Expand All @@ -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`:
Expand Down
8 changes: 8 additions & 0 deletions src/main/kotlin/no/nav/security/mock/oauth2/OAuth2Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -37,6 +38,7 @@ data class OAuth2Config
class OAuth2TokenProviderDeserializer : JsonDeserializer<OAuth2TokenProvider>() {
data class ProviderConfig(
val keyProvider: KeyProviderConfig?,
val systemTime: String?,
)

data class KeyProviderConfig(
Expand All @@ -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,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class OAuth2TokenProvider
@JvmOverloads
constructor(
private val keyProvider: KeyProvider = KeyProvider(),
val systemTime: Instant? = null,
) {
@JvmOverloads
fun publicJwkSet(issuerId: String = "default"): JWKSet {
Expand Down Expand Up @@ -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())))
Expand All @@ -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))
Expand Down Expand Up @@ -150,7 +151,7 @@ class OAuth2TokenProvider
additionalClaims: Map<String, Any>,
expiry: Long,
) = JWTClaimsSet.Builder().let { builder ->
val now = Instant.now()
val now = systemTime.orNow()
builder.subject(subject)
.audience(audience)
.issuer(issuerUrl.toString())
Expand All @@ -163,4 +164,6 @@ class OAuth2TokenProvider
builder.addClaims(additionalClaims)
builder.build()
}

private fun Instant?.orNow(): Instant = this ?: Instant.now()
}
15 changes: 15 additions & 0 deletions src/test/kotlin/no/nav/security/mock/oauth2/OAuth2ConfigTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,6 @@ internal class OAuth2TokenCallbackTest {
it.addClaims(tokenRequest) shouldContainAll mapOf("tid" to "test-tid")
}
}

}

private fun authCodeRequest(vararg formParams: Pair<String, String>) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 =
Expand Down

0 comments on commit 4e9e5a3

Please sign in to comment.