From e9e822d401706c89c8709ba1ac81c8d190dd19f0 Mon Sep 17 00:00:00 2001 From: Mikael Bjerga <6940327+bjerga@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:30:58 +0100 Subject: [PATCH] Erstatt token provider med forenklet versjon (#10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Erstatt token provider med forenklet versjon * Bruk bedre navn på exception * Bruk korrekt ktlint-versjon i workflow * Legg til tester * Legg til dokumentasjon --- .github/workflows/ktlint.yml | 2 +- README.md | 27 +++++- build.gradle.kts | 26 +++--- gradle.properties | 21 ++--- .../tokenprovider/AccessTokenProvider.kt | 5 -- .../tokenprovider/DefaultOAuth2HttpClient.kt | 40 --------- .../tokenprovider/{Utils.kt => HttpUtils.kt} | 4 +- .../OAuth2ClientCredentialsTokenGetter.kt | 22 +++++ .../tokenprovider/OAuth2Environment.kt | 33 ++++++++ .../tokenprovider/OAuth2TokenProvider.kt | 16 ---- .../RestSTSAccessTokenProvider.kt | 83 ------------------- .../tokenprovider/TokenClient.kt | 41 +++++++++ .../tokenprovider/TokenResolver.kt | 13 --- .../helsearbeidsgiver/tokenprovider/Mock.kt | 49 +++++++++++ .../tokenprovider/MockAccessTokenProvider.kt | 40 --------- ...Auth2ClientCredentialsTokenGetterKtTest.kt | 51 ++++++++++++ .../tokenprovider/OAuth2EnvironmentTest.kt | 33 ++++++++ .../RestSTSAccessTokenProviderTest.kt | 22 ----- src/test/resources/mockJwkPublicKey.json | 8 ++ .../sts-mock-data/valid-sts-token.json | 3 - 20 files changed, 286 insertions(+), 253 deletions(-) delete mode 100644 src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/AccessTokenProvider.kt delete mode 100644 src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/DefaultOAuth2HttpClient.kt rename src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/{Utils.kt => HttpUtils.kt} (88%) create mode 100644 src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/OAuth2ClientCredentialsTokenGetter.kt create mode 100644 src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/OAuth2Environment.kt delete mode 100644 src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/OAuth2TokenProvider.kt delete mode 100644 src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/RestSTSAccessTokenProvider.kt create mode 100644 src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/TokenClient.kt delete mode 100644 src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/TokenResolver.kt create mode 100644 src/test/kotlin/no/nav/helsearbeidsgiver/tokenprovider/Mock.kt delete mode 100644 src/test/kotlin/no/nav/helsearbeidsgiver/tokenprovider/MockAccessTokenProvider.kt create mode 100644 src/test/kotlin/no/nav/helsearbeidsgiver/tokenprovider/OAuth2ClientCredentialsTokenGetterKtTest.kt create mode 100644 src/test/kotlin/no/nav/helsearbeidsgiver/tokenprovider/OAuth2EnvironmentTest.kt delete mode 100644 src/test/kotlin/no/nav/helsearbeidsgiver/tokenprovider/RestSTSAccessTokenProviderTest.kt create mode 100644 src/test/resources/mockJwkPublicKey.json delete mode 100644 src/test/resources/sts-mock-data/valid-sts-token.json diff --git a/.github/workflows/ktlint.yml b/.github/workflows/ktlint.yml index 8d2ac55..f307622 100644 --- a/.github/workflows/ktlint.yml +++ b/.github/workflows/ktlint.yml @@ -11,6 +11,6 @@ jobs: - name: "Install ktlint" uses: nbadal/action-ktlint-setup@v1 with: - ktlint_version: '1.0.0' + ktlint_version: '1.1.1' - run: ktlint shell: bash \ No newline at end of file diff --git a/README.md b/README.md index 9191352..d926536 100644 --- a/README.md +++ b/README.md @@ -1 +1,26 @@ -# helsearbeidsgiver-tokenprovider \ No newline at end of file +# helsearbeidsgiver-tokenprovider + +Denne pakken inneholder hjelpemetoder for å hente OAuth2-token for tilgang mellom apper (med grant type "client_credentials"). + +Verdiene som er påkrevd i `OAuth2Environment` finnes typisk som systemvaribler. +Systemvariablene stammer fra en Kubernetes-secret som automatisk legges til Nais-apper som har aktivert Azure AD +([se hvordan her](https://doc.nav.cloud.nais.io/reference/application-spec/?h=azure#azureapplicationenabled)). +Kubernetes-secreten vil hete `azure--` og inneholde systemvariablene som leses i eksempelet nedenfor. + +```kt +import no.nav.helsearbeidsgiver.tokenprovider.OAuth2Environment +import no.nav.helsearbeidsgiver.tokenprovider.oauth2ClientCredentialsTokenGetter + +val oauth2Environment = OAuth2Environment( + scope = "api://dev-gcp.eksempel-scope/.default", + wellKnownUrl = "AZURE_APP_WELL_KNOWN_URL".let(System::getenv), + tokenEndpointUrl = "AZURE_OPENID_CONFIG_TOKEN_ENDPOINT".let(System::getenv), + clientId = "AZURE_APP_CLIENT_ID".let(System::getenv), + clientSecret = "AZURE_APP_CLIENT_SECRET".let(System::getenv), + clientJwk = "AZURE_APP_JWK".let(System::getenv) +) + +val tokenGetter = oauth2ClientCredentialsTokenGetter(oauth2Environment) + +val accessToken = tokenGetter() +``` diff --git a/build.gradle.kts b/build.gradle.kts index da81b86..3f66083 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,37 +31,33 @@ repositories { } dependencies { + val hagUtilsVersion: String by project + val jacksonVersion: String by project + val kotestVersion: String by project val ktorVersion: String by project + val logbackVersion: String by project val mockkVersion: String by project - val kotlinCoroutinesVersion: String by project - val kotlinSerializationVersion: String by project - val junitJupiterVersion: String by project - val tokenClientCoreVersion: String by project val nimbusJoseJwtVersion: String by project - val logbackVersion: String by project - val slf4jVersion: String by project - val jacksonVersion: String by project - val hagUtilsVersion: String by project + val tokenClientCoreVersion: String by project - runtimeOnly("ch.qos.logback:logback-classic:$logbackVersion") + api("io.ktor:ktor-client-core:$ktorVersion") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion") implementation("com.nimbusds:nimbus-jose-jwt:$nimbusJoseJwtVersion") implementation("io.ktor:ktor-client-apache5:$ktorVersion") implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") - implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("io.ktor:ktor-serialization-jackson:$ktorVersion") implementation("no.nav.helsearbeidsgiver:utils:$hagUtilsVersion") implementation("no.nav.security:token-client-core:$tokenClientCoreVersion") implementation("no.nav.security:token-validation-ktor-v2:$tokenClientCoreVersion") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion") - implementation("org.slf4j:slf4j-api:$slf4jVersion") + + runtimeOnly("ch.qos.logback:logback-classic:$logbackVersion") testImplementation(testFixtures("no.nav.helsearbeidsgiver:utils:$hagUtilsVersion")) - testImplementation("io.ktor:ktor-client-mock:$ktorVersion") + testImplementation("io.kotest:kotest-assertions-core:$kotestVersion") + testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion") testImplementation("io.mockk:mockk:$mockkVersion") - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutinesVersion") - testImplementation("org.junit.jupiter:junit-jupiter:$junitJupiterVersion") + testImplementation("io.ktor:ktor-client-mock:$ktorVersion") } publishing { diff --git a/gradle.properties b/gradle.properties index 278d598..22b614c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,17 +2,14 @@ kotlin.code.style=official # Plugin versions kotlinVersion=1.9.22 -kotlinterVersion=4.0.0 +kotlinterVersion=4.2.0 # Dependency versions -junitJupiterVersion=5.10.0 -ktorVersion=2.3.5 -mockkVersion=1.13.8 -kotlinCoroutinesVersion=1.7.3 -kotlinSerializationVersion=1.6.0 -tokenClientCoreVersion=3.1.7 -nimbusJoseJwtVersion=9.36 -logbackVersion=1.4.11 -slf4jVersion=2.0.9 -jacksonVersion=2.15.2 -hagUtilsVersion=0.7.0 +hagUtilsVersion=0.8.0 +jacksonVersion=2.16.1 +kotestVersion=5.8.0 +ktorVersion=2.3.8 +logbackVersion=1.5.2 +mockkVersion=1.13.10 +nimbusJoseJwtVersion=9.37.3 +tokenClientCoreVersion=4.1.3 diff --git a/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/AccessTokenProvider.kt b/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/AccessTokenProvider.kt deleted file mode 100644 index bf35e2b..0000000 --- a/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/AccessTokenProvider.kt +++ /dev/null @@ -1,5 +0,0 @@ -package no.nav.helsearbeidsgiver.tokenprovider - -interface AccessTokenProvider { - fun getToken(): String -} diff --git a/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/DefaultOAuth2HttpClient.kt b/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/DefaultOAuth2HttpClient.kt deleted file mode 100644 index 5524440..0000000 --- a/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/DefaultOAuth2HttpClient.kt +++ /dev/null @@ -1,40 +0,0 @@ -package no.nav.helsearbeidsgiver.tokenprovider - -import io.ktor.client.call.body -import io.ktor.client.plugins.ClientRequestException -import io.ktor.client.request.forms.submitForm -import io.ktor.http.Parameters -import kotlinx.coroutines.runBlocking -import no.nav.security.token.support.client.core.http.OAuth2HttpClient -import no.nav.security.token.support.client.core.http.OAuth2HttpRequest -import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenResponse -import org.slf4j.LoggerFactory - -class DefaultOAuth2HttpClient() : OAuth2HttpClient { - private val httpClient = createHttpClient() - - override fun post(oAuth2HttpRequest: OAuth2HttpRequest): OAuth2AccessTokenResponse { - return runBlocking { - try { - return@runBlocking httpClient.submitForm( - url = oAuth2HttpRequest.tokenEndpointUrl.toString(), - formParameters = - Parameters.build { - oAuth2HttpRequest.formParameters.forEach { - append(it.key, it.value) - } - }, - ).body() - } catch (ex: Exception) { - if (ex is ClientRequestException) { - logger.error(ex.response.body()) - } - throw ex - } - } - } - - companion object { - private val logger = LoggerFactory.getLogger(DefaultOAuth2HttpClient::class.java) - } -} diff --git a/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/Utils.kt b/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/HttpUtils.kt similarity index 88% rename from src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/Utils.kt rename to src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/HttpUtils.kt index c7c665c..9800125 100644 --- a/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/Utils.kt +++ b/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/HttpUtils.kt @@ -8,9 +8,9 @@ import io.ktor.client.engine.apache5.Apache5 import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.serialization.jackson.jackson -internal fun createHttpClient(): HttpClient = HttpClient(Apache5) { configureClientConfig() } +internal fun createHttpClient(): HttpClient = HttpClient(Apache5) { customConfig() } -internal fun HttpClientConfig<*>.configureClientConfig() { +internal fun HttpClientConfig<*>.customConfig() { expectSuccess = true install(ContentNegotiation) { jackson { diff --git a/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/OAuth2ClientCredentialsTokenGetter.kt b/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/OAuth2ClientCredentialsTokenGetter.kt new file mode 100644 index 0000000..9040f5b --- /dev/null +++ b/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/OAuth2ClientCredentialsTokenGetter.kt @@ -0,0 +1,22 @@ +package no.nav.helsearbeidsgiver.tokenprovider + +import io.ktor.client.HttpClient +import no.nav.security.token.support.client.core.oauth2.ClientCredentialsGrantRequest +import no.nav.security.token.support.client.core.oauth2.ClientCredentialsTokenClient + +/** @param httpClient Dersom egendefinert klient brukes så kan gal konfigurasjon føre til feil. */ +fun oauth2ClientCredentialsTokenGetter( + env: OAuth2Environment, + httpClient: HttpClient = createHttpClient(), +): () -> String { + val tokenClient = TokenClient(httpClient).let(::ClientCredentialsTokenClient) + + val request = env.toClientCredentialsProperties().let(::ClientCredentialsGrantRequest) + + return { + tokenClient.getTokenResponse(request).accessToken + ?: throw MissingAccessTokenException() + } +} + +class MissingAccessTokenException : Exception() diff --git a/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/OAuth2Environment.kt b/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/OAuth2Environment.kt new file mode 100644 index 0000000..b5e0141 --- /dev/null +++ b/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/OAuth2Environment.kt @@ -0,0 +1,33 @@ +package no.nav.helsearbeidsgiver.tokenprovider + +import com.nimbusds.oauth2.sdk.GrantType +import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod +import no.nav.security.token.support.client.core.ClientAuthenticationProperties +import no.nav.security.token.support.client.core.ClientProperties +import java.net.URI + +data class OAuth2Environment( + val scope: String, + val wellKnownUrl: String, + val tokenEndpointUrl: String, + val clientId: String, + val clientSecret: String, + val clientJwk: String, +) { + internal fun toClientCredentialsProperties(): ClientProperties = + ClientProperties( + tokenEndpointUrl = tokenEndpointUrl.let(::URI), + wellKnownUrl = wellKnownUrl.let(::URI), + grantType = GrantType.CLIENT_CREDENTIALS, + scope = scope.split(","), + authentication = + ClientAuthenticationProperties( + clientId = clientId, + clientAuthMethod = ClientAuthenticationMethod.CLIENT_SECRET_POST, + clientSecret = clientSecret, + clientJwk = clientJwk, + ), + resourceUrl = null, + tokenExchange = null, + ) +} diff --git a/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/OAuth2TokenProvider.kt b/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/OAuth2TokenProvider.kt deleted file mode 100644 index 2c5d79b..0000000 --- a/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/OAuth2TokenProvider.kt +++ /dev/null @@ -1,16 +0,0 @@ -package no.nav.helsearbeidsgiver.tokenprovider - -import no.nav.security.token.support.client.core.ClientProperties -import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenService - -/** - * OAuth2 Token-klient for å hente access token for bruk i andre tjenester, feks joark, PDL eller Oppgave. - */ -class OAuth2TokenProvider( - private val oauth2Service: OAuth2AccessTokenService, - private val clientProperties: ClientProperties, -) : AccessTokenProvider { - override fun getToken(): String { - return oauth2Service.getAccessToken(clientProperties).accessToken - } -} diff --git a/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/RestSTSAccessTokenProvider.kt b/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/RestSTSAccessTokenProvider.kt deleted file mode 100644 index e0f0c30..0000000 --- a/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/RestSTSAccessTokenProvider.kt +++ /dev/null @@ -1,83 +0,0 @@ -package no.nav.helsearbeidsgiver.tokenprovider - -import com.nimbusds.jwt.JWT -import com.nimbusds.jwt.JWTParser -import io.ktor.client.call.body -import io.ktor.client.request.basicAuth -import io.ktor.client.request.get -import io.ktor.client.request.header -import io.ktor.http.ContentType -import io.ktor.http.HttpHeaders -import kotlinx.coroutines.runBlocking -import org.slf4j.LoggerFactory -import java.time.Instant -import java.util.Date - -/** - * STS-klient for å hente access token for bruk i andre tjenester, feks joark, PDL eller Oppgave. - * - * Det returnerte tokenet representerer den angitte servicebrukeren (username, password) - * - * Cacher tokenet til det 5 minutter unna å bli ugyldig. - * - * STS skal fases ut til fordel for OAuth2 Client Credentials og Token Exchange (TokenX) - */ -class RestSTSAccessTokenProvider( - private val username: String, - private val password: String, - stsEndpoint: String, -) : AccessTokenProvider { - private val httpClient = createHttpClient() - private val endpointURI: String - - private var currentToken: JwtToken - - init { - endpointURI = "$stsEndpoint?grant_type=client_credentials&scope=openid" - currentToken = runBlocking { requestToken() } - } - - override fun getToken(): String { - if (isExpired(currentToken, Date.from(Instant.now().plusSeconds(300)))) { - logger.debug("OIDC Token is expired, getting a new one from the STS") - currentToken = runBlocking { requestToken() } - logger.debug("Hentet nytt token fra sts som går ut ${currentToken.expirationTime}") - } - return currentToken.tokenAsString - } - - private suspend fun requestToken(): JwtToken { - val response = - runBlocking { - httpClient.get(endpointURI) { - header(HttpHeaders.Accept, ContentType.Application.Json.toString()) - basicAuth(username = username, password = password) - } - } - .body() - - return JwtToken(response.access_token) - } - - private fun isExpired( - jwtToken: JwtToken, - date: Date, - ): Boolean { - return date.after(jwtToken.expirationTime) && - jwtToken.expirationTime.before(date) - } - - private class JwtToken(encodedToken: String) { - val tokenAsString: String = encodedToken - val jwt: JWT = JWTParser.parse(encodedToken) - val expirationTime = jwt.jwtClaimsSet.expirationTime - } - - private data class STSOidcResponse( - val access_token: String, - ) - - companion object { - private val logger = LoggerFactory.getLogger(RestSTSAccessTokenProvider::class.java) - } -} diff --git a/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/TokenClient.kt b/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/TokenClient.kt new file mode 100644 index 0000000..dbb21d3 --- /dev/null +++ b/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/TokenClient.kt @@ -0,0 +1,41 @@ +package no.nav.helsearbeidsgiver.tokenprovider + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.plugins.ClientRequestException +import io.ktor.client.request.forms.submitForm +import io.ktor.http.parametersOf +import kotlinx.coroutines.runBlocking +import no.nav.helsearbeidsgiver.utils.log.logger +import no.nav.helsearbeidsgiver.utils.log.sikkerLogger +import no.nav.security.token.support.client.core.http.OAuth2HttpClient +import no.nav.security.token.support.client.core.http.OAuth2HttpRequest +import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenResponse + +internal class TokenClient( + private val httpClient: HttpClient, +) : OAuth2HttpClient { + private val logger = logger() + private val sikkerLogger = sikkerLogger() + + override fun post(req: OAuth2HttpRequest): OAuth2AccessTokenResponse = + runBlocking { + try { + httpClient.submitForm( + url = req.tokenEndpointUrl.toString(), + formParameters = + req.formParameters + .mapValues { listOf(it.value) } + .let(::parametersOf), + ).body() + } catch (e: Exception) { + if (e is ClientRequestException) { + "Noe gikk galt under henting av av OAuth2-token.".also { + logger.error(it) + sikkerLogger.error("$it. Error response: ${e.response.body()}") + } + } + throw e + } + } +} diff --git a/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/TokenResolver.kt b/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/TokenResolver.kt deleted file mode 100644 index a2e24c4..0000000 --- a/src/main/kotlin/no/nav/helsearbeidsgiver/tokenprovider/TokenResolver.kt +++ /dev/null @@ -1,13 +0,0 @@ -package no.nav.helsearbeidsgiver.tokenprovider - -import no.nav.security.token.support.client.core.context.JwtBearerTokenResolver -import no.nav.security.token.support.v2.TokenValidationContextPrincipal -import java.util.Optional - -class TokenResolver : JwtBearerTokenResolver { - var tokenPrincipal: TokenValidationContextPrincipal? = null - - override fun token(): Optional { - return tokenPrincipal?.context?.firstValidToken?.map { it.tokenAsString } ?: Optional.empty() - } -} diff --git a/src/test/kotlin/no/nav/helsearbeidsgiver/tokenprovider/Mock.kt b/src/test/kotlin/no/nav/helsearbeidsgiver/tokenprovider/Mock.kt new file mode 100644 index 0000000..2fa26f7 --- /dev/null +++ b/src/test/kotlin/no/nav/helsearbeidsgiver/tokenprovider/Mock.kt @@ -0,0 +1,49 @@ +package no.nav.helsearbeidsgiver.tokenprovider + +import io.ktor.client.HttpClient +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.headersOf +import no.nav.helsearbeidsgiver.utils.test.json.removeJsonWhitespace +import no.nav.helsearbeidsgiver.utils.test.resource.readResource + +object Mock { + val oauth2Environment = + OAuth2Environment( + scope = "scope1,scope2", + wellKnownUrl = "http://well.known.url", + tokenEndpointUrl = "http://token.endpoint.url", + clientId = "clientId", + clientSecret = "clientSecret", + clientJwk = "mockJwkPublicKey.json".readResource(), + ) + + fun oauth2AccessTokenResponseJson(accessToken: String): String = + """ + { + "access_token": "$accessToken" + } + """ + .removeJsonWhitespace() + + fun httpClient( + status: HttpStatusCode, + content: String, + ): HttpClient { + val mockEngine = + MockEngine { + respond( + content = content, + status = status, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()), + ) + } + + return HttpClient(mockEngine) { + customConfig() + } + } +} diff --git a/src/test/kotlin/no/nav/helsearbeidsgiver/tokenprovider/MockAccessTokenProvider.kt b/src/test/kotlin/no/nav/helsearbeidsgiver/tokenprovider/MockAccessTokenProvider.kt deleted file mode 100644 index 9dd6d0c..0000000 --- a/src/test/kotlin/no/nav/helsearbeidsgiver/tokenprovider/MockAccessTokenProvider.kt +++ /dev/null @@ -1,40 +0,0 @@ -package no.nav.helsearbeidsgiver.tokenprovider - -import io.ktor.client.HttpClient -import io.ktor.client.engine.mock.MockEngine -import io.ktor.client.engine.mock.respond -import io.ktor.http.ContentType -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode -import io.ktor.http.headersOf -import io.mockk.every -import no.nav.helsearbeidsgiver.utils.test.mock.mockStatic -import no.nav.helsearbeidsgiver.utils.test.resource.readResource - -object MockResponse { - val validStsResponse = "sts-mock-data/valid-sts-token.json".readResource() -} - -fun mockAccessTokenProvider( - status: HttpStatusCode, - content: String, -): RestSTSAccessTokenProvider { - val mockEngine = - MockEngine { - respond( - content = content, - status = status, - headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()), - ) - } - - val mockHttpClient = - HttpClient(mockEngine) { - configureClientConfig() - } - - return mockStatic(::createHttpClient) { - every { createHttpClient() } returns mockHttpClient - RestSTSAccessTokenProvider("", "", "") - } -} diff --git a/src/test/kotlin/no/nav/helsearbeidsgiver/tokenprovider/OAuth2ClientCredentialsTokenGetterKtTest.kt b/src/test/kotlin/no/nav/helsearbeidsgiver/tokenprovider/OAuth2ClientCredentialsTokenGetterKtTest.kt new file mode 100644 index 0000000..35e6ca3 --- /dev/null +++ b/src/test/kotlin/no/nav/helsearbeidsgiver/tokenprovider/OAuth2ClientCredentialsTokenGetterKtTest.kt @@ -0,0 +1,51 @@ +package no.nav.helsearbeidsgiver.tokenprovider + +import io.kotest.assertions.throwables.shouldThrowExactly +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.ktor.http.HttpStatusCode +import io.mockk.every +import io.mockk.mockk +import no.nav.helsearbeidsgiver.utils.test.mock.mockConstructor +import no.nav.security.token.support.client.core.OAuth2ClientException +import no.nav.security.token.support.client.core.oauth2.ClientCredentialsTokenClient +import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenResponse + +class OAuth2ClientCredentialsTokenGetterKtTest : FunSpec({ + + test("tokenGetter henter access token") { + val accessToken = "You shall pass!" + + val mockHttpClient = + Mock.httpClient( + status = HttpStatusCode.OK, + content = Mock.oauth2AccessTokenResponseJson(accessToken), + ) + + val tokenGetter = oauth2ClientCredentialsTokenGetter(Mock.oauth2Environment, mockHttpClient) + + tokenGetter() shouldBe accessToken + } + + test("tokenGetter kaster exception dersom access token er tom") { + mockConstructor(ClientCredentialsTokenClient::class) { + every { anyConstructed().getTokenResponse(any()) } returns OAuth2AccessTokenResponse() + + val tokenGetter = oauth2ClientCredentialsTokenGetter(Mock.oauth2Environment, mockk()) + + shouldThrowExactly(tokenGetter) + } + } + + test("tokenGetter kaster exception dersom kall feiler") { + val mockHttpClient = + Mock.httpClient( + status = HttpStatusCode.InternalServerError, + content = "Trøbbel i tårnet!", + ) + + val tokenGetter = oauth2ClientCredentialsTokenGetter(Mock.oauth2Environment, mockHttpClient) + + shouldThrowExactly(tokenGetter) + } +}) diff --git a/src/test/kotlin/no/nav/helsearbeidsgiver/tokenprovider/OAuth2EnvironmentTest.kt b/src/test/kotlin/no/nav/helsearbeidsgiver/tokenprovider/OAuth2EnvironmentTest.kt new file mode 100644 index 0000000..849b125 --- /dev/null +++ b/src/test/kotlin/no/nav/helsearbeidsgiver/tokenprovider/OAuth2EnvironmentTest.kt @@ -0,0 +1,33 @@ +package no.nav.helsearbeidsgiver.tokenprovider + +import com.nimbusds.oauth2.sdk.GrantType +import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe + +class OAuth2EnvironmentTest : FunSpec({ + + test("toClientCredentialsProperties") { + val props = Mock.oauth2Environment.toClientCredentialsProperties() + + props.scope shouldContainExactly listOf("scope1", "scope2") + + props.tokenEndpointUrl.shouldNotBeNull() + props.tokenEndpointUrl.toString() shouldBe Mock.oauth2Environment.tokenEndpointUrl + + props.grantType shouldBe GrantType.CLIENT_CREDENTIALS + + props.authentication.clientId shouldBe Mock.oauth2Environment.clientId + props.authentication.clientAuthMethod shouldBe ClientAuthenticationMethod.CLIENT_SECRET_POST + props.authentication.clientSecret.shouldNotBeNull() + props.authentication.clientSecret shouldBe Mock.oauth2Environment.clientSecret + props.authentication.clientJwk.shouldNotBeNull() + props.authentication.clientJwk shouldBe Mock.oauth2Environment.clientJwk + + props.resourceUrl.shouldBeNull() + props.tokenExchange.shouldBeNull() + } +}) diff --git a/src/test/kotlin/no/nav/helsearbeidsgiver/tokenprovider/RestSTSAccessTokenProviderTest.kt b/src/test/kotlin/no/nav/helsearbeidsgiver/tokenprovider/RestSTSAccessTokenProviderTest.kt deleted file mode 100644 index 0026a4e..0000000 --- a/src/test/kotlin/no/nav/helsearbeidsgiver/tokenprovider/RestSTSAccessTokenProviderTest.kt +++ /dev/null @@ -1,22 +0,0 @@ -package no.nav.helsearbeidsgiver.tokenprovider - -import io.ktor.client.plugins.ServerResponseException -import io.ktor.http.HttpStatusCode -import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Assertions.assertThrows -import org.junit.jupiter.api.Test - -class RestSTSAccessTokenProviderTest { - @Test - internal fun `valid answer from STS returns valid token, second call gives cached answer`() { - assertNotNull("", mockAccessTokenProvider(HttpStatusCode.OK, MockResponse.validStsResponse).getToken()) - assertNotNull("", mockAccessTokenProvider(HttpStatusCode.OK, MockResponse.validStsResponse).getToken()) - } - - @Test - internal fun `Error response (5xx) from STS throws exception`() { - assertThrows(ServerResponseException::class.java) { - mockAccessTokenProvider(HttpStatusCode.InternalServerError, MockResponse.validStsResponse) - } - } -} diff --git a/src/test/resources/mockJwkPublicKey.json b/src/test/resources/mockJwkPublicKey.json new file mode 100644 index 0000000..0319b19 --- /dev/null +++ b/src/test/resources/mockJwkPublicKey.json @@ -0,0 +1,8 @@ +{ + "kty": "RSA", + "e": "AQAB", + "use": "sig", + "kid": "J4zdfFuIMuJwqVK2aWSJ69Qo6WOpZbqTflFjYt0b42c", + "alg": "RS256", + "n": "gwtDNkiXE8MxuCcEmC47G4MoQOXRBeH1aB5UHBb8gm03nfgu4HvqglczVX1ZDyRqbMcFluRuEbnRUG86pTV5GtDdB2EusRMANRAzGx8kebNsxCA4OeaAyRgVPzlHqpwdfDGghwiPoaknOe0WLpWQbCyG5o8zpjfo0Mo7MkXoGaOgLOQnQTrquWwxoxHo-ZXrFpFE7Xg_h-RwZK9F8reye1VOwQm0T5IYJDdqmGvPrmWN8QOxiwj5Mue_BfdC-K2XwQ-wj-I4YGU00D9PuCpWiPaaIievLe1dbeLslM6zE087qbsU3ZZhW5djcIX1cxy2uKscvNMDmhG9QIgjjGV7aw" +} diff --git a/src/test/resources/sts-mock-data/valid-sts-token.json b/src/test/resources/sts-mock-data/valid-sts-token.json deleted file mode 100644 index 2b73151..0000000 --- a/src/test/resources/sts-mock-data/valid-sts-token.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTcyMzkwMjJ9.k_SimHta0DlhbRkKZIrtKFczsXac9YID1Vwz2O3suuI" -} \ No newline at end of file