Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tokenprovider): support setting a static system time #668

Merged
merged 1 commit into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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