diff --git a/.gitignore b/.gitignore index f8b92c3..3060674 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .gradle build +.idea \ No newline at end of file diff --git a/azure-exchange/src/main/kotlin/no/nav/tms/token/support/azure/exchange/service/NonCachingTokendingsService.kt b/azure-exchange/src/main/kotlin/no/nav/tms/token/support/azure/exchange/service/NonCachingTokendingsService.kt index 3d48ea1..cab3570 100644 --- a/azure-exchange/src/main/kotlin/no/nav/tms/token/support/azure/exchange/service/NonCachingTokendingsService.kt +++ b/azure-exchange/src/main/kotlin/no/nav/tms/token/support/azure/exchange/service/NonCachingTokendingsService.kt @@ -8,43 +8,50 @@ import no.nav.tms.token.support.azure.exchange.consumer.AzureConsumer class NonCachingAzureService internal constructor( - private val azureConsumer: AzureConsumer, - issuer: String, - clientId: String, - privateJwk: String -): AzureService { + private val azureConsumer: AzureConsumer, + issuer: String, + clientId: String, + privateJwk: String +) : AzureService { private val clientAssertionService = ClientAssertionService(privateJwk, clientId, issuer) - override suspend fun getAccessToken(targetApp: String): String { + override suspend fun getAccessToken(targetApp: String): String = try { val jwt = clientAssertionService.createClientAssertion() - return azureConsumer.fetchToken(jwt, targetApp).accessToken + azureConsumer.fetchToken(jwt, targetApp).accessToken + } catch (throwable: Throwable) { + throw AzureExchangeException(throwable, targetApp) } + + } class CachingAzureService internal constructor( - private val azureConsumer: AzureConsumer, - issuer: String, - clientId: String, - privateJwk: String, - maxCacheEntries: Long, - cacheExpiryMarginSeconds: Int, -): AzureService { + private val azureConsumer: AzureConsumer, + issuer: String, + clientId: String, + privateJwk: String, + maxCacheEntries: Long, + cacheExpiryMarginSeconds: Int, +) : AzureService { private val cache = CacheBuilder.buildCache(maxCacheEntries, cacheExpiryMarginSeconds) private val clientAssertionService = ClientAssertionService(privateJwk, clientId, issuer) - override suspend fun getAccessToken(targetApp: String): String { + override suspend fun getAccessToken(targetApp: String): String = + try { + cache.get(targetApp) { + runBlocking { + performTokenExchange(targetApp) + } + }.accessToken + } catch (throwable: Throwable) { + throw AzureExchangeException(throwable, targetApp) + } - return cache.get(targetApp) { - runBlocking { - performTokenExchange(targetApp) - } - }.accessToken - } private suspend fun performTokenExchange(targetApp: String): AccessTokenEntry { val jwt = clientAssertionService.createClientAssertion() @@ -54,3 +61,16 @@ class CachingAzureService internal constructor( return AccessTokenEntry.fromResponse(response) } } + +class AzureExchangeException(val originalThrowable: Throwable, targetApp: String) : + Exception() { + + val stackTraceSummary = + originalThrowable.stackTrace.firstOrNull()?.let { stacktraceElement -> + """ Azureexchange feiler for $targetApp + Origin: ${stacktraceElement.fileName ?: "---"} ${stacktraceElement.methodName ?: "----"} linenumber:${stacktraceElement.lineNumber} + Message: "${originalThrowable::class.simpleName} ${originalThrowable.message?.let { ":$it" }}" + """.trimIndent() + } ?: "${originalThrowable::class.simpleName} ${originalThrowable.message?.let { ":$it" }}" +} + diff --git a/azure-exchange/src/test/kotlin/no/nav/tms/token/support/azure/exchange/AzureServiceTest.kt b/azure-exchange/src/test/kotlin/no/nav/tms/token/support/azure/exchange/AzureServiceTest.kt index 08bc51f..7c08e3a 100644 --- a/azure-exchange/src/test/kotlin/no/nav/tms/token/support/azure/exchange/AzureServiceTest.kt +++ b/azure-exchange/src/test/kotlin/no/nav/tms/token/support/azure/exchange/AzureServiceTest.kt @@ -7,10 +7,13 @@ import io.kotest.matchers.shouldNotBe import io.mockk.* import kotlinx.coroutines.runBlocking import no.nav.tms.token.support.azure.exchange.consumer.AzureConsumer +import no.nav.tms.token.support.azure.exchange.service.AzureExchangeException import no.nav.tms.token.support.azure.exchange.service.CachingAzureService import no.nav.tms.token.support.azure.exchange.service.NonCachingAzureService import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.net.SocketTimeoutException internal class AzureServiceTest { @@ -91,7 +94,7 @@ internal class AzureServiceTest { cachingAzureService.getAccessToken(target) } - coVerify(exactly = 1) {azureConsumer.fetchToken(any(), target) } + coVerify(exactly = 1) { azureConsumer.fetchToken(any(), target) } } @Test @@ -110,7 +113,7 @@ internal class AzureServiceTest { cachingAzureService.getAccessToken(target) } - coVerify(exactly = 3) {azureConsumer.fetchToken(any(), target) } + coVerify(exactly = 3) { azureConsumer.fetchToken(any(), target) } } @Test @@ -134,12 +137,55 @@ internal class AzureServiceTest { val result3 = runBlocking { cachingAzureService.getAccessToken(target1) } val result4 = runBlocking { cachingAzureService.getAccessToken(target2) } - coVerify(exactly = 1) {azureConsumer.fetchToken(any(), target1) } - coVerify(exactly = 1) {azureConsumer.fetchToken(any(), target2) } + coVerify(exactly = 1) { azureConsumer.fetchToken(any(), target1) } + coVerify(exactly = 1) { azureConsumer.fetchToken(any(), target2) } result1 shouldBe result3 result2 shouldBe result4 result1 shouldNotBe result2 result3 shouldNotBe result4 } + + @Test + fun `Should throw AzureExchangeException if exchangeprocess fails`() { + assertNonCachingServiceThrows { IllegalArgumentException() } + assertNonCachingServiceThrows { SocketTimeoutException() } + assertNonCachingServiceThrows { Error() } + assertCachingServiceThrows { IllegalArgumentException() } + assertCachingServiceThrows { SocketTimeoutException() } + assertCachingServiceThrows { Error() } + + } + + fun assertNonCachingServiceThrows( throwable: () -> Throwable) = run { + NonCachingAzureService( + azureConsumer = mockk().apply { + coEvery { fetchToken(any(), any()) } throws throwable() + }, + clientId = "some:client", + issuer = "some:issuer", + privateJwk = privateJwk, + ).apply { + assertThrows { runBlocking { getAccessToken("appappapp") } } + } + } + + fun assertCachingServiceThrows(throwable: () -> Throwable) = run { + CachingAzureService( + azureConsumer = mockk().apply { + coEvery { fetchToken(any(), any()) } throws throwable() + }, + clientId = "some:client", + privateJwk = privateJwk, + issuer = "some:issuer", + maxCacheEntries = 1, + cacheExpiryMarginSeconds = 6 + + ).apply { + assertThrows { runBlocking { getAccessToken("token") } } + } + } + + } + diff --git a/tokendings-exchange/src/main/kotlin/no/nav/tms/token/support/tokendings/exchange/consumer/TokendingsConsumer.kt b/tokendings-exchange/src/main/kotlin/no/nav/tms/token/support/tokendings/exchange/consumer/TokendingsConsumer.kt index 2800d95..760f821 100644 --- a/tokendings-exchange/src/main/kotlin/no/nav/tms/token/support/tokendings/exchange/consumer/TokendingsConsumer.kt +++ b/tokendings-exchange/src/main/kotlin/no/nav/tms/token/support/tokendings/exchange/consumer/TokendingsConsumer.kt @@ -34,3 +34,4 @@ internal class TokendingsConsumer( } } } + diff --git a/tokendings-exchange/src/main/kotlin/no/nav/tms/token/support/tokendings/exchange/service/tokendingsServiceImpl.kt b/tokendings-exchange/src/main/kotlin/no/nav/tms/token/support/tokendings/exchange/service/tokendingsServiceImpl.kt index 1e5ccb5..78b9c5f 100644 --- a/tokendings-exchange/src/main/kotlin/no/nav/tms/token/support/tokendings/exchange/service/tokendingsServiceImpl.kt +++ b/tokendings-exchange/src/main/kotlin/no/nav/tms/token/support/tokendings/exchange/service/tokendingsServiceImpl.kt @@ -11,42 +11,51 @@ import no.nav.tms.token.support.tokendings.exchange.consumer.TokendingsConsumer import no.nav.tms.token.support.tokendings.exchange.service.ClientAssertion.createSignedAssertion class NonCachingTokendingsService internal constructor( - private val tokendingsConsumer: TokendingsConsumer, - private val jwtAudience: String, - private val clientId: String, - privateJwk: String -): TokendingsService { + private val tokendingsConsumer: TokendingsConsumer, + private val jwtAudience: String, + private val clientId: String, + privateJwk: String +) : TokendingsService { private val privateRsaKey = RSAKey.parse(privateJwk) override suspend fun exchangeToken(token: String, targetApp: String): String { - val jwt = createSignedAssertion(clientId, jwtAudience, privateRsaKey) + try { + val jwt = createSignedAssertion(clientId, jwtAudience, privateRsaKey) + + return tokendingsConsumer.exchangeToken(token, jwt, targetApp).accessToken + } catch (throwable: Throwable) { + throw TokendingsExchangeException(throwable, clientId) + } - return tokendingsConsumer.exchangeToken(token, jwt, targetApp).accessToken } } class CachingTokendingsService internal constructor( - private val tokendingsConsumer: TokendingsConsumer, - private val jwtAudience: String, - private val clientId: String, - privateJwk: String, - maxCacheEntries: Long, - cacheExpiryMarginSeconds: Int, -): TokendingsService { + private val tokendingsConsumer: TokendingsConsumer, + private val jwtAudience: String, + private val clientId: String, + privateJwk: String, + maxCacheEntries: Long, + cacheExpiryMarginSeconds: Int, +) : TokendingsService { private val cache = CacheBuilder.buildCache(maxCacheEntries, cacheExpiryMarginSeconds) private val privateRsaKey = RSAKey.parse(privateJwk) override suspend fun exchangeToken(token: String, targetApp: String): String { - val cacheKey = TokenStringUtil.createCacheKey(token, targetApp) - - return cache.get(cacheKey) { - runBlocking { - performTokenExchange(token, targetApp) - } - }.accessToken + try { + val cacheKey = TokenStringUtil.createCacheKey(token, targetApp) + + return cache.get(cacheKey) { + runBlocking { + performTokenExchange(token, targetApp) + } + }.accessToken + } catch (throwable: Throwable) { + throw TokendingsExchangeException(throwable, clientId) + } } private suspend fun performTokenExchange(token: String, targetApp: String): AccessTokenEntry { @@ -68,3 +77,16 @@ internal object TokenStringUtil { return AccessTokenKey(subject, securityLevel, targetApp) } } + +class TokendingsExchangeException(val originalThrowable: Throwable, clientId: String) : + Exception() { + + val stackTraceSummary = + originalThrowable.stackTrace.firstOrNull()?.let { stacktraceElement -> + """ Tokendingsexchange feiler for $clientId + Origin: ${stacktraceElement.fileName ?: "---"} ${stacktraceElement.methodName ?: "----"} linenumber:${stacktraceElement.lineNumber} + Message: "${originalThrowable::class.simpleName} ${originalThrowable.message?.let { ":$it" }}" + """.trimIndent() + } ?: "${originalThrowable::class.simpleName} ${originalThrowable.message?.let { ":$it" }}" +} + diff --git a/tokendings-exchange/src/test/kotlin/no/nav/tms/token/support/tokendings/exchange/TokendingsServiceTest.kt b/tokendings-exchange/src/test/kotlin/no/nav/tms/token/support/tokendings/exchange/TokendingsServiceTest.kt index 8f610b0..d7faf37 100644 --- a/tokendings-exchange/src/test/kotlin/no/nav/tms/token/support/tokendings/exchange/TokendingsServiceTest.kt +++ b/tokendings-exchange/src/test/kotlin/no/nav/tms/token/support/tokendings/exchange/TokendingsServiceTest.kt @@ -11,8 +11,11 @@ import no.nav.tms.token.support.tokendings.exchange.consumer.TokendingsConsumer import no.nav.tms.token.support.tokendings.exchange.service.CachingTokendingsService import no.nav.tms.token.support.tokendings.exchange.service.NonCachingTokendingsService import no.nav.tms.token.support.tokendings.exchange.service.TokenStringUtil +import no.nav.tms.token.support.tokendings.exchange.service.TokendingsExchangeException import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.net.SocketTimeoutException internal class TokendingsServiceTest { @@ -21,8 +24,10 @@ internal class TokendingsServiceTest { private val clientId = "cluster:namespace:thisApi" private val privateJwk = JwkBuilder.generateJwk() - private val nonCachingtokendingsService = NonCachingTokendingsService(tokendingsConsumer, jwtAudience, clientId, privateJwk) - private val cachingTokendingsService = CachingTokendingsService(tokendingsConsumer, jwtAudience, clientId, privateJwk, 10, 5) + private val nonCachingtokendingsService = + NonCachingTokendingsService(tokendingsConsumer, jwtAudience, clientId, privateJwk) + private val cachingTokendingsService = + CachingTokendingsService(tokendingsConsumer, jwtAudience, clientId, privateJwk, 10, 5) @AfterEach fun cleanup() { @@ -39,9 +44,9 @@ internal class TokendingsServiceTest { val exchangedToken = "" val target = "cluster:namespace:otherApi" - coEvery { - tokendingsConsumer.exchangeToken(any(), capture(assertion), target) - } returns TokendingsResponseObjectMother.createTokendingsResponse(exchangedToken) + coEvery { + tokendingsConsumer.exchangeToken(any(), capture(assertion), target) + } returns TokendingsResponseObjectMother.createTokendingsResponse(exchangedToken) val result = runBlocking { nonCachingtokendingsService.exchangeToken(token, target) @@ -113,7 +118,7 @@ internal class TokendingsServiceTest { cachingTokendingsService.exchangeToken(token, target) } - coVerify(exactly = 1) {tokendingsConsumer.exchangeToken(any(), any(), target) } + coVerify(exactly = 1) { tokendingsConsumer.exchangeToken(any(), any(), target) } } @Test @@ -140,7 +145,7 @@ internal class TokendingsServiceTest { cachingTokendingsService.exchangeToken(token, target) } - coVerify(exactly = 3) {tokendingsConsumer.exchangeToken(any(), any(), target) } + coVerify(exactly = 3) { tokendingsConsumer.exchangeToken(any(), any(), target) } } @Test @@ -181,8 +186,8 @@ internal class TokendingsServiceTest { runBlocking { cachingTokendingsService.exchangeToken(token, target1) } runBlocking { cachingTokendingsService.exchangeToken(token, target2) } - coVerify(exactly = 1) {tokendingsConsumer.exchangeToken(any(), any(), target1) } - coVerify(exactly = 1) {tokendingsConsumer.exchangeToken(any(), any(), target2) } + coVerify(exactly = 1) { tokendingsConsumer.exchangeToken(any(), any(), target1) } + coVerify(exactly = 1) { tokendingsConsumer.exchangeToken(any(), any(), target2) } result1 shouldBe result3 result2 shouldBe result4 @@ -230,12 +235,52 @@ internal class TokendingsServiceTest { runBlocking { cachingTokendingsService.exchangeToken(token1, target) } runBlocking { cachingTokendingsService.exchangeToken(token2, target) } - coVerify(exactly = 1) {tokendingsConsumer.exchangeToken(token1, any(), target) } - coVerify(exactly = 1) {tokendingsConsumer.exchangeToken(token2, any(), target) } + coVerify(exactly = 1) { tokendingsConsumer.exchangeToken(token1, any(), target) } + coVerify(exactly = 1) { tokendingsConsumer.exchangeToken(token2, any(), target) } result1 shouldBe result3 result2 shouldBe result4 result1 shouldNotBe result2 result3 shouldNotBe result4 } + + @Test + fun `Should throw TokendingsExchangeException if exchangeprocess fails`() { + assertNonCachingServiceThrows { IllegalArgumentException() } + assertNonCachingServiceThrows { SocketTimeoutException() } + assertNonCachingServiceThrows { Error() } + assertCachingServiceThrows { IllegalArgumentException() } + assertCachingServiceThrows { SocketTimeoutException() } + assertCachingServiceThrows { Error() } + + } + + fun assertNonCachingServiceThrows( throwable: () -> Throwable) = run { + NonCachingTokendingsService( + tokendingsConsumer = mockk().apply { + coEvery { exchangeToken(any(), any(), any()) } throws throwable() + }, + jwtAudience = "some:aud", + clientId = "some:client", + privateJwk = privateJwk + ).apply { + assertThrows { runBlocking { exchangeToken("token", "token") } } + } + } + + fun assertCachingServiceThrows( throwable: () -> Throwable) = run { + CachingTokendingsService( + tokendingsConsumer = mockk().apply { + coEvery { exchangeToken(any(), any(), any()) } throws throwable() + }, + jwtAudience = "some:aud", + clientId = "some:client", + privateJwk = privateJwk, + maxCacheEntries = 1, + cacheExpiryMarginSeconds = 6 + ).apply { + assertThrows { runBlocking { exchangeToken("token", "token") } } + } + } } +