diff --git a/buildSrc/src/main/kotlin/RecommendedVersions.kt b/buildSrc/src/main/kotlin/dependencies.kt similarity index 90% rename from buildSrc/src/main/kotlin/RecommendedVersions.kt rename to buildSrc/src/main/kotlin/dependencies.kt index cd34fb7..67b4e4f 100644 --- a/buildSrc/src/main/kotlin/RecommendedVersions.kt +++ b/buildSrc/src/main/kotlin/dependencies.kt @@ -11,7 +11,6 @@ object Kluent { object Kotlin { const val version = "1.8.10" - private const val groupId = "org.jetbrains.kotlin" } object Kotest { @@ -40,8 +39,16 @@ object Ktor { const val serverForwardedHeaders = "$groupId:ktor-server-forwarded-header:$version" } +object KotlinLogging { + private const val groupId = "io.github.oshai" + private const val version = "5.0.2" + + const val logging = "$groupId:kotlin-logging:$version" +} + + object Logback { - private const val version = "1.2.3" + private const val version = "1.4.11" const val classic = "ch.qos.logback:logback-classic:$version" } diff --git a/token-support-authentication-installer-mock/README.md b/token-support-authentication-installer-mock/README.md deleted file mode 100644 index fd353aa..0000000 --- a/token-support-authentication-installer-mock/README.md +++ /dev/null @@ -1,102 +0,0 @@ -# token-support-authentication-installer - -Denne modulen tilbyr en måte å installere flere av bibliotekets autentikator-mocks i parallell. - - -## Nais-yaml - -Krav for nais-yaml spesifikasjon er avhengig av hvilke autentikatorer som skal installeres. Referer til README for -autentikatorer som skal installeres. - -## Oppsett - -Biblioteket tilbyr en funksjon `Application.installMockedAuthenticators` lar en installere autentikatorer slik det gjøres -i andre moduler: - -```kotlin -fun Application.mainModule() { - - installMockedAuthenticators { - installIdPortenAuthMock { - ... - } - - installTokenXAuthMock { - ... - } - } -} -``` - -Dette gjør det mulig å velge autentikator for beskyttede endepunkt som følger: - -```kotlin -fun Application.mainModule() { - - installAuthenticators { - installIdPortenAuthMock { - ... - } - - installTokenXAuthMock { - ... - } - } - - routing { - authenticate(IdPortenCookieAuthenticator.name) { - get("/sikretForIdPorten") { - call.respond(HttpStatusCode.OK) - } - } - - authenticate(TokenXAuthenticator.name) { - get("/sikretForTokenX") { - call.respond(HttpStatusCode.OK) - } - } - } -} -``` - -Referer til README til hver enkelt autentikator for mer informasjon om oppsett og bruk. - -### Default authenticator - -Det er også mulig å velge opp til 1 autentikator som default, slik at den ikke trenger å navngis for hvert sikrede endepunkt. - -Eksempel for oppsett og bruk: - -```kotlin -fun Application.mainModule() { - - installAuthenticators { - installIdPortenAuthMock { - ... - setAsDefault = true - } - - installTokenXAuthMock { - ... - } - } - - routing { - authenticate { - get("/sikretForIdPorten") { - call.respond(HttpStatusCode.OK) - } - } - - authenticate(TokenXAuthenticator.name) { - get("/sikretForTokenX") { - call.respond(HttpStatusCode.OK) - } - } - } -} -``` - -## Bruk av biblioteket ved lokal kjøring - -Som andre mock-moduler skal denne ikke kjøres i miljø. diff --git a/token-support-authentication-installer-mock/build.gradle.kts b/token-support-authentication-installer-mock/build.gradle.kts deleted file mode 100644 index fa719b1..0000000 --- a/token-support-authentication-installer-mock/build.gradle.kts +++ /dev/null @@ -1,48 +0,0 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -plugins { - `maven-publish` - `java-library` - kotlin("jvm") - kotlin("plugin.serialization") -} - -dependencies { - api(kotlin("stdlib-jdk8")) - implementation(project(":token-support-azure-validation-mock")) - implementation(project(":token-support-tokenx-validation-mock")) - implementation(project(":token-support-idporten-sidecar-mock")) - implementation(Ktor.serverAuth) - testImplementation(kotlin("test-junit5")) - testImplementation(Kluent.kluent) - testImplementation(Ktor.serverTestHost) - testImplementation(Mockk.mockk) -} - -repositories { - mavenCentral() - mavenLocal() -} - -publishing { - repositories{ - mavenLocal() - } - - publications { - create("local") { - from(components["java"]) - } - } -} - -tasks.withType { - kotlinOptions.jvmTarget = "17" -} - -tasks { - - withType { - useJUnitPlatform() - } -} diff --git a/token-support-authentication-installer-mock/src/main/kotlin/no/nav/tms/token/support/authentication/installer/mock/MockedAuthenticatorInstaller.kt b/token-support-authentication-installer-mock/src/main/kotlin/no/nav/tms/token/support/authentication/installer/mock/MockedAuthenticatorInstaller.kt deleted file mode 100644 index 50fd4b9..0000000 --- a/token-support-authentication-installer-mock/src/main/kotlin/no/nav/tms/token/support/authentication/installer/mock/MockedAuthenticatorInstaller.kt +++ /dev/null @@ -1,90 +0,0 @@ -package no.nav.tms.token.support.authentication.installer.mock - -import io.ktor.server.application.* -import io.ktor.server.auth.* -import no.nav.tms.token.support.azure.validation.mock.AzureMockInstaller.performAzureMockAuthenticatorInstallation -import no.nav.tms.token.support.azure.validation.mock.AzureMockedAuthenticatorConfig -import no.nav.tms.token.support.idporten.sidecar.mock.IdPortenMockInstaller.performIdPortenMockInstallation -import no.nav.tms.token.support.idporten.sidecar.mock.IdPortenMockedAuthenticatorConfig -import no.nav.tms.token.support.tokenx.validation.mock.TokenXMockInstaller.performTokenXMockInstallation -import no.nav.tms.token.support.tokenx.validation.mock.TokenXMockedAuthenticatorConfig -import org.slf4j.LoggerFactory - -internal object MockedAuthenticatorInstaller { - private val log = LoggerFactory.getLogger(MockedAuthenticatorInstaller::class.java) - - fun Application.performInstallation(config: MockedAuthenticatorConfig) { - checkUsage(config) - validateDefaultToggle(config) - - val application = this - - install(Authentication) { - - val idPortenConfig = config.idPortenConfig - - val authContext = this - - if (idPortenConfig != null) { - MockedInstallerProxy.invokeIdPortenMockInstaller(application, idPortenConfig, authContext) - } - - val tokenXConfig = config.tokenXConfig - - if (tokenXConfig != null) { - MockedInstallerProxy.invokeTokenXMockInstaller(application, tokenXConfig, authContext) - } - - val azureConfig = config.azureConfig - - if (azureConfig != null) { - MockedInstallerProxy.invokeAzureMockInstaller(application, azureConfig, authContext) - } - } - } - - private fun checkUsage(config: MockedAuthenticatorConfig) { - val numberInstalled = listOf(config.idPortenConfig, config.tokenXConfig, config.azureConfig).count { it != null } - - if (numberInstalled < 2) { - log.info("Using the token-support-authentication-installer module is not strictly necessary when installing less than two authenticators.") - } - } - - private fun validateDefaultToggle(config: MockedAuthenticatorConfig) { - val numberSetAsDefault = listOf( - config.idPortenConfig?.setAsDefault, - config.tokenXConfig?.setAsDefault, - config.azureConfig?.setAsDefault - ).count { it == true } - - require(numberSetAsDefault < 2) { "At most one authenticator can be set as default." } - } -} - -// The primary purpose of this simple proxy is to enable testing without compromising on legibility too much -internal object MockedInstallerProxy { - internal fun invokeIdPortenMockInstaller( - application: Application, - idPortenConfig: IdPortenMockedAuthenticatorConfig, - existingAuthContext: AuthenticationConfig) { - - return application.performIdPortenMockInstallation(idPortenConfig, existingAuthContext) - } - - internal fun invokeTokenXMockInstaller( - application: Application, - tokenXConfig: TokenXMockedAuthenticatorConfig, - existingAuthContext: AuthenticationConfig) { - - application.performTokenXMockInstallation(tokenXConfig, existingAuthContext) - } - - internal fun invokeAzureMockInstaller( - application: Application, - azureConfig: AzureMockedAuthenticatorConfig, - existingAuthContext: AuthenticationConfig) { - - application.performAzureMockAuthenticatorInstallation(azureConfig, existingAuthContext) - } -} diff --git a/token-support-authentication-installer-mock/src/main/kotlin/no/nav/tms/token/support/authentication/installer/mock/config.kt b/token-support-authentication-installer-mock/src/main/kotlin/no/nav/tms/token/support/authentication/installer/mock/config.kt deleted file mode 100644 index 9f1b513..0000000 --- a/token-support-authentication-installer-mock/src/main/kotlin/no/nav/tms/token/support/authentication/installer/mock/config.kt +++ /dev/null @@ -1,32 +0,0 @@ -package no.nav.tms.token.support.authentication.installer.mock - -import io.ktor.server.application.* -import no.nav.tms.token.support.authentication.installer.mock.MockedAuthenticatorInstaller.performInstallation -import no.nav.tms.token.support.azure.validation.mock.AzureMockedAuthenticatorConfig -import no.nav.tms.token.support.idporten.sidecar.mock.IdPortenMockedAuthenticatorConfig -import no.nav.tms.token.support.tokenx.validation.mock.TokenXMockedAuthenticatorConfig - -fun Application.installMockedAuthenticators(configure: MockedAuthenticatorConfig.() -> Unit = {}) { - val config = MockedAuthenticatorConfig().also(configure) - - performInstallation(config) -} - -class MockedAuthenticatorConfig { - internal var idPortenConfig: IdPortenMockedAuthenticatorConfig? = null - internal var tokenXConfig: TokenXMockedAuthenticatorConfig? = null - internal var azureConfig: AzureMockedAuthenticatorConfig? = null - - fun installIdPortenAuthMock(configure: IdPortenMockedAuthenticatorConfig.() -> Unit) { - idPortenConfig = IdPortenMockedAuthenticatorConfig().also(configure) - } - - fun installTokenXAuthMock(configure: TokenXMockedAuthenticatorConfig.() -> Unit) { - tokenXConfig = TokenXMockedAuthenticatorConfig().also(configure) - } - - fun installAzureAuthMock(configure: AzureMockedAuthenticatorConfig.() -> Unit) { - azureConfig = AzureMockedAuthenticatorConfig().also(configure) - } -} - diff --git a/token-support-authentication-installer-mock/src/test/kotlin/no/nav/tms/token/support/authentication/installer/MockedAuthenticatorInstallerTest.kt b/token-support-authentication-installer-mock/src/test/kotlin/no/nav/tms/token/support/authentication/installer/MockedAuthenticatorInstallerTest.kt deleted file mode 100644 index 744b017..0000000 --- a/token-support-authentication-installer-mock/src/test/kotlin/no/nav/tms/token/support/authentication/installer/MockedAuthenticatorInstallerTest.kt +++ /dev/null @@ -1,160 +0,0 @@ -package no.nav.tms.token.support.authentication.installer - -import io.ktor.server.application.* -import io.ktor.server.testing.* -import io.mockk.every -import io.mockk.mockkObject -import io.mockk.unmockkObject -import io.mockk.verify -import no.nav.tms.token.support.authentication.installer.mock.MockedInstallerProxy -import no.nav.tms.token.support.authentication.installer.mock.installMockedAuthenticators -import org.amshove.kluent.`should throw` -import org.amshove.kluent.invoking -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test - -internal class MockedAuthenticatorInstallerTest { - - @BeforeEach - fun setupMock() { - mockkObject(MockedInstallerProxy) - } - - @AfterEach - fun cleanup() { - unmockkObject(MockedInstallerProxy) - } - - @Test - fun `Should invoke only ID-porten installer when that is requested`() { - every { MockedInstallerProxy.invokeIdPortenMockInstaller(any(), any(), any()) } returns Unit - - val setup: Application.() -> Unit = { - installMockedAuthenticators { - installIdPortenAuthMock { } - } - } - - testApplication { - application { - setup() - } - } - - verify(exactly = 1) { MockedInstallerProxy.invokeIdPortenMockInstaller(any(), any(), any()) } - verify(exactly = 0) { MockedInstallerProxy.invokeTokenXMockInstaller(any(), any(), any()) } - verify(exactly = 0) { MockedInstallerProxy.invokeAzureMockInstaller(any(), any(), any()) } - } - - @Test - fun `Should invoke only TokenX installer when that is requested`() { - every { MockedInstallerProxy.invokeTokenXMockInstaller(any(), any(), any()) } returns Unit - - val setup: Application.() -> Unit = { - installMockedAuthenticators { - installTokenXAuthMock { } - } - } - - testApplication { - application { - setup() - } - } - - verify(exactly = 0) { MockedInstallerProxy.invokeIdPortenMockInstaller(any(), any(), any()) } - verify(exactly = 1) { MockedInstallerProxy.invokeTokenXMockInstaller(any(), any(), any()) } - verify(exactly = 0) { MockedInstallerProxy.invokeAzureMockInstaller(any(), any(), any()) } - } - - @Test - fun `Should invoke only Azure installer when that is requested`() { - every { MockedInstallerProxy.invokeAzureMockInstaller(any(), any(), any()) } returns Unit - - val setup: Application.() -> Unit = { - installMockedAuthenticators { - installAzureAuthMock { } - } - } - - testApplication { - application { - setup() - } - } - - verify(exactly = 0) { MockedInstallerProxy.invokeIdPortenMockInstaller(any(), any(), any()) } - verify(exactly = 0) { MockedInstallerProxy.invokeTokenXMockInstaller(any(), any(), any()) } - verify(exactly = 1) { MockedInstallerProxy.invokeAzureMockInstaller(any(), any(), any()) } - } - - @Test - fun `Should enable invoking several installers at once`() { - every { MockedInstallerProxy.invokeIdPortenMockInstaller(any(), any(), any()) } returns Unit - every { MockedInstallerProxy.invokeTokenXMockInstaller(any(), any(), any()) } returns Unit - every { MockedInstallerProxy.invokeAzureMockInstaller(any(), any(), any()) } returns Unit - - val setup: Application.() -> Unit = { - installMockedAuthenticators { - installIdPortenAuthMock { } - installTokenXAuthMock { } - installAzureAuthMock { } - } - } - - testApplication { - application { - setup() - } - } - - verify(exactly = 1) { MockedInstallerProxy.invokeIdPortenMockInstaller(any(), any(), any()) } - verify(exactly = 1) { MockedInstallerProxy.invokeTokenXMockInstaller(any(), any(), any()) } - verify(exactly = 1) { MockedInstallerProxy.invokeAzureMockInstaller(any(), any(), any()) } - } - - @Test - fun `Should allow one authenticator to be set as default`() { - every { MockedInstallerProxy.invokeIdPortenMockInstaller(any(), any(), any()) } returns Unit - every { MockedInstallerProxy.invokeTokenXMockInstaller(any(), any(), any()) } returns Unit - every { MockedInstallerProxy.invokeAzureMockInstaller(any(), any(), any()) } returns Unit - - val setup: Application.() -> Unit = { - installMockedAuthenticators { - installIdPortenAuthMock { setAsDefault = true } - installTokenXAuthMock { } - installAzureAuthMock { } - } - } - - testApplication { - application { - setup() - } - } - } - - @Test - fun `Should throw error if more than one authenticator is set as default`() { - every { MockedInstallerProxy.invokeIdPortenMockInstaller(any(), any(), any()) } returns Unit - every { MockedInstallerProxy.invokeTokenXMockInstaller(any(), any(), any()) } returns Unit - every { MockedInstallerProxy.invokeAzureMockInstaller(any(), any(), any()) } returns Unit - - val setup: Application.() -> Unit = { - installMockedAuthenticators { - installIdPortenAuthMock { setAsDefault = true } - installTokenXAuthMock { setAsDefault = true } - installAzureAuthMock { } - } - } - - invoking { - testApplication { - application { - setup() - } - } - } `should throw` IllegalArgumentException::class - } -} diff --git a/token-support-authentication-installer/README.md b/token-support-authentication-installer/README.md deleted file mode 100644 index 87fed1e..0000000 --- a/token-support-authentication-installer/README.md +++ /dev/null @@ -1,104 +0,0 @@ -# token-support-authentication-installer - -Denne modulen tilbyr en måte å installere flere av bibliotekets autentikatorer i parallell. Det er ikke nødvendig å -bruke denne modulen dersom en kun ønsker å installere én autentikator. - - -## Nais-yaml - -Krav for nais-yaml spesifikasjon er avhengig av hvilke autentikatorer som skal installeres. Referer til README for -autentikatorer som skal installeres. - -## Oppsett - -Biblioteket tilbyr en funksjon `Application.installAuthenticators` lar en installere autentikatorer slik det gjøres -i andre moduler: - -```kotlin -fun Application.mainModule() { - - installAuthenticators { - installIdPortenAuth { - ... - } - - installTokenXAuth { - ... - } - } -} -``` - -Dette gjør det mulig å velge autentikator for beskyttede endepunkt som følger: - -```kotlin -fun Application.mainModule() { - - installAuthenticators { - installIdPortenAuth { - ... - } - - installTokenXAuth { - ... - } - } - - routing { - authenticate(IdPortenCookieAuthenticator.name) { - get("/sikretForIdPorten") { - call.respond(HttpStatusCode.OK) - } - } - - authenticate(TokenXAuthenticator.name) { - get("/sikretForTokenX") { - call.respond(HttpStatusCode.OK) - } - } - } -} -``` - -Referer til README til hver enkelt autentikator for mer informasjon om oppsett og bruk. - -### Default authenticator - -Det er også mulig å velge opp til 1 autentikator som default, slik at den ikke trenger å navngis for hvert sikrede endepunkt. - -Eksempel for oppsett og bruk: - -```kotlin -fun Application.mainModule() { - - installAuthenticators { - installIdPortenAuth { - ... - setAsDefault = true - } - - installTokenXAuth { - ... - } - } - - routing { - authenticate { - get("/sikretForIdPorten") { - call.respond(HttpStatusCode.OK) - } - } - - authenticate(TokenXAuthenticator.name) { - get("/sikretForTokenX") { - call.respond(HttpStatusCode.OK) - } - } - } -} -``` - -## Bruk av biblioteket ved lokal kjøring - -Denne modulen krever ingen ting ekstra for å kunne kjøres lokalt. Referer til README for autentikatorer som skal -installeres for mer info. diff --git a/token-support-authentication-installer/build.gradle.kts b/token-support-authentication-installer/build.gradle.kts deleted file mode 100644 index 142a4a5..0000000 --- a/token-support-authentication-installer/build.gradle.kts +++ /dev/null @@ -1,49 +0,0 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -plugins { - `maven-publish` - `java-library` - kotlin("jvm") - kotlin("plugin.serialization") -} - -dependencies { - api(kotlin("stdlib-jdk8")) - implementation(Logback.classic) - implementation(project(":token-support-azure-validation")) - implementation(project(":token-support-tokenx-validation")) - implementation(project(":token-support-idporten-sidecar")) - implementation(Ktor.serverAuth) - testImplementation(kotlin("test-junit5")) - testImplementation(Kluent.kluent) - testImplementation(Ktor.serverTestHost) - testImplementation(Mockk.mockk) -} - -repositories { - mavenCentral() - mavenLocal() -} - -publishing { - repositories{ - mavenLocal() - } - - publications { - create("local") { - from(components["java"]) - } - } -} - -tasks.withType { - kotlinOptions.jvmTarget = "17" -} - -tasks { - - withType { - useJUnitPlatform() - } -} diff --git a/token-support-authentication-installer/src/main/kotlin/no/nav/tms/token/support/authentication/installer/AuthenticatorInstaller.kt b/token-support-authentication-installer/src/main/kotlin/no/nav/tms/token/support/authentication/installer/AuthenticatorInstaller.kt deleted file mode 100644 index 8c100b3..0000000 --- a/token-support-authentication-installer/src/main/kotlin/no/nav/tms/token/support/authentication/installer/AuthenticatorInstaller.kt +++ /dev/null @@ -1,99 +0,0 @@ -package no.nav.tms.token.support.authentication.installer - -import io.ktor.server.application.* -import io.ktor.server.auth.* -import no.nav.tms.token.support.azure.validation.AzureAuthenticatorConfig -import no.nav.tms.token.support.azure.validation.AzureInstaller.performAzureAuthenticatorInstallation -import no.nav.tms.token.support.idporten.sidecar.IdPortenInstaller.performIdPortenAuthenticatorInstallation -import no.nav.tms.token.support.idporten.sidecar.IdPortenRoutesConfig -import no.nav.tms.token.support.idporten.sidecar.IdportenAuthenticationConfig -import no.nav.tms.token.support.tokenx.validation.TokenXAuthenticatorConfig -import no.nav.tms.token.support.tokenx.validation.TokenXInstaller.performTokenXAuthenticatorInstallation -import org.slf4j.LoggerFactory - -internal object AuthenticatorInstaller { - private val log = LoggerFactory.getLogger(AuthenticatorInstaller::class.java) - - fun Application.performInstallation(config: AuthenticatorConfig) { - checkUsage(config) - validateDefaultToggle(config) - - var idPortenRoutesConfig: IdPortenRoutesConfig? = null - - val application = this - - install(Authentication) { - - val idPortenConfig = config.idPortenConfig - - val authContext = this - - if (idPortenConfig != null) { - idPortenRoutesConfig = InstallerProxy.invokeIdPortenInstaller(application, idPortenConfig, authContext) - } - - val tokenXConfig = config.tokenXConfig - - if (tokenXConfig != null) { - InstallerProxy.invokeTokenXInstaller(application, tokenXConfig, authContext) - } - - val azureConfig = config.azureConfig - - if (azureConfig != null) { - InstallerProxy.invokeAzureInstaller(application, azureConfig, authContext) - } - } - - setupIdPortenRoutesIfRequired(idPortenRoutesConfig) - } - - private fun Application.setupIdPortenRoutesIfRequired(routesConfig: IdPortenRoutesConfig?) { - routesConfig?.setupRoutes?.invoke(this) - } - - private fun checkUsage(config: AuthenticatorConfig) { - val numberInstalled = listOf(config.idPortenConfig, config.tokenXConfig, config.azureConfig).count { it != null } - - if (numberInstalled < 2) { - log.info("Using the token-support-authentication-installer module is not strictly necessary when installing less than two authenticators.") - } - } - - private fun validateDefaultToggle(config: AuthenticatorConfig) { - val numberSetAsDefault = listOf( - config.idPortenConfig?.setAsDefault, - config.tokenXConfig?.setAsDefault, - config.azureConfig?.setAsDefault - ).count { it == true } - - require(numberSetAsDefault < 2) { "At most one authenticator can be set as default." } - } -} - -// The primary purpose of this simple proxy is to enable testing without compromising on legibility too much -internal object InstallerProxy { - internal fun invokeIdPortenInstaller( - application: Application, - idPortenConfig: IdportenAuthenticationConfig, - existingAuthContext: AuthenticationConfig): IdPortenRoutesConfig { - - return application.performIdPortenAuthenticatorInstallation(idPortenConfig, existingAuthContext) - } - - internal fun invokeTokenXInstaller( - application: Application, - tokenXConfig: TokenXAuthenticatorConfig, - existingAuthContext: AuthenticationConfig) { - - application.performTokenXAuthenticatorInstallation(tokenXConfig, existingAuthContext) - } - - internal fun invokeAzureInstaller( - application: Application, - azureConfig: AzureAuthenticatorConfig, - existingAuthContext: AuthenticationConfig) { - - application.performAzureAuthenticatorInstallation(azureConfig, existingAuthContext) - } -} diff --git a/token-support-authentication-installer/src/main/kotlin/no/nav/tms/token/support/authentication/installer/config.kt b/token-support-authentication-installer/src/main/kotlin/no/nav/tms/token/support/authentication/installer/config.kt deleted file mode 100644 index 08311c4..0000000 --- a/token-support-authentication-installer/src/main/kotlin/no/nav/tms/token/support/authentication/installer/config.kt +++ /dev/null @@ -1,32 +0,0 @@ -package no.nav.tms.token.support.authentication.installer - -import io.ktor.server.application.* -import no.nav.tms.token.support.authentication.installer.AuthenticatorInstaller.performInstallation -import no.nav.tms.token.support.azure.validation.AzureAuthenticatorConfig -import no.nav.tms.token.support.idporten.sidecar.IdportenAuthenticationConfig -import no.nav.tms.token.support.tokenx.validation.TokenXAuthenticatorConfig - -fun Application.installAuthenticators(configure: AuthenticatorConfig.() -> Unit = {}) { - val config = AuthenticatorConfig().also(configure) - - performInstallation(config) -} - -class AuthenticatorConfig { - internal var idPortenConfig: IdportenAuthenticationConfig? = null - internal var tokenXConfig: TokenXAuthenticatorConfig? = null - internal var azureConfig: AzureAuthenticatorConfig? = null - - fun installIdPortenAuth(configure: IdportenAuthenticationConfig.() -> Unit) { - idPortenConfig = IdportenAuthenticationConfig().also(configure) - } - - fun installTokenXAuth(configure: TokenXAuthenticatorConfig.() -> Unit) { - tokenXConfig = TokenXAuthenticatorConfig().also(configure) - } - - fun installAzureAuth(configure: AzureAuthenticatorConfig.() -> Unit) { - azureConfig = AzureAuthenticatorConfig().also(configure) - } -} - diff --git a/token-support-authentication-installer/src/test/kotlin/no/nav/tms/token/support/authentication/installer/AuthenticatorInstallerTest.kt b/token-support-authentication-installer/src/test/kotlin/no/nav/tms/token/support/authentication/installer/AuthenticatorInstallerTest.kt deleted file mode 100644 index 14d2042..0000000 --- a/token-support-authentication-installer/src/test/kotlin/no/nav/tms/token/support/authentication/installer/AuthenticatorInstallerTest.kt +++ /dev/null @@ -1,160 +0,0 @@ -package no.nav.tms.token.support.authentication.installer - -import io.ktor.server.application.* -import io.ktor.server.testing.* -import io.mockk.every -import io.mockk.mockkObject -import io.mockk.unmockkObject -import io.mockk.verify -import no.nav.tms.token.support.idporten.sidecar.IdPortenRoutesConfig -import org.amshove.kluent.`should throw` -import org.amshove.kluent.invoking -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test - - -internal class AuthenticatorInstallerTest { - - @BeforeEach - fun setupMock() { - mockkObject(InstallerProxy) - } - - @AfterEach - fun cleanup() { - unmockkObject(InstallerProxy) - } - - @Test - fun `Should invoke only ID-porten installer when that is requested`() { - every { InstallerProxy.invokeIdPortenInstaller(any(), any(), any()) } returns IdPortenRoutesConfig { } - - val setup: Application.() -> Unit = { - installAuthenticators { - installIdPortenAuth { } - } - } - - testApplication { - application { - setup() - } - } - - verify(exactly = 1) { InstallerProxy.invokeIdPortenInstaller(any(), any(), any()) } - verify(exactly = 0) { InstallerProxy.invokeTokenXInstaller(any(), any(), any()) } - verify(exactly = 0) { InstallerProxy.invokeAzureInstaller(any(), any(), any()) } - } - - @Test - fun `Should invoke only TokenX installer when that is requested`() { - every { InstallerProxy.invokeTokenXInstaller(any(), any(), any()) } returns Unit - - val setup: Application.() -> Unit = { - installAuthenticators { - installTokenXAuth { } - } - } - - testApplication { - application { - setup() - } - } - - verify(exactly = 0) { InstallerProxy.invokeIdPortenInstaller(any(), any(), any()) } - verify(exactly = 1) { InstallerProxy.invokeTokenXInstaller(any(), any(), any()) } - verify(exactly = 0) { InstallerProxy.invokeAzureInstaller(any(), any(), any()) } - } - - @Test - fun `Should invoke only Azure installer when that is requested`() { - every { InstallerProxy.invokeAzureInstaller(any(), any(), any()) } returns Unit - - val setup: Application.() -> Unit = { - installAuthenticators { - installAzureAuth { } - } - } - - testApplication { - application { - setup() - } - } - - verify(exactly = 0) { InstallerProxy.invokeIdPortenInstaller(any(), any(), any()) } - verify(exactly = 0) { InstallerProxy.invokeTokenXInstaller(any(), any(), any()) } - verify(exactly = 1) { InstallerProxy.invokeAzureInstaller(any(), any(), any()) } - } - - @Test - fun `Should enable invoking several installers at once`() { - every { InstallerProxy.invokeIdPortenInstaller(any(), any(), any()) } returns IdPortenRoutesConfig { } - every { InstallerProxy.invokeTokenXInstaller(any(), any(), any()) } returns Unit - every { InstallerProxy.invokeAzureInstaller(any(), any(), any()) } returns Unit - - val setup: Application.() -> Unit = { - installAuthenticators { - installIdPortenAuth { } - installTokenXAuth { } - installAzureAuth { } - } - } - - testApplication { - application { - setup() - } - } - - verify(exactly = 1) { InstallerProxy.invokeIdPortenInstaller(any(), any(), any()) } - verify(exactly = 1) { InstallerProxy.invokeTokenXInstaller(any(), any(), any()) } - verify(exactly = 1) { InstallerProxy.invokeAzureInstaller(any(), any(), any()) } - } - - @Test - fun `Should allow one authenticator to be set as default`() { - every { InstallerProxy.invokeIdPortenInstaller(any(), any(), any()) } returns IdPortenRoutesConfig { } - every { InstallerProxy.invokeTokenXInstaller(any(), any(), any()) } returns Unit - every { InstallerProxy.invokeAzureInstaller(any(), any(), any()) } returns Unit - - val setup: Application.() -> Unit = { - installAuthenticators { - installIdPortenAuth { setAsDefault = true } - installTokenXAuth { } - installAzureAuth { } - } - } - - testApplication { - application { - setup() - } - } - } - - @Test - fun `Should throw error if more than one authenticator is set as default`() { - every { InstallerProxy.invokeIdPortenInstaller(any(), any(), any()) } returns IdPortenRoutesConfig { } - every { InstallerProxy.invokeTokenXInstaller(any(), any(), any()) } returns Unit - every { InstallerProxy.invokeAzureInstaller(any(), any(), any()) } returns Unit - - val setup: Application.() -> Unit = { - installAuthenticators { - installIdPortenAuth { setAsDefault = true } - installTokenXAuth { setAsDefault = true } - installAzureAuth { } - } - } - - invoking { - testApplication { - application { - setup() - } - } - } `should throw` IllegalArgumentException::class - } -} diff --git a/token-support-azure-exchange/README.md b/token-support-azure-exchange/README.md index 45db080..275e71b 100644 --- a/token-support-azure-exchange/README.md +++ b/token-support-azure-exchange/README.md @@ -20,7 +20,7 @@ Biblioteket tilbyr ett interface `AzureService` med to implementasjoner, `Cachin Disse bygges ved hjelp av `AzureServiceBuilder`: ```kotlin -fun Application.mainModule() { +fun Application.setup() { val serviceWithCache = AzureServiceBuilder.buildAzureService( cachingEnabled = true, diff --git a/token-support-azure-exchange/build.gradle.kts b/token-support-azure-exchange/build.gradle.kts index e4e222d..c6abbe3 100644 --- a/token-support-azure-exchange/build.gradle.kts +++ b/token-support-azure-exchange/build.gradle.kts @@ -10,6 +10,7 @@ plugins { dependencies { api(kotlin("stdlib-jdk8")) implementation(Logback.classic) + implementation(KotlinLogging.logging) implementation(Caffeine.caffeine) implementation(Ktor.serverAuth) implementation(Ktor.serverAuthJwt) diff --git a/token-support-azure-validation-mock/README.md b/token-support-azure-validation-mock/README.md index 596d302..369cb79 100644 --- a/token-support-azure-validation-mock/README.md +++ b/token-support-azure-validation-mock/README.md @@ -10,6 +10,7 @@ For å kunne autentisere et endepunkt må man først installere autentikatoren. Denne har 3 variabler: +- `authenticatorName`: Bestemmer navnet på autentikatoren. Default `AzureAuthenticator.name` - `setAsDefault`: (Optional) Setter denne autentikatoren som default. Default 'false' - `alwaysAuthenticated`: (Optional) Bestemmer om alle kall skal være godkjent eller motsatt. Default 'false' - `staticJwtOverride`: (Optional) Bestemmer hvilket token som evt skal settes i AzurePrincipal. Default 'null'. @@ -17,12 +18,14 @@ Denne har 3 variabler: Eksempel på konfigurasjon: ```kotlin -fun Application.mainModule() { +fun Application.setup() { - installAzureAuthMock { - setAsDefault = false - alwaysAuthenticated = false - staticJwtOverride = null + authentication { + azureMock { + setAsDefault = false + alwaysAuthenticated = false + staticJwtOverride = null + } } } ``` @@ -31,10 +34,12 @@ Deretter kan man autentisere bestemte endepunkt som følger. Hvis ikke denne aut viktig å ha med navnet på autentikatoren. ```kotlin -fun Application.mainModule() { +fun Application.setup() { - installAzureAuthMock { - setAsDefault = false + authentication { + azureMock { + setAsDefault = false + } } routing { diff --git a/token-support-azure-validation-mock/build.gradle.kts b/token-support-azure-validation-mock/build.gradle.kts index 4a157c7..e14f89b 100644 --- a/token-support-azure-validation-mock/build.gradle.kts +++ b/token-support-azure-validation-mock/build.gradle.kts @@ -10,6 +10,8 @@ plugins { dependencies { api(kotlin("stdlib-jdk8")) implementation(project(":token-support-azure-validation")) + implementation(Logback.classic) + implementation(KotlinLogging.logging) implementation(Ktor.serverAuth) implementation(Ktor.serverAuthJwt) implementation(Ktor.clientJson) diff --git a/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/AzureMockInstaller.kt b/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/AzureMockInstaller.kt deleted file mode 100644 index 47d8a0f..0000000 --- a/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/AzureMockInstaller.kt +++ /dev/null @@ -1,34 +0,0 @@ -package no.nav.tms.token.support.azure.validation.mock - -import io.ktor.server.application.* -import io.ktor.server.auth.* -import no.nav.tms.token.support.azure.validation.AzureAuthenticator -import no.nav.tms.token.support.azure.validation.mock.intercept.AuthInfo -import no.nav.tms.token.support.azure.validation.mock.intercept.azureAuthMock - -object AzureMockInstaller { - fun Application.performAzureMockAuthenticatorInstallation( - config: AzureMockedAuthenticatorConfig, - existingAuthContext: AuthenticationConfig? = null - ) { - val authenticatorName = getAuthenticatorName(config.setAsDefault) - - val authInfo = AuthInfo(config.alwaysAuthenticated, config.staticJwtOverride) - - if (existingAuthContext == null) { - install(Authentication) { - azureAuthMock(authenticatorName, authInfo) - } - } else { - existingAuthContext.azureAuthMock(authenticatorName, authInfo) - } - } - - private fun getAuthenticatorName(isDefault: Boolean): String? { - return if (isDefault) { - null - } else { - AzureAuthenticator.name - } - } -} diff --git a/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/config.kt b/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/config.kt index 41698cb..ecf08b0 100644 --- a/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/config.kt +++ b/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/config.kt @@ -1,10 +1,12 @@ package no.nav.tms.token.support.azure.validation.mock import io.ktor.server.application.* -import no.nav.tms.token.support.azure.validation.mock.AzureMockInstaller.performAzureMockAuthenticatorInstallation +import io.ktor.server.auth.* +import no.nav.tms.token.support.azure.validation.AzureAuthenticator +import no.nav.tms.token.support.azure.validation.mock.install.AzureMockInstaller.performAzureMockAuthenticatorInstallation -fun Application.installAzureAuthMock(configure: AzureMockedAuthenticatorConfig.() -> Unit = {}) { +fun AuthenticationConfig.azureMock(configure: AzureMockedAuthenticatorConfig.() -> Unit = {}) { val config = AzureMockedAuthenticatorConfig().also(configure) performAzureMockAuthenticatorInstallation(config) @@ -12,6 +14,7 @@ fun Application.installAzureAuthMock(configure: AzureMockedAuthenticatorConfig.( // Configuration provided by library user. See readme for example of use class AzureMockedAuthenticatorConfig { + var authenticatorName: String = AzureAuthenticator.name var setAsDefault: Boolean = false var alwaysAuthenticated: Boolean = false var staticJwtOverride: String? = null diff --git a/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/intercept/AuthInfo.kt b/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/install/AuthInfo.kt similarity index 59% rename from token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/intercept/AuthInfo.kt rename to token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/install/AuthInfo.kt index f9bc844..1d187a3 100644 --- a/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/intercept/AuthInfo.kt +++ b/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/install/AuthInfo.kt @@ -1,4 +1,4 @@ -package no.nav.tms.token.support.azure.validation.mock.intercept +package no.nav.tms.token.support.azure.validation.mock.install internal data class AuthInfo( val alwaysAuthenticated: Boolean, diff --git a/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/install/AzureMockInstaller.kt b/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/install/AzureMockInstaller.kt new file mode 100644 index 0000000..26e0117 --- /dev/null +++ b/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/install/AzureMockInstaller.kt @@ -0,0 +1,24 @@ +package no.nav.tms.token.support.azure.validation.mock.install + +import io.ktor.server.auth.* +import no.nav.tms.token.support.azure.validation.AzureAuthenticator +import no.nav.tms.token.support.azure.validation.mock.AzureMockedAuthenticatorConfig + +internal object AzureMockInstaller { + fun AuthenticationConfig.performAzureMockAuthenticatorInstallation( + config: AzureMockedAuthenticatorConfig, + ) { + registerAzureProviderMock( + authenticatorName = getAuthenticatorName(config), + authInfo = AuthInfo(config.alwaysAuthenticated, config.staticJwtOverride) + ) + } + + private fun getAuthenticatorName(config: AzureMockedAuthenticatorConfig): String? { + return if (config.setAsDefault) { + null + } else { + config.authenticatorName + } + } +} diff --git a/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/intercept/AzurePrincipalBuilder.kt b/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/install/AzurePrincipalBuilder.kt similarity index 95% rename from token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/intercept/AzurePrincipalBuilder.kt rename to token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/install/AzurePrincipalBuilder.kt index eb51100..ca97a12 100644 --- a/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/intercept/AzurePrincipalBuilder.kt +++ b/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/install/AzurePrincipalBuilder.kt @@ -1,4 +1,4 @@ -package no.nav.tms.token.support.azure.validation.mock.intercept +package no.nav.tms.token.support.azure.validation.mock.install import com.auth0.jwt.JWT import com.auth0.jwt.interfaces.DecodedJWT diff --git a/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/intercept/JwkBuilder.kt b/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/install/JwkBuilder.kt similarity index 86% rename from token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/intercept/JwkBuilder.kt rename to token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/install/JwkBuilder.kt index 07aa281..092940c 100644 --- a/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/intercept/JwkBuilder.kt +++ b/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/install/JwkBuilder.kt @@ -1,4 +1,4 @@ -package no.nav.tms.token.support.azure.validation.mock.intercept +package no.nav.tms.token.support.azure.validation.mock.install import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.jwk.KeyUse diff --git a/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/install/tokenAuthentication.kt b/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/install/tokenAuthentication.kt new file mode 100644 index 0000000..c377beb --- /dev/null +++ b/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/install/tokenAuthentication.kt @@ -0,0 +1,43 @@ +package no.nav.tms.token.support.azure.validation.mock.install + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.server.auth.* +import io.ktor.http.* +import io.ktor.serialization.* +import io.ktor.server.response.* +import org.slf4j.LoggerFactory + +internal fun AuthenticationConfig.registerAzureProviderMock(authenticatorName: String?, authInfo: AuthInfo) { + + AccessTokenAuthenticationProvider.Configuration(authenticatorName) + .let { config -> AccessTokenAuthenticationProvider(authInfo, config) } + .let { provider -> register(provider) } +} + +private fun AuthenticationContext.respondUnauthorized(message: String) { + + challenge("Unauthenticated", AuthenticationFailedCause.InvalidCredentials) { challenge, call -> + call.respond(HttpStatusCode.Unauthorized, message) + challenge.complete() + } +} + +private class AccessTokenAuthenticationProvider( + val authInfo: AuthInfo, + config: Configuration +) : AuthenticationProvider(config) { + + private val log = KotlinLogging.logger { } + + override suspend fun onAuthenticate(context: AuthenticationContext) { + if (authInfo.alwaysAuthenticated) { + log.debug {"Auth is valid as azure-mock is set to never authorized." } + context.principal(AzurePrincipalBuilder.createPrincipal(authInfo)) + } else { + log.debug {"Responding 401 as azure-mock is set to never authorized." } + context.respondUnauthorized("Never authorized.") + } + } + + class Configuration(name: String?) : Config(name) +} diff --git a/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/intercept/tokenAuthentication.kt b/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/intercept/tokenAuthentication.kt deleted file mode 100644 index e086e06..0000000 --- a/token-support-azure-validation-mock/src/main/kotlin/no/nav/tms/token/support/azure/validation/mock/intercept/tokenAuthentication.kt +++ /dev/null @@ -1,48 +0,0 @@ -package no.nav.tms.token.support.azure.validation.mock.intercept - -import com.auth0.jwt.JWT -import io.ktor.server.application.* -import io.ktor.server.auth.* -import io.ktor.http.* -import io.ktor.server.response.* -import no.nav.tms.token.support.azure.validation.AzurePrincipal -import org.slf4j.LoggerFactory - -internal fun AuthenticationConfig.azureAuthMock(authenticatorName: String?, authInfo: AuthInfo) { - - val provider = AccessTokenAuthenticationProvider.build(authInfo, authenticatorName) - - register(provider) -} - -private fun AuthenticationContext.respondUnauthorized(message: String) { - - challenge("Unauthenticated", AuthenticationFailedCause.InvalidCredentials) { challenge, call -> - call.respond(HttpStatusCode.Unauthorized, message) - challenge.complete() - } -} - -private class AccessTokenAuthenticationProvider constructor( - val authInfo: AuthInfo, - config: Configuration -) : AuthenticationProvider(config) { - - private val log = LoggerFactory.getLogger(AccessTokenAuthenticationProvider::class.java) - - override suspend fun onAuthenticate(context: AuthenticationContext) { - if (authInfo.alwaysAuthenticated) { - log.debug("Auth is valid as azure-mock is set to never authorized.") - context.principal(AzurePrincipalBuilder.createPrincipal(authInfo)) - } else { - log.debug("Responding 401 as azure-mock is set to never authorized.") - context.respondUnauthorized("Never authorized.") - } - } - - class Configuration(name: String?) : AuthenticationProvider.Config(name) - - companion object { - fun build(authInfo: AuthInfo, name: String?) = AccessTokenAuthenticationProvider(authInfo, Configuration(name)) - } -} diff --git a/token-support-azure-validation-mock/src/test/kotlin/no/nav/tms/token/support/azure/validation/mock/AzureAuthTest.kt b/token-support-azure-validation-mock/src/test/kotlin/no/nav/tms/token/support/azure/validation/mock/AzureAuthTest.kt index 513a67f..75e1a9c 100644 --- a/token-support-azure-validation-mock/src/test/kotlin/no/nav/tms/token/support/azure/validation/mock/AzureAuthTest.kt +++ b/token-support-azure-validation-mock/src/test/kotlin/no/nav/tms/token/support/azure/validation/mock/AzureAuthTest.kt @@ -23,8 +23,11 @@ internal class AzureAuthTest { application { testApi { - installAzureAuthMock { - alwaysAuthenticated = false + authentication { + azureMock { + alwaysAuthenticated = false + staticJwtOverride = jwtOverrideString + } } } } @@ -39,9 +42,11 @@ internal class AzureAuthTest { application { testApi { - installAzureAuthMock { - alwaysAuthenticated = true - staticJwtOverride = jwtOverrideString + authentication { + azureMock { + alwaysAuthenticated = true + staticJwtOverride = jwtOverrideString + } } } } @@ -57,9 +62,11 @@ internal class AzureAuthTest { application { testApi { - installAzureAuthMock { - alwaysAuthenticated = true - staticJwtOverride = null + authentication { + azureMock { + alwaysAuthenticated = true + staticJwtOverride = jwtOverrideString + } } } } @@ -75,9 +82,12 @@ internal class AzureAuthTest { application { testApiWithDefault { - installAzureAuthMock { - setAsDefault = true - alwaysAuthenticated = false + authentication { + azureMock { + setAsDefault = true + alwaysAuthenticated = false + staticJwtOverride = jwtOverrideString + } } } } diff --git a/token-support-azure-validation-mock/src/test/kotlin/no/nav/tms/token/support/azure/validation/mock/JwtBuilder.kt b/token-support-azure-validation-mock/src/test/kotlin/no/nav/tms/token/support/azure/validation/mock/JwtBuilder.kt index 201c078..411845d 100644 --- a/token-support-azure-validation-mock/src/test/kotlin/no/nav/tms/token/support/azure/validation/mock/JwtBuilder.kt +++ b/token-support-azure-validation-mock/src/test/kotlin/no/nav/tms/token/support/azure/validation/mock/JwtBuilder.kt @@ -8,7 +8,7 @@ import com.nimbusds.jose.JWSHeader import com.nimbusds.jose.crypto.RSASSASigner import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT -import no.nav.tms.token.support.azure.validation.mock.intercept.JwkBuilder +import no.nav.tms.token.support.azure.validation.mock.install.JwkBuilder import java.time.Instant import java.util.* diff --git a/token-support-azure-validation/README.md b/token-support-azure-validation/README.md index d2870b4..873e888 100644 --- a/token-support-azure-validation/README.md +++ b/token-support-azure-validation/README.md @@ -19,16 +19,19 @@ For å kunne autentisere et endepunkt må man først installere autentikatoren. Denne har 2 variabler: +- `authenticatorName`: Bestemmer navnet på autentikatoren. Default `AzureAuthenticator.name` - `setAsDefault`: (Optional) Setter denne autentikatoren som default. Default 'false' - `enableDefaultProxy`: (Optional) Bestemmer hvorvidt system-default proxy skal brukes ved kall mot andre tjenester. Nødvendig for on-prem apper med webproxy. Eksempel på konfigurasjon: ```kotlin -fun Application.mainModule() { +fun Application.setup() { - installAzureAuth { - setAsDefault = false + authentication { + azure { + setAsDefault = false + } } } ``` @@ -37,10 +40,12 @@ Deretter kan man autentisere bestemte endepunkt som følger. Hvis ikke denne aut viktig å ha med navnet på autentikatoren. ```kotlin -fun Application.mainModule() { +fun Application.setup() { - installAzureAuth { - setAsDefault = false + authentication { + azure { + setAsDefault = false + } } routing { @@ -56,10 +61,12 @@ fun Application.mainModule() { Typisk eksempel på bruk i miljø og dette er default authenticator: ```kotlin -fun Application.mainModule() { +fun Application.setup() { - installAzureAuth { - setAsDefault = true + authentication { + azure { + setAsDefault = true + } } routing { diff --git a/token-support-azure-validation/build.gradle.kts b/token-support-azure-validation/build.gradle.kts index ab13aa7..a005967 100644 --- a/token-support-azure-validation/build.gradle.kts +++ b/token-support-azure-validation/build.gradle.kts @@ -10,6 +10,7 @@ plugins { dependencies { api(kotlin("stdlib-jdk8")) implementation(Logback.classic) + implementation(KotlinLogging.logging) implementation(Ktor.serverAuth) implementation(Ktor.serverAuthJwt) implementation(Ktor.clientApache) diff --git a/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/AzureInstaller.kt b/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/AzureInstaller.kt deleted file mode 100644 index 1cad413..0000000 --- a/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/AzureInstaller.kt +++ /dev/null @@ -1,33 +0,0 @@ -package no.nav.tms.token.support.azure.validation - -import io.ktor.server.application.* -import io.ktor.server.auth.* -import no.nav.tms.token.support.azure.validation.config.RuntimeContext -import no.nav.tms.token.support.azure.validation.intercept.azureAccessToken - -object AzureInstaller { - fun Application.performAzureAuthenticatorInstallation( - config: AzureAuthenticatorConfig, - existingAuthContext: AuthenticationConfig? = null - ) { - val authenticatorName = getAuthenticatorName(config.setAsDefault) - - val runtimeContext = RuntimeContext(config.enableDefaultProxy) - - if (existingAuthContext == null) { - install(Authentication) { - azureAccessToken(authenticatorName, runtimeContext.verifierWrapper) - } - } else { - existingAuthContext.azureAccessToken(authenticatorName, runtimeContext.verifierWrapper) - } - } - - private fun getAuthenticatorName(isDefault: Boolean): String? { - return if (isDefault) { - null - } else { - AzureAuthenticator.name - } - } -} diff --git a/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/config.kt b/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/config.kt index 330809a..52f7049 100644 --- a/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/config.kt +++ b/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/config.kt @@ -1,17 +1,17 @@ package no.nav.tms.token.support.azure.validation -import io.ktor.server.application.* -import no.nav.tms.token.support.azure.validation.AzureInstaller.performAzureAuthenticatorInstallation +import io.ktor.server.auth.* +import no.nav.tms.token.support.azure.validation.install.AzureInstaller.performAzureAuthenticatorInstallation -fun Application.installAzureAuth(configure: AzureAuthenticatorConfig.() -> Unit = {}) { - val config = AzureAuthenticatorConfig().also(configure) - - performAzureAuthenticatorInstallation(config) -} +fun AuthenticationConfig.azure(configure: AzureAuthenticatorConfig.() -> Unit = {}) = + AzureAuthenticatorConfig() + .also(configure) + .let { performAzureAuthenticatorInstallation(it) } // Configuration provided by library user. See readme for example of use class AzureAuthenticatorConfig { + var authenticatorName: String = AzureAuthenticator.name var setAsDefault: Boolean = false var enableDefaultProxy: Boolean = false } diff --git a/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/config/Environment.kt b/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/config/Environment.kt deleted file mode 100644 index a0b3fd9..0000000 --- a/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/config/Environment.kt +++ /dev/null @@ -1,11 +0,0 @@ -package no.nav.tms.token.support.azure.validation.config - -internal data class Environment( - val azureClientId: String = getAzureEnvVar("AZURE_APP_CLIENT_ID"), - val azureWellKnownUrl: String = getAzureEnvVar("AZURE_APP_WELL_KNOWN_URL") -) - -private fun getAzureEnvVar(varName: String): String { - return System.getenv(varName) - ?: throw IllegalArgumentException("Fant ikke $varName for azure. Påse at nais.yaml er konfigurert riktig.") -} diff --git a/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/config/JwkProviderBuilder.kt b/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/config/JwkProviderBuilder.kt deleted file mode 100644 index 0ae4f21..0000000 --- a/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/config/JwkProviderBuilder.kt +++ /dev/null @@ -1,13 +0,0 @@ -package no.nav.tms.token.support.azure.validation.config - -import com.auth0.jwk.JwkProvider -import com.auth0.jwk.JwkProviderBuilder -import java.net.URL -import java.util.concurrent.TimeUnit - -internal object JwkProviderBuilder { - fun createJwkProvider(metadata: OauthServerConfigurationMetadata): JwkProvider = JwkProviderBuilder(URL(metadata.jwksUri)) - .cached(10, 24, TimeUnit.HOURS) - .rateLimited(10, 1, TimeUnit.MINUTES) - .build() -} diff --git a/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/config/OauthServerConfigurationMetadata.kt b/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/config/OauthServerConfigurationMetadata.kt deleted file mode 100644 index ac1766a..0000000 --- a/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/config/OauthServerConfigurationMetadata.kt +++ /dev/null @@ -1,12 +0,0 @@ -package no.nav.tms.token.support.azure.validation.config - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -internal data class OauthServerConfigurationMetadata( - @SerialName("issuer") val issuer: String, - @SerialName("token_endpoint") val tokenEndpoint: String, - @SerialName("jwks_uri") val jwksUri: String, - @SerialName("authorization_endpoint") var authorizationEndpoint: String = "" -) diff --git a/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/config/RuntimeContext.kt b/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/config/RuntimeContext.kt deleted file mode 100644 index eac1c02..0000000 --- a/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/config/RuntimeContext.kt +++ /dev/null @@ -1,25 +0,0 @@ -package no.nav.tms.token.support.azure.validation.config - -import io.ktor.client.* -import kotlinx.coroutines.runBlocking -import no.nav.tms.token.support.azure.validation.config.JwkProviderBuilder.createJwkProvider -import no.nav.tms.token.support.azure.validation.intercept.TokenVerifier - -internal class RuntimeContext(enableDefaultProxy: Boolean) { - private val environment = Environment() - - private val httpClient = HttpClientBuilder.build(enableDefaultProxy) - private val metadata = fetchMetadata(httpClient, environment.azureWellKnownUrl) - - private val jwkProvider = createJwkProvider(metadata) - - val verifierWrapper = TokenVerifier( - jwkProvider = jwkProvider, - clientId = environment.azureClientId, - issuer = metadata.issuer - ) -} - -private fun fetchMetadata(httpClient: HttpClient, idPortenUrl: String) = runBlocking { - httpClient.getOAuthServerConfigurationMetadata(idPortenUrl) -} diff --git a/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/config/httpClient.kt b/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/config/httpClient.kt deleted file mode 100644 index c0d7e69..0000000 --- a/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/config/httpClient.kt +++ /dev/null @@ -1,17 +0,0 @@ -package no.nav.tms.token.support.azure.validation.config - -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.request.* -import io.ktor.http.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -internal suspend fun HttpClient.getOAuthServerConfigurationMetadata(url: String) - : OauthServerConfigurationMetadata = withContext(Dispatchers.IO) { - request { - method = HttpMethod.Get - url(url) - accept(ContentType.Application.Json) - }.body() -} diff --git a/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/install/AzureInstaller.kt b/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/install/AzureInstaller.kt new file mode 100644 index 0000000..466e673 --- /dev/null +++ b/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/install/AzureInstaller.kt @@ -0,0 +1,24 @@ +package no.nav.tms.token.support.azure.validation.install + +import io.ktor.server.auth.* +import no.nav.tms.token.support.azure.validation.AzureAuthenticator +import no.nav.tms.token.support.azure.validation.AzureAuthenticatorConfig + +internal object AzureInstaller { + fun AuthenticationConfig.performAzureAuthenticatorInstallation( + config: AzureAuthenticatorConfig + ) { + registerAzureValidationProvider( + authenticatorName = getAuthenticatorName(config), + tokenVerifier = initializeTokenVerifier(config.enableDefaultProxy) + ) + } + + private fun getAuthenticatorName(config: AzureAuthenticatorConfig): String? { + return if (config.setAsDefault) { + null + } else { + config.authenticatorName + } + } +} diff --git a/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/config/HttpClientBuilder.kt b/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/install/HttpClientBuilder.kt similarity index 94% rename from token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/config/HttpClientBuilder.kt rename to token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/install/HttpClientBuilder.kt index e05ca1e..6e9e9cf 100644 --- a/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/config/HttpClientBuilder.kt +++ b/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/install/HttpClientBuilder.kt @@ -1,4 +1,4 @@ -package no.nav.tms.token.support.azure.validation.config +package no.nav.tms.token.support.azure.validation.install import io.ktor.client.* import io.ktor.client.engine.apache.* diff --git a/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/intercept/tokenAuthentication.kt b/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/install/tokenAuthentication.kt similarity index 66% rename from token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/intercept/tokenAuthentication.kt rename to token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/install/tokenAuthentication.kt index f19a0d5..43d5394 100644 --- a/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/intercept/tokenAuthentication.kt +++ b/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/install/tokenAuthentication.kt @@ -1,27 +1,27 @@ -package no.nav.tms.token.support.azure.validation.intercept +package no.nav.tms.token.support.azure.validation.install +import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.http.* import io.ktor.server.response.* -import io.ktor.util.pipeline.* import no.nav.tms.token.support.azure.validation.AzureHeader import no.nav.tms.token.support.azure.validation.AzurePrincipal import org.slf4j.LoggerFactory -internal fun AuthenticationConfig.azureAccessToken(authenticatorName: String?, verifier: TokenVerifier) { +internal fun AuthenticationConfig.registerAzureValidationProvider(authenticatorName: String?, tokenVerifier: TokenVerifier) { - val provider = AccessTokenAuthenticationProvider.build(verifier, authenticatorName) - - register(provider) + AccessTokenAuthenticationProvider.Configuration(authenticatorName) + .let { config -> AccessTokenAuthenticationProvider(tokenVerifier, config) } + .let { provider -> register(provider) } } -private class AccessTokenAuthenticationProvider constructor( +private class AccessTokenAuthenticationProvider( val verifier: TokenVerifier, config: Config ) : AuthenticationProvider(config) { - val log = LoggerFactory.getLogger(AccessTokenAuthenticationProvider::class.java) + val log = KotlinLogging.logger { } override suspend fun onAuthenticate(context: AuthenticationContext) { val accessToken = context.call.bearerToken @@ -30,21 +30,16 @@ private class AccessTokenAuthenticationProvider constructor( val decodedJWT = verifier.verify(accessToken) context.principal(AzurePrincipal(decodedJWT)) } catch (e: Exception) { - val message = e.message ?: e.javaClass.simpleName - log.debug("Token verification failed: {}", message) + log.debug(e) { "Token verification failed" } context.respondUnauthorized("Invalid or expired token.") } } else { - log.debug("No bearer token found.") + log.debug { "No bearer token found." } context.respondUnauthorized("No bearer token found.") } } - class Configuration(name: String?) : AuthenticationProvider.Config(name) - - companion object { - fun build(verifier: TokenVerifier, name: String?) = AccessTokenAuthenticationProvider(verifier, Configuration(name)) - } + class Configuration(name: String?) : Config(name) } private fun AuthenticationContext.respondUnauthorized(message: String) { diff --git a/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/install/tokenVerifier.kt b/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/install/tokenVerifier.kt new file mode 100644 index 0000000..270e8ad --- /dev/null +++ b/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/install/tokenVerifier.kt @@ -0,0 +1,89 @@ +package no.nav.tms.token.support.azure.validation.install + +import com.auth0.jwk.Jwk +import com.auth0.jwk.JwkProvider +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import com.auth0.jwt.interfaces.DecodedJWT +import com.auth0.jwt.interfaces.JWTVerifier +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.http.* +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.net.URL +import java.security.interfaces.RSAPublicKey +import java.util.concurrent.TimeUnit + +private fun getAzureClientId() = getAzureEnvVar("AZURE_APP_CLIENT_ID") +private fun getAzureWellKnownUrl() = getAzureEnvVar("AZURE_APP_WELL_KNOWN_URL") + +internal fun initializeTokenVerifier( + enableDefaultProxy: Boolean, +) : TokenVerifier { + + val metadata = fetchMetadata( + client = HttpClientBuilder.build(enableDefaultProxy), + wellKnownUrl = getAzureWellKnownUrl() + ) + + val jwkProvider = JwlProviderBuilder.createJwkProvider(metadata) + + return TokenVerifier( + jwkProvider = jwkProvider, + issuer = metadata.issuer, + clientId = getAzureClientId() + ) +} + +internal class TokenVerifier( + private val jwkProvider: JwkProvider, + private val clientId: String, + private val issuer: String +) { + + fun verify(accessToken: String): DecodedJWT { + return JWT.decode(accessToken).keyId + .let { kid -> jwkProvider.get(kid) } + .run { azureAccessTokenVerifier(clientId, issuer) } + .run { verify(accessToken) } + } + + private fun Jwk.azureAccessTokenVerifier(clientId: String, issuer: String): JWTVerifier = + JWT.require(this.RSA256()) + .withAudience(clientId) + .withIssuer(issuer) + .build() + + private fun Jwk.RSA256() = Algorithm.RSA256(publicKey as RSAPublicKey, null) +} + + +@Serializable +internal data class OauthServerConfigurationMetadata( + @SerialName("issuer") val issuer: String, + @SerialName("token_endpoint") val tokenEndpoint: String, + @SerialName("jwks_uri") val jwksUri: String, + @SerialName("authorization_endpoint") var authorizationEndpoint: String = "" +) + +private fun fetchMetadata(client: HttpClient, wellKnownUrl: String): OauthServerConfigurationMetadata = runBlocking { + client.request { + method = HttpMethod.Get + url(wellKnownUrl) + accept(ContentType.Application.Json) + }.body() +} + +internal object JwlProviderBuilder { + fun createJwkProvider(metadata: OauthServerConfigurationMetadata): JwkProvider = + com.auth0.jwk.JwkProviderBuilder(URL(metadata.jwksUri)) + .cached(10, 24, TimeUnit.HOURS) + .rateLimited(10, 1, TimeUnit.MINUTES) + .build() +} + +private fun getAzureEnvVar(varName: String) = System.getenv(varName) + ?: throw IllegalArgumentException("Fant ikke $varName for azure. Påse at nais.yaml er konfigurert riktig.") diff --git a/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/intercept/TokenVerifier.kt b/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/intercept/TokenVerifier.kt deleted file mode 100644 index bcf2258..0000000 --- a/token-support-azure-validation/src/main/kotlin/no/nav/tms/token/support/azure/validation/intercept/TokenVerifier.kt +++ /dev/null @@ -1,33 +0,0 @@ -package no.nav.tms.token.support.azure.validation.intercept - -import com.auth0.jwk.Jwk -import com.auth0.jwk.JwkProvider -import com.auth0.jwt.JWT -import com.auth0.jwt.algorithms.Algorithm -import com.auth0.jwt.interfaces.DecodedJWT -import com.auth0.jwt.interfaces.JWTVerifier -import java.security.interfaces.RSAPublicKey - -internal class TokenVerifier( - private val jwkProvider: JwkProvider, - private val clientId: String, - private val issuer: String -) { - - fun verify(accessToken: String): DecodedJWT { - return JWT.decode(accessToken).keyId - .let { kid -> jwkProvider.get(kid) } - .run { azureAccessTokenVerifier(clientId, issuer) } - .run { verify(accessToken) } - } - - private fun Jwk.azureAccessTokenVerifier(clientId: String, issuer: String): JWTVerifier = - JWT.require(this.RSA256()) - .withAudience(clientId) - .withIssuer(issuer) - .build() - - private fun Jwk.RSA256() = Algorithm.RSA256(publicKey as RSAPublicKey, null) -} - - diff --git a/token-support-azure-validation/src/test/kotlin/no/nav/tms/token/support/azure/validation/AzureAuthIT.kt b/token-support-azure-validation/src/test/kotlin/no/nav/tms/token/support/azure/validation/AzureAuthIT.kt index fdd5909..2bb9e8d 100644 --- a/token-support-azure-validation/src/test/kotlin/no/nav/tms/token/support/azure/validation/AzureAuthIT.kt +++ b/token-support-azure-validation/src/test/kotlin/no/nav/tms/token/support/azure/validation/AzureAuthIT.kt @@ -4,17 +4,17 @@ package no.nav.tms.token.support.azure.validation import io.kotest.extensions.system.withEnvironment import io.ktor.client.call.* import io.ktor.client.request.* +import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* -import io.ktor.http.* import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.server.testing.* import io.mockk.every import io.mockk.mockkObject import io.mockk.unmockkObject -import no.nav.tms.token.support.azure.validation.config.HttpClientBuilder -import no.nav.tms.token.support.azure.validation.config.JwkProviderBuilder +import no.nav.tms.token.support.azure.validation.install.HttpClientBuilder +import no.nav.tms.token.support.azure.validation.install.JwlProviderBuilder import org.amshove.kluent.`should be equal to` import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach @@ -38,15 +38,15 @@ internal class AzureAuthIT { @BeforeEach fun setupMock() { mockkObject(HttpClientBuilder) - mockkObject(JwkProviderBuilder) + mockkObject(JwlProviderBuilder) every { HttpClientBuilder.build(any()) } returns mockedClient - every { JwkProviderBuilder.createJwkProvider(any()) } returns mockedJwkProvider + every { JwlProviderBuilder.createJwkProvider(any()) } returns mockedJwkProvider } @AfterEach fun cleanUp() { unmockkObject(HttpClientBuilder) - unmockkObject(JwkProviderBuilder) + unmockkObject(JwlProviderBuilder) } @Test @@ -232,7 +232,9 @@ internal class AzureAuthIT { private fun Application.testApi() = withEnvironment(envVars) { - installAzureAuth() + authentication { + azure() + } routing { authenticate(AzureAuthenticator.name) { @@ -245,8 +247,10 @@ internal class AzureAuthIT { private fun Application.testApiWithDefault() = withEnvironment(envVars) { - installAzureAuth { - setAsDefault = true + authentication { + azure { + setAsDefault = true + } } routing { diff --git a/token-support-azure-validation/src/test/kotlin/no/nav/tms/token/support/azure/validation/mockedClient.kt b/token-support-azure-validation/src/test/kotlin/no/nav/tms/token/support/azure/validation/mockedClient.kt index 2f5da56..fcf9c8a 100644 --- a/token-support-azure-validation/src/test/kotlin/no/nav/tms/token/support/azure/validation/mockedClient.kt +++ b/token-support-azure-validation/src/test/kotlin/no/nav/tms/token/support/azure/validation/mockedClient.kt @@ -8,7 +8,7 @@ import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.serialization.kotlinx.json.* import kotlinx.serialization.encodeToString import no.nav.tms.token.support.azure.validation.ObjectMapper.kotlinxMapper -import no.nav.tms.token.support.azure.validation.config.OauthServerConfigurationMetadata +import no.nav.tms.token.support.azure.validation.install.OauthServerConfigurationMetadata diff --git a/token-support-idporten-sidecar-mock/README.md b/token-support-idporten-sidecar-mock/README.md index 36903b8..ee56e67 100644 --- a/token-support-idporten-sidecar-mock/README.md +++ b/token-support-idporten-sidecar-mock/README.md @@ -8,13 +8,14 @@ Kun ment å brukes for testing, og bør ikke havne i miljø. For å kunne autentisere et endepunkt må man først installere autentikatoren. -Denne har 1 variabel: +Denne har en rekke variabler: -`setAsDefault`: (Optional) Setter denne autentikatoren som default. Default 'false' -`alwaysAuthenticated`: (Optional) Bestemmer om alle kall skal være godkjent eller motsatt. Default 'false' -`staticLevelOfAssurance`: Bestemmer hvilket innloggingsnivå bruker er logget inn med. Default 'null'. -`staticUserPid`: Bestemmer hvilken ident bruker er logget inn med. Default 'null'. -`staticJwtOverride`: Bestemmer hvilket token som evt skal settes i AzurePrincipal. Default 'null'. +- `authenticatorName`: Bestemmer navnet på autentikatoren. Default `IdPortenAuthenticator.name` +- `setAsDefault`: (Optional) Setter denne autentikatoren som default. Default 'false' +- `alwaysAuthenticated`: (Optional) Bestemmer om alle kall skal være godkjent eller motsatt. Default 'false' +- `staticLevelOfAssurance`: Bestemmer hvilket innloggingsnivå bruker er logget inn med. Default 'null'. +- `staticUserPid`: Bestemmer hvilken ident bruker er logget inn med. Default 'null'. +- `staticJwtOverride`: Bestemmer hvilket token som evt skal settes i AzurePrincipal. Default 'null'. Dersom alwaysAuthenticated er 'true', må enten 'staticLevelOfAssurance' og 'staticUserPid' være satt, eller så må 'staticJwtOverride' være satt. 'staticJwtOverride' må ha claims 'acr_values' (satt til `idporten-loa-substantial` eller `idporten-loa-high`) og 'pid'. @@ -23,7 +24,7 @@ Dersom alle feltene er satt er det 'staticJwtOverride' som er gjeldende. Eksempel på konfigurasjon: ```kotlin -fun Application.mainModule() { +fun Application.setup() { installIdPortenAuthMock { setAsDefault = false @@ -38,7 +39,7 @@ Deretter kan man autentisere bestemte endepunkt som følger. Hvis ikke denne aut viktig å ha med navnet på autentikatoren. ```kotlin -fun Application.mainModule() { +fun Application.setup() { installIdPortenAuth { setAsDefault = false diff --git a/token-support-idporten-sidecar-mock/build.gradle.kts b/token-support-idporten-sidecar-mock/build.gradle.kts index 2d4c6b8..d1eea41 100644 --- a/token-support-idporten-sidecar-mock/build.gradle.kts +++ b/token-support-idporten-sidecar-mock/build.gradle.kts @@ -10,6 +10,8 @@ plugins { dependencies { api(kotlin("stdlib-jdk8")) implementation(project(":token-support-idporten-sidecar")) + implementation(Logback.classic) + implementation(KotlinLogging.logging) implementation(Ktor.serverAuth) implementation(Ktor.serverAuthJwt) implementation(Ktor.clientJson) diff --git a/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/IdPortenMockInstaller.kt b/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/IdPortenMockInstaller.kt deleted file mode 100644 index 0f410c2..0000000 --- a/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/IdPortenMockInstaller.kt +++ /dev/null @@ -1,34 +0,0 @@ -package no.nav.tms.token.support.idporten.sidecar.mock - -import io.ktor.server.application.* -import io.ktor.server.auth.* -import no.nav.tms.token.support.idporten.sidecar.IdPortenCookieAuthenticator -import no.nav.tms.token.support.idporten.sidecar.mock.authentication.AuthInfoValidator.validateAuthInfo -import no.nav.tms.token.support.idporten.sidecar.mock.authentication.idPortenAuthMock - -object IdPortenMockInstaller { - fun Application.performIdPortenMockInstallation( - config: IdPortenMockedAuthenticatorConfig, - existingAuthContext: AuthenticationConfig? = null - ) { - val authenticatorName = getAuthenticatorName(config.setAsDefault) - - val authInfo = validateAuthInfo(config) - - if (existingAuthContext == null) { - install(Authentication) { - idPortenAuthMock(authenticatorName, authInfo) - } - } else { - existingAuthContext.idPortenAuthMock(authenticatorName, authInfo) - } - } - - private fun getAuthenticatorName(isDefault: Boolean): String? { - return if (isDefault) { - null - } else { - IdPortenCookieAuthenticator.name - } - } -} diff --git a/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/authentication/tokenAuthentication.kt b/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/authentication/tokenAuthentication.kt deleted file mode 100644 index 1a7435a..0000000 --- a/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/authentication/tokenAuthentication.kt +++ /dev/null @@ -1,45 +0,0 @@ -package no.nav.tms.token.support.idporten.sidecar.mock.authentication - -import io.ktor.server.auth.* -import io.ktor.http.* -import io.ktor.server.response.* -import org.slf4j.LoggerFactory - -internal fun AuthenticationConfig.idPortenAuthMock(authenticatorName: String?, authInfo: AuthInfo) { - - val provider = AccessTokenAuthenticationProvider.build(authInfo, authenticatorName) - - register(provider) -} - -private fun AuthenticationContext.respondUnauthorized(message: String) { - - challenge("Unauthenticated", AuthenticationFailedCause.InvalidCredentials) { challenge, call -> - call.respond(HttpStatusCode.Unauthorized, message) - challenge.complete() - } -} - -private class AccessTokenAuthenticationProvider constructor( - val authInfo: AuthInfo, - config: Configuration -) : AuthenticationProvider(config) { - - private val log = LoggerFactory.getLogger(AccessTokenAuthenticationProvider::class.java) - - override suspend fun onAuthenticate(context: AuthenticationContext) { - if (authInfo.alwaysAuthenticated) { - log.debug("Auth is valid as idporten-mock is set to never authorized.") - context.principal(IdPortenPrincipalBuilder.createPrincipal(authInfo)) - } else { - log.debug("Responding 401 as idporten-mock is set to never authorized.") - context.respondUnauthorized("Never authorized.") - } - } - - class Configuration(name: String?) : AuthenticationProvider.Config(name) - - companion object { - fun build(authInfo: AuthInfo, name: String?) = AccessTokenAuthenticationProvider(authInfo, Configuration(name)) - } -} diff --git a/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/config.kt b/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/config.kt index fdbcec4..12ff2ee 100644 --- a/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/config.kt +++ b/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/config.kt @@ -1,30 +1,14 @@ package no.nav.tms.token.support.idporten.sidecar.mock -import io.ktor.server.application.* import io.ktor.server.auth.* -import no.nav.tms.token.support.idporten.sidecar.IdPortenCookieAuthenticator -import no.nav.tms.token.support.idporten.sidecar.mock.authentication.AuthInfoValidator -import no.nav.tms.token.support.idporten.sidecar.mock.authentication.idPortenAuthMock +import no.nav.tms.token.support.idporten.sidecar.IdPortenAuthenticator +import no.nav.tms.token.support.idporten.sidecar.mock.install.IdPortenMockInstaller.performIdPortenMockInstallation -fun Application.installIdPortenAuthMock(configure: IdPortenMockedAuthenticatorConfig.() -> Unit = {}) { +fun AuthenticationConfig.idPortenMock(configure: IdPortenMockedAuthenticatorConfig.() -> Unit = {}) { val config = IdPortenMockedAuthenticatorConfig().also(configure) - val authenticatorName = getAuthenticatorName(config.setAsDefault) - - val authInfo = AuthInfoValidator.validateAuthInfo(config) - - install(Authentication) { - idPortenAuthMock(authenticatorName, authInfo) - } -} - -private fun getAuthenticatorName(isDefault: Boolean): String? { - return if (isDefault) { - null - } else { - IdPortenCookieAuthenticator.name - } + performIdPortenMockInstallation(config) } enum class LevelOfAssurance(val claim: String) { @@ -36,6 +20,7 @@ enum class LevelOfAssurance(val claim: String) { // Configuration provided by library user. See readme for example of use class IdPortenMockedAuthenticatorConfig { + var authenticatorName: String = IdPortenAuthenticator.name var setAsDefault: Boolean = false var alwaysAuthenticated: Boolean = false var staticLevelOfAssurance: LevelOfAssurance? = null diff --git a/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/tokendings/AuthInfo.kt b/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/install/AuthInfo.kt similarity index 69% rename from token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/tokendings/AuthInfo.kt rename to token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/install/AuthInfo.kt index 65d127e..b19ff8c 100644 --- a/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/tokendings/AuthInfo.kt +++ b/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/install/AuthInfo.kt @@ -1,4 +1,4 @@ -package no.nav.tms.token.support.tokenx.validation.mock.tokendings +package no.nav.tms.token.support.idporten.sidecar.mock.install internal data class AuthInfo( val alwaysAuthenticated: Boolean, diff --git a/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/authentication/AuthInfoValidator.kt b/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/install/AuthInfoValidator.kt similarity index 96% rename from token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/authentication/AuthInfoValidator.kt rename to token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/install/AuthInfoValidator.kt index 72ce0f1..4d9d3a6 100644 --- a/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/authentication/AuthInfoValidator.kt +++ b/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/install/AuthInfoValidator.kt @@ -1,4 +1,4 @@ -package no.nav.tms.token.support.idporten.sidecar.mock.authentication +package no.nav.tms.token.support.idporten.sidecar.mock.install import com.auth0.jwt.JWT import no.nav.tms.token.support.idporten.sidecar.mock.IdPortenMockedAuthenticatorConfig diff --git a/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/install/IdPortenMockInstaller.kt b/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/install/IdPortenMockInstaller.kt new file mode 100644 index 0000000..344fd94 --- /dev/null +++ b/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/install/IdPortenMockInstaller.kt @@ -0,0 +1,23 @@ +package no.nav.tms.token.support.idporten.sidecar.mock.install + +import io.ktor.server.auth.* +import no.nav.tms.token.support.idporten.sidecar.mock.IdPortenMockedAuthenticatorConfig + +internal object IdPortenMockInstaller { + fun AuthenticationConfig.performIdPortenMockInstallation( + config: IdPortenMockedAuthenticatorConfig + ) { + registerIdPortenProviderMock( + authenticatorName = getAuthenticatorName(config), + authInfo = AuthInfoValidator.validateAuthInfo(config) + ) + } + + private fun getAuthenticatorName(config: IdPortenMockedAuthenticatorConfig): String? { + return if (config.setAsDefault) { + null + } else { + config.authenticatorName + } + } +} diff --git a/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/authentication/IdPortenPrincipalBuilder.kt b/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/install/IdPortenPrincipalBuilder.kt similarity index 90% rename from token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/authentication/IdPortenPrincipalBuilder.kt rename to token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/install/IdPortenPrincipalBuilder.kt index 49ee67a..d8f7500 100644 --- a/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/authentication/IdPortenPrincipalBuilder.kt +++ b/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/install/IdPortenPrincipalBuilder.kt @@ -1,4 +1,4 @@ -package no.nav.tms.token.support.idporten.sidecar.mock.authentication +package no.nav.tms.token.support.idporten.sidecar.mock.install import com.auth0.jwt.JWT import com.auth0.jwt.interfaces.DecodedJWT @@ -8,7 +8,7 @@ import com.nimbusds.jose.JWSHeader import com.nimbusds.jose.crypto.RSASSASigner import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT -import no.nav.tms.token.support.idporten.sidecar.authentication.IdPortenTokenPrincipal +import no.nav.tms.token.support.idporten.sidecar.IdPortenTokenPrincipal import java.time.Instant import java.time.temporal.ChronoUnit.HOURS import java.util.* diff --git a/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/tokendings/JwkBuilder.kt b/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/install/JwkBuilder.kt similarity index 85% rename from token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/tokendings/JwkBuilder.kt rename to token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/install/JwkBuilder.kt index a9d1695..ca3c8cb 100644 --- a/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/tokendings/JwkBuilder.kt +++ b/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/install/JwkBuilder.kt @@ -1,4 +1,4 @@ -package no.nav.tms.token.support.tokenx.validation.mock.tokendings +package no.nav.tms.token.support.idporten.sidecar.mock.install import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.jwk.KeyUse diff --git a/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/install/tokenAuthentication.kt b/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/install/tokenAuthentication.kt new file mode 100644 index 0000000..57dde60 --- /dev/null +++ b/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/install/tokenAuthentication.kt @@ -0,0 +1,43 @@ +package no.nav.tms.token.support.idporten.sidecar.mock.install + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.server.auth.* +import io.ktor.http.* +import io.ktor.serialization.* +import io.ktor.server.response.* +import org.slf4j.LoggerFactory + +internal fun AuthenticationConfig.registerIdPortenProviderMock(authenticatorName: String?, authInfo: AuthInfo) { + + AccessTokenAuthenticationProvider.Configuration(authenticatorName) + .let { config -> AccessTokenAuthenticationProvider(authInfo, config) } + .let { provider -> register(provider) } +} + +private fun AuthenticationContext.respondUnauthorized(message: String) { + + challenge("Unauthenticated", AuthenticationFailedCause.InvalidCredentials) { challenge, call -> + call.respond(HttpStatusCode.Unauthorized, message) + challenge.complete() + } +} + +private class AccessTokenAuthenticationProvider ( + val authInfo: AuthInfo, + config: Configuration +) : AuthenticationProvider(config) { + + private val log = KotlinLogging.logger { } + + override suspend fun onAuthenticate(context: AuthenticationContext) { + if (authInfo.alwaysAuthenticated) { + log.debug { "Auth is valid as idporten-mock is set to never authorized." } + context.principal(IdPortenPrincipalBuilder.createPrincipal(authInfo)) + } else { + log.debug { "Responding 401 as idporten-mock is set to never authorized." } + context.respondUnauthorized("Never authorized.") + } + } + + class Configuration(name: String?) : Config(name) +} diff --git a/token-support-idporten-sidecar-mock/src/test/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/IdPortenAuthTest.kt b/token-support-idporten-sidecar-mock/src/test/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/IdPortenAuthTest.kt index 649c1bf..1311adf 100644 --- a/token-support-idporten-sidecar-mock/src/test/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/IdPortenAuthTest.kt +++ b/token-support-idporten-sidecar-mock/src/test/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/IdPortenAuthTest.kt @@ -8,7 +8,7 @@ import io.ktor.http.* import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.server.testing.* -import no.nav.tms.token.support.idporten.sidecar.IdPortenCookieAuthenticator +import no.nav.tms.token.support.idporten.sidecar.IdPortenAuthenticator import no.nav.tms.token.support.idporten.sidecar.user.IdportenUserFactory import org.amshove.kluent.`should be equal to` import org.junit.jupiter.api.Test @@ -22,8 +22,10 @@ internal class IdPortenAuthTest { application { testApi { - installIdPortenAuthMock { - alwaysAuthenticated = false + authentication { + idPortenMock { + alwaysAuthenticated = false + } } } } @@ -38,10 +40,12 @@ internal class IdPortenAuthTest { application { testApi { - installIdPortenAuthMock { - alwaysAuthenticated = true - staticUserPid = userPid - staticLevelOfAssurance = LevelOfAssurance.HIGH + authentication { + idPortenMock { + alwaysAuthenticated = true + staticUserPid = userPid + staticLevelOfAssurance = LevelOfAssurance.HIGH + } } } } @@ -57,9 +61,11 @@ internal class IdPortenAuthTest { application { testApiWithDefault { - installIdPortenAuthMock { - setAsDefault = true - alwaysAuthenticated = false + authentication { + idPortenMock { + setAsDefault = true + alwaysAuthenticated = false + } } } } @@ -74,7 +80,7 @@ internal class IdPortenAuthTest { authConfig() routing { - authenticate(IdPortenCookieAuthenticator.name) { + authenticate(IdPortenAuthenticator.name) { get("/test") { val user = IdportenUserFactory.createIdportenUser(call) call.respondText(user.ident) diff --git a/token-support-idporten-sidecar/README.md b/token-support-idporten-sidecar/README.md index 980f016..5715393 100644 --- a/token-support-idporten-sidecar/README.md +++ b/token-support-idporten-sidecar/README.md @@ -21,23 +21,23 @@ For å kunne autentisere et endepunkt må man først installere autentikatoren. Her er det en rekke variabler: +- `authenticatorName`: Bestemmer navnet på autentikatoren. Default `IdPortenAuthenticator.name` - `setAsDefault`: (Optional) Setter denne autentikatoren som default. Default 'false' -- `loginLevel`: (Optional) Setter minimum sikkerhetsnivå for alle innlogginger. Default 'LEVEL_4' -- `postLoginRedirectUri`: (Optional) Url der bruker havner etter login dersom vi ikke finner en "redirect_uri" cookie. Default "" -- `enableDefaultProxy`: (Optional) Bestemmer hvorvidt system-default proxy skal brukes ved kall mot andre tjenester. Nødvendig for on-prem apper med webproxy. -- `fallbackCookieEnabled`: (Optional) Bestemmer om token kan hentes fra cookie dersom vi ikke finner auth-header. Ment for lokal kjøring. Default 'false'. -- `fallbackCookieName`: (Optional/Required) Bestemmer hvilken cookie token skal leses fra. Må kun settes hvis fallback er enablet. +- `loginLevel`: Deprecated - Bruk `levelOfAssurance` i stedet. +- `levelOfAssurance` (Optional) Setter minimum level-of-assurance for endepunkt. Default 'HIGH' +- `enableDefaultProxy`: (Optional) Bestemmer hvorvidt system-default proxy skal brukes ved kall mot andre tjenester. Nødvendig for on-prem apper med webproxy. Default 'false'. Eksempel på konfigurasjon: ```kotlin -fun Application.mainModule() { +fun Application.setup() { - installIdPortenAuth { - postLoginRedirectUri = '/post/login' - setAsDefault = false - loginLevel = LoginLevel.LEVEL_3 - enableDefaultProxy = false + authentication { + idPorten { + setAsDefault = false + levelOfAssurance = LevelOfAssurance.SUBSTANTIAL + enableDefaultProxy = false + } } } ``` @@ -45,13 +45,14 @@ fun Application.mainModule() { Deretter kan man autentisere bestemte endepunkt som følger. Det er viktig å ha med navnet på autentikatoren. ```kotlin -fun Application.mainModule() { +fun Application.setup() { - installIdPortenAuth { - postLoginRedirectUri = '/post/login' - setAsDefault = false - loginLevel = LoginLevel.LEVEL_3 - enableDefaultProxy = false + authentication { + idPorten { + setAsDefault = false + levelOfAssurance = LevelOfAssurance.SUBSTANTIAL + enableDefaultProxy = false + } } routing { @@ -67,11 +68,13 @@ fun Application.mainModule() { Typisk eksempel på bruk i miljø i et fagområde som krever nivå 4 og dette er default authenticator: ```kotlin -fun Application.mainModule() { +fun Application.setup() { - installIdPortenAuth { - setAsDefault = true - loginLevel = LoginLevel.LEVEL_4 + authentication { + idPorten { + setAsDefault = true + levelOfAssurance = LevelOfAssurance.HIGH + } } routing { @@ -84,8 +87,56 @@ fun Application.mainModule() { } ``` -Etter bruker her har logget inn i idporten vil den få et jwt token lagret i cookie 'user_id_token'. -Denne cookien er scopet til domene og rootpath for appen. +## Login plugin + +Bilbioteket tilbyr en plugin som setter opp endepunkt for å fasilitere innloggingsflyt. + +Disse er +- `/login`: Setter i gang innlogging hos ID-porten via sidecar. +- `/login/status`: Viser status for innlogging - om bruker er innlogget og med hvilket nivå. + +Plugin har to variabler: + +- `enableDefaultProxy`: (Optional) Bestemmer hvorvidt system-default proxy skal brukes ved kall mot andre tjenester. Default 'false'. +- `routesPrefix`: (Optional) Bestemmer en relativ path der endepunktene plasseres. Default 'null'. + +Eksempel på oppsett: + +```kotlin +fun Application.setup() { + install(IdPortenLogin) { + routesPrefix = '/implicit/root/path' + enableDefaultProxy = false + } +} +``` + +Eksempel på bruk: + +Installere Plugin: + +```kotlin +fun Application.setup() { + install(IdPortenLogin) +} +``` + +Initiere login på med 'substantial' level of assurance: + +`https://backend.nav.no/login?loa=substantial&redirect_uri=https://frontend.nav.no` + +Sjekke status etter innlogging + +`https://backend.nav.no/login/status` + +svarer med: +```json +{ + "authenticated": true, + "level": 3, + "levelOfAssurance": "substantial" +} +``` ## IdportenUser @@ -97,7 +148,7 @@ Dette kan gjøres som følger: authenticate(IdPortenCookieAuthenticator.name) { get("/sikret") { val user = IdportenUserFactory.createNewAuthenticatedUser(call) - call.respond("Du er logger inn som $user.") + call.respond("Du er logget inn som $user.") } } ``` diff --git a/token-support-idporten-sidecar/build.gradle.kts b/token-support-idporten-sidecar/build.gradle.kts index 16c3f00..5203c31 100644 --- a/token-support-idporten-sidecar/build.gradle.kts +++ b/token-support-idporten-sidecar/build.gradle.kts @@ -10,6 +10,7 @@ plugins { dependencies { api(kotlin("stdlib-jdk8")) implementation(Logback.classic) + implementation(KotlinLogging.logging) implementation(Ktor.serverAuth) implementation(Ktor.serverAuthJwt) implementation(Ktor.clientApache) diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/IdPortenInstaller.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/IdPortenInstaller.kt deleted file mode 100644 index ad0768e..0000000 --- a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/IdPortenInstaller.kt +++ /dev/null @@ -1,106 +0,0 @@ -package no.nav.tms.token.support.idporten.sidecar - -import io.ktor.server.application.* -import io.ktor.server.auth.* - -import io.ktor.server.plugins.forwardedheaders.* -import io.ktor.server.routing.* -import no.nav.tms.token.support.idporten.sidecar.authentication.LevelOfAssuranceInternal -import no.nav.tms.token.support.idporten.sidecar.authentication.config.RuntimeContext -import no.nav.tms.token.support.idporten.sidecar.authentication.idPortenAccessToken -import no.nav.tms.token.support.idporten.sidecar.authentication.idPortenLoginApi -import no.nav.tms.token.support.idporten.sidecar.authentication.logout.idPortenLogoutApi -import org.slf4j.LoggerFactory - -object IdPortenInstaller { - private val log = LoggerFactory.getLogger(IdPortenInstaller::class.java) - - fun Application.performIdPortenAuthenticatorInstallation( - config: IdportenAuthenticationConfig, - existingAuthContext: AuthenticationConfig? = null - ): IdPortenRoutesConfig { - validateIdPortenConfig(config) - - val runtimeContext = RuntimeContext( - postLoginRedirectUri = config.postLoginRedirectUri, - usesRootPath = config.inheritProjectRootPath, - contextPath = rootPath(config), - minLevelOfAssurance = getMinLoa(config.levelOfAssurance, config.loginLevel), - enableDefaultProxy = config.enableDefaultProxy, - fallbackTokenCookieEnabled = config.fallbackCookieEnabled, - fallbackTokenCookieName = config.fallbackTokenCookieName - ) - - installXForwardedHeaderSupportIfMissing() - - if (existingAuthContext == null) { - install(Authentication) { - setupAuthenticators(config, runtimeContext) - } - } else { - existingAuthContext.setupAuthenticators(config, runtimeContext) - } - - // Return routes to be enabled as a callback, as it as to be run after all other authenticators have been installed - return IdPortenRoutesConfig { - // Register endpoints '/login', '/login/status', 'login/callback' and '/logout' - routing { - idPortenLoginApi(runtimeContext) - idPortenLogoutApi(runtimeContext) - } - } - } - - private fun Application.rootPath(config: IdportenAuthenticationConfig): String { - return if (config.inheritProjectRootPath) { - environment.rootPath - } else { - config.rootPath - } - } - - private fun getMinLoa(loa: LevelOfAssurance, loginLevel: LoginLevel?): LevelOfAssuranceInternal { - return if (loginLevel != null) { - log.warn("loginLevel will be deprecated as of Q4 2023. Use levelOfAssurance setting instead.") - when (loginLevel) { - LoginLevel.LEVEL_3 -> LevelOfAssuranceInternal.Substantial - LoginLevel.LEVEL_4 -> LevelOfAssuranceInternal.High - } - } else { - when (loa) { - LevelOfAssurance.SUBSTANTIAL -> LevelOfAssuranceInternal.Substantial - LevelOfAssurance.HIGH -> LevelOfAssuranceInternal.High - } - } - } - - private fun validateIdPortenConfig(config: IdportenAuthenticationConfig) { - if (config.fallbackCookieEnabled) { - require(config.fallbackTokenCookieName.isNotBlank()) { "Navn på token-cookie må spesifiseres hvis fallback er enablet." } - } - } - - private fun AuthenticationConfig.setupAuthenticators(config: IdportenAuthenticationConfig, runtimeContext: RuntimeContext) { - val authenticatorName = getAuthenticatorName(config.setAsDefault) - - // Register authenticator for id-porten tokens - // This can apply to any number of endpoints. - idPortenAccessToken(authenticatorName, runtimeContext.authConfiguration) - } - - private fun Application.installXForwardedHeaderSupportIfMissing() { - if (pluginOrNull(XForwardedHeaders) == null) { - install(XForwardedHeaders) - } - } - - private fun getAuthenticatorName(isDefault: Boolean): String? { - return if (isDefault) { - null - } else { - IdPortenCookieAuthenticator.name - } - } -} - -class IdPortenRoutesConfig(val setupRoutes: Application.() -> Unit) diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/IdPortenLogin.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/IdPortenLogin.kt new file mode 100644 index 0000000..800d665 --- /dev/null +++ b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/IdPortenLogin.kt @@ -0,0 +1,36 @@ +package no.nav.tms.token.support.idporten.sidecar + +import io.ktor.server.application.* +import io.ktor.server.routing.* +import io.ktor.util.* +import no.nav.tms.token.support.idporten.sidecar.install.idPortenLoginApi +import no.nav.tms.token.support.idporten.sidecar.install.initializeTokenVerifier + + +class IdPortenLoginConfig { + var enableDefaultProxy: Boolean = false + var routesPrefix: String? = null +} + +class IdPortenLogin { + + companion object : BaseApplicationPlugin { + + override val key: AttributeKey = AttributeKey("IdPortenLogin") + + override fun install(pipeline: Application, configure: IdPortenLoginConfig.() -> Unit): IdPortenLogin { + + val config = IdPortenLoginConfig().also(configure) + + pipeline.routing { + idPortenLoginApi( + tokenVerifier = initializeTokenVerifier(config.enableDefaultProxy, null), + routesPrefix = config.routesPrefix + ) + } + + return IdPortenLogin() + } + } +} + diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/IdPortenTokenPrincipal.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/IdPortenTokenPrincipal.kt new file mode 100644 index 0000000..04485ff --- /dev/null +++ b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/IdPortenTokenPrincipal.kt @@ -0,0 +1,10 @@ +package no.nav.tms.token.support.idporten.sidecar + +import com.auth0.jwt.interfaces.DecodedJWT +import io.ktor.server.auth.* + +data class IdPortenTokenPrincipal( + val accessToken: DecodedJWT +) : Principal { + fun ident(identClaim: String = "pid"): String = accessToken.getClaim(identClaim).asString() +} diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/AuthConfiguration.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/AuthConfiguration.kt deleted file mode 100644 index 4d7e70b..0000000 --- a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/AuthConfiguration.kt +++ /dev/null @@ -1,17 +0,0 @@ -package no.nav.tms.token.support.idporten.sidecar.authentication - -import com.auth0.jwk.JwkProvider - -internal class AuthConfiguration( - jwkProvider: JwkProvider, - issuer: String, - minLevelOfAssurance: LevelOfAssuranceInternal, - val fallbackTokenCookieEnabled: Boolean, - val fallbackTokenCookieName: String -) { - val tokenVerifier = TokenVerifierBuilder.buildTokenVerifier( - jwkProvider = jwkProvider, - issuer = issuer, - minLevelOfAssurance = minLevelOfAssurance - ) -} diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/IdPortenTokenPrincipal.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/IdPortenTokenPrincipal.kt deleted file mode 100644 index 144aaa4..0000000 --- a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/IdPortenTokenPrincipal.kt +++ /dev/null @@ -1,9 +0,0 @@ -package no.nav.tms.token.support.idporten.sidecar.authentication - -import com.auth0.jwt.interfaces.DecodedJWT -import io.ktor.server.auth.* - -data class IdPortenTokenPrincipal( - val accessToken: DecodedJWT -) : Principal - diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/LoginStatus.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/LoginStatus.kt deleted file mode 100644 index f457df9..0000000 --- a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/LoginStatus.kt +++ /dev/null @@ -1,20 +0,0 @@ -package no.nav.tms.token.support.idporten.sidecar.authentication - -import kotlinx.serialization.Serializable -import no.nav.tms.token.support.idporten.sidecar.authentication.LevelOfAssuranceInternal.* - -@Serializable -internal data class LoginStatus( - val authenticated: Boolean, - val level: Int?, - val levelOfAssurance: String? -) { - companion object { - fun unauthenticated() = LoginStatus(false, null, null) - fun authenticated(levelOfAssuranceInternal: LevelOfAssuranceInternal) = when (levelOfAssuranceInternal) { - Level3, Substantial -> LoginStatus(true, 3, Substantial.name) - Level4, High -> LoginStatus(true, 4, High.name) - else -> throw IllegalStateException() - } - } -} diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/OauthServerConfigurationMetadata.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/OauthServerConfigurationMetadata.kt deleted file mode 100644 index 2a831b3..0000000 --- a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/OauthServerConfigurationMetadata.kt +++ /dev/null @@ -1,11 +0,0 @@ -package no.nav.tms.token.support.idporten.sidecar.authentication - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -// This object holds info returned from idporten's well-known-url -@Serializable -internal data class OauthServerConfigurationMetadata( - @SerialName("issuer") val issuer: String, - @SerialName("jwks_uri") val jwksUri: String, -) diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/TokenVerifier.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/TokenVerifier.kt deleted file mode 100644 index fe6fd3a..0000000 --- a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/TokenVerifier.kt +++ /dev/null @@ -1,46 +0,0 @@ -package no.nav.tms.token.support.idporten.sidecar.authentication - -import com.auth0.jwk.Jwk -import com.auth0.jwk.JwkProvider -import com.auth0.jwt.JWT -import com.auth0.jwt.algorithms.Algorithm -import com.auth0.jwt.interfaces.DecodedJWT -import com.auth0.jwt.interfaces.JWTVerifier -import java.security.interfaces.RSAPublicKey - -internal class TokenVerifier( - private val jwkProvider: JwkProvider, - private val issuer: String, - private val minLevelOfAssurance: LevelOfAssuranceInternal -) { - - private val acrClaim = "acr" - - fun verifyAccessToken(accessToken: String): DecodedJWT { - return buildVerifier(accessToken) - .verify(accessToken) - .also { verifyLevelOfAssurance(it) } - } - - private fun buildVerifier(accessToken: String): JWTVerifier { - return JWT.decode(accessToken).keyId - .let { kid -> jwkProvider.get(kid) } - .let { JWT.require(it.RSA256()) } - .withIssuer(issuer) - .build() - } - - private fun Jwk.RSA256() = Algorithm.RSA256(publicKey as RSAPublicKey, null) - - private fun verifyLevelOfAssurance(decodedToken: DecodedJWT) { - val acrClaim = decodedToken.getClaim(acrClaim) - - val levelOfAssurance = LevelOfAssuranceInternal.fromAcr(acrClaim.asString()) - - if (levelOfAssurance.relativeValue < minLevelOfAssurance.relativeValue) { - throw RuntimeException("Level of assurance too low.") - } - } -} - - diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/TokenVerifierBuilder.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/TokenVerifierBuilder.kt deleted file mode 100644 index a0e3ed8..0000000 --- a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/TokenVerifierBuilder.kt +++ /dev/null @@ -1,16 +0,0 @@ -package no.nav.tms.token.support.idporten.sidecar.authentication - -import com.auth0.jwk.JwkProvider - -// Allow easier mocking of TokenVerifier -internal object TokenVerifierBuilder { - fun buildTokenVerifier( - jwkProvider: JwkProvider, - issuer: String, - minLevelOfAssurance: LevelOfAssuranceInternal, - ) = TokenVerifier( - jwkProvider = jwkProvider, - issuer = issuer, - minLevelOfAssurance = minLevelOfAssurance - ) -} diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/accessToken.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/accessToken.kt deleted file mode 100644 index 4b28d01..0000000 --- a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/accessToken.kt +++ /dev/null @@ -1,26 +0,0 @@ -package no.nav.tms.token.support.idporten.sidecar.authentication - -import io.ktor.http.* -import io.ktor.server.request.* - -internal fun fetchAccessToken(request: ApplicationRequest, config: AuthConfiguration): String? { - val token = request.bearerToken - - return when { - token != null -> token - config.fallbackTokenCookieEnabled -> tokenCookie(request, config.fallbackTokenCookieName) - else -> null - } -} - -private val bearerRegex = "Bearer .+".toRegex() - -private val ApplicationRequest.bearerToken: String? get() { - return call.request.headers[HttpHeaders.Authorization] - ?.takeIf { bearerRegex.matches(it) } - ?.let { it.split(" ")[1] } -} - -private fun tokenCookie(request: ApplicationRequest, tokenCookieName: String): String? { - return request.cookies[tokenCookieName] -} diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/accessTokenAuthentication.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/accessTokenAuthentication.kt deleted file mode 100644 index 7e97ba6..0000000 --- a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/accessTokenAuthentication.kt +++ /dev/null @@ -1,65 +0,0 @@ -package no.nav.tms.token.support.idporten.sidecar.authentication - -import com.auth0.jwt.interfaces.DecodedJWT -import io.ktor.server.auth.* -import io.ktor.http.* -import io.ktor.server.response.* -import org.slf4j.LoggerFactory - -private val log = LoggerFactory.getLogger(AccessTokenAuthenticationProvider::class.java) - -// This method configures an authenticator which checks if an end user has hit an authenticated endpoint -// with a valid token. If the user is missing the token, or the provided token is invalid, we respond with http-code 401 -internal fun AuthenticationConfig.idPortenAccessToken(authenticatorName: String?, config: AuthConfiguration) { - - val provider = AccessTokenAuthenticationProvider.build(config, authenticatorName) - - register(provider) -} - -private class AccessTokenAuthenticationProvider constructor( - private val authConfig: AuthConfiguration, - config: Config -) : AuthenticationProvider(config) { - - override suspend fun onAuthenticate(context: AuthenticationContext) { - val call = context.call - - val accessToken = fetchAccessToken(call.request, authConfig) - - if (accessToken != null) { - try { - val decodedJWT = getVerifiedToken(accessToken, authConfig) - context.principal(IdPortenTokenPrincipal(decodedJWT)) - } catch (e: Throwable) { - val message = e.message ?: e.javaClass.simpleName - log.debug("Token verification failed: {}", message) - context.challengeAndRespondUnauthorized() - } - } else { - log.debug("Token missing. No header or fallback cookie provided.") - context.challengeAndRespondUnauthorized() - } - } - - class Configuration(name: String?) : AuthenticationProvider.Config(name) - - companion object { - fun build(authConfig: AuthConfiguration, name: String?) = AccessTokenAuthenticationProvider(authConfig, Configuration(name)) - } - -} - -private fun AuthenticationContext.challengeAndRespondUnauthorized() { - challenge("JWTAuthKey", AuthenticationFailedCause.InvalidCredentials) { challenge, call -> - call.respond(HttpStatusCode.Unauthorized) - challenge.complete() - } -} - -private fun getVerifiedToken(accessToken: String, config: AuthConfiguration): DecodedJWT { - val verifier = config.tokenVerifier - - return verifier.verifyAccessToken(accessToken) -} - diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/config/Environment.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/config/Environment.kt deleted file mode 100644 index 6da8f6b..0000000 --- a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/config/Environment.kt +++ /dev/null @@ -1,10 +0,0 @@ -package no.nav.tms.token.support.idporten.sidecar.authentication.config - -internal class Environment ( - val idportenWellKnownUrl: String = getIdportenEnvVar("IDPORTEN_WELL_KNOWN_URL") -) - -private fun getIdportenEnvVar(varName: String): String { - return System.getenv(varName) - ?: throw IllegalArgumentException("Fant ikke $varName som brukes i token-support-idporten. Påse at nais.yaml er konfigurert riktig.") -} diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/config/RuntimeContext.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/config/RuntimeContext.kt deleted file mode 100644 index b06b72c..0000000 --- a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/config/RuntimeContext.kt +++ /dev/null @@ -1,47 +0,0 @@ -package no.nav.tms.token.support.idporten.sidecar.authentication.config - -import com.auth0.jwk.JwkProvider -import com.auth0.jwk.JwkProviderBuilder -import io.ktor.client.* -import kotlinx.coroutines.runBlocking -import no.nav.tms.token.support.idporten.sidecar.authentication.AuthConfiguration -import no.nav.tms.token.support.idporten.sidecar.authentication.LevelOfAssuranceInternal -import no.nav.tms.token.support.idporten.sidecar.authentication.OauthServerConfigurationMetadata -import no.nav.tms.token.support.idporten.sidecar.authentication.config.HttpClientBuilder.buildHttpClient -import java.net.URL -import java.util.concurrent.TimeUnit - -internal class RuntimeContext( - val postLoginRedirectUri: String, - val usesRootPath: Boolean, - val contextPath: String, - fallbackTokenCookieEnabled: Boolean, - fallbackTokenCookieName: String, - minLevelOfAssurance: LevelOfAssuranceInternal, - - enableDefaultProxy: Boolean -) { - val environment = Environment() - - val httpClient = buildHttpClient(enableDefaultProxy) - - val metadata = fetchMetadata(httpClient, environment.idportenWellKnownUrl) - val jwkProvider = createJwkProvider(metadata) - - val authConfiguration = AuthConfiguration( - jwkProvider = jwkProvider, - issuer = metadata.issuer, - fallbackTokenCookieEnabled = fallbackTokenCookieEnabled, - fallbackTokenCookieName = fallbackTokenCookieName, - minLevelOfAssurance = minLevelOfAssurance - ) -} - -private fun fetchMetadata(httpClient: HttpClient, idPortenUrl: String) = runBlocking { - httpClient.getOAuthServerConfigurationMetadata(idPortenUrl) -} - -private fun createJwkProvider(metadata: OauthServerConfigurationMetadata): JwkProvider = JwkProviderBuilder(URL(metadata.jwksUri)) - .cached(10, 24, TimeUnit.HOURS) - .rateLimited(10, 1, TimeUnit.MINUTES) - .build() diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/config/constants.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/config/constants.kt deleted file mode 100644 index eea950f..0000000 --- a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/config/constants.kt +++ /dev/null @@ -1,5 +0,0 @@ -package no.nav.tms.token.support.idporten.sidecar.authentication.config - -internal object Idporten { - const val postLoginRedirectCookie = "redirect_uri" -} diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/config/httpClient.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/config/httpClient.kt deleted file mode 100644 index 8de0c74..0000000 --- a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/config/httpClient.kt +++ /dev/null @@ -1,19 +0,0 @@ -package no.nav.tms.token.support.idporten.sidecar.authentication.config - -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.request.* -import io.ktor.http.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import no.nav.tms.token.support.idporten.sidecar.authentication.OauthServerConfigurationMetadata - - -internal suspend fun HttpClient.getOAuthServerConfigurationMetadata(url: String) - : OauthServerConfigurationMetadata = withContext(Dispatchers.IO) { - request { - method = HttpMethod.Get - url(url) - accept(ContentType.Application.Json) - }.body() -} diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/loginApi.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/loginApi.kt deleted file mode 100644 index 6bae79a..0000000 --- a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/loginApi.kt +++ /dev/null @@ -1,80 +0,0 @@ -package no.nav.tms.token.support.idporten.sidecar.authentication - -import com.auth0.jwt.interfaces.DecodedJWT -import io.ktor.server.application.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import no.nav.tms.token.support.idporten.sidecar.authentication.config.Idporten -import no.nav.tms.token.support.idporten.sidecar.authentication.config.RuntimeContext -import no.nav.tms.token.support.idporten.sidecar.user.IdportenUserFactory - -internal fun Routing.idPortenLoginApi(runtimeContext: RuntimeContext) { - - if (runtimeContext.usesRootPath || runtimeContext.contextPath.isBlank()) { - loginEndPoints(runtimeContext) - } else { - route("/${runtimeContext.contextPath}") { - loginEndPoints(runtimeContext) - } - } -} - -private fun Route.loginEndPoints(runtimeContext: RuntimeContext) { - get("/login") { - val redirectUri = call.redirectUri - - if (redirectUri != null) { - call.response.cookies.append(Idporten.postLoginRedirectCookie, redirectUri, path = "/${runtimeContext.contextPath}") - } - - call.respondRedirect(getLoginUrl(runtimeContext.contextPath, call.securityLevel)) - } - - get("/login/status") { - val idToken = call.validAccessTokenOrNull(runtimeContext.authConfiguration) - - if (idToken == null) { - call.respond(LoginStatus.unauthenticated()) - } else { - call.respond(LoginStatus.authenticated(IdportenUserFactory.extractLevelOfAssurance(idToken))) - } - } - - get("/login/callback") { - call.response.cookies.appendExpired(Idporten.postLoginRedirectCookie, path = "/${runtimeContext.contextPath}") - call.respondRedirect(call.request.cookies[Idporten.postLoginRedirectCookie] ?: runtimeContext.postLoginRedirectUri) - } -} - -private fun ApplicationCall.validAccessTokenOrNull(config: AuthConfiguration): DecodedJWT? { - - val accessToken = fetchAccessToken(request, config) - - return if (accessToken != null) { - try { - config.tokenVerifier.verifyAccessToken(accessToken) - } catch (e: Throwable) { - null - } - } else { - null - } -} - -private fun getLoginUrl(contextPath: String, securityLevel: String?): String { - val redirectPath = if (contextPath.isBlank()) { - "/oauth2/login?redirect=/login/callback" - } else { - "/$contextPath/oauth2/login?redirect=/$contextPath/login/callback" - } - - return if (securityLevel != null) { - "$redirectPath&level=$securityLevel" - } else { - redirectPath - } -} - -private val ApplicationCall.redirectUri: String? get() = request.queryParameters["redirect_uri"] - -private val ApplicationCall.securityLevel: String? get() = request.queryParameters["level"] diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/logout/logoutApi.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/logout/logoutApi.kt deleted file mode 100644 index 602cbbc..0000000 --- a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/logout/logoutApi.kt +++ /dev/null @@ -1,27 +0,0 @@ -package no.nav.tms.token.support.idporten.sidecar.authentication.logout - -import io.ktor.server.application.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import no.nav.tms.token.support.idporten.sidecar.authentication.config.RuntimeContext - -internal fun Routing.idPortenLogoutApi(context: RuntimeContext) { - - if (context.contextPath.isBlank()) { - get("/logout") { - call.respondRedirect(getLogoutUrl(context.contextPath)) - } - } else { - get("/${context.contextPath}/logout") { - call.respondRedirect(getLogoutUrl(context.contextPath)) - } - } -} - -private fun getLogoutUrl(contextPath: String): String { - return if (contextPath.isBlank()) { - "/oauth2/logout" - } else { - "/$contextPath/oauth2/logout" - } -} diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/config.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/config.kt index 0e62ca7..465622b 100644 --- a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/config.kt +++ b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/config.kt @@ -1,35 +1,28 @@ package no.nav.tms.token.support.idporten.sidecar -import io.ktor.server.application.* -import no.nav.tms.token.support.idporten.sidecar.IdPortenInstaller.performIdPortenAuthenticatorInstallation +import io.ktor.server.auth.* +import no.nav.tms.token.support.idporten.sidecar.install.IdPortenInstaller.performIdPortenAuthenticatorInstallation import no.nav.tms.token.support.idporten.sidecar.LevelOfAssurance.HIGH -// This method is responsible for wiring up all the necessary endpoints and registering the authenticators. -// Users of this library should only have to make use of this method to enable idporten auth. -fun Application.installIdPortenAuth(configure: IdportenAuthenticationConfig.() -> Unit) { - val config = IdportenAuthenticationConfig().apply(configure) - - val routesConfig = performIdPortenAuthenticatorInstallation(config) - routesConfig.setupRoutes(this) -} +// This method is responsible for registering the authenticators. +// Users of this library should only have to make use of this method to enable idporten auth. +fun AuthenticationConfig.idPorten(configure: IdportenAuthenticationConfig.() -> Unit) = + IdportenAuthenticationConfig() + .apply(configure) + .let { performIdPortenAuthenticatorInstallation(it) } // Configuration provided by library user. See readme for example of use class IdportenAuthenticationConfig { + var authenticatorName: String = IdPortenAuthenticator.name + var setAsDefault: Boolean = false - var postLoginRedirectUri: String = "" @Deprecated("Numbered login levels are deprecated as of Q4 2023. Set levelOfAssurance instead") var loginLevel: LoginLevel? = null var levelOfAssurance: LevelOfAssurance = HIGH var enableDefaultProxy: Boolean = false - - var inheritProjectRootPath: Boolean = true - var rootPath: String = "" - - var fallbackCookieEnabled: Boolean = false - var fallbackTokenCookieName: String = "" } enum class LevelOfAssurance { @@ -42,6 +35,6 @@ enum class LoginLevel { } // Name of token authenticator. See README for example of use -object IdPortenCookieAuthenticator { - const val name = "idporten_wonderwall" +object IdPortenAuthenticator { + const val name = "idporten_access_token" } diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/config/HttpClientBuilder.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/install/HttpClientBuilder.kt similarity index 93% rename from token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/config/HttpClientBuilder.kt rename to token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/install/HttpClientBuilder.kt index a23c4ee..6eb917c 100644 --- a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/config/HttpClientBuilder.kt +++ b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/install/HttpClientBuilder.kt @@ -1,14 +1,13 @@ -package no.nav.tms.token.support.idporten.sidecar.authentication.config +package no.nav.tms.token.support.idporten.sidecar.install import io.ktor.client.* import io.ktor.client.engine.apache.* import io.ktor.client.plugins.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.serialization.kotlinx.json.* +import kotlinx.serialization.json.Json import org.apache.http.impl.conn.SystemDefaultRoutePlanner import java.net.ProxySelector -import kotlinx.serialization.json.Json - internal object HttpClientBuilder { internal fun buildHttpClient(enableDefaultProxy: Boolean): HttpClient { @@ -36,4 +35,3 @@ internal object HttpClientBuilder { } } } - diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/install/IdPortenInstaller.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/install/IdPortenInstaller.kt new file mode 100644 index 0000000..1ea9719 --- /dev/null +++ b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/install/IdPortenInstaller.kt @@ -0,0 +1,50 @@ +package no.nav.tms.token.support.idporten.sidecar.install + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.server.auth.* +import no.nav.tms.token.support.idporten.sidecar.IdportenAuthenticationConfig +import no.nav.tms.token.support.idporten.sidecar.LevelOfAssurance +import no.nav.tms.token.support.idporten.sidecar.LoginLevel + +internal object IdPortenInstaller { + private val log = KotlinLogging.logger { } + + // Register authenticator for id-porten tokens + // This can apply to any number of endpoints. + fun AuthenticationConfig.performIdPortenAuthenticatorInstallation( + config: IdportenAuthenticationConfig + ) { + val tokenVerifier = initializeTokenVerifier( + enableDefaultProxy = config.enableDefaultProxy, + minLevelOfAssurance = getMinLoa(config.levelOfAssurance, config.loginLevel) + ) + + registerIdPortenValidationProvider( + authenticatorName = getAuthenticatorName(config), + tokenVerifier = tokenVerifier + ) + } + + private fun getMinLoa(loa: LevelOfAssurance, loginLevel: LoginLevel?): IdPortenLevelOfAssurance { + return if (loginLevel != null) { + log.warn { "loginLevel will be deprecated as of Q4 2023. Use levelOfAssurance setting instead." } + when (loginLevel) { + LoginLevel.LEVEL_3 -> IdPortenLevelOfAssurance.Substantial + LoginLevel.LEVEL_4 -> IdPortenLevelOfAssurance.High + } + } else { + when (loa) { + LevelOfAssurance.SUBSTANTIAL -> IdPortenLevelOfAssurance.Substantial + LevelOfAssurance.HIGH -> IdPortenLevelOfAssurance.High + } + } + } + + private fun getAuthenticatorName(config: IdportenAuthenticationConfig): String? { + return if (config.setAsDefault) { + null + } else { + config.authenticatorName + } + } +} diff --git a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/tokendings/LevelOfAssuranceInternal.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/install/IdPortenLevelOfAssurance.kt similarity index 69% rename from token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/tokendings/LevelOfAssuranceInternal.kt rename to token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/install/IdPortenLevelOfAssurance.kt index 25765fc..fc8600f 100644 --- a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/tokendings/LevelOfAssuranceInternal.kt +++ b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/install/IdPortenLevelOfAssurance.kt @@ -1,6 +1,6 @@ -package no.nav.tms.token.support.tokenx.validation.tokendings +package no.nav.tms.token.support.idporten.sidecar.install -internal enum class LevelOfAssuranceInternal(val acr: String, val relativeValue: Int) { +internal enum class IdPortenLevelOfAssurance(val acr: String, val relativeValue: Int) { Level3("Level3", 2), Level4("Level4", 3), Low("idporten-loa-low", 1), @@ -8,7 +8,7 @@ internal enum class LevelOfAssuranceInternal(val acr: String, val relativeValue: High("idporten-loa-high", 3); companion object { - fun fromAcr(acr: String): LevelOfAssuranceInternal { + fun fromAcr(acr: String): IdPortenLevelOfAssurance { return values() .find { it.acr.lowercase() == acr.lowercase() } ?: throw IllegalStateException("Could not find matching LoA for claim $acr.") diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/install/accessToken.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/install/accessToken.kt new file mode 100644 index 0000000..a82b268 --- /dev/null +++ b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/install/accessToken.kt @@ -0,0 +1,13 @@ +package no.nav.tms.token.support.idporten.sidecar.install + +import io.ktor.http.* +import io.ktor.server.request.* + +internal fun fetchAccessToken(request: ApplicationRequest) = + request.call + .request + .headers[HttpHeaders.Authorization] + ?.takeIf { bearerRegex.matches(it) } + ?.let { it.split(" ")[1] } + +private val bearerRegex = "Bearer .+".toRegex() diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/install/accessTokenAuthentication.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/install/accessTokenAuthentication.kt new file mode 100644 index 0000000..9ec4ab3 --- /dev/null +++ b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/install/accessTokenAuthentication.kt @@ -0,0 +1,51 @@ +package no.nav.tms.token.support.idporten.sidecar.install + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.server.auth.* +import io.ktor.http.* +import io.ktor.server.response.* +import no.nav.tms.token.support.idporten.sidecar.IdPortenTokenPrincipal +import org.slf4j.LoggerFactory + +private val log = KotlinLogging.logger { } + +// This method configures an authenticator which checks if an end user has hit an authenticated endpoint +// with a valid token. If the user is missing the token, or the provided token is invalid, we respond with http-code 401 +internal fun AuthenticationConfig.registerIdPortenValidationProvider(authenticatorName: String?, tokenVerifier: TokenVerifier) = + AccessTokenAuthenticationProvider.Configuration(authenticatorName) + .let { config -> AccessTokenAuthenticationProvider(tokenVerifier, config) } + .let { provider -> register(provider) } + +private class AccessTokenAuthenticationProvider( + private val tokenVerifier: TokenVerifier, + config: Config +) : AuthenticationProvider(config) { + + override suspend fun onAuthenticate(context: AuthenticationContext) { + val call = context.call + + val accessToken = fetchAccessToken(call.request) + + if (accessToken != null) { + try { + val decodedJWT = tokenVerifier.verifyAccessToken(accessToken) + context.principal(IdPortenTokenPrincipal(decodedJWT)) + } catch (e: Throwable) { + log.debug(e) { "Token verification failed" } + context.challengeAndRespondUnauthorized() + } + } else { + log.debug { "Token missing. No header or fallback cookie provided." } + context.challengeAndRespondUnauthorized() + } + } + + class Configuration(name: String?) : Config(name) +} + +private fun AuthenticationContext.challengeAndRespondUnauthorized() { + challenge("JWTAuthKey", AuthenticationFailedCause.InvalidCredentials) { challenge, call -> + call.respond(HttpStatusCode.Unauthorized) + challenge.complete() + } +} diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/install/loginApi.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/install/loginApi.kt new file mode 100644 index 0000000..de4f90c --- /dev/null +++ b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/install/loginApi.kt @@ -0,0 +1,124 @@ +package no.nav.tms.token.support.idporten.sidecar.install + +import com.auth0.jwt.interfaces.DecodedJWT +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.util.date.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import no.nav.tms.token.support.idporten.sidecar.user.IdportenUserFactory + +private const val postLoginRedirectCookie = "redirect_uri" + +internal fun Routing.idPortenLoginApi(tokenVerifier: TokenVerifier, routesPrefix: String?) { + + if (routesPrefix != null) { + route("/$routesPrefix") { + loginEndPoints(tokenVerifier, routesPrefix) + } + } else { + loginEndPoints(tokenVerifier, routesPrefix) + } +} + +private fun Route.loginEndPoints(tokenVerifier: TokenVerifier, routesPrefix: String?) { + get("/login") { + val redirectUri = call.redirectUri + + if (redirectUri != null) { + call.response.cookies.append(postLoginRedirectCookie, redirectUri) + } + + call.respondRedirect(getLoginUrl(routesPrefix, call.levelOfAssurance)) + } + + get("/login/status") { + val idToken = call.validAccessTokenOrNull(tokenVerifier) + + if (idToken == null) { + call.respondJson(LoginStatus.unauthenticated()) + } else { + call.respondJson(LoginStatus.authenticated(IdportenUserFactory.extractLevelOfAssurance(idToken))) + } + } + + get("/login/callback") { + call.response.cookies.append( + name = postLoginRedirectCookie, + value = "", + expires = GMTDate.START + ) + + call.request.cookies[postLoginRedirectCookie] + ?.let { call.respondRedirect(it) } + ?: call.respond(HttpStatusCode.OK, "Login successful") + } +} + +private val objectMapper = Json + +private suspend fun ApplicationCall.respondJson(status: LoginStatus) { + response.headers.append(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + respond( + status = HttpStatusCode.OK, + message = objectMapper.encodeToString(status) + ) +} + +@Serializable +internal data class LoginStatus( + val authenticated: Boolean, + val level: Int?, + val levelOfAssurance: String? +) { + companion object { + fun unauthenticated() = LoginStatus(false, null, null) + fun authenticated(levelOfAssuranceInternal: IdPortenLevelOfAssurance) = when (levelOfAssuranceInternal) { + IdPortenLevelOfAssurance.Level3, IdPortenLevelOfAssurance.Substantial -> LoginStatus(true, 3, IdPortenLevelOfAssurance.Substantial.name) + IdPortenLevelOfAssurance.Level4, IdPortenLevelOfAssurance.High -> LoginStatus(true, 4, IdPortenLevelOfAssurance.High.name) + else -> throw IllegalStateException() + } + } +} + +private fun ApplicationCall.validAccessTokenOrNull(tokenVerifier: TokenVerifier): DecodedJWT? { + + val accessToken = fetchAccessToken(request) + + return if (accessToken != null) { + try { + tokenVerifier.verifyAccessToken(accessToken) + } catch (e: Throwable) { + null + } + } else { + null + } +} + +private fun getLoginUrl(contextPath: String?, levelOfAssurance: String?): String { + val redirectPath = if (contextPath != null) { + "/$contextPath/oauth2/login?redirect=/$contextPath/login/callback" + } else { + "/oauth2/login?redirect=/login/callback" + } + + return if (levelOfAssurance != null) { + "$redirectPath&level=$levelOfAssurance" + } else { + redirectPath + } +} + +private val ApplicationCall.redirectUri: String? get() = request.queryParameters["redirect_uri"] + +private val ApplicationCall.levelOfAssurance: String? get() = + when (val loa = request.queryParameters["loa"] ?: request.queryParameters["level"]) { + null -> null + "high", "level4", "Level4" -> "idporten-loa-high" + "substantial", "level3", "Level3" -> "idporten-loa-substantial" + else -> loa + } diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/install/tokenVerifier.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/install/tokenVerifier.kt new file mode 100644 index 0000000..595a54e --- /dev/null +++ b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/install/tokenVerifier.kt @@ -0,0 +1,113 @@ +package no.nav.tms.token.support.idporten.sidecar.install + +import com.auth0.jwk.Jwk +import com.auth0.jwk.JwkProvider +import com.auth0.jwk.JwkProviderBuilder +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import com.auth0.jwt.interfaces.DecodedJWT +import com.auth0.jwt.interfaces.JWTVerifier +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.http.* +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.net.URL +import java.security.interfaces.RSAPublicKey +import java.util.concurrent.TimeUnit + +private fun getIdportenWellKnownUrl() = System.getenv("IDPORTEN_WELL_KNOWN_URL") + ?: throw IllegalArgumentException("Fant ikke IDPORTEN_WELL_KNOWN_URL som brukes i token-support-idporten-sidecar. Påse at nais.yaml er konfigurert riktig.") + +internal fun initializeTokenVerifier( + enableDefaultProxy: Boolean, + minLevelOfAssurance: IdPortenLevelOfAssurance?, +) : TokenVerifier { + + val metadata = fetchMetadata( + client = HttpClientBuilder.buildHttpClient(enableDefaultProxy), + wellKnownUrl = getIdportenWellKnownUrl() + ) + + val jwkProvider = createJwkProvider(metadata) + + return TokenVerifier.build( + jwkProvider = jwkProvider, + issuer = metadata.issuer, + minLevelOfAssurance = minLevelOfAssurance + ) +} + +internal class TokenVerifier private constructor( + private val jwkProvider: JwkProvider, + private val issuer: String, + private val minLevelOfAssurance: IdPortenLevelOfAssurance? +) { + + private val acrClaim = "acr" + + companion object { + fun build( + jwkProvider: JwkProvider, + issuer: String, + minLevelOfAssurance: IdPortenLevelOfAssurance? + ) = TokenVerifier( + jwkProvider = jwkProvider, + issuer = issuer, + minLevelOfAssurance = minLevelOfAssurance, + ) + } + + fun verifyAccessToken(accessToken: String): DecodedJWT { + return buildVerifier(accessToken) + .verify(accessToken) + .also { verifyMinimumLoA(it) } + } + + private fun buildVerifier(accessToken: String): JWTVerifier { + return JWT.decode(accessToken).keyId + .let { kid -> jwkProvider.get(kid) } + .let { JWT.require(it.RSA256()) } + .withIssuer(issuer) + .build() + } + + private fun Jwk.RSA256() = Algorithm.RSA256(publicKey as RSAPublicKey, null) + + private fun verifyMinimumLoA(decodedToken: DecodedJWT) { + if (minLevelOfAssurance == null) { + return + } + + val acrClaim = decodedToken.getClaim(acrClaim) + + val levelOfAssurance = IdPortenLevelOfAssurance.fromAcr(acrClaim.asString()) + + if (levelOfAssurance.relativeValue < minLevelOfAssurance.relativeValue) { + throw RuntimeException("Level of assurance too low.") + } + } +} + +@Serializable +internal data class OauthServerConfigurationMetadata( + @SerialName("issuer") val issuer: String, + @SerialName("jwks_uri") val jwksUri: String, +) + +private fun fetchMetadata(client: HttpClient, wellKnownUrl: String): OauthServerConfigurationMetadata = runBlocking { + client.request { + method = HttpMethod.Get + url(wellKnownUrl) + accept(ContentType.Application.Json) + }.body() +} + +private fun createJwkProvider(metadata: OauthServerConfigurationMetadata): JwkProvider = JwkProviderBuilder(URL(metadata.jwksUri)) + .cached(10, 24, TimeUnit.HOURS) + .rateLimited(10, 1, TimeUnit.MINUTES) + .build() + + diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/user/IdportenUserFactory.kt b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/user/IdportenUserFactory.kt index 2d43919..f039969 100644 --- a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/user/IdportenUserFactory.kt +++ b/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/user/IdportenUserFactory.kt @@ -5,9 +5,9 @@ import io.ktor.server.application.* import io.ktor.server.auth.* import no.nav.tms.token.support.idporten.sidecar.LevelOfAssurance import no.nav.tms.token.support.idporten.sidecar.LevelOfAssurance.* -import no.nav.tms.token.support.idporten.sidecar.authentication.IdPortenTokenPrincipal -import no.nav.tms.token.support.idporten.sidecar.authentication.LevelOfAssuranceInternal -import no.nav.tms.token.support.idporten.sidecar.authentication.LevelOfAssuranceInternal.* +import no.nav.tms.token.support.idporten.sidecar.IdPortenTokenPrincipal +import no.nav.tms.token.support.idporten.sidecar.install.IdPortenLevelOfAssurance +import no.nav.tms.token.support.idporten.sidecar.install.IdPortenLevelOfAssurance.* import java.time.Instant // This creates an IdportenUser based on user jwt claims @@ -22,8 +22,8 @@ object IdportenUserFactory { return createIdportenUser(principal, identClaim) } - internal fun extractLevelOfAssurance(accessToken: DecodedJWT): LevelOfAssuranceInternal { - return LevelOfAssuranceInternal.fromAcr(accessToken.getClaim("acr").asString()) + internal fun extractLevelOfAssurance(accessToken: DecodedJWT): IdPortenLevelOfAssurance { + return IdPortenLevelOfAssurance.fromAcr(accessToken.getClaim("acr").asString()) } private fun createIdportenUser(principal: IdPortenTokenPrincipal, identClaim: String): IdportenUser { @@ -44,7 +44,7 @@ object IdportenUserFactory { return IdportenUser(ident, loginLevel, levelOfAssurance, expirationTime, accessToken) } - private fun mapLoginLevel(levelOfAssurance: LevelOfAssuranceInternal): Int { + private fun mapLoginLevel(levelOfAssurance: IdPortenLevelOfAssurance): Int { return when (levelOfAssurance) { Level3, Substantial -> 3 @@ -53,7 +53,7 @@ object IdportenUserFactory { } } - private fun mapLevelOfAssurance(levelOfAssurance: LevelOfAssuranceInternal): LevelOfAssurance { + private fun mapLevelOfAssurance(levelOfAssurance: IdPortenLevelOfAssurance): LevelOfAssurance { return when (levelOfAssurance) { Level3, Substantial -> SUBSTANTIAL Level4, High -> HIGH diff --git a/token-support-idporten-sidecar/src/test/kotlin/no/nav/tms/token/support/idporten/sidecar/IdportenAuthIT.kt b/token-support-idporten-sidecar/src/test/kotlin/no/nav/tms/token/support/idporten/sidecar/IdPortenAuthIT.kt similarity index 60% rename from token-support-idporten-sidecar/src/test/kotlin/no/nav/tms/token/support/idporten/sidecar/IdportenAuthIT.kt rename to token-support-idporten-sidecar/src/test/kotlin/no/nav/tms/token/support/idporten/sidecar/IdPortenAuthIT.kt index f10ca58..dc8cd9b 100644 --- a/token-support-idporten-sidecar/src/test/kotlin/no/nav/tms/token/support/idporten/sidecar/IdportenAuthIT.kt +++ b/token-support-idporten-sidecar/src/test/kotlin/no/nav/tms/token/support/idporten/sidecar/IdPortenAuthIT.kt @@ -10,15 +10,14 @@ import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.server.testing.* import io.mockk.* -import no.nav.tms.token.support.idporten.sidecar.authentication.TokenVerifier -import no.nav.tms.token.support.idporten.sidecar.authentication.TokenVerifierBuilder -import no.nav.tms.token.support.idporten.sidecar.authentication.config.HttpClientBuilder +import no.nav.tms.token.support.idporten.sidecar.install.TokenVerifier +import no.nav.tms.token.support.idporten.sidecar.install.HttpClientBuilder import org.amshove.kluent.`should be equal to` import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -internal class IdportenAuthIT { +internal class IdPortenAuthIT { private val envVars = listOf( "IDPORTEN_WELL_KNOWN_URL" to "http://mocked-issuer/config", @@ -30,13 +29,11 @@ internal class IdportenAuthIT { private val dummyToken = "token" - private val fallbackCookieName = "fallback" - @BeforeEach fun setupMock() { - mockkObject(TokenVerifierBuilder) + mockkObject(TokenVerifier) mockkObject(HttpClientBuilder) - every { TokenVerifierBuilder.buildTokenVerifier(any(), any(), any()) } returns verifier + every { TokenVerifier.build(any(), any(), any()) } returns verifier every { HttpClientBuilder.buildHttpClient(any()) } returns mockedClient } @@ -44,7 +41,7 @@ internal class IdportenAuthIT { fun cleanUp() { clearMocks(verifier) unmockkObject(HttpClientBuilder) - unmockkObject(TokenVerifierBuilder) + unmockkObject(TokenVerifier) } @Test @@ -105,60 +102,54 @@ internal class IdportenAuthIT { } @Test - fun `Should ignore fallback cookie when not enabled`() = testApplication { - - application { - testApiWithDefault(fallbackEnabled = false) - } - - every { verifier.verifyAccessToken(dummyToken) } returns dummyJwt - - val status = client.get("/test") { - headers.append(HttpHeaders.Cookie, "$fallbackCookieName=$dummyToken") - }.status - - status `should be equal to` HttpStatusCode.Unauthorized - } - - @Test - fun `Should use fallback cookie when enabled`() = testApplication { + fun `Allows installing multiple authorizers in parallel`() = testApplication { application { - testApiWithDefault(fallbackEnabled = true) + withEnvironment(envVars) { + authentication { + idPorten { + setAsDefault = true + levelOfAssurance = LevelOfAssurance.HIGH + } + idPorten { + setAsDefault = false + authenticatorName = "other" + } + } + } + routing { + authenticate { + get("/test/one") { + call.respond(HttpStatusCode.OK) + } + } + authenticate("other") { + get("test/two") { + call.respond(HttpStatusCode.OK) + } + } + } } every { verifier.verifyAccessToken(dummyToken) } returns dummyJwt - val status = client.get("/test") { - headers.append(HttpHeaders.Cookie, "$fallbackCookieName=$dummyToken") - }.status - - status `should be equal to` HttpStatusCode.OK - } - - @Test - fun `Requesting logout should redirect to logout url`() = testApplication { - - application { - testApiWithDefault() - } - - val clientOverride = createClient { - followRedirects = false - } - - val response = clientOverride.get("/logout") + client.get("/test/one") { + headers.append(HttpHeaders.Authorization, "Bearer $dummyToken") + }.status `should be equal to` HttpStatusCode.OK - response.headers["Location"]!! `should be equal to` "/oauth2/logout" + client.get("/test/two") { + headers.append(HttpHeaders.Authorization, "Bearer $dummyToken") + }.status `should be equal to` HttpStatusCode.OK } private fun Application.testApi() = withEnvironment(envVars) { - installIdPortenAuth { + authentication { + idPorten { } } routing { - authenticate(IdPortenCookieAuthenticator.name) { + authenticate(IdPortenAuthenticator.name) { get("/test") { call.respond(HttpStatusCode.OK) } @@ -166,17 +157,15 @@ internal class IdportenAuthIT { } } - private fun Application.testApiWithDefault(fallbackEnabled: Boolean = false) = withEnvironment(envVars) { + private fun Application.testApiWithDefault() = withEnvironment(envVars) { - installIdPortenAuth { - setAsDefault = true - - if (fallbackEnabled) { - fallbackCookieEnabled = true - fallbackTokenCookieName = fallbackCookieName + authentication { + idPorten { + setAsDefault = true } } + routing { authenticate { get("/test") { diff --git a/token-support-idporten-sidecar/src/test/kotlin/no/nav/tms/token/support/idporten/sidecar/IdPortenPluginTest.kt b/token-support-idporten-sidecar/src/test/kotlin/no/nav/tms/token/support/idporten/sidecar/IdPortenPluginTest.kt new file mode 100644 index 0000000..aa7c8ee --- /dev/null +++ b/token-support-idporten-sidecar/src/test/kotlin/no/nav/tms/token/support/idporten/sidecar/IdPortenPluginTest.kt @@ -0,0 +1,126 @@ +package no.nav.tms.token.support.idporten.sidecar + +import com.auth0.jwt.interfaces.Claim +import com.auth0.jwt.interfaces.DecodedJWT +import io.kotest.extensions.system.withEnvironment +import io.ktor.client.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.testing.* +import io.ktor.util.* +import io.mockk.* +import io.mockk.InternalPlatformDsl.toStr +import kotlinx.serialization.json.* +import no.nav.tms.token.support.idporten.sidecar.install.HttpClientBuilder +import no.nav.tms.token.support.idporten.sidecar.install.IdPortenLevelOfAssurance +import no.nav.tms.token.support.idporten.sidecar.install.TokenVerifier +import org.amshove.kluent.`should be equal to` +import org.amshove.kluent.`should be instance of` +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class IdPortenPluginTest { + private val envVars = listOf( + "IDPORTEN_WELL_KNOWN_URL" to "http://mocked-issuer/config", + "IDPORTEN_CLIENT_ID" to "123456", + ).toMap() + + private val verifier: TokenVerifier = mockk() + private val dummyJwt: DecodedJWT = mockk() + + private val dummyToken = "token" + + private val objectMapper = Json + + @BeforeEach + fun setupMock() { + mockkObject(TokenVerifier) + mockkObject(HttpClientBuilder) + every { TokenVerifier.build(any(), any(), any()) } returns verifier + every { HttpClientBuilder.buildHttpClient(any()) } returns mockedClient + } + + @AfterEach + fun cleanUp() { + clearMocks(verifier) + unmockkObject(HttpClientBuilder) + unmockkObject(TokenVerifier) + } + + @Test + fun `Enables login endpoint which redirects to callback`() = loginApiTest { client -> + + client.get("/login").let { + it.status `should be equal to` HttpStatusCode.Found + it.headers["location"] `should be equal to` "/oauth2/login?redirect=/login/callback" + } + } + + @Test + fun `Status endpoint returns login status when unauthorized`() = loginApiTest { client -> + + client.get("/login/status") { + accept(ContentType.Application.Json) + }.let { response -> + response.status `should be equal to` HttpStatusCode.OK + response.bodyAsText() + .let(objectMapper::parseToJsonElement) + .let { it as JsonObject } + .let { + it["authenticated"]?.jsonPrimitive?.boolean `should be equal to` false + it["level"] `should be instance of` JsonNull::class + it["levelOfAssurance"] `should be instance of` JsonNull::class + } + } + } + + @Test + fun `Status endpoint returns login status when authorized`() = loginApiTest { client -> + + every { verifier.verifyAccessToken(dummyToken) } returns dummyJwt + + val ident = "123" + + val acrClaim: Claim = mockk() + every { acrClaim.asString() } returns IdPortenLevelOfAssurance.High.acr + + val identClaim: Claim = mockk() + every { identClaim.asString() } returns ident + + every { dummyJwt.getClaim("acr") } returns acrClaim + every { dummyJwt.getClaim("pid") } returns identClaim + + client.get("/login/status") { + bearerAuth(dummyToken) + accept(ContentType.Application.Json) + }.let { response -> + response.status `should be equal to` HttpStatusCode.OK + response.bodyAsText() + .let(objectMapper::parseToJsonElement) + .let { it as JsonObject } + .let { + it["authenticated"]?.jsonPrimitive?.boolean `should be equal to` true + it["level"]?.jsonPrimitive?.int `should be equal to` 4 + it["levelOfAssurance"]?.jsonPrimitive?.content `should be equal to` IdPortenLevelOfAssurance.High.name + } + } + } + + @KtorDsl + private fun loginApiTest(block: suspend TestApplicationBuilder.(HttpClient) -> Unit) = testApplication { + application { + withEnvironment(envVars) { + install(IdPortenLogin) + } + } + + val client = createClient { + followRedirects = false + } + + block(client) + } +} diff --git a/token-support-idporten-sidecar/src/test/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/TokenVerifierTest.kt b/token-support-idporten-sidecar/src/test/kotlin/no/nav/tms/token/support/idporten/sidecar/install/TokenVerifierTest.kt similarity index 87% rename from token-support-idporten-sidecar/src/test/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/TokenVerifierTest.kt rename to token-support-idporten-sidecar/src/test/kotlin/no/nav/tms/token/support/idporten/sidecar/install/TokenVerifierTest.kt index aed1fea..983aa12 100644 --- a/token-support-idporten-sidecar/src/test/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/TokenVerifierTest.kt +++ b/token-support-idporten-sidecar/src/test/kotlin/no/nav/tms/token/support/idporten/sidecar/install/TokenVerifierTest.kt @@ -1,4 +1,4 @@ -package no.nav.tms.token.support.idporten.sidecar.authentication +package no.nav.tms.token.support.idporten.sidecar.install import com.auth0.jwk.Jwk import com.auth0.jwk.JwkProvider @@ -35,10 +35,10 @@ internal class TokenVerifierTest { @Test fun `Should accept valid token`() { - val verifier = TokenVerifier( + val verifier = TokenVerifier.build( jwkProvider = jwkProvider, issuer = issuer, - minLevelOfAssurance = LevelOfAssuranceInternal.High + minLevelOfAssurance = IdPortenLevelOfAssurance.High ) val token = JwtBuilder.generateJwtString( @@ -58,10 +58,10 @@ internal class TokenVerifierTest { @Test fun `Should not accept token with invalid issuer`() { - val verifier = TokenVerifier( + val verifier = TokenVerifier.build( jwkProvider = jwkProvider, issuer = issuer, - minLevelOfAssurance = LevelOfAssuranceInternal.High + minLevelOfAssurance = IdPortenLevelOfAssurance.High ) @@ -82,10 +82,10 @@ internal class TokenVerifierTest { @Test fun `Should not accept expired token`() { - val verifier = TokenVerifier( + val verifier = TokenVerifier.build( jwkProvider = jwkProvider, issuer = issuer, - minLevelOfAssurance = LevelOfAssuranceInternal.High + minLevelOfAssurance = IdPortenLevelOfAssurance.High ) @@ -106,10 +106,10 @@ internal class TokenVerifierTest { @Test fun `Should not accept token with too low login level`() { - val verifier = TokenVerifier( + val verifier = TokenVerifier.build( jwkProvider = jwkProvider, issuer = issuer, - minLevelOfAssurance = LevelOfAssuranceInternal.High + minLevelOfAssurance = IdPortenLevelOfAssurance.High ) diff --git a/token-support-idporten-sidecar/src/test/kotlin/no/nav/tms/token/support/idporten/sidecar/mockedClient.kt b/token-support-idporten-sidecar/src/test/kotlin/no/nav/tms/token/support/idporten/sidecar/mockedClient.kt index aa18b6a..4cc45cf 100644 --- a/token-support-idporten-sidecar/src/test/kotlin/no/nav/tms/token/support/idporten/sidecar/mockedClient.kt +++ b/token-support-idporten-sidecar/src/test/kotlin/no/nav/tms/token/support/idporten/sidecar/mockedClient.kt @@ -8,7 +8,7 @@ import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.serialization.kotlinx.json.* import kotlinx.serialization.encodeToString import no.nav.tms.token.support.idporten.sidecar.ObjectMapper.kotlinxMapper -import no.nav.tms.token.support.idporten.sidecar.authentication.OauthServerConfigurationMetadata +import no.nav.tms.token.support.idporten.sidecar.install.OauthServerConfigurationMetadata val mockedClient = HttpClient(MockEngine) { diff --git a/token-support-tokendings-exchange/README.md b/token-support-tokendings-exchange/README.md index 505ef74..8589e2c 100644 --- a/token-support-tokendings-exchange/README.md +++ b/token-support-tokendings-exchange/README.md @@ -34,7 +34,7 @@ Biblioteket tilbyr ett interface `TokendingsService` med to implementasjoner, `C Disse bygges ved hjelp av `TokendingsServiceBuilder`: ```kotlin -fun Application.mainModule() { +fun Application.setup() { val serviceWithCache = TokendingsServiceBuilder.buildTokendingsService( cachingEnabled = true, diff --git a/token-support-tokendings-exchange/build.gradle.kts b/token-support-tokendings-exchange/build.gradle.kts index b6b7c4b..54d3b61 100644 --- a/token-support-tokendings-exchange/build.gradle.kts +++ b/token-support-tokendings-exchange/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(Ktor.serverAuthJwt) implementation(Ktor.serverNetty) implementation(Logback.classic) + implementation(KotlinLogging.logging) implementation(Nimbusds.joseJwt) testImplementation(kotlin("test-junit5")) testImplementation(Kluent.kluent) diff --git a/token-support-tokenx-validation-mock/README.md b/token-support-tokenx-validation-mock/README.md index e8c7613..95f1456 100644 --- a/token-support-tokenx-validation-mock/README.md +++ b/token-support-tokenx-validation-mock/README.md @@ -8,13 +8,14 @@ Kun ment å brukes for testing, og bør ikke havne i miljø. For å kunne autentisere et endepunkt må man først installere autentikatoren. -Denne har 1 variabel: +Denne har en rekke = variabler: -`setAsDefault`: (Optional) Setter denne autentikatoren som default. Default 'false' -`alwaysAuthenticated`: (Optional) Bestemmer om alle kall skal være godkjent eller motsatt. Default 'false' -`staticLevelOfAssurance`: Bestemmer hvilket innloggingsnivå bruker er logget inn med. Default 'null'. -`staticUserPid`: Bestemmer hvilken ident bruker er logget inn med. Default 'null'. -`staticJwtOverride`: Bestemmer hvilket token som evt skal settes i AzurePrincipal. Default 'null'. +- `authenticatorName`: Bestemmer navnet på autentikatoren. Default `TokenXAuthenticator.name` +- `setAsDefault`: (Optional) Setter denne autentikatoren som default. Default 'false' +- `alwaysAuthenticated`: (Optional) Bestemmer om alle kall skal være godkjent eller motsatt. Default 'false' +- `staticLevelOfAssurance`: Bestemmer hvilket innloggingsnivå bruker er logget inn med. Default 'null'. +- `staticUserPid`: Bestemmer hvilken ident bruker er logget inn med. Default 'null'. +- `staticJwtOverride`: Bestemmer hvilket token som evt skal settes i AzurePrincipal. Default 'null'. Dersom alwaysAuthenticated er 'true', må enten 'staticLevelOfAssurance' og 'staticUserPid' være satt, eller så må 'staticJwtOverride' være satt. 'staticJwtOverride' må ha claims 'acr' (satt til `idporten-loa-substantial` eller `idporten-loa-high`) og 'pid'. @@ -23,13 +24,15 @@ Dersom alle feltene er satt er det 'staticJwtOverride' som er gjeldende. Eksempel på konfigurasjon: ```kotlin -fun Application.mainModule() { +fun Application.setup() { - installTokenXAuthMock { - setAsDefault = false - alwaysAuthenticated = true - staticLevelOfAssurance = LEVEL_4 - staticUserPid = '123' + authentication { + tokenXMock { + setAsDefault = false + alwaysAuthenticated = true + staticLevelOfAssurance = LEVEL_4 + staticUserPid = '123' + } } } ``` @@ -38,10 +41,12 @@ Deretter kan man autentisere bestemte endepunkt som følger. Hvis ikke denne aut viktig å ha med navnet på autentikatoren. ```kotlin -fun Application.mainModule() { +fun Application.setup() { - installTokenXAuth { - setAsDefault = false + authentication { + tokenX { + setAsDefault = false + } } routing { diff --git a/token-support-tokenx-validation-mock/build.gradle.kts b/token-support-tokenx-validation-mock/build.gradle.kts index b0b49ce..c4cb3de 100644 --- a/token-support-tokenx-validation-mock/build.gradle.kts +++ b/token-support-tokenx-validation-mock/build.gradle.kts @@ -10,6 +10,8 @@ plugins { dependencies { api(kotlin("stdlib-jdk8")) implementation(project(":token-support-tokenx-validation")) + implementation(Logback.classic) + implementation(KotlinLogging.logging) implementation(Ktor.serverAuth) implementation(Ktor.serverAuthJwt) implementation(Ktor.clientJson) diff --git a/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/TokenXMockInstaller.kt b/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/TokenXMockInstaller.kt deleted file mode 100644 index 3903cc1..0000000 --- a/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/TokenXMockInstaller.kt +++ /dev/null @@ -1,34 +0,0 @@ -package no.nav.tms.token.support.tokenx.validation.mock - -import io.ktor.server.application.* -import io.ktor.server.auth.* -import no.nav.tms.token.support.tokenx.validation.TokenXAuthenticator -import no.nav.tms.token.support.tokenx.validation.mock.tokendings.AuthInfoValidator.validateAuthInfo -import no.nav.tms.token.support.tokenx.validation.mock.tokendings.tokenXAuthMock - -object TokenXMockInstaller { - fun Application.performTokenXMockInstallation( - config: TokenXMockedAuthenticatorConfig, - existingAuthContext: AuthenticationConfig? = null - ) { - val authenticatorName = getAuthenticatorName(config.setAsDefault) - - val authInfo = validateAuthInfo(config) - - if (existingAuthContext == null) { - install(Authentication) { - tokenXAuthMock(authenticatorName, authInfo) - } - } else { - existingAuthContext.tokenXAuthMock(authenticatorName, authInfo) - } - } - - private fun getAuthenticatorName(isDefault: Boolean): String? { - return if (isDefault) { - null - } else { - TokenXAuthenticator.name - } - } -} diff --git a/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/config.kt b/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/config.kt index 98b6b3c..0224953 100644 --- a/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/config.kt +++ b/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/config.kt @@ -1,30 +1,14 @@ package no.nav.tms.token.support.tokenx.validation.mock -import io.ktor.server.application.* import io.ktor.server.auth.* import no.nav.tms.token.support.tokenx.validation.TokenXAuthenticator -import no.nav.tms.token.support.tokenx.validation.mock.tokendings.AuthInfoValidator -import no.nav.tms.token.support.tokenx.validation.mock.tokendings.tokenXAuthMock +import no.nav.tms.token.support.tokenx.validation.mock.install.TokenXMockInstaller.performTokenXMockInstallation -fun Application.installTokenXAuthMock(configure: TokenXMockedAuthenticatorConfig.() -> Unit = {}) { +fun AuthenticationConfig.tokenXMock(configure: TokenXMockedAuthenticatorConfig.() -> Unit = {}) { val config = TokenXMockedAuthenticatorConfig().also(configure) - val authenticatorName = getAuthenticatorName(config.setAsDefault) - - val authInfo = AuthInfoValidator.validateAuthInfo(config) - - install(Authentication) { - tokenXAuthMock(authenticatorName, authInfo) - } -} - -private fun getAuthenticatorName(isDefault: Boolean): String? { - return if (isDefault) { - null - } else { - TokenXAuthenticator.name - } + performTokenXMockInstallation(config) } enum class LevelOfAssurance(val claim: String) { @@ -36,6 +20,7 @@ enum class LevelOfAssurance(val claim: String) { // Configuration provided by library user. See readme for example of use class TokenXMockedAuthenticatorConfig { + var authenticatorName: String = TokenXAuthenticator.name var setAsDefault: Boolean = false var alwaysAuthenticated: Boolean = false var staticLevelOfAssurance: LevelOfAssurance? = null diff --git a/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/authentication/AuthInfo.kt b/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/install/AuthInfo.kt similarity index 69% rename from token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/authentication/AuthInfo.kt rename to token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/install/AuthInfo.kt index 729c0e4..9947622 100644 --- a/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/authentication/AuthInfo.kt +++ b/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/install/AuthInfo.kt @@ -1,4 +1,4 @@ -package no.nav.tms.token.support.idporten.sidecar.mock.authentication +package no.nav.tms.token.support.tokenx.validation.mock.install internal data class AuthInfo( val alwaysAuthenticated: Boolean, diff --git a/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/tokendings/AuthInfoValidator.kt b/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/install/AuthInfoValidator.kt similarity index 96% rename from token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/tokendings/AuthInfoValidator.kt rename to token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/install/AuthInfoValidator.kt index 832f24b..e230802 100644 --- a/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/tokendings/AuthInfoValidator.kt +++ b/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/install/AuthInfoValidator.kt @@ -1,4 +1,4 @@ -package no.nav.tms.token.support.tokenx.validation.mock.tokendings +package no.nav.tms.token.support.tokenx.validation.mock.install import com.auth0.jwt.JWT import no.nav.tms.token.support.tokenx.validation.mock.LevelOfAssurance diff --git a/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/authentication/JwkBuilder.kt b/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/install/JwkBuilder.kt similarity index 85% rename from token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/authentication/JwkBuilder.kt rename to token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/install/JwkBuilder.kt index 5be6f10..d7149e1 100644 --- a/token-support-idporten-sidecar-mock/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/mock/authentication/JwkBuilder.kt +++ b/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/install/JwkBuilder.kt @@ -1,4 +1,4 @@ -package no.nav.tms.token.support.idporten.sidecar.mock.authentication +package no.nav.tms.token.support.tokenx.validation.mock.install import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.jwk.KeyUse diff --git a/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/install/TokenXMockInstaller.kt b/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/install/TokenXMockInstaller.kt new file mode 100644 index 0000000..3f0cad0 --- /dev/null +++ b/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/install/TokenXMockInstaller.kt @@ -0,0 +1,24 @@ +package no.nav.tms.token.support.tokenx.validation.mock.install + +import io.ktor.server.auth.* +import no.nav.tms.token.support.tokenx.validation.TokenXAuthenticator +import no.nav.tms.token.support.tokenx.validation.mock.TokenXMockedAuthenticatorConfig + +internal object TokenXMockInstaller { + fun AuthenticationConfig.performTokenXMockInstallation( + config: TokenXMockedAuthenticatorConfig + ) { + registerTokenXProviderMock( + authenticatorName = getAuthenticatorName(config), + authInfo = AuthInfoValidator.validateAuthInfo(config) + ) + } + + private fun getAuthenticatorName(config: TokenXMockedAuthenticatorConfig): String? { + return if (config.setAsDefault) { + null + } else { + config.authenticatorName + } + } +} \ No newline at end of file diff --git a/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/tokendings/TokenxPrincipalBuilder.kt b/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/install/TokenxPrincipalBuilder.kt similarity index 89% rename from token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/tokendings/TokenxPrincipalBuilder.kt rename to token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/install/TokenxPrincipalBuilder.kt index 3091543..ffc4c31 100644 --- a/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/tokendings/TokenxPrincipalBuilder.kt +++ b/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/install/TokenxPrincipalBuilder.kt @@ -1,4 +1,4 @@ -package no.nav.tms.token.support.tokenx.validation.mock.tokendings +package no.nav.tms.token.support.tokenx.validation.mock.install import com.auth0.jwt.JWT import com.auth0.jwt.interfaces.DecodedJWT @@ -6,10 +6,9 @@ import com.nimbusds.jose.JOSEObjectType import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.JWSHeader import com.nimbusds.jose.crypto.RSASSASigner -import com.nimbusds.jose.jwk.RSAKey import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT -import no.nav.tms.token.support.tokenx.validation.tokendings.TokenXPrincipal +import no.nav.tms.token.support.tokenx.validation.TokenXPrincipal import java.time.Instant import java.time.temporal.ChronoUnit.HOURS import java.util.* diff --git a/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/install/tokenAuthentication.kt b/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/install/tokenAuthentication.kt new file mode 100644 index 0000000..58bfef0 --- /dev/null +++ b/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/install/tokenAuthentication.kt @@ -0,0 +1,43 @@ +package no.nav.tms.token.support.tokenx.validation.mock.install + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.server.auth.* +import io.ktor.http.* +import io.ktor.serialization.* +import io.ktor.server.response.* +import org.slf4j.LoggerFactory + +internal fun AuthenticationConfig.registerTokenXProviderMock(authenticatorName: String?, authInfo: AuthInfo) { + + AccessTokenAuthenticationProvider.Configuration(authenticatorName) + .let { config -> AccessTokenAuthenticationProvider(authInfo, config) } + .let { provider -> register(provider) } +} + +private fun AuthenticationContext.respondUnauthorized(message: String) { + + challenge("Unauthenticated", AuthenticationFailedCause.InvalidCredentials) { challenge, call -> + call.respond(HttpStatusCode.Unauthorized, message) + challenge.complete() + } +} + +private class AccessTokenAuthenticationProvider( + val authInfo: AuthInfo, + config: Configuration +) : AuthenticationProvider(config) { + + private val log = KotlinLogging.logger { } + + override suspend fun onAuthenticate(context: AuthenticationContext) { + if (authInfo.alwaysAuthenticated) { + log.debug { "Auth is valid as tokenx-mock is set to never authorized." } + context.principal(TokenxPrincipalBuilder.createPrincipal(authInfo)) + } else { + log.debug { "Responding 401 as tokenx-mock is set to never authorized." } + context.respondUnauthorized("Never authorized.") + } + } + + class Configuration(name: String?) : Config(name) +} diff --git a/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/tokendings/tokenAuthentication.kt b/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/tokendings/tokenAuthentication.kt deleted file mode 100644 index a16ecf6..0000000 --- a/token-support-tokenx-validation-mock/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/mock/tokendings/tokenAuthentication.kt +++ /dev/null @@ -1,45 +0,0 @@ -package no.nav.tms.token.support.tokenx.validation.mock.tokendings - -import io.ktor.server.auth.* -import io.ktor.http.* -import io.ktor.server.response.* -import org.slf4j.LoggerFactory - -internal fun AuthenticationConfig.tokenXAuthMock(authenticatorName: String?, authInfo: AuthInfo) { - - val provider = AccessTokenAuthenticationProvider.build(authInfo, authenticatorName) - - register(provider) -} - -private fun AuthenticationContext.respondUnauthorized(message: String) { - - challenge("Unauthenticated", AuthenticationFailedCause.InvalidCredentials) { challenge, call -> - call.respond(HttpStatusCode.Unauthorized, message) - challenge.complete() - } -} - -private class AccessTokenAuthenticationProvider constructor( - val authInfo: AuthInfo, - config: Configuration -) : AuthenticationProvider(config) { - - private val log = LoggerFactory.getLogger(AccessTokenAuthenticationProvider::class.java) - - override suspend fun onAuthenticate(context: AuthenticationContext) { - if (authInfo.alwaysAuthenticated) { - log.debug("Auth is valid as tokenx-mock is set to never authorized.") - context.principal(TokenxPrincipalBuilder.createPrincipal(authInfo)) - } else { - log.debug("Responding 401 as tokenx-mock is set to never authorized.") - context.respondUnauthorized("Never authorized.") - } - } - - class Configuration(name: String?) : AuthenticationProvider.Config(name) - - companion object { - fun build(authInfo: AuthInfo, name: String?) = AccessTokenAuthenticationProvider(authInfo, Configuration(name)) - } -} diff --git a/token-support-tokenx-validation-mock/src/test/kotlin/no/nav/tms/token/support/tokenx/validation/mock/TokenXAuthTest.kt b/token-support-tokenx-validation-mock/src/test/kotlin/no/nav/tms/token/support/tokenx/validation/mock/TokenXAuthTest.kt index 8f946f2..d842c3d 100644 --- a/token-support-tokenx-validation-mock/src/test/kotlin/no/nav/tms/token/support/tokenx/validation/mock/TokenXAuthTest.kt +++ b/token-support-tokenx-validation-mock/src/test/kotlin/no/nav/tms/token/support/tokenx/validation/mock/TokenXAuthTest.kt @@ -22,9 +22,12 @@ internal class TokenXAuthTest { application { testApi { - installTokenXAuthMock { - alwaysAuthenticated = false + authentication { + tokenXMock { + alwaysAuthenticated = false + } } + } } @@ -38,10 +41,12 @@ internal class TokenXAuthTest { application { testApi { - installTokenXAuthMock { - alwaysAuthenticated = true - staticUserPid = userPid - staticLevelOfAssurance = LevelOfAssurance.LEVEL_4 + authentication { + tokenXMock { + alwaysAuthenticated = true + staticUserPid = userPid + staticLevelOfAssurance = LevelOfAssurance.LEVEL_4 + } } } } @@ -57,9 +62,11 @@ internal class TokenXAuthTest { application { testApiWithDefault { - installTokenXAuthMock { - setAsDefault = true - alwaysAuthenticated = false + authentication { + tokenXMock { + setAsDefault = true + alwaysAuthenticated = false + } } } } diff --git a/token-support-tokenx-validation/README.md b/token-support-tokenx-validation/README.md index 92f29db..460d512 100644 --- a/token-support-tokenx-validation/README.md +++ b/token-support-tokenx-validation/README.md @@ -17,17 +17,20 @@ spec: For å kunne autentisere et endepunkt må man først installere autentikatoren. -Denne har 1 variabel: +Denne har 2 variabler: -`setAsDefault`: (Optional) Setter denne autentikatoren som default. Default 'false' +- `authenticatorName`: Bestemmer navnet på autentikatoren. Default `TokenXAuthenticator.name` +- `setAsDefault`: (Optional) Setter denne autentikatoren som default. Default 'false' Eksempel på konfigurasjon: ```kotlin -fun Application.mainModule() { +fun Application.setup() { - installTokenXAuth { - setAsDefault = false + authentication { + tokenX { + setAsDefault = false + } } } ``` @@ -36,10 +39,12 @@ Deretter kan man autentisere bestemte endepunkt som følger. Hvis ikke denne aut viktig å ha med navnet på autentikatoren. ```kotlin -fun Application.mainModule() { +fun Application.setup() { - installTokenXAuth { - setAsDefault = false + authentication { + tokenX { + setAsDefault = false + } } routing { @@ -55,10 +60,12 @@ fun Application.mainModule() { Typisk eksempel på bruk i miljø og dette er default authenticator: ```kotlin -fun Application.mainModule() { +fun Application.setup() { - installTokenXAuth { - setAsDefault = true + authentication { + tokenX { + setAsDefault = true + } } routing { diff --git a/token-support-tokenx-validation/build.gradle.kts b/token-support-tokenx-validation/build.gradle.kts index 02c16a0..e6b10f3 100644 --- a/token-support-tokenx-validation/build.gradle.kts +++ b/token-support-tokenx-validation/build.gradle.kts @@ -10,6 +10,7 @@ plugins { dependencies { api(kotlin("stdlib-jdk8")) implementation(Logback.classic) + implementation(KotlinLogging.logging) implementation(Ktor.clientApache) implementation(Ktor.clientContentNegotiation) implementation(Ktor.clientJson) @@ -18,6 +19,9 @@ dependencies { implementation(Ktor.serverAuth) implementation(Ktor.serverAuthJwt) implementation(Nimbusds.joseJwt) + implementation("io.ktor:ktor-server-auth-jvm:2.3.0") + implementation("io.ktor:ktor-server-core-jvm:2.3.0") + implementation("io.ktor:ktor-server-auth-ldap-jvm:2.3.0") testImplementation(kotlin("test-junit5")) testImplementation(Kluent.kluent) testImplementation(Mockk.mockk) diff --git a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/TokenXInstaller.kt b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/TokenXInstaller.kt deleted file mode 100644 index 61549ee..0000000 --- a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/TokenXInstaller.kt +++ /dev/null @@ -1,43 +0,0 @@ -package no.nav.tms.token.support.tokenx.validation - -import io.ktor.server.application.* -import io.ktor.server.auth.* -import no.nav.tms.token.support.tokenx.validation.config.RuntimeContext -import no.nav.tms.token.support.tokenx.validation.tokendings.LevelOfAssuranceInternal -import no.nav.tms.token.support.tokenx.validation.tokendings.tokenXAccessToken - -object TokenXInstaller { - fun Application.performTokenXAuthenticatorInstallation( - config: TokenXAuthenticatorConfig, - existingAuthContext: AuthenticationConfig? = null - ) { - val authenticatorName = getAuthenticatorName(config.setAsDefault) - - val runtimeContext = RuntimeContext( - minLevelOfAssurance = getMinLoa(config.levelOfAssurance) - ) - - if (existingAuthContext == null) { - install(Authentication) { - tokenXAccessToken(authenticatorName, runtimeContext.verifierWrapper) - } - } else { - existingAuthContext.tokenXAccessToken(authenticatorName, runtimeContext.verifierWrapper) - } - } - - private fun getAuthenticatorName(isDefault: Boolean): String? { - return if (isDefault) { - null - } else { - TokenXAuthenticator.name - } - } - - private fun getMinLoa(loa: LevelOfAssurance): LevelOfAssuranceInternal { - return when (loa) { - LevelOfAssurance.SUBSTANTIAL -> LevelOfAssuranceInternal.Substantial - LevelOfAssurance.HIGH -> LevelOfAssuranceInternal.High - } - } -} diff --git a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/TokenXPrincipal.kt b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/TokenXPrincipal.kt new file mode 100644 index 0000000..9db9c11 --- /dev/null +++ b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/TokenXPrincipal.kt @@ -0,0 +1,8 @@ +package no.nav.tms.token.support.tokenx.validation + +import com.auth0.jwt.interfaces.DecodedJWT +import io.ktor.server.auth.* + +data class TokenXPrincipal(val decodedJWT: DecodedJWT) : Principal { + fun ident(identClaim: String = "pid"): String = decodedJWT.getClaim(identClaim).asString() +} diff --git a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/config.kt b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/config.kt index 96abb1f..9ec8173 100644 --- a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/config.kt +++ b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/config.kt @@ -1,15 +1,11 @@ package no.nav.tms.token.support.tokenx.validation -import io.ktor.server.application.* import io.ktor.server.auth.* -import io.ktor.http.* import no.nav.tms.token.support.tokenx.validation.LevelOfAssurance.SUBSTANTIAL -import no.nav.tms.token.support.tokenx.validation.TokenXInstaller.performTokenXAuthenticatorInstallation -import no.nav.tms.token.support.tokenx.validation.config.RuntimeContext -import no.nav.tms.token.support.tokenx.validation.tokendings.tokenXAccessToken +import no.nav.tms.token.support.tokenx.validation.install.TokenXInstaller.performTokenXAuthenticatorInstallation -fun Application.installTokenXAuth(configure: TokenXAuthenticatorConfig.() -> Unit = {}) { +fun AuthenticationConfig.tokenX(configure: TokenXAuthenticatorConfig.() -> Unit = {}) { val config = TokenXAuthenticatorConfig().also(configure) performTokenXAuthenticatorInstallation(config) @@ -17,12 +13,13 @@ fun Application.installTokenXAuth(configure: TokenXAuthenticatorConfig.() -> Uni // Configuration provided by library user. See readme for example of use class TokenXAuthenticatorConfig { + var authenticatorName: String = TokenXAuthenticator.name var setAsDefault: Boolean = false var levelOfAssurance: LevelOfAssurance = SUBSTANTIAL } object TokenXAuthenticator { - const val name = "tokenx_bearer_access_token" + const val name = "tokenx_access_token" } object TokenXHeader { diff --git a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/config/Environment.kt b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/config/Environment.kt deleted file mode 100644 index 10f5d2d..0000000 --- a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/config/Environment.kt +++ /dev/null @@ -1,11 +0,0 @@ -package no.nav.tms.token.support.tokenx.validation.config - -internal data class Environment( - val tokenxClientId: String = getTokenxEnvVar("TOKEN_X_CLIENT_ID"), - val tokenxWellKnownUrl: String = getTokenxEnvVar("TOKEN_X_WELL_KNOWN_URL") -) - -private fun getTokenxEnvVar(varName: String): String { - return System.getenv(varName) - ?: throw IllegalArgumentException("Fant ikke $varName for tokenx. Påse at nais.yaml er konfigurert riktig.") -} diff --git a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/config/JwkProviderBuilder.kt b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/config/JwkProviderBuilder.kt deleted file mode 100644 index b14de6c..0000000 --- a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/config/JwkProviderBuilder.kt +++ /dev/null @@ -1,13 +0,0 @@ -package no.nav.tms.token.support.tokenx.validation.config - -import com.auth0.jwk.JwkProvider -import com.auth0.jwk.JwkProviderBuilder -import java.net.URL -import java.util.concurrent.TimeUnit - -internal object JwkProviderBuilder { - fun createJwkProvider(metadata: OauthServerConfigurationMetadata): JwkProvider = JwkProviderBuilder(URL(metadata.jwksUri)) - .cached(10, 24, TimeUnit.HOURS) - .rateLimited(10, 1, TimeUnit.MINUTES) - .build() -} diff --git a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/config/OauthServerConfigurationMetadata.kt b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/config/OauthServerConfigurationMetadata.kt deleted file mode 100644 index 427a5db..0000000 --- a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/config/OauthServerConfigurationMetadata.kt +++ /dev/null @@ -1,12 +0,0 @@ -package no.nav.tms.token.support.tokenx.validation.config - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -internal data class OauthServerConfigurationMetadata( - @SerialName("issuer") val issuer: String, - @SerialName("token_endpoint") val tokenEndpoint: String, - @SerialName("jwks_uri") val jwksUri: String, - @SerialName("authorization_endpoint") var authorizationEndpoint: String = "" -) diff --git a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/config/RuntimeContext.kt b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/config/RuntimeContext.kt deleted file mode 100644 index 914146f..0000000 --- a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/config/RuntimeContext.kt +++ /dev/null @@ -1,29 +0,0 @@ -package no.nav.tms.token.support.tokenx.validation.config - -import io.ktor.client.* -import kotlinx.coroutines.runBlocking -import no.nav.tms.token.support.tokenx.validation.config.JwkProviderBuilder.createJwkProvider -import no.nav.tms.token.support.tokenx.validation.tokendings.LevelOfAssuranceInternal -import no.nav.tms.token.support.tokenx.validation.tokendings.TokenVerifier - -internal class RuntimeContext( - minLevelOfAssurance: LevelOfAssuranceInternal -) { - private val environment = Environment() - - private val httpClient = HttpClientBuilder.build() - private val metadata = fetchMetadata(httpClient, environment.tokenxWellKnownUrl) - - private val jwkProvider = createJwkProvider(metadata) - - val verifierWrapper = TokenVerifier( - jwkProvider = jwkProvider, - clientId = environment.tokenxClientId, - issuer = metadata.issuer, - minLevelOfAssurance = minLevelOfAssurance - ) -} - -private fun fetchMetadata(httpClient: HttpClient, idPortenUrl: String) = runBlocking { - httpClient.getOAuthServerConfigurationMetadata(idPortenUrl) -} diff --git a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/config/httpClient.kt b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/config/httpClient.kt deleted file mode 100644 index c42bee5..0000000 --- a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/config/httpClient.kt +++ /dev/null @@ -1,17 +0,0 @@ -package no.nav.tms.token.support.tokenx.validation.config - -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.request.* -import io.ktor.http.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -internal suspend fun HttpClient.getOAuthServerConfigurationMetadata(url: String) - : OauthServerConfigurationMetadata = withContext(Dispatchers.IO) { - request { - method = HttpMethod.Get - url(url) - accept(ContentType.Application.Json) - }.body() -} diff --git a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/config/HttpClientBuilder.kt b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/install/HttpClientBuilder.kt similarity index 90% rename from token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/config/HttpClientBuilder.kt rename to token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/install/HttpClientBuilder.kt index df01329..124c32a 100644 --- a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/config/HttpClientBuilder.kt +++ b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/install/HttpClientBuilder.kt @@ -1,4 +1,4 @@ -package no.nav.tms.token.support.tokenx.validation.config +package no.nav.tms.token.support.tokenx.validation.install import io.ktor.client.* import io.ktor.client.engine.apache.* diff --git a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/LevelOfAssuranceInternal.kt b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/install/IdPortenLevelOfAssurance.kt similarity index 68% rename from token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/LevelOfAssuranceInternal.kt rename to token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/install/IdPortenLevelOfAssurance.kt index cf8fea6..a10f417 100644 --- a/token-support-idporten-sidecar/src/main/kotlin/no/nav/tms/token/support/idporten/sidecar/authentication/LevelOfAssuranceInternal.kt +++ b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/install/IdPortenLevelOfAssurance.kt @@ -1,6 +1,6 @@ -package no.nav.tms.token.support.idporten.sidecar.authentication +package no.nav.tms.token.support.tokenx.validation.install -internal enum class LevelOfAssuranceInternal(val acr: String, val relativeValue: Int) { +internal enum class IdPortenLevelOfAssurance(val acr: String, val relativeValue: Int) { Level3("Level3", 2), Level4("Level4", 3), Low("idporten-loa-low", 1), @@ -8,7 +8,7 @@ internal enum class LevelOfAssuranceInternal(val acr: String, val relativeValue: High("idporten-loa-high", 3); companion object { - fun fromAcr(acr: String): LevelOfAssuranceInternal { + fun fromAcr(acr: String): IdPortenLevelOfAssurance { return values() .find { it.acr.lowercase() == acr.lowercase() } ?: throw IllegalStateException("Could not find matching LoA for claim $acr.") diff --git a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/install/TokenXInstaller.kt b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/install/TokenXInstaller.kt new file mode 100644 index 0000000..68e6b48 --- /dev/null +++ b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/install/TokenXInstaller.kt @@ -0,0 +1,35 @@ +package no.nav.tms.token.support.tokenx.validation.install + +import io.ktor.server.auth.* +import no.nav.tms.token.support.tokenx.validation.LevelOfAssurance +import no.nav.tms.token.support.tokenx.validation.TokenXAuthenticatorConfig + +internal object TokenXInstaller { + fun AuthenticationConfig.performTokenXAuthenticatorInstallation( + config: TokenXAuthenticatorConfig + ) { + val tokenVerifier = initializeTokenVerifier( + minLevelOfAssurance = getMinLoa(config.levelOfAssurance) + ) + + registerTokenXValidatorProvider( + authenticatorName = getAuthenticatorName(config), + tokenVerifier = tokenVerifier + ) + } + + private fun getAuthenticatorName(config: TokenXAuthenticatorConfig): String? { + return if (config.setAsDefault) { + null + } else { + config.authenticatorName + } + } + + private fun getMinLoa(loa: LevelOfAssurance): IdPortenLevelOfAssurance { + return when (loa) { + LevelOfAssurance.SUBSTANTIAL -> IdPortenLevelOfAssurance.Substantial + LevelOfAssurance.HIGH -> IdPortenLevelOfAssurance.High + } + } +} diff --git a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/tokendings/tokenAuthentication.kt b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/install/tokenAuthentication.kt similarity index 61% rename from token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/tokendings/tokenAuthentication.kt rename to token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/install/tokenAuthentication.kt index 5ff44e6..99379ba 100644 --- a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/tokendings/tokenAuthentication.kt +++ b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/install/tokenAuthentication.kt @@ -1,18 +1,18 @@ -package no.nav.tms.token.support.tokenx.validation.tokendings +package no.nav.tms.token.support.tokenx.validation.install +import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.http.* import io.ktor.server.response.* -import io.ktor.util.pipeline.* import no.nav.tms.token.support.tokenx.validation.TokenXHeader -import org.slf4j.LoggerFactory +import no.nav.tms.token.support.tokenx.validation.TokenXPrincipal -internal fun AuthenticationConfig.tokenXAccessToken(authenticatorName: String?, verifier: TokenVerifier) { +internal fun AuthenticationConfig.registerTokenXValidatorProvider(authenticatorName: String?, tokenVerifier: TokenVerifier) { - val provider = AccessTokenAuthenticationProvider.build(verifier, authenticatorName) - - register(provider) + AccessTokenAuthenticationProvider.Configuration(authenticatorName) + .let { config -> AccessTokenAuthenticationProvider(tokenVerifier, config) } + .let { provider -> register(provider) } } private fun AuthenticationContext.respondUnauthorized(message: String) { @@ -42,10 +42,9 @@ private fun ApplicationCall.tokenFromAuthHeader(): String? { ?.let { it.split(" ")[1] } } +private class AccessTokenAuthenticationProvider (val verifier: TokenVerifier, config: Configuration) : AuthenticationProvider(config) { -private class AccessTokenAuthenticationProvider constructor(val verifier: TokenVerifier, config: Configuration) : AuthenticationProvider(config) { - - private val log = LoggerFactory.getLogger(AccessTokenAuthenticationProvider::class.java) + private val log = KotlinLogging.logger { } override suspend fun onAuthenticate(context: AuthenticationContext) { val call = context.call @@ -56,19 +55,14 @@ private class AccessTokenAuthenticationProvider constructor(val verifier: TokenV val decodedJWT = verifier.verify(accessToken) context.principal(TokenXPrincipal(decodedJWT)) } catch (e: Exception) { - val message = e.message ?: e.javaClass.simpleName - log.debug("Token verification failed: {}", message) + log.debug(e) { "Token verification failed." } context.respondUnauthorized("Invalid or expired token.") } } else { - log.debug("No bearer token found.") + log.debug { "No bearer token found." } context.respondUnauthorized("No bearer token found.") } } - class Configuration(name: String?) : AuthenticationProvider.Config(name) - - companion object { - fun build(verifier: TokenVerifier, name: String?) = AccessTokenAuthenticationProvider(verifier, Configuration(name)) - } + class Configuration(name: String?) : Config(name) } diff --git a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/install/tokenVerifier.kt b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/install/tokenVerifier.kt new file mode 100644 index 0000000..960d5e9 --- /dev/null +++ b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/install/tokenVerifier.kt @@ -0,0 +1,103 @@ +package no.nav.tms.token.support.tokenx.validation.install + +import com.auth0.jwk.Jwk +import com.auth0.jwk.JwkProvider +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import com.auth0.jwt.interfaces.DecodedJWT +import com.auth0.jwt.interfaces.JWTVerifier +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.http.* +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.net.URL +import java.security.interfaces.RSAPublicKey +import java.util.concurrent.TimeUnit + +private fun getTokenxClientId() = getTokenxEnvVar("TOKEN_X_CLIENT_ID") +private fun getTokenxWellKnownUrl() = getTokenxEnvVar("TOKEN_X_WELL_KNOWN_URL") + +internal fun initializeTokenVerifier(minLevelOfAssurance: IdPortenLevelOfAssurance): TokenVerifier { + val metadata = fetchMetadata( + httpClient = HttpClientBuilder.build(), + wellKnownUrl = getTokenxWellKnownUrl() + ) + + val jwkProvider = JwkProviderBuilder.createJwkProvider(metadata) + + return TokenVerifier( + jwkProvider = jwkProvider, + clientId = getTokenxClientId(), + issuer = metadata.issuer, + minLevelOfAssurance = minLevelOfAssurance + ) +} + +internal class TokenVerifier( + private val jwkProvider: JwkProvider, + private val clientId: String, + private val issuer: String, + private val minLevelOfAssurance: IdPortenLevelOfAssurance +) { + + private val acrClaim = "acr" + + fun verify(accessToken: String): DecodedJWT { + return buildVerifier(accessToken) + .verify(accessToken) + .also { verifyLevelOfAssurance(it) } + } + + private fun buildVerifier(accessToken: String): JWTVerifier { + return JWT.decode(accessToken).keyId + .let { kid -> jwkProvider.get(kid) } + .let { JWT.require(it.RSA256()) } + .withIssuer(issuer) + .withAudience(clientId) + .build() + } + + private fun Jwk.RSA256() = Algorithm.RSA256(publicKey as RSAPublicKey, null) + + private fun verifyLevelOfAssurance(decodedToken: DecodedJWT) { + val acrClaim = decodedToken.getClaim(acrClaim) + + val levelOfAssurance = IdPortenLevelOfAssurance.fromAcr(acrClaim.asString()) + + if (levelOfAssurance.relativeValue < minLevelOfAssurance.relativeValue) { + throw RuntimeException("Level of assurance too low") + } + } +} + +@Serializable +internal data class OauthServerConfigurationMetadata( + @SerialName("issuer") val issuer: String, + @SerialName("token_endpoint") val tokenEndpoint: String, + @SerialName("jwks_uri") val jwksUri: String, + @SerialName("authorization_endpoint") var authorizationEndpoint: String = "" +) + +private fun fetchMetadata(httpClient: HttpClient, wellKnownUrl: String): OauthServerConfigurationMetadata = runBlocking { + httpClient.request { + method = HttpMethod.Get + url(wellKnownUrl) + accept(ContentType.Application.Json) + }.body() +} + +internal object JwkProviderBuilder { + fun createJwkProvider(metadata: OauthServerConfigurationMetadata): JwkProvider = + com.auth0.jwk.JwkProviderBuilder(URL(metadata.jwksUri)) + .cached(10, 24, TimeUnit.HOURS) + .rateLimited(10, 1, TimeUnit.MINUTES) + .build() +} + +private fun getTokenxEnvVar(varName: String): String { + return System.getenv(varName) + ?: throw IllegalArgumentException("Fant ikke $varName for tokenx. Påse at nais.yaml er konfigurert riktig.") +} diff --git a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/tokendings/TokenVerifier.kt b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/tokendings/TokenVerifier.kt deleted file mode 100644 index 18442ff..0000000 --- a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/tokendings/TokenVerifier.kt +++ /dev/null @@ -1,48 +0,0 @@ -package no.nav.tms.token.support.tokenx.validation.tokendings - -import com.auth0.jwk.Jwk -import com.auth0.jwk.JwkProvider -import com.auth0.jwt.JWT -import com.auth0.jwt.algorithms.Algorithm -import com.auth0.jwt.interfaces.DecodedJWT -import com.auth0.jwt.interfaces.JWTVerifier -import java.security.interfaces.RSAPublicKey - -internal class TokenVerifier( - private val jwkProvider: JwkProvider, - private val clientId: String, - private val issuer: String, - private val minLevelOfAssurance: LevelOfAssuranceInternal -) { - - private val acrClaim = "acr" - - fun verify(accessToken: String): DecodedJWT { - return buildVerifier(accessToken) - .verify(accessToken) - .also { verifyLevelOfAssurance(it) } - } - - private fun buildVerifier(accessToken: String): JWTVerifier { - return JWT.decode(accessToken).keyId - .let { kid -> jwkProvider.get(kid) } - .let { JWT.require(it.RSA256()) } - .withIssuer(issuer) - .withAudience(clientId) - .build() - } - - private fun Jwk.RSA256() = Algorithm.RSA256(publicKey as RSAPublicKey, null) - - private fun verifyLevelOfAssurance(decodedToken: DecodedJWT) { - val acrClaim = decodedToken.getClaim(acrClaim) - - val levelOfAssurance = LevelOfAssuranceInternal.fromAcr(acrClaim.asString()) - - if (levelOfAssurance.relativeValue < minLevelOfAssurance.relativeValue) { - throw RuntimeException("Level of assurance too low") - } - } -} - - diff --git a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/tokendings/TokenXPrincipal.kt b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/tokendings/TokenXPrincipal.kt deleted file mode 100644 index b8a19b4..0000000 --- a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/tokendings/TokenXPrincipal.kt +++ /dev/null @@ -1,6 +0,0 @@ -package no.nav.tms.token.support.tokenx.validation.tokendings - -import com.auth0.jwt.interfaces.DecodedJWT -import io.ktor.server.auth.* - -data class TokenXPrincipal(val decodedJWT: DecodedJWT) : Principal diff --git a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/user/TokenXUserFactory.kt b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/user/TokenXUserFactory.kt index ced6534..30ca415 100644 --- a/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/user/TokenXUserFactory.kt +++ b/token-support-tokenx-validation/src/main/kotlin/no/nav/tms/token/support/tokenx/validation/user/TokenXUserFactory.kt @@ -6,9 +6,9 @@ import io.ktor.server.auth.* import no.nav.tms.token.support.tokenx.validation.LevelOfAssurance import no.nav.tms.token.support.tokenx.validation.LevelOfAssurance.HIGH import no.nav.tms.token.support.tokenx.validation.LevelOfAssurance.SUBSTANTIAL -import no.nav.tms.token.support.tokenx.validation.tokendings.LevelOfAssuranceInternal -import no.nav.tms.token.support.tokenx.validation.tokendings.LevelOfAssuranceInternal.* -import no.nav.tms.token.support.tokenx.validation.tokendings.TokenXPrincipal +import no.nav.tms.token.support.tokenx.validation.install.IdPortenLevelOfAssurance +import no.nav.tms.token.support.tokenx.validation.install.IdPortenLevelOfAssurance.* +import no.nav.tms.token.support.tokenx.validation.TokenXPrincipal import java.time.Instant object TokenXUserFactory { @@ -27,7 +27,7 @@ object TokenXUserFactory { val ident: String = token.getClaim(identClaim).asString() - val acrLoA = LevelOfAssuranceInternal.fromAcr(token.getClaim("acr").asString()) + val acrLoA = IdPortenLevelOfAssurance.fromAcr(token.getClaim("acr").asString()) val loginLevel = mapLoginLevel(acrLoA) val levelOfAssurance = mapLevelOfAssurance(acrLoA) @@ -40,7 +40,7 @@ object TokenXUserFactory { return TokenXUser(ident, loginLevel, levelOfAssurance, expirationTime, token) } - private fun mapLoginLevel(levelOfAssurance: LevelOfAssuranceInternal): Int { + private fun mapLoginLevel(levelOfAssurance: IdPortenLevelOfAssurance): Int { return when (levelOfAssurance) { Level3, Substantial -> 3 @@ -49,7 +49,7 @@ object TokenXUserFactory { } } - private fun mapLevelOfAssurance(levelOfAssurance: LevelOfAssuranceInternal): LevelOfAssurance { + private fun mapLevelOfAssurance(levelOfAssurance: IdPortenLevelOfAssurance): LevelOfAssurance { return when (levelOfAssurance) { Level3, Substantial -> SUBSTANTIAL Level4, High -> HIGH diff --git a/token-support-tokenx-validation/src/test/kotlin/no/nav/tms/token/support/tokenx/validation/JwtBuilder.kt b/token-support-tokenx-validation/src/test/kotlin/no/nav/tms/token/support/tokenx/validation/JwtBuilder.kt index db15060..ca8bbc9 100644 --- a/token-support-tokenx-validation/src/test/kotlin/no/nav/tms/token/support/tokenx/validation/JwtBuilder.kt +++ b/token-support-tokenx-validation/src/test/kotlin/no/nav/tms/token/support/tokenx/validation/JwtBuilder.kt @@ -7,12 +7,12 @@ import com.nimbusds.jose.crypto.RSASSASigner import com.nimbusds.jose.jwk.RSAKey import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT -import no.nav.tms.token.support.tokenx.validation.tokendings.LevelOfAssuranceInternal +import no.nav.tms.token.support.tokenx.validation.install.IdPortenLevelOfAssurance import java.time.Instant import java.util.* internal object JwtBuilder { - fun generateJwtString(clientId: String, loa: LevelOfAssuranceInternal, issuer: String, rsaKey: RSAKey): String { + fun generateJwtString(clientId: String, loa: IdPortenLevelOfAssurance, issuer: String, rsaKey: RSAKey): String { val now = Date.from(Instant.now()) return JWTClaimsSet.Builder() .issuer(issuer) diff --git a/token-support-tokenx-validation/src/test/kotlin/no/nav/tms/token/support/tokenx/validation/TokenXAuthIT.kt b/token-support-tokenx-validation/src/test/kotlin/no/nav/tms/token/support/tokenx/validation/TokenXAuthIT.kt index 25e9119..4e0d5c4 100644 --- a/token-support-tokenx-validation/src/test/kotlin/no/nav/tms/token/support/tokenx/validation/TokenXAuthIT.kt +++ b/token-support-tokenx-validation/src/test/kotlin/no/nav/tms/token/support/tokenx/validation/TokenXAuthIT.kt @@ -15,10 +15,9 @@ import io.mockk.mockkObject import io.mockk.unmockkObject import no.nav.tms.token.support.tokenx.validation.LevelOfAssurance.HIGH import no.nav.tms.token.support.tokenx.validation.LevelOfAssurance.SUBSTANTIAL -import no.nav.tms.token.support.tokenx.validation.config.HttpClientBuilder -import no.nav.tms.token.support.tokenx.validation.config.JwkProviderBuilder -import no.nav.tms.token.support.tokenx.validation.tokendings.LevelOfAssuranceInternal -import no.nav.tms.token.support.tokenx.validation.tokendings.LevelOfAssuranceInternal.* +import no.nav.tms.token.support.tokenx.validation.install.HttpClientBuilder +import no.nav.tms.token.support.tokenx.validation.install.JwkProviderBuilder +import no.nav.tms.token.support.tokenx.validation.install.IdPortenLevelOfAssurance.* import org.amshove.kluent.`should be equal to` import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach @@ -29,8 +28,8 @@ internal class TokenXAuthIT { private val clientId = "cluster:namespace:appname" private val envVars = listOf( - "TOKEN_X_CLIENT_ID" to clientId, - "TOKEN_X_WELL_KNOWN_URL" to "http://tokendings-url/config" + "TOKEN_X_CLIENT_ID" to clientId, + "TOKEN_X_WELL_KNOWN_URL" to "http://tokendings-url/config" ).toMap() private val privateJwk = JwkJwtBuilder.generateJwk() @@ -274,11 +273,11 @@ internal class TokenXAuthIT { testApi(minLoa = SUBSTANTIAL) } - val loaLowToken = JwtBuilder.generateJwtString(clientId, Substantial, idportenMetadata.issuer, privateJwk) + val loaSubstantialToken = JwtBuilder.generateJwtString(clientId, Substantial, idportenMetadata.issuer, privateJwk) val level3Token = JwtBuilder.generateJwtString(clientId, Level3, idportenMetadata.issuer, privateJwk) val loaLowResponse = client.get("/test") { - headers.append(HttpHeaders.Authorization, "Bearer $loaLowToken") + headers.append(HttpHeaders.Authorization, "Bearer $loaSubstantialToken") } val level3Response = client.get("/test") { headers.append(HttpHeaders.Authorization, "Bearer $level3Token") @@ -301,14 +300,57 @@ internal class TokenXAuthIT { response.body() `should be equal to` "No bearer token found." } - private fun Application.testApi(minLoa: LevelOfAssurance? = null) = withEnvironment(envVars) { + @Test + fun `Allows verifying different apis with different configurations`() = testApplication { - installTokenXAuth { - if (minLoa != null) { - levelOfAssurance = minLoa + application { + withEnvironment(envVars) { + authentication { + tokenX { + setAsDefault = true + levelOfAssurance = HIGH + } + tokenX { + setAsDefault = false + authenticatorName = "substantial" + levelOfAssurance = SUBSTANTIAL + } + } + } + routing { + authenticate { + get("/test/one") { + call.respond(HttpStatusCode.OK) + } + } + authenticate("substantial") { + get("test/two") { + call.respond(HttpStatusCode.OK) + } + } } } + val loaSubstantialToken = JwtBuilder.generateJwtString(clientId, Substantial, idportenMetadata.issuer, privateJwk) + + client.get("/test/one") { + headers.append(HttpHeaders.Authorization, "Bearer $loaSubstantialToken") + }.status `should be equal to` HttpStatusCode.Unauthorized + + client.get("/test/two") { + headers.append(HttpHeaders.Authorization, "Bearer $loaSubstantialToken") + }.status `should be equal to` HttpStatusCode.OK + } + + private fun Application.testApi(minLoa: LevelOfAssurance? = null) = withEnvironment(envVars) { + + authentication{ + tokenX { + if (minLoa != null) { + levelOfAssurance = minLoa + } + } + } routing { authenticate(TokenXAuthenticator.name) { @@ -321,8 +363,10 @@ internal class TokenXAuthIT { private fun Application.testApiWithDefault() = withEnvironment(envVars) { - installTokenXAuth { - setAsDefault = true + authentication { + tokenX { + setAsDefault = true + } } routing { diff --git a/token-support-tokenx-validation/src/test/kotlin/no/nav/tms/token/support/tokenx/validation/mockedClient.kt b/token-support-tokenx-validation/src/test/kotlin/no/nav/tms/token/support/tokenx/validation/mockedClient.kt index fbf6685..264756e 100644 --- a/token-support-tokenx-validation/src/test/kotlin/no/nav/tms/token/support/tokenx/validation/mockedClient.kt +++ b/token-support-tokenx-validation/src/test/kotlin/no/nav/tms/token/support/tokenx/validation/mockedClient.kt @@ -8,7 +8,7 @@ import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.serialization.kotlinx.json.* import kotlinx.serialization.encodeToString import no.nav.tms.token.support.tokenx.validation.ObjectMapper.kotlinxMapper -import no.nav.tms.token.support.tokenx.validation.config.OauthServerConfigurationMetadata +import no.nav.tms.token.support.tokenx.validation.install.OauthServerConfigurationMetadata