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

Regex based matching in RequestMappingTokenCallback and request parameters used as variables inside claims #578

Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ open class DefaultOAuth2TokenCallback

data class RequestMappingTokenCallback(
val issuerId: String,
val requestMappings: Set<RequestMapping>,
val requestMappings: List<RequestMapping>,
val tokenExpiry: Long = Duration.ofHours(1).toSeconds(),
) : OAuth2TokenCallback {
override fun issuerId(): String = issuerId
Expand All @@ -89,31 +89,59 @@ data class RequestMappingTokenCallback(

override fun tokenExpiry(): Long = tokenExpiry

private fun Set<RequestMapping>.getClaims(tokenRequest: TokenRequest): Map<String, Any> {
private fun List<RequestMapping>.getClaims(tokenRequest: TokenRequest): Map<String, Any> {
val claims = firstOrNull { it.isMatch(tokenRequest) }?.claims ?: emptyMap()
return if (tokenRequest.grantType() == GrantType.CLIENT_CREDENTIALS && claims["sub"] == "\${clientId}") {
claims + ("sub" to tokenRequest.clientIdAsString())
} else {
claims
val customParameters = tokenRequest.customParameters.mapValues { (_, value) -> value.first() }
val variables =
if (tokenRequest.grantType() == GrantType.CLIENT_CREDENTIALS) {
customParameters + ("clientId" to tokenRequest.clientIdAsString())
} else {
customParameters
}
return claims.mapValues { (_, value) ->
when (value) {
is String -> replaceVariables(value, variables)
is List<*> ->
value.map { v ->
if (v is String) {
replaceVariables(v, variables)
} else {
v
}
}
else -> value
}
}
}

private inline fun <reified T> Set<RequestMapping>.getClaimOrNull(
private inline fun <reified T> List<RequestMapping>.getClaimOrNull(
tokenRequest: TokenRequest,
key: String,
): T? = getClaims(tokenRequest)[key] as? T

private fun Set<RequestMapping>.getTypeHeader(tokenRequest: TokenRequest) = firstOrNull { it.isMatch(tokenRequest) }?.typeHeader ?: JOSEObjectType.JWT.type
private fun List<RequestMapping>.getTypeHeader(tokenRequest: TokenRequest) = firstOrNull { it.isMatch(tokenRequest) }?.typeHeader ?: JOSEObjectType.JWT.type

private fun replaceVariables(
input: String,
replacements: Map<String, String>,
): String {
val pattern = Regex("""\$\{(\w+)}""")
return pattern.replace(input) { result ->
val variableName = result.groupValues[1]
val replacement = replacements[variableName]
replacement ?: result.value
}
}
}

data class RequestMapping(
private val requestParam: String,
private val match: String = "*",
private val match: String,
val claims: Map<String, Any> = emptyMap(),
val typeHeader: String = JOSEObjectType.JWT.type,
) {
fun isMatch(tokenRequest: TokenRequest): Boolean =
tokenRequest.toHTTPRequest().queryParameters[requestParam]?.any {
if (match != "*") it == match else true
match == "*" || match == it || match.toRegex().matchEntire(it) != null
} ?: false
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ internal class OAuth2TokenCallbackTest {
RequestMappingTokenCallback(
issuerId = "issuer1",
requestMappings =
setOf(
listOf(
RequestMapping(
requestParam = "scope",
match = "scope1",
Expand All @@ -39,6 +39,15 @@ internal class OAuth2TokenCallbackTest {
"custom" to "custom2",
),
),
RequestMapping(
requestParam = "audience",
match = "https://myapp.com/jwt/aud/.*",
claims =
mapOf(
"sub" to "\${clientId}",
"aud" to listOf("\${audience}"),
),
),
RequestMapping(
requestParam = "grant_type",
match = "authorization_code",
Expand Down Expand Up @@ -104,6 +113,17 @@ internal class OAuth2TokenCallbackTest {
issuer1.typeHeader(grantTypeShouldMatch) shouldBe "JWT"
}
}

@Test
fun `token request with request params matching requestmapping should return specific claims from callback with audience`() {
val grantTypeShouldMatch = clientCredentialsRequest("audience" to "https://myapp.com/jwt/aud/xxx")
assertSoftly {
issuer1.subject(grantTypeShouldMatch) shouldBe clientId
issuer1.audience(grantTypeShouldMatch) shouldBe listOf("https://myapp.com/jwt/aud/xxx")
issuer1.tokenExpiry() shouldBe 120
issuer1.typeHeader(grantTypeShouldMatch) shouldBe "JWT"
}
}
}

@Nested
Expand Down