diff --git a/azure-token-client/src/main/kotlin/com/github/navikt/tbd_libs/azure/AzureTokenClient.kt b/azure-token-client/src/main/kotlin/com/github/navikt/tbd_libs/azure/AzureTokenClient.kt index a51d38b..8c457b2 100644 --- a/azure-token-client/src/main/kotlin/com/github/navikt/tbd_libs/azure/AzureTokenClient.kt +++ b/azure-token-client/src/main/kotlin/com/github/navikt/tbd_libs/azure/AzureTokenClient.kt @@ -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) } @@ -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 = 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" } } } diff --git a/azure-token-client/src/main/kotlin/com/github/navikt/tbd_libs/azure/AzureTokenProvider.kt b/azure-token-client/src/main/kotlin/com/github/navikt/tbd_libs/azure/AzureTokenProvider.kt index 75a10ab..1f308cd 100644 --- a/azure-token-client/src/main/kotlin/com/github/navikt/tbd_libs/azure/AzureTokenProvider.kt +++ b/azure-token-client/src/main/kotlin/com/github/navikt/tbd_libs/azure/AzureTokenProvider.kt @@ -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 } \ No newline at end of file diff --git a/azure-token-client/src/main/kotlin/com/github/navikt/tbd_libs/azure/InMemoryAzureTokenCache.kt b/azure-token-client/src/main/kotlin/com/github/navikt/tbd_libs/azure/InMemoryAzureTokenCache.kt index f8b5a47..c48a2ce 100644 --- a/azure-token-client/src/main/kotlin/com/github/navikt/tbd_libs/azure/InMemoryAzureTokenCache.kt +++ b/azure-token-client/src/main/kotlin/com/github/navikt/tbd_libs/azure/InMemoryAzureTokenCache.kt @@ -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() - 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() + } } \ No newline at end of file diff --git a/azure-token-client/src/test/kotlin/com/github/navikt/tbd_libs/azure/AzureTokenClientTest.kt b/azure-token-client/src/test/kotlin/com/github/navikt/tbd_libs/azure/AzureTokenClientTest.kt index 9e89be9..b8c575a 100644 --- a/azure-token-client/src/test/kotlin/com/github/navikt/tbd_libs/azure/AzureTokenClientTest.kt +++ b/azure-token-client/src/test/kotlin/com/github/navikt/tbd_libs/azure/AzureTokenClientTest.kt @@ -3,11 +3,14 @@ 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 { @@ -15,6 +18,7 @@ class AzureTokenClientTest { 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 @@ -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 { val httpClient = mockOkResponse() val client = AzureTokenClient(tokenEndpoint, CLIENT_ID, authMethod, httpClient) return httpClient to client.bearerToken(SCOPE) } + + private fun requestOboToken(authMethod: AzureAuthMethod): Pair { + val httpClient = mockOkResponse() + val client = AzureTokenClient(tokenEndpoint, CLIENT_ID, authMethod, httpClient) + return httpClient to client.onBehalfOfToken(SCOPE, OTHER_TOKEN) + } } \ No newline at end of file diff --git a/azure-token-client/src/test/kotlin/com/github/navikt/tbd_libs/azure/InMemoryAzureTokenCacheTest.kt b/azure-token-client/src/test/kotlin/com/github/navikt/tbd_libs/azure/InMemoryAzureTokenCacheTest.kt index 5b3247f..ce024fb 100644 --- a/azure-token-client/src/test/kotlin/com/github/navikt/tbd_libs/azure/InMemoryAzureTokenCacheTest.kt +++ b/azure-token-client/src/test/kotlin/com/github/navikt/tbd_libs/azure/InMemoryAzureTokenCacheTest.kt @@ -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 @@ -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(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(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) + } } \ No newline at end of file diff --git a/azure-token-client/src/test/kotlin/com/github/navikt/tbd_libs/azure/MockHttpClient.kt b/azure-token-client/src/test/kotlin/com/github/navikt/tbd_libs/azure/MockHttpClient.kt index 08aaaa4..de7a141 100644 --- a/azure-token-client/src/test/kotlin/com/github/navikt/tbd_libs/azure/MockHttpClient.kt +++ b/azure-token-client/src/test/kotlin/com/github/navikt/tbd_libs/azure/MockHttpClient.kt @@ -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" } }