Skip to content

Commit 4e9e5a3

Browse files
authored
feat(tokenprovider): support setting a static systemtime (#668)
* tokens will be issued with this time if set
1 parent 68664e8 commit 4e9e5a3

File tree

7 files changed

+84
-9
lines changed

7 files changed

+84
-9
lines changed

README.md

+27-1
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ fun loginWithIdTokenForAcrClaimEqualsLevel4() {
175175
}
176176
~~~
177177

178+
178179
##### Testing an API requiring access_token (e.g. a signed JWT)
179180

180181
```kotlin
@@ -183,6 +184,20 @@ val token: SignedJWT = oAuth2Server.issueToken(issuerId, "someclientid", Default
183184
val request = // ....
184185
request.addHeader("Authorization", "Bearer ${token.serialize()}")
185186
```
187+
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:
188+
189+
```kotlin
190+
@Test
191+
fun testWithSpecificTime() {
192+
val server = MockOAuth2Server(
193+
config = OAuth2Config(
194+
tokenProvider = OAuth2TokenProvider(systemTime = Instant.parse("2020-01-21T00:00:00Z")
195+
)
196+
)
197+
val token = server.issueToken(issuerId = "issuer1")
198+
// do whatever token testing you need to do here and assert the token has iat=2020-01-21T00:00:00Z
199+
}
200+
```
186201

187202
##### More examples
188203

@@ -285,6 +300,17 @@ add this to your config with preferred `JWS algorithm`:
285300
}
286301
```
287302
303+
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.
304+
The following configuration will set the system time to `2020-01-21T00:00:00Z`:
305+
306+
```json
307+
{
308+
"tokenProvider" : {
309+
"systemTime" : "2020-01-21T00:00:00Z"
310+
}
311+
}
312+
```
313+
288314
| Property | Description |
289315
|----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
290316
| `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`:
294320
| `httpServer` | A string identifying the httpserver to use. Must match one of the following enum values: `MockWebServerWrapper` or `NettyWrapper` |
295321
| `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. |
296322
297-
*From the JSON example above:*
323+
*From the first JSON example above:*
298324
299325
A token request to `http://localhost:8080/issuer1/token` with parameter `scope` equal to `scope1` will match the first `tokenCallback`:
300326

src/main/kotlin/no/nav/security/mock/oauth2/OAuth2Config.kt

+8
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import no.nav.security.mock.oauth2.token.OAuth2TokenCallback
1919
import no.nav.security.mock.oauth2.token.OAuth2TokenProvider
2020
import no.nav.security.mock.oauth2.token.RequestMappingTokenCallback
2121
import java.io.File
22+
import java.time.Instant
2223

2324
data class OAuth2Config
2425
@JvmOverloads
@@ -37,6 +38,7 @@ data class OAuth2Config
3738
class OAuth2TokenProviderDeserializer : JsonDeserializer<OAuth2TokenProvider>() {
3839
data class ProviderConfig(
3940
val keyProvider: KeyProviderConfig?,
41+
val systemTime: String?,
4042
)
4143

4244
data class KeyProviderConfig(
@@ -60,11 +62,17 @@ data class OAuth2Config
6062
listOf(JWK.parse(it))
6163
} ?: emptyList()
6264

65+
val systemTime =
66+
config.systemTime?.let {
67+
Instant.parse(it)
68+
}
69+
6370
return OAuth2TokenProvider(
6471
KeyProvider(
6572
jwks,
6673
config.keyProvider?.algorithm ?: JWSAlgorithm.RS256.name,
6774
),
75+
systemTime,
6876
)
6977
}
7078
}

src/main/kotlin/no/nav/security/mock/oauth2/StandaloneMockOAuth2Server.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
package no.nav.security.mock.oauth2
22

33
import ch.qos.logback.classic.ClassicConstants
4-
import java.io.File
5-
import java.io.FileNotFoundException
6-
import java.net.InetAddress
7-
import java.net.InetSocketAddress
84
import no.nav.security.mock.oauth2.StandaloneConfig.hostname
95
import no.nav.security.mock.oauth2.StandaloneConfig.oauth2Config
106
import no.nav.security.mock.oauth2.StandaloneConfig.port
117
import no.nav.security.mock.oauth2.http.NettyWrapper
128
import no.nav.security.mock.oauth2.http.OAuth2HttpResponse
139
import no.nav.security.mock.oauth2.http.route
10+
import java.io.File
11+
import java.io.FileNotFoundException
12+
import java.net.InetAddress
13+
import java.net.InetSocketAddress
1414

1515
object StandaloneConfig {
1616
const val JSON_CONFIG = "JSON_CONFIG"

src/main/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenProvider.kt

+6-3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class OAuth2TokenProvider
2323
@JvmOverloads
2424
constructor(
2525
private val keyProvider: KeyProvider = KeyProvider(),
26+
val systemTime: Instant? = null,
2627
) {
2728
@JvmOverloads
2829
fun publicJwkSet(issuerId: String = "default"): JWKSet {
@@ -66,7 +67,7 @@ class OAuth2TokenProvider
6667
issuerUrl: HttpUrl,
6768
claimsSet: JWTClaimsSet,
6869
oAuth2TokenCallback: OAuth2TokenCallback,
69-
) = Instant.now().let { now ->
70+
) = systemTime.orNow().let { now ->
7071
JWTClaimsSet.Builder(claimsSet)
7172
.issuer(issuerUrl.toString())
7273
.expirationTime(Date.from(now.plusSeconds(oAuth2TokenCallback.tokenExpiry())))
@@ -86,7 +87,7 @@ class OAuth2TokenProvider
8687
issuerId: String = "default",
8788
): SignedJWT =
8889
JWTClaimsSet.Builder().let { builder ->
89-
val now = Instant.now()
90+
val now = systemTime.orNow()
9091
builder
9192
.issueTime(Date.from(now))
9293
.notBeforeTime(Date.from(now))
@@ -150,7 +151,7 @@ class OAuth2TokenProvider
150151
additionalClaims: Map<String, Any>,
151152
expiry: Long,
152153
) = JWTClaimsSet.Builder().let { builder ->
153-
val now = Instant.now()
154+
val now = systemTime.orNow()
154155
builder.subject(subject)
155156
.audience(audience)
156157
.issuer(issuerUrl.toString())
@@ -163,4 +164,6 @@ class OAuth2TokenProvider
163164
builder.addClaims(additionalClaims)
164165
builder.build()
165166
}
167+
168+
private fun Instant?.orNow(): Instant = this ?: Instant.now()
166169
}

src/test/kotlin/no/nav/security/mock/oauth2/OAuth2ConfigTest.kt

+15
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,21 @@ internal class OAuth2ConfigTest {
7474
}.message shouldContain "Unsupported algorithm: EdDSA"
7575
}
7676

77+
@Test
78+
fun `create config from json with tokenprovider system time set`() {
79+
val config =
80+
OAuth2Config.fromJson(
81+
"""
82+
{
83+
"tokenProvider" : {
84+
"systemTime" : "2020-01-21T00:00:00Z"
85+
}
86+
}
87+
""".trimIndent(),
88+
)
89+
config.tokenProvider.systemTime shouldBe Instant.parse("2020-01-21T00:00:00Z")
90+
}
91+
7792
@Test
7893
fun `create NettyWrapper with https enabled and provided keystore`() {
7994
val server = OAuth2Config.fromJson(nettyWithProvidedKeystore).httpServer as NettyWrapper

src/test/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenCallbackTest.kt

-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,6 @@ internal class OAuth2TokenCallbackTest {
161161
it.addClaims(tokenRequest) shouldContainAll mapOf("tid" to "test-tid")
162162
}
163163
}
164-
165164
}
166165

167166
private fun authCodeRequest(vararg formParams: Pair<String, String>) =

src/test/kotlin/no/nav/security/mock/oauth2/token/OAuth2TokenProviderRSATest.kt

+24
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
1616
import org.junit.jupiter.api.Test
1717
import org.junit.jupiter.params.ParameterizedTest
1818
import org.junit.jupiter.params.provider.ValueSource
19+
import java.time.Instant
20+
import java.time.temporal.ChronoUnit
21+
import java.util.Date
1922

2023
internal class OAuth2TokenProviderRSATest {
2124
private val tokenProvider = OAuth2TokenProvider()
@@ -94,6 +97,27 @@ internal class OAuth2TokenProviderRSATest {
9497
}
9598
}
9699

100+
@Test
101+
fun `token should have issuedAt set to systemTime if set, otherwise use now()`() {
102+
val yesterDay = Instant.now().minus(1, ChronoUnit.DAYS)
103+
val tokenProvider = OAuth2TokenProvider(systemTime = yesterDay)
104+
105+
tokenProvider.exchangeAccessToken(
106+
tokenRequest =
107+
nimbusTokenRequest(
108+
"id",
109+
"grant_type" to GrantType.CLIENT_CREDENTIALS.value,
110+
"scope" to "scope1",
111+
),
112+
issuerUrl = "http://default_if_not_overridden".toHttpUrl(),
113+
claimsSet = tokenProvider.jwt(mapOf()).jwtClaimsSet,
114+
oAuth2TokenCallback = DefaultOAuth2TokenCallback(),
115+
).asClue {
116+
it.jwtClaimsSet.issueTime shouldBe Date.from(tokenProvider.systemTime)
117+
println(it.serialize())
118+
}
119+
}
120+
97121
private fun idToken(issuerUrl: String): SignedJWT =
98122
tokenProvider.idToken(
99123
tokenRequest =

0 commit comments

Comments
 (0)