diff --git a/app/src/main/kotlin/no/nav/navnosearchadminapi/consumer/azuread/AzureADConsumer.kt b/app/src/main/kotlin/no/nav/navnosearchadminapi/consumer/azuread/AzureADConsumer.kt new file mode 100644 index 0000000..690edd0 --- /dev/null +++ b/app/src/main/kotlin/no/nav/navnosearchadminapi/consumer/azuread/AzureADConsumer.kt @@ -0,0 +1,43 @@ +package no.nav.navnosearchadminapi.consumer.azuread + +import no.nav.navnosearchadminapi.consumer.azuread.dto.inbound.TokenRequest +import no.nav.navnosearchadminapi.consumer.azuread.dto.outbound.TokenResponse +import no.nav.navnosearchadminapi.exception.TokenFetchException +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.MediaType +import org.springframework.stereotype.Component +import org.springframework.util.MultiValueMap +import org.springframework.web.client.HttpStatusCodeException +import org.springframework.web.client.RestTemplate +import org.springframework.web.client.exchange + +@Component +class AzureADConsumer( + val restTemplate: RestTemplate, + @Value("\${no.nav.security.jwt.issuer.azuread.accepted-audience}") val clientId: String, + @Value("\${no.nav.security.jwt.issuer.azuread.client-secret}") val clientSecret: String, + @Value("\${no.nav.security.jwt.issuer.azuread.token-endpoint}") val tokenEndpoint: String, +) { + fun getAccessToken(scope: String): String { + try { + return restTemplate.exchange( + tokenEndpoint, + HttpMethod.POST, + createRequestEntity(scope), + TokenResponse::class + ).body!!.accessToken + } catch (e: HttpStatusCodeException) { + throw TokenFetchException("Henting av Azure AD access token feilet. ${e.message}", e) + } + } + + private fun createRequestEntity(scope: String): HttpEntity> { + return HttpEntity( + TokenRequest(clientId = clientId, clientSecret = clientSecret, scope = scope).asMultiValueMap(), + HttpHeaders().apply { contentType = MediaType.APPLICATION_FORM_URLENCODED } + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/no/nav/navnosearchadminapi/consumer/azuread/dto/inbound/TokenRequest.kt b/app/src/main/kotlin/no/nav/navnosearchadminapi/consumer/azuread/dto/inbound/TokenRequest.kt new file mode 100644 index 0000000..6e9b261 --- /dev/null +++ b/app/src/main/kotlin/no/nav/navnosearchadminapi/consumer/azuread/dto/inbound/TokenRequest.kt @@ -0,0 +1,29 @@ +package no.nav.navnosearchadminapi.consumer.azuread.dto.inbound + +import org.springframework.util.LinkedMultiValueMap +import org.springframework.util.MultiValueMap + +data class TokenRequest( + val clientId: String, + val clientSecret: String, + val grantType: String = CLIENT_CREDENTIALS, + val scope: String, +) { + fun asMultiValueMap(): MultiValueMap { + return LinkedMultiValueMap().apply { + add(CLIENT_ID, clientId) + add(CLIENT_SECRET, clientSecret) + add(GRANT_TYPE, grantType) + add(SCOPE, scope) + } + } + + companion object { + private const val CLIENT_ID = "client_id" + private const val CLIENT_SECRET = "client_secret" + private const val GRANT_TYPE = "grant_type" + private const val SCOPE = "scope" + + private const val CLIENT_CREDENTIALS = "client_credentials" + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/no/nav/navnosearchadminapi/consumer/azuread/dto/outbound/TokenResponse.kt b/app/src/main/kotlin/no/nav/navnosearchadminapi/consumer/azuread/dto/outbound/TokenResponse.kt new file mode 100644 index 0000000..dd43c92 --- /dev/null +++ b/app/src/main/kotlin/no/nav/navnosearchadminapi/consumer/azuread/dto/outbound/TokenResponse.kt @@ -0,0 +1,7 @@ +package no.nav.navnosearchadminapi.consumer.azuread.dto.outbound + +import com.fasterxml.jackson.annotation.JsonProperty + +data class TokenResponse( + @JsonProperty("access_token") val accessToken: String, +) \ No newline at end of file diff --git a/app/src/main/kotlin/no/nav/navnosearchadminapi/consumer/kodeverk/KodeverkConsumer.kt b/app/src/main/kotlin/no/nav/navnosearchadminapi/consumer/kodeverk/KodeverkConsumer.kt index 54639fb..924caac 100644 --- a/app/src/main/kotlin/no/nav/navnosearchadminapi/consumer/kodeverk/KodeverkConsumer.kt +++ b/app/src/main/kotlin/no/nav/navnosearchadminapi/consumer/kodeverk/KodeverkConsumer.kt @@ -1,5 +1,6 @@ package no.nav.navnosearchadminapi.consumer.kodeverk +import no.nav.navnosearchadminapi.consumer.azuread.AzureADConsumer import no.nav.navnosearchadminapi.consumer.kodeverk.dto.KodeverkResponse import no.nav.navnosearchadminapi.exception.KodeverkConsumerException import org.springframework.beans.factory.annotation.Value @@ -13,7 +14,12 @@ import org.springframework.web.client.RestTemplate import java.util.* @Component -class KodeverkConsumer(val restTemplate: RestTemplate, @Value("\${kodeverk.spraak.url}") val kodeverkUrl: String) { +class KodeverkConsumer( + val restTemplate: RestTemplate, + val azureADConsumer: AzureADConsumer, + @Value("\${kodeverk.spraak.url}") val kodeverkUrl: String, + @Value("\${kodeverk.scope}") val kodeverkScope: String, +) { @Cacheable("spraakkoder") fun fetchSpraakKoder(): KodeverkResponse { @@ -31,9 +37,12 @@ class KodeverkConsumer(val restTemplate: RestTemplate, @Value("\${kodeverk.spraa } private fun headers(): HttpHeaders { - return HttpHeaders().apply { - set(NAV_CALL_ID, UUID.randomUUID().toString()) - set(NAV_CONSUMER_ID, NAVNO_SEARCH_ADMIN_API) + return azureADConsumer.getAccessToken(kodeverkScope).let { accessToken -> + HttpHeaders().apply { + setBearerAuth(accessToken) + set(NAV_CALL_ID, UUID.randomUUID().toString()) + set(NAV_CONSUMER_ID, NAVNO_SEARCH_ADMIN_API) + } } } diff --git a/app/src/main/kotlin/no/nav/navnosearchadminapi/exception/TokenFetchException.kt b/app/src/main/kotlin/no/nav/navnosearchadminapi/exception/TokenFetchException.kt new file mode 100644 index 0000000..e5e0d15 --- /dev/null +++ b/app/src/main/kotlin/no/nav/navnosearchadminapi/exception/TokenFetchException.kt @@ -0,0 +1,4 @@ +package no.nav.navnosearchadminapi.exception + +class TokenFetchException(message: String, cause: Throwable) : Exception(message, cause) { +} \ No newline at end of file diff --git a/app/src/main/resources/application.yml b/app/src/main/resources/application.yml index fcfba15..3fb4b21 100644 --- a/app/src/main/resources/application.yml +++ b/app/src/main/resources/application.yml @@ -19,6 +19,7 @@ management: kodeverk: spraak: url: ${KODEVERK_SPRAAK_URL} + scope: ${KODEVERK_SCOPE} api-key: ${API_KEY} @@ -27,3 +28,5 @@ no.nav.security.jwt: azuread: discoveryurl: ${AZURE_APP_WELL_KNOWN_URL} accepted-audience: ${AZURE_APP_CLIENT_ID} + client-secret: ${AZURE_APP_CLIENT_SECRET} + token-endpoint: ${AZURE_OPENID_CONFIG_TOKEN_ENDPOINT} diff --git a/app/src/test/kotlin/no/nav/navnosearchadminapi/integrationtests/AbstractIntegrationTest.kt b/app/src/test/kotlin/no/nav/navnosearchadminapi/integrationtests/AbstractIntegrationTest.kt index 3c10952..05bcfd6 100644 --- a/app/src/test/kotlin/no/nav/navnosearchadminapi/integrationtests/AbstractIntegrationTest.kt +++ b/app/src/test/kotlin/no/nav/navnosearchadminapi/integrationtests/AbstractIntegrationTest.kt @@ -1,7 +1,13 @@ package no.nav.navnosearchadminapi.integrationtests +import com.fasterxml.jackson.databind.ObjectMapper +import com.github.tomakehurst.wiremock.client.WireMock.aResponse +import com.github.tomakehurst.wiremock.client.WireMock.post +import com.github.tomakehurst.wiremock.client.WireMock.stubFor +import com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching import com.nimbusds.jose.JOSEObjectType import no.nav.navnosearchadminapi.common.repository.ContentRepository +import no.nav.navnosearchadminapi.consumer.azuread.dto.outbound.TokenResponse import no.nav.navnosearchadminapi.integrationtests.config.OpensearchConfiguration import no.nav.navnosearchadminapi.rest.aspect.HeaderCheckAspect.Companion.API_KEY_HEADER import no.nav.navnosearchadminapi.utils.initialTestData @@ -18,6 +24,9 @@ import org.springframework.cache.CacheManager import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock import org.springframework.context.annotation.Import import org.springframework.http.HttpHeaders +import org.springframework.http.HttpHeaders.CONTENT_TYPE +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType.APPLICATION_JSON_VALUE import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.junit.jupiter.SpringExtension import org.testcontainers.junit.jupiter.Testcontainers @@ -31,6 +40,9 @@ import org.testcontainers.junit.jupiter.Testcontainers @EnableMockOAuth2Server abstract class AbstractIntegrationTest { + @Autowired + lateinit var objectMapper: ObjectMapper + @Autowired lateinit var restTemplate: TestRestTemplate @@ -46,7 +58,8 @@ abstract class AbstractIntegrationTest { @LocalServerPort var serverPort: Int? = null - @Value("\${api-key}") lateinit var apiKey: String + @Value("\${api-key}") + lateinit var apiKey: String fun host() = "http://localhost:$serverPort" @@ -57,6 +70,16 @@ abstract class AbstractIntegrationTest { repository.saveAll(initialTestData) } + fun mockAzuread() { + stubFor( + post(urlPathMatching("/azuread")).willReturn( + aResponse().withStatus(HttpStatus.OK.value()) + .withBody(objectMapper.writeValueAsString(TokenResponse(accessToken = "token"))) + .withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE) + ) + ) + } + fun headers(isAuthValid: Boolean = true, isApiKeyValid: Boolean = true): HttpHeaders { val headers = HttpHeaders() val token = if (isAuthValid) { diff --git a/app/src/test/kotlin/no/nav/navnosearchadminapi/integrationtests/AdminIntegrationTest.kt b/app/src/test/kotlin/no/nav/navnosearchadminapi/integrationtests/AdminIntegrationTest.kt index d8360f6..02518ac 100644 --- a/app/src/test/kotlin/no/nav/navnosearchadminapi/integrationtests/AdminIntegrationTest.kt +++ b/app/src/test/kotlin/no/nav/navnosearchadminapi/integrationtests/AdminIntegrationTest.kt @@ -1,6 +1,5 @@ package no.nav.navnosearchadminapi.integrationtests -import com.fasterxml.jackson.databind.ObjectMapper import com.github.tomakehurst.wiremock.client.WireMock import com.github.tomakehurst.wiremock.client.WireMock.aResponse import com.github.tomakehurst.wiremock.client.WireMock.get @@ -24,12 +23,11 @@ import org.springframework.http.ResponseEntity class AdminIntegrationTest : AbstractIntegrationTest() { - val objectMapper = ObjectMapper() - @BeforeEach fun setup() { WireMock.reset() setupIndex() + mockAzuread() stubFor( get(urlPathMatching("/kodeverk")).willReturn( aResponse().withStatus(HttpStatus.OK.value()) diff --git a/app/src/test/resources/application-test.yml b/app/src/test/resources/application-test.yml index e8475bb..fcafe9c 100644 --- a/app/src/test/resources/application-test.yml +++ b/app/src/test/resources/application-test.yml @@ -1,11 +1,14 @@ kodeverk: spraak: url: http://localhost:${wiremock.server.port}/kodeverk + scope: kodeverk-scope no.nav.security.jwt: issuer: azuread: discoveryurl: http://localhost:${mock-oauth2-server.port}/azuread/.well-known/openid-configuration accepted-audience: someaudience + client-secret: somesecret + token-endpoint: http://localhost:${wiremock.server.port}/azuread api-key: dummy \ No newline at end of file