Skip to content

Commit

Permalink
støtte for on-behalf-of-token
Browse files Browse the repository at this point in the history
  • Loading branch information
davidsteinsland committed Jan 9, 2024
1 parent 829520a commit 864ddaf
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,13 @@ class AzureTokenClient(
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.registerModule(JavaTimeModule())
) : AzureTokenProvider {
override fun bearerToken(scope: String): AzureToken {
val body = requestToken(scope) ?: throw AzureClientException("Tom responskropp fra Azure")

override fun onBehalfOfToken(scope: String, token: String) =
håndterTokenRespons(requestOnBehalfOfToken(scope, token))
override fun bearerToken(scope: String) =
håndterTokenRespons(requestBearerToken(scope))

private fun håndterTokenRespons(body: String): AzureToken {
val tokenResponse = deserializeToken(body) ?: kastExceptionVedFeil(body)
return AzureToken(tokenResponse.token, tokenResponse.expirationTime)
}
Expand Down Expand Up @@ -53,22 +58,34 @@ class AzureTokenClient(
}
}

private fun requestToken(scope: String): String? {
private fun requestBearerToken(scope: String) = requestToken(buildTokenRequestBody(scope))
private fun requestOnBehalfOfToken(scope: String, token: String) = requestToken(buildOnBehalfOfRequestBody(scope, token))

private fun requestToken(body: String): String {
val request = HttpRequest.newBuilder(tokenEndpoint)
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(buildRequestBody(scope)))
.POST(HttpRequest.BodyPublishers.ofString(body))
.build()

val response = client.send(request, HttpResponse.BodyHandlers.ofString())
return response.body()
return response.body() ?: throw AzureClientException("Tom responskropp fra Azure")
}
private fun buildTokenRequestBody(scope: String): String {
return buildRequestBody(scope, "client_credentials")
}
private fun buildOnBehalfOfRequestBody(scope: String, token: String): String {
return buildRequestBody(scope, "urn:ietf:params:oauth:grant-type:jwt-bearer", mapOf(
"requested_token_use" to "on_behalf_of",
"assertion" to token
))
}
private fun buildRequestBody(scope: String): String {
private fun buildRequestBody(scope: String, grantType: String, additionalPayload: Map<String, String> = emptyMap()): String {
val standardPayload = mapOf(
"client_id" to clientId,
"scope" to scope,
"grant_type" to "client_credentials"
"grant_type" to grantType
)
return (standardPayload + authMethod.requestParameters()).entries
return (standardPayload + additionalPayload + authMethod.requestParameters()).entries
.joinToString(separator = "&") { (k, v) -> "$k=$v" }
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.github.navikt.tbd_libs.azure

interface AzureTokenProvider {
fun onBehalfOfToken(scope: String, token: String): AzureToken
fun bearerToken(scope: String): AzureToken
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
package com.github.navikt.tbd_libs.azure

import java.security.MessageDigest
import java.util.concurrent.ConcurrentHashMap

class InMemoryAzureTokenCache(private val other: AzureTokenProvider) : AzureTokenProvider {
private val cache = ConcurrentHashMap<String, AzureToken>()

override fun bearerToken(scope: String): AzureToken {
return cachedToken(scope) ?: lagreNy(scope)
}
override fun onBehalfOfToken(scope: String, token: String) =
fraCacheEllerHentNy(oboCacheKey(token, scope)) { other.onBehalfOfToken(scope, token) }

override fun bearerToken(scope: String) =
fraCacheEllerHentNy(scope) { other.bearerToken(scope) }

private fun fraCacheEllerHentNy(cacheKey: String, hentNy: () -> AzureToken) =
cachedToken(cacheKey) ?: lagreNy(cacheKey, hentNy())

private fun cachedToken(scope: String) = cache[scope]?.takeUnless { it.isExpired }
private fun lagreNy(scope: String) = other.bearerToken(scope).also { cache[scope] = it }
private fun cachedToken(cacheKey: String) = cache[cacheKey]?.takeUnless { it.isExpired }
private fun lagreNy(cacheKey: String, token: AzureToken) = token.also { cache[cacheKey] = it }


@OptIn(ExperimentalStdlibApi::class)
private fun oboCacheKey(token: String, scope: String): String {
val nøkkel = "${token}${scope}".toByteArray()
val md = MessageDigest.getInstance("SHA-256")
val digest = md.digest(nøkkel)
return digest.toHexString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@ package com.github.navikt.tbd_libs.azure
import com.github.navikt.tbd_libs.azure.MockHttpClient.Companion.mockOkResponse
import com.github.navikt.tbd_libs.azure.MockHttpClient.Companion.verifiserJwtRequestBody
import com.github.navikt.tbd_libs.azure.MockHttpClient.Companion.verifiserClientSecretRequestBody
import com.github.navikt.tbd_libs.azure.MockHttpClient.Companion.verifiserOBOClientSecretRequestBody
import com.github.navikt.tbd_libs.azure.MockHttpClient.Companion.verifiserOBOJwtRequestBody
import com.github.navikt.tbd_libs.azure.MockHttpClient.Companion.verifiserPOST
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.net.URI
import java.net.http.HttpClient
import java.time.LocalDateTime

class AzureTokenClientTest {
private companion object {
private val tokenEndpoint = URI("http://token-service/v1/token")
private const val CLIENT_ID = "test-client-id"
private const val SECRET_VALUE = "foobar"
private const val SCOPE = "testscope"
private const val OTHER_TOKEN = "eyJ0eXAiOiJKV1QiLCJub25jZSI6IkFRQUJBQUFBQUFCbm..."
}

@Test
Expand All @@ -35,9 +39,33 @@ class AzureTokenClientTest {
verifiserJwtRequestBody(httpClient, CLIENT_ID, SCOPE, SECRET_VALUE)
}

@Test
fun `fetch obo token from client secret`() {
val authMethod = AzureAuthMethod.Secret(SECRET_VALUE)
val (httpClient, result) = requestOboToken(authMethod)
assertEquals("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik1uQ19WWmNBVGZNNXBP...", result.token)
verifiserPOST(httpClient)
verifiserOBOClientSecretRequestBody(httpClient, CLIENT_ID, SCOPE, OTHER_TOKEN, SECRET_VALUE)
}

@Test
fun `fetch obo token from jwt`() {
val authMethod = AzureAuthMethod.Jwt(SECRET_VALUE)
val (httpClient, result) = requestOboToken(authMethod)
assertEquals("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik1uQ19WWmNBVGZNNXBP...", result.token)
verifiserPOST(httpClient)
verifiserOBOJwtRequestBody(httpClient, CLIENT_ID, SCOPE, OTHER_TOKEN, SECRET_VALUE)
}

private fun requestToken(authMethod: AzureAuthMethod): Pair<HttpClient, AzureToken> {
val httpClient = mockOkResponse()
val client = AzureTokenClient(tokenEndpoint, CLIENT_ID, authMethod, httpClient)
return httpClient to client.bearerToken(SCOPE)
}

private fun requestOboToken(authMethod: AzureAuthMethod): Pair<HttpClient, AzureToken> {
val httpClient = mockOkResponse()
val client = AzureTokenClient(tokenEndpoint, CLIENT_ID, authMethod, httpClient)
return httpClient to client.onBehalfOfToken(SCOPE, OTHER_TOKEN)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.github.navikt.tbd_libs.azure
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.Assertions.assertSame
import org.junit.jupiter.api.Test
import java.time.LocalDateTime

Expand Down Expand Up @@ -38,4 +39,26 @@ class InMemoryAzureTokenCacheTest {
cache.bearerToken(scope) // andre kall, går ikke via cache
verify(exactly = 2) { mock.bearerToken(scope) }
}

@Test
fun `henter obo token når cache er tom`() {
val mock = mockk<AzureTokenProvider>(relaxed = true)
val cache = InMemoryAzureTokenCache(mock)
val scope = "testscope"
cache.onBehalfOfToken(scope, "ett token")
cache.onBehalfOfToken(scope, "to token")
verify(exactly = 2) { mock.onBehalfOfToken(scope, any()) }
}

@Test
fun `henter obo token fra cache`() {
val mock = mockk<AzureTokenProvider>(relaxed = true)
val cache = InMemoryAzureTokenCache(mock)
val scope = "testscope"
val result1 = cache.onBehalfOfToken(scope, "ett token")
cache.onBehalfOfToken(scope, "to token")
val result2 = cache.onBehalfOfToken(scope, "ett token")
verify(exactly = 1) { mock.onBehalfOfToken(scope, "ett token") }
assertSame(result1, result2)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,21 @@ class MockHttpClient {
}
}

fun verifiserJwtRequestBody(httpClient: HttpClient, clientId: String, scope: String, certificate: String) {
fun verifiserJwtRequestBody(httpClient: HttpClient, clientId: String, scope: String, jwt: String) {
verifiserRequestBody(httpClient) { requestBody ->
requestBody == "client_id=$clientId&scope=$scope&grant_type=client_credentials&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=$certificate"
requestBody == "client_id=$clientId&scope=$scope&grant_type=client_credentials&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=$jwt"
}
}

fun verifiserOBOClientSecretRequestBody(httpClient: HttpClient, clientId: String, scope: String, otherToken: String, clientSecret: String) {
verifiserRequestBody(httpClient) { requestBody ->
requestBody == "client_id=$clientId&scope=$scope&grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&requested_token_use=on_behalf_of&assertion=$otherToken&client_secret=$clientSecret"
}
}

fun verifiserOBOJwtRequestBody(httpClient: HttpClient, clientId: String, scope: String, otherToken: String, jwt: String) {
verifiserRequestBody(httpClient) { requestBody ->
requestBody == "client_id=$clientId&scope=$scope&grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&requested_token_use=on_behalf_of&assertion=$otherToken&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=$jwt"
}
}

Expand Down

0 comments on commit 864ddaf

Please sign in to comment.