Skip to content

Commit 4aab3a5

Browse files
tommytroenybelMekk
andauthored
feat: support objects and lists in request mapping claims (#699)
* fixes #683 and #674 Co-authored-by: ybelmekk <[email protected]>
1 parent f662d5f commit 4aab3a5

File tree

5 files changed

+136
-33
lines changed

5 files changed

+136
-33
lines changed

build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ dependencies {
6666
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
6767
implementation("org.freemarker:freemarker:$freemarkerVersion")
6868
implementation("org.bouncycastle:bcpkix-jdk18on:$bouncyCastleVersion")
69+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
6970
testImplementation("org.assertj:assertj-core:$assertjVersion")
7071
testImplementation("org.junit.jupiter:junit-jupiter-api:$junitJupiterVersion")
7172
testImplementation("org.junit.jupiter:junit-jupiter-params:$junitJupiterVersion")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package no.nav.security.mock.oauth2.extensions
2+
3+
/**
4+
* Replaces all template values denoted with ${key} in a map with the corresponding values from the templates map.
5+
*
6+
* @param templates a map of template values
7+
* @return a new map with all template values replaced
8+
*/
9+
fun Map<String, Any>.replaceValues(templates: Map<String, Any>): Map<String, Any> {
10+
fun replaceTemplateString(
11+
value: String,
12+
templates: Map<String, Any>,
13+
): String {
14+
val regex = Regex("""\$\{(\w+)\}""")
15+
return regex.replace(value) { matchResult ->
16+
val key = matchResult.groupValues[1]
17+
templates[key]?.toString() ?: matchResult.value
18+
}
19+
}
20+
21+
fun replaceValue(value: Any): Any {
22+
return when (value) {
23+
is String -> replaceTemplateString(value, templates)
24+
is List<*> -> value.map { it?.let { replaceValue(it) } }
25+
is Map<*, *> -> value.mapValues { v -> v.value?.let { replaceValue(it) } }
26+
else -> value
27+
}
28+
}
29+
30+
return this.mapValues { replaceValue(it.value) }
31+
}

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

+9-33
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.nimbusds.oauth2.sdk.GrantType
55
import com.nimbusds.oauth2.sdk.TokenRequest
66
import no.nav.security.mock.oauth2.extensions.clientIdAsString
77
import no.nav.security.mock.oauth2.extensions.grantType
8+
import no.nav.security.mock.oauth2.extensions.replaceValues
89
import no.nav.security.mock.oauth2.extensions.scopesWithoutOidcScopes
910
import no.nav.security.mock.oauth2.extensions.tokenExchangeGrantOrNull
1011
import java.time.Duration
@@ -89,27 +90,14 @@ data class RequestMappingTokenCallback(
8990

9091
private fun List<RequestMapping>.getClaims(tokenRequest: TokenRequest): Map<String, Any> {
9192
val claims = firstOrNull { it.isMatch(tokenRequest) }?.claims ?: emptyMap()
92-
val customParameters = tokenRequest.customParameters.mapValues { (_, value) -> value.first() }
93-
val variables =
94-
if (tokenRequest.grantType() == GrantType.CLIENT_CREDENTIALS) {
95-
customParameters + ("clientId" to tokenRequest.clientIdAsString())
96-
} else {
97-
customParameters
98-
}
99-
return claims.mapValues { (_, value) ->
100-
when (value) {
101-
is String -> replaceVariables(value, variables)
102-
is List<*> ->
103-
value.map { v ->
104-
if (v is String) {
105-
replaceVariables(v, variables)
106-
} else {
107-
v
108-
}
109-
}
110-
else -> value
111-
}
112-
}
93+
val templateParams = tokenRequest.toHTTPRequest().bodyAsFormParameters.mapValues { it.value.joinToString(separator = " ") }
94+
95+
// in case client_id is not set as form param but as basic auth, we add it to the template params in two different formats for backwards compatibility
96+
return claims.replaceValues(
97+
templateParams +
98+
mapOf("clientId" to tokenRequest.clientIdAsString()) +
99+
mapOf("client_id" to tokenRequest.clientIdAsString()),
100+
)
113101
}
114102

115103
private inline fun <reified T> List<RequestMapping>.getClaimOrNull(
@@ -118,18 +106,6 @@ data class RequestMappingTokenCallback(
118106
): T? = getClaims(tokenRequest)[key] as? T
119107

120108
private fun List<RequestMapping>.getTypeHeader(tokenRequest: TokenRequest) = firstOrNull { it.isMatch(tokenRequest) }?.typeHeader ?: JOSEObjectType.JWT.type
121-
122-
private fun replaceVariables(
123-
input: String,
124-
replacements: Map<String, String>,
125-
): String {
126-
val pattern = Regex("""\$\{(\w+)}""")
127-
return pattern.replace(input) { result ->
128-
val variableName = result.groupValues[1]
129-
val replacement = replacements[variableName]
130-
replacement ?: result.value
131-
}
132-
}
133109
}
134110

135111
data class RequestMapping(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package no.nav.security.mock.oauth2.extensions
2+
3+
import io.kotest.assertions.asClue
4+
import io.kotest.matchers.shouldBe
5+
import org.junit.jupiter.api.Test
6+
7+
class TemplateTest {
8+
@Test
9+
fun `template values in map should be replaced`() {
10+
val templates =
11+
mapOf(
12+
"templateVal1" to "val1",
13+
"templateVal2" to "val2",
14+
"templateListVal" to "listVal1",
15+
)
16+
17+
mapOf(
18+
"object1" to mapOf("key1" to "\${templateVal1}"),
19+
"object2" to "\${templateVal2}",
20+
"nestedObject" to mapOf("nestedKey" to mapOf("nestedKeyAgain" to "\${templateVal2}")),
21+
"list1" to listOf("\${templateListVal}"),
22+
).replaceValues(templates).asClue {
23+
it["object1"] shouldBe mapOf("key1" to "val1")
24+
it["list1"] shouldBe listOf("listVal1")
25+
println(it)
26+
}
27+
}
28+
}

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

+67
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,73 @@ internal class OAuth2TokenCallbackTest {
124124
issuer1.typeHeader(grantTypeShouldMatch) shouldBe "JWT"
125125
}
126126
}
127+
128+
@Test
129+
fun `token request with custom parameters in token request should include claims with placeholder names`() {
130+
val request =
131+
clientCredentialsRequest(
132+
"scope" to "testscope:something another:scope",
133+
"mock_token_type" to "custom",
134+
)
135+
RequestMappingTokenCallback(
136+
issuerId = "issuer1",
137+
requestMappings =
138+
listOf(
139+
RequestMapping(
140+
requestParam = "scope",
141+
match = "testscope:.*",
142+
claims =
143+
mapOf(
144+
"sub" to "\${clientId}",
145+
"scope" to "\${scope}",
146+
"mock_token_type" to "\${mock_token_type}",
147+
),
148+
),
149+
),
150+
).addClaims(request).asClue {
151+
it shouldContainAll mapOf("sub" to clientId, "scope" to "testscope:something another:scope", "mock_token_type" to "custom")
152+
}
153+
}
154+
}
155+
156+
@Test
157+
fun `token request with custom parameters in token request should include claims with placeholder names`() {
158+
val request =
159+
clientCredentialsRequest(
160+
"mock_token_type" to "custom",
161+
"participantId" to "participantId",
162+
"actAs" to "actAs",
163+
"readAs" to "readAs",
164+
)
165+
RequestMappingTokenCallback(
166+
issuerId = "issuer1",
167+
requestMappings =
168+
listOf(
169+
RequestMapping(
170+
requestParam = "mock_token_type",
171+
match = "custom",
172+
claims =
173+
mapOf(
174+
"https://daml.com/ledger-api" to
175+
mapOf(
176+
"participantId" to "\${participantId}",
177+
"actAs" to listOf("\${actAs}"),
178+
"readAs" to listOf("\${readAs}"),
179+
),
180+
),
181+
),
182+
),
183+
).addClaims(request).asClue {
184+
it shouldContainAll
185+
mapOf(
186+
"https://daml.com/ledger-api" to
187+
mapOf(
188+
"participantId" to "participantId",
189+
"actAs" to listOf("actAs"),
190+
"readAs" to listOf("readAs"),
191+
),
192+
)
193+
}
127194
}
128195

129196
@Nested

0 commit comments

Comments
 (0)