Skip to content

Commit 26be300

Browse files
committed
Added <issuer>/testutils/* paths
There are two testutils endpoints added. GET <issuer>/testutils/fulljwkssecret gives you the full backing JWK POST <issuer>/testutils/sigclaims gives you back a Base64 encoded signed token with the provided claims and expiry time. The endpoint expects JSON with a 'claims' field, and an optional 'expiry' field that contains a string compatible with the java.time.Duration format, like 'PT1H' Both endpoints are introduced to make it possible to create valid, custom tokens for testing purposes.
1 parent 8ee8441 commit 26be300

File tree

4 files changed

+84
-1
lines changed

4 files changed

+84
-1
lines changed

src/main/kotlin/no/nav/security/mock/oauth2/extensions/HttpUrlExtensions.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ object OAuth2Endpoints {
2323
const val USER_INFO = "/userinfo"
2424
const val DEBUGGER = "/debugger"
2525
const val DEBUGGER_CALLBACK = "/debugger/callback"
26+
const val TESTUTILS_JWKS = "/testutils/fulljwkssecret"
27+
const val TESTUTILS_SIGN = "/testutils/signclaims"
2628

2729
val all = listOf(
2830
OAUTH2_WELL_KNOWN,
@@ -33,7 +35,9 @@ object OAuth2Endpoints {
3335
JWKS,
3436
USER_INFO,
3537
DEBUGGER,
36-
DEBUGGER_CALLBACK
38+
DEBUGGER_CALLBACK,
39+
TESTUTILS_JWKS,
40+
TESTUTILS_SIGN
3741
)
3842
}
3943

src/main/kotlin/no/nav/security/mock/oauth2/http/OAuth2HttpRequestHandler.kt

+54
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package no.nav.security.mock.oauth2.http
22

3+
import com.fasterxml.jackson.core.JacksonException
4+
import com.fasterxml.jackson.databind.node.ObjectNode
5+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
36
import com.nimbusds.oauth2.sdk.ErrorObject
47
import com.nimbusds.oauth2.sdk.GeneralException
58
import com.nimbusds.oauth2.sdk.GrantType
@@ -20,6 +23,8 @@ import no.nav.security.mock.oauth2.extensions.OAuth2Endpoints.END_SESSION
2023
import no.nav.security.mock.oauth2.extensions.OAuth2Endpoints.JWKS
2124
import no.nav.security.mock.oauth2.extensions.OAuth2Endpoints.OAUTH2_WELL_KNOWN
2225
import no.nav.security.mock.oauth2.extensions.OAuth2Endpoints.OIDC_WELL_KNOWN
26+
import no.nav.security.mock.oauth2.extensions.OAuth2Endpoints.TESTUTILS_JWKS
27+
import no.nav.security.mock.oauth2.extensions.OAuth2Endpoints.TESTUTILS_SIGN
2328
import no.nav.security.mock.oauth2.extensions.OAuth2Endpoints.TOKEN
2429
import no.nav.security.mock.oauth2.extensions.isPrompt
2530
import no.nav.security.mock.oauth2.extensions.issuerId
@@ -82,6 +87,7 @@ class OAuth2HttpRequestHandler(private val config: OAuth2Config) {
8287
endSession()
8388
userInfo(config.tokenProvider)
8489
preflight()
90+
testutils()
8591
get("/favicon.ico") { OAuth2HttpResponse(status = 200) }
8692
attach(debuggerRequestHandler)
8793
}
@@ -157,4 +163,52 @@ class OAuth2HttpRequestHandler(private val config: OAuth2Config) {
157163
config.tokenCallbacks.firstOrNull { it.issuerId() == issuerId } ?: DefaultOAuth2TokenCallback(issuerId = issuerId)
158164
}
159165
}
166+
167+
private fun Route.Builder.testutils() = apply {
168+
169+
get(TESTUTILS_JWKS) {
170+
val jwk = config.tokenProvider.fullJwkSet(it.url.issuerId())
171+
json(jwk.toJSONObject(false))
172+
}
173+
post(TESTUTILS_SIGN) {
174+
val issuerId = it.url.issuerId()
175+
val om = jacksonObjectMapper()
176+
try {
177+
val parsedbody = om.readValue(it.body, ObjectNode::class.java)
178+
179+
val claimsMap = when {
180+
parsedbody.has("claims") -> {
181+
val claims = mutableMapOf<String, String>()
182+
parsedbody.get("claims").fields().forEach {
183+
claims[it.key] = it.value.toString()
184+
}
185+
claims.toMap()
186+
}
187+
else -> mapOf()
188+
}
189+
val expiryDuration = when {
190+
parsedbody.has("expiry") -> {
191+
val expirytxt = parsedbody.get("expiry").asText("PT1H")
192+
java.time.Duration.parse(expirytxt)
193+
}
194+
else -> java.time.Duration.parse("PT1H")
195+
}
196+
val signedJwt = config.tokenProvider.jwt(claimsMap, expiryDuration, issuerId)
197+
198+
OAuth2HttpResponse(status = 200, body = (signedJwt.serialize()))
199+
200+
} catch (ex: JacksonException) {
201+
OAuth2HttpResponse(status = 400, body = ex.message.toString())
202+
} catch (ex: java.time.format.DateTimeParseException) {
203+
val outputStr = ex.message.toString() + listOf(
204+
"\n" + "`" + ex.parsedString + "`",
205+
"\nExamples of java.time.Duration string format:\n",
206+
"P1D (1 day), ",
207+
"PT1H (1 hour), ",
208+
"P0DT0H10M30S (0 days, 0 hours, 10 minutes, 30 seconds)",
209+
).joinToString("")
210+
OAuth2HttpResponse(status = 400, body = outputStr)
211+
}
212+
}
213+
}
160214
}

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

+5
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,9 @@ class OAuth2TokenProvider @JvmOverloads constructor(
145145
builder.addClaims(additionalClaims)
146146
builder.build()
147147
}
148+
149+
fun fullJwkSet(issuerId: String): JWKSet {
150+
val jwk = keyProvider.signingKey(issuerId)
151+
return JWKSet(jwk)
152+
}
148153
}

src/test/kotlin/no/nav/security/mock/oauth2/http/OAuth2HttpRequestHandlerTest.kt

+20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package no.nav.security.mock.oauth2.http
22

3+
import com.fasterxml.jackson.databind.node.ArrayNode
4+
import com.fasterxml.jackson.databind.node.ObjectNode
5+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
36
import io.kotest.assertions.asClue
47
import io.kotest.matchers.shouldBe
58
import no.nav.security.mock.oauth2.OAuth2Config
@@ -9,6 +12,8 @@ import no.nav.security.mock.oauth2.extensions.OAuth2Endpoints.END_SESSION
912
import no.nav.security.mock.oauth2.extensions.OAuth2Endpoints.JWKS
1013
import no.nav.security.mock.oauth2.extensions.OAuth2Endpoints.OAUTH2_WELL_KNOWN
1114
import no.nav.security.mock.oauth2.extensions.OAuth2Endpoints.OIDC_WELL_KNOWN
15+
import no.nav.security.mock.oauth2.extensions.OAuth2Endpoints.TESTUTILS_JWKS
16+
import no.nav.security.mock.oauth2.extensions.OAuth2Endpoints.TESTUTILS_SIGN
1217
import no.nav.security.mock.oauth2.extensions.OAuth2Endpoints.TOKEN
1318
import no.nav.security.mock.oauth2.extensions.OAuth2Endpoints.USER_INFO
1419
import no.nav.security.mock.oauth2.token.OAuth2TokenProvider
@@ -64,6 +69,21 @@ internal class OAuth2HttpRequestHandlerTest {
6469
expectedResponse = OAuth2HttpResponse(status = 302)
6570
),
6671
request(path = "/favicon.ico", method = "GET", expectedResponse = OAuth2HttpResponse(status = 200)),
72+
request(path= "/issuer1$TESTUTILS_JWKS", method="GET", expectedResponse = OAuth2HttpResponse(status = 200)),
73+
request(
74+
path= "/issuer1$TESTUTILS_SIGN",
75+
method="POST",
76+
body= (jacksonObjectMapper().createObjectNode().apply {
77+
this.put("expiry","PT1H")
78+
this.set<ObjectNode>("claims", jacksonObjectMapper().createObjectNode().apply {
79+
this.set<ArrayNode>("groups", jacksonObjectMapper().createArrayNode().apply{
80+
this.add("grp1")
81+
this.add("grp2")
82+
})
83+
})
84+
}).toString(),
85+
expectedResponse = OAuth2HttpResponse(status = 200)
86+
)
6787
)
6888

6989
private fun request(path: String, method: String, headers: Headers = Headers.headersOf(), body: String? = null, expectedResponse: OAuth2HttpResponse) =

0 commit comments

Comments
 (0)