From 33d6f7af8040edea6e49867cb434612b9c5a45b6 Mon Sep 17 00:00:00 2001 From: Thomas Johansen Date: Wed, 30 Oct 2024 10:14:47 +0100 Subject: [PATCH] Lagt til felles sikkerhetsmodul --- lib/security/README.md | 3 + lib/security/build.gradle.kts | 24 ++++ .../exception/BearerTokenManglerException.kt | 6 + .../exception/IngenTilgangException.kt | 6 + .../exception/UgyldigBearerTokenException.kt | 6 + .../exception/UgyldigBrukerException.kt | 6 + .../security/authentication/model/Bruker.kt | 11 ++ .../authentication/model/Identitetsnummer.kt | 10 ++ .../authentication/token/AccessToken.kt | 95 ++++++++++++++ .../context/AuthorizationContext.kt | 6 + .../authorization/context/RequestContext.kt | 48 +++++++ .../authorization/context/SecurityContext.kt | 53 ++++++++ .../interceptor/AuthorizationInterceptor.kt | 25 ++++ .../authorization/model/AccessDecision.kt | 7 + .../authorization/model/AccessOperation.kt | 3 + .../authorization/model/AccessResult.kt | 6 + .../authorization/policy/AccessPolicy.kt | 8 ++ .../policy/AccessPolicyEvaluator.kt | 21 +++ .../authorization/RouteAuthorizationTest.kt | 61 +++++++++ .../authorization/policy/TestPolicies.kt | 19 +++ .../security/test/TestApplicationContext.kt | 120 ++++++++++++++++++ .../nav/paw/security/test/TokenTestUtils.kt | 80 ++++++++++++ settings.gradle.kts | 1 + 23 files changed, 625 insertions(+) create mode 100644 lib/security/README.md create mode 100644 lib/security/build.gradle.kts create mode 100644 lib/security/src/main/kotlin/no/nav/paw/security/authentication/exception/BearerTokenManglerException.kt create mode 100644 lib/security/src/main/kotlin/no/nav/paw/security/authentication/exception/IngenTilgangException.kt create mode 100644 lib/security/src/main/kotlin/no/nav/paw/security/authentication/exception/UgyldigBearerTokenException.kt create mode 100644 lib/security/src/main/kotlin/no/nav/paw/security/authentication/exception/UgyldigBrukerException.kt create mode 100644 lib/security/src/main/kotlin/no/nav/paw/security/authentication/model/Bruker.kt create mode 100644 lib/security/src/main/kotlin/no/nav/paw/security/authentication/model/Identitetsnummer.kt create mode 100644 lib/security/src/main/kotlin/no/nav/paw/security/authentication/token/AccessToken.kt create mode 100644 lib/security/src/main/kotlin/no/nav/paw/security/authorization/context/AuthorizationContext.kt create mode 100644 lib/security/src/main/kotlin/no/nav/paw/security/authorization/context/RequestContext.kt create mode 100644 lib/security/src/main/kotlin/no/nav/paw/security/authorization/context/SecurityContext.kt create mode 100644 lib/security/src/main/kotlin/no/nav/paw/security/authorization/interceptor/AuthorizationInterceptor.kt create mode 100644 lib/security/src/main/kotlin/no/nav/paw/security/authorization/model/AccessDecision.kt create mode 100644 lib/security/src/main/kotlin/no/nav/paw/security/authorization/model/AccessOperation.kt create mode 100644 lib/security/src/main/kotlin/no/nav/paw/security/authorization/model/AccessResult.kt create mode 100644 lib/security/src/main/kotlin/no/nav/paw/security/authorization/policy/AccessPolicy.kt create mode 100644 lib/security/src/main/kotlin/no/nav/paw/security/authorization/policy/AccessPolicyEvaluator.kt create mode 100644 lib/security/src/test/kotlin/no/nav/paw/security/authorization/RouteAuthorizationTest.kt create mode 100644 lib/security/src/test/kotlin/no/nav/paw/security/authorization/policy/TestPolicies.kt create mode 100644 lib/security/src/test/kotlin/no/nav/paw/security/test/TestApplicationContext.kt create mode 100644 lib/security/src/test/kotlin/no/nav/paw/security/test/TokenTestUtils.kt diff --git a/lib/security/README.md b/lib/security/README.md new file mode 100644 index 00000000..6e62c421 --- /dev/null +++ b/lib/security/README.md @@ -0,0 +1,3 @@ +# Sikkerhetsmodul + +Felles sikkerhetsmodul for autentisering og autorisering. diff --git a/lib/security/build.gradle.kts b/lib/security/build.gradle.kts new file mode 100644 index 00000000..02f9a88b --- /dev/null +++ b/lib/security/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + kotlin("jvm") +} + +dependencies { + implementation(project(":lib:error-handling")) + implementation(libs.ktor.server.auth) + implementation(libs.logbackClassic) + implementation(libs.nav.security.tokenValidationKtorV2) + + //Test + testImplementation(project(":lib:pdl-client")) + testImplementation(libs.nav.poao.tilgangClient) + testImplementation(libs.nav.security.mockOauth2Server) + testImplementation(libs.bundles.testLibsWithUnitTesting) + testImplementation(libs.ktor.server.testJvm) + testImplementation(libs.ktor.client.contentNegotiation) + testImplementation(libs.ktor.serialization.jackson) + testImplementation(libs.jackson.datatypeJsr310) +} + +tasks.withType().configureEach { + useJUnitPlatform() +} diff --git a/lib/security/src/main/kotlin/no/nav/paw/security/authentication/exception/BearerTokenManglerException.kt b/lib/security/src/main/kotlin/no/nav/paw/security/authentication/exception/BearerTokenManglerException.kt new file mode 100644 index 00000000..f69672e9 --- /dev/null +++ b/lib/security/src/main/kotlin/no/nav/paw/security/authentication/exception/BearerTokenManglerException.kt @@ -0,0 +1,6 @@ +package no.nav.paw.security.authentication.exception + +import no.nav.paw.error.exception.AuthenticationException + +class BearerTokenManglerException(message: String) : + AuthenticationException("PAW_BEARER_TOKEN_MANGLER", message) \ No newline at end of file diff --git a/lib/security/src/main/kotlin/no/nav/paw/security/authentication/exception/IngenTilgangException.kt b/lib/security/src/main/kotlin/no/nav/paw/security/authentication/exception/IngenTilgangException.kt new file mode 100644 index 00000000..973c8fe0 --- /dev/null +++ b/lib/security/src/main/kotlin/no/nav/paw/security/authentication/exception/IngenTilgangException.kt @@ -0,0 +1,6 @@ +package no.nav.paw.security.authentication.exception + +import no.nav.paw.error.exception.AuthorizationException + +class IngenTilgangException(message: String) : + AuthorizationException("PAW_INGEN_TILGANG", message) \ No newline at end of file diff --git a/lib/security/src/main/kotlin/no/nav/paw/security/authentication/exception/UgyldigBearerTokenException.kt b/lib/security/src/main/kotlin/no/nav/paw/security/authentication/exception/UgyldigBearerTokenException.kt new file mode 100644 index 00000000..5f64f323 --- /dev/null +++ b/lib/security/src/main/kotlin/no/nav/paw/security/authentication/exception/UgyldigBearerTokenException.kt @@ -0,0 +1,6 @@ +package no.nav.paw.security.authentication.exception + +import no.nav.paw.error.exception.AuthorizationException + +class UgyldigBearerTokenException(message: String) : + AuthorizationException("PAW_UGYLDIG_BEARER_TOKEN", message) \ No newline at end of file diff --git a/lib/security/src/main/kotlin/no/nav/paw/security/authentication/exception/UgyldigBrukerException.kt b/lib/security/src/main/kotlin/no/nav/paw/security/authentication/exception/UgyldigBrukerException.kt new file mode 100644 index 00000000..119f7ab9 --- /dev/null +++ b/lib/security/src/main/kotlin/no/nav/paw/security/authentication/exception/UgyldigBrukerException.kt @@ -0,0 +1,6 @@ +package no.nav.paw.security.authentication.exception + +import no.nav.paw.error.exception.AuthorizationException + +class UgyldigBrukerException(message: String) : + AuthorizationException("PAW_UGYLDIG_BRUKER", message) \ No newline at end of file diff --git a/lib/security/src/main/kotlin/no/nav/paw/security/authentication/model/Bruker.kt b/lib/security/src/main/kotlin/no/nav/paw/security/authentication/model/Bruker.kt new file mode 100644 index 00000000..e7313539 --- /dev/null +++ b/lib/security/src/main/kotlin/no/nav/paw/security/authentication/model/Bruker.kt @@ -0,0 +1,11 @@ +package no.nav.paw.security.authentication.model + +import java.util.* + +sealed class Bruker( + open val ident: ID +) + +data class Sluttbruker(override val ident: Identitetsnummer) : Bruker(ident) +data class NavAnsatt(val oid: UUID, override val ident: String) : Bruker(ident) +data class M2MToken(val oid: UUID) : Bruker("N/A") diff --git a/lib/security/src/main/kotlin/no/nav/paw/security/authentication/model/Identitetsnummer.kt b/lib/security/src/main/kotlin/no/nav/paw/security/authentication/model/Identitetsnummer.kt new file mode 100644 index 00000000..f371c5b7 --- /dev/null +++ b/lib/security/src/main/kotlin/no/nav/paw/security/authentication/model/Identitetsnummer.kt @@ -0,0 +1,10 @@ +package no.nav.paw.security.authentication.model + +@JvmInline +value class Identitetsnummer(val verdi: String) { + override fun toString(): String { + return "*".repeat(verdi.length) + } +} + +fun String.asIdentitetsnummer(): Identitetsnummer = Identitetsnummer(this) diff --git a/lib/security/src/main/kotlin/no/nav/paw/security/authentication/token/AccessToken.kt b/lib/security/src/main/kotlin/no/nav/paw/security/authentication/token/AccessToken.kt new file mode 100644 index 00000000..113a8dc0 --- /dev/null +++ b/lib/security/src/main/kotlin/no/nav/paw/security/authentication/token/AccessToken.kt @@ -0,0 +1,95 @@ +package no.nav.paw.security.authentication.token + +import no.nav.paw.security.authentication.exception.UgyldigBearerTokenException +import no.nav.paw.security.authentication.model.Identitetsnummer +import no.nav.security.token.support.core.context.TokenValidationContext +import no.nav.security.token.support.core.jwt.JwtToken +import java.util.* + +data class AccessToken( + val jwt: String, + val issuer: Issuer, + val claims: Claims +) { + fun isM2MToken(): Boolean { + return claims.getOrNull(Roles)?.contains("access_as_application") ?: false + } +} + +sealed class Issuer(val name: String) + +data object IdPorten : Issuer("idporten") +data object TokenX : Issuer("tokenx") +data object AzureAd : Issuer("azure") + +class Claims(private val claims: Map, Any>) { + @Suppress("UNCHECKED_CAST") + fun getOrNull(claim: Claim): T? = claims[claim] as T? + + @Suppress("UNCHECKED_CAST") + fun getOrThrow(claim: Claim): T = claims[claim] as T? + ?: throw UgyldigBearerTokenException("Bearer Token mangler påkrevd claim ${claim.name}") + + fun isEmpty(): Boolean = claims.isEmpty() + + fun contains(claim: Claim<*>): Boolean = claims.containsKey(claim) +} + +abstract class Claim( + open val name: String, + open val resolve: (Any) -> A +) + +sealed class SingleClaim( + override val name: String, + override val resolve: (Any) -> A +) : Claim(name, resolve) + +sealed class ListClaim( + override val name: String, + override val resolve: (Any) -> List +) : Claim>(name, resolve) + +data object PID : SingleClaim("pid", { Identitetsnummer(it.toString()) }) +data object OID : SingleClaim("oid", { UUID.fromString(it.toString()) }) +data object Name : SingleClaim("name", { it.toString() }) +data object NavIdent : SingleClaim("NAVident", { it.toString() }) +data object Roles : ListClaim("roles", { value -> (value as List<*>).map { it.toString() } }) + +sealed class ResolveToken(val issuer: Issuer, val claims: List>) + +data object IdPortenToken : ResolveToken(IdPorten, listOf(PID)) +data object TokenXToken : ResolveToken(TokenX, listOf(PID)) +data object AzureAdToken : ResolveToken(AzureAd, listOf(OID, Name, NavIdent, Roles)) + +private val validTokens: List = listOf(IdPortenToken, TokenXToken, AzureAdToken) + +fun TokenValidationContext.resolveTokens(): List { + return validTokens + .mapNotNull { resolveToken -> + getJwtToken(resolveToken.issuer.name)?.let { resolveToken to it } + } + .map { (resolveToken, jwtToken) -> + val claims = jwtToken.resolveClaims(resolveToken) + AccessToken(jwtToken.encodedToken, resolveToken.issuer, claims) + } +} + +private fun JwtToken.resolveClaims(resolveToken: ResolveToken): Claims { + val claims = resolveToken.claims + .mapNotNull { claim -> + when (claim) { + is ListClaim<*> -> { + val value = jwtTokenClaims.getAsList(claim.name) + value?.let { claim to claim.resolve(value) } + } + + else -> { + val value = jwtTokenClaims.getStringClaim(claim.name) + value?.let { claim to claim.resolve(value) } + } + } + } + .toMap() + return Claims(claims) +} diff --git a/lib/security/src/main/kotlin/no/nav/paw/security/authorization/context/AuthorizationContext.kt b/lib/security/src/main/kotlin/no/nav/paw/security/authorization/context/AuthorizationContext.kt new file mode 100644 index 00000000..cd8b1668 --- /dev/null +++ b/lib/security/src/main/kotlin/no/nav/paw/security/authorization/context/AuthorizationContext.kt @@ -0,0 +1,6 @@ +package no.nav.paw.security.authorization.context + +data class AuthorizationContext( + val requestContext: RequestContext, + val securityContext: SecurityContext +) diff --git a/lib/security/src/main/kotlin/no/nav/paw/security/authorization/context/RequestContext.kt b/lib/security/src/main/kotlin/no/nav/paw/security/authorization/context/RequestContext.kt new file mode 100644 index 00000000..2a537822 --- /dev/null +++ b/lib/security/src/main/kotlin/no/nav/paw/security/authorization/context/RequestContext.kt @@ -0,0 +1,48 @@ +package no.nav.paw.security.authorization.context + +import io.ktor.http.HttpHeaders +import io.ktor.server.application.ApplicationCall +import io.ktor.server.application.call +import io.ktor.server.auth.principal +import io.ktor.server.request.ApplicationRequest +import io.ktor.util.pipeline.PipelineContext +import no.nav.security.token.support.v2.TokenValidationContextPrincipal + +sealed class NavHeader(val name: String) + +data object TraceParent : NavHeader("traceparent") +data object NavCallId : NavHeader("Nav-Call-Id") +data object NavConsumerId : NavHeader("Nav-Consumer-Id") +data object NavIdent : NavHeader("Nav-Ident") + +data class RequestHeaders( + val authorization: String?, + val navCallId: String?, + val traceParent: String?, + val navConsumerId: String?, + val navIdent: String?, +) + +data class RequestContext( + val request: ApplicationRequest, + val headers: RequestHeaders, + val principal: TokenValidationContextPrincipal? +) + +fun PipelineContext.resolveRequestContext(): RequestContext { + return RequestContext( + request = call.request, + headers = resolveRequestHeaders(), + principal = call.principal() + ) +} + +fun PipelineContext.resolveRequestHeaders(): RequestHeaders { + return RequestHeaders( + authorization = call.request.headers[HttpHeaders.Authorization], + traceParent = call.request.headers[TraceParent.name], + navCallId = call.request.headers[NavCallId.name], + navConsumerId = call.request.headers[NavConsumerId.name], + navIdent = call.request.headers[NavIdent.name], + ) +} \ No newline at end of file diff --git a/lib/security/src/main/kotlin/no/nav/paw/security/authorization/context/SecurityContext.kt b/lib/security/src/main/kotlin/no/nav/paw/security/authorization/context/SecurityContext.kt new file mode 100644 index 00000000..087b5efd --- /dev/null +++ b/lib/security/src/main/kotlin/no/nav/paw/security/authorization/context/SecurityContext.kt @@ -0,0 +1,53 @@ +package no.nav.paw.security.authorization.context + +import no.nav.paw.security.authentication.exception.BearerTokenManglerException +import no.nav.paw.security.authentication.exception.UgyldigBearerTokenException +import no.nav.paw.security.authentication.model.Bruker +import no.nav.paw.security.authentication.model.M2MToken +import no.nav.paw.security.authentication.model.NavAnsatt +import no.nav.paw.security.authentication.model.Sluttbruker +import no.nav.paw.security.authentication.token.AccessToken +import no.nav.paw.security.authentication.token.AzureAd +import no.nav.paw.security.authentication.token.IdPorten +import no.nav.paw.security.authentication.token.NavIdent +import no.nav.paw.security.authentication.token.OID +import no.nav.paw.security.authentication.token.PID +import no.nav.paw.security.authentication.token.TokenX +import no.nav.paw.security.authentication.token.resolveTokens + +data class SecurityContext( + val bruker: Bruker<*>, + val accessToken: AccessToken +) + +fun RequestContext.resolveSecurityContext(): SecurityContext { + val tokenContext = principal?.context ?: throw BearerTokenManglerException("Bearer Token mangler") + + val accessToken = tokenContext.resolveTokens().firstOrNull() // Kan støtte flere tokens + ?: throw UgyldigBearerTokenException("Ingen gyldige Bearer Tokens funnet") + + if (accessToken.claims.isEmpty()) { + throw UgyldigBearerTokenException("Bearer Token mangler påkrevd innhold") + } + + val bruker = when (accessToken.issuer) { + is IdPorten -> Sluttbruker(accessToken.claims.getOrThrow(PID)) + is TokenX -> Sluttbruker(accessToken.claims.getOrThrow(PID)) + is AzureAd -> { + if (accessToken.isM2MToken()) { + if (headers.navIdent.isNullOrBlank()) { + M2MToken(accessToken.claims.getOrThrow(OID)) + } else { + NavAnsatt(accessToken.claims.getOrThrow(OID), headers.navIdent) + } + } else { + NavAnsatt(accessToken.claims.getOrThrow(OID), accessToken.claims.getOrThrow(NavIdent)) + } + } + } + + return SecurityContext( + bruker = bruker, + accessToken = accessToken + ) +} diff --git a/lib/security/src/main/kotlin/no/nav/paw/security/authorization/interceptor/AuthorizationInterceptor.kt b/lib/security/src/main/kotlin/no/nav/paw/security/authorization/interceptor/AuthorizationInterceptor.kt new file mode 100644 index 00000000..af023103 --- /dev/null +++ b/lib/security/src/main/kotlin/no/nav/paw/security/authorization/interceptor/AuthorizationInterceptor.kt @@ -0,0 +1,25 @@ +package no.nav.paw.security.authorization.interceptor + +import io.ktor.server.application.ApplicationCall +import io.ktor.util.pipeline.PipelineContext +import no.nav.paw.security.authorization.context.AuthorizationContext +import no.nav.paw.security.authorization.context.resolveRequestContext +import no.nav.paw.security.authorization.context.resolveSecurityContext +import no.nav.paw.security.authorization.policy.AccessPolicy +import no.nav.paw.security.authorization.policy.AccessPolicyEvaluator +import org.slf4j.LoggerFactory + +private val logger = LoggerFactory.getLogger("no.nav.paw.logger.security.authorization") + +suspend fun PipelineContext.authorize( + policies: List = emptyList(), + body: suspend PipelineContext.(AuthorizationContext) -> Unit +): PipelineContext { + logger.debug("Kjører autorisasjon") + val requestContext = resolveRequestContext() + val securityContext = requestContext.resolveSecurityContext() + val authorizationContext = AuthorizationContext(requestContext, securityContext) + AccessPolicyEvaluator(policies).checkAccess(authorizationContext) + body(authorizationContext) + return this +} \ No newline at end of file diff --git a/lib/security/src/main/kotlin/no/nav/paw/security/authorization/model/AccessDecision.kt b/lib/security/src/main/kotlin/no/nav/paw/security/authorization/model/AccessDecision.kt new file mode 100644 index 00000000..b45b3bc9 --- /dev/null +++ b/lib/security/src/main/kotlin/no/nav/paw/security/authorization/model/AccessDecision.kt @@ -0,0 +1,7 @@ +package no.nav.paw.security.authorization.model + +enum class AccessDecision { + PERMIT, + DENY, + DEFER +} \ No newline at end of file diff --git a/lib/security/src/main/kotlin/no/nav/paw/security/authorization/model/AccessOperation.kt b/lib/security/src/main/kotlin/no/nav/paw/security/authorization/model/AccessOperation.kt new file mode 100644 index 00000000..f86f50d1 --- /dev/null +++ b/lib/security/src/main/kotlin/no/nav/paw/security/authorization/model/AccessOperation.kt @@ -0,0 +1,3 @@ +package no.nav.paw.security.authorization.model + +enum class AccessOperation { READ, WRITE } \ No newline at end of file diff --git a/lib/security/src/main/kotlin/no/nav/paw/security/authorization/model/AccessResult.kt b/lib/security/src/main/kotlin/no/nav/paw/security/authorization/model/AccessResult.kt new file mode 100644 index 00000000..58774a10 --- /dev/null +++ b/lib/security/src/main/kotlin/no/nav/paw/security/authorization/model/AccessResult.kt @@ -0,0 +1,6 @@ +package no.nav.paw.security.authorization.model + +data class AccessResult( + val decision: AccessDecision, + val details: String +) \ No newline at end of file diff --git a/lib/security/src/main/kotlin/no/nav/paw/security/authorization/policy/AccessPolicy.kt b/lib/security/src/main/kotlin/no/nav/paw/security/authorization/policy/AccessPolicy.kt new file mode 100644 index 00000000..e1b31008 --- /dev/null +++ b/lib/security/src/main/kotlin/no/nav/paw/security/authorization/policy/AccessPolicy.kt @@ -0,0 +1,8 @@ +package no.nav.paw.security.authorization.policy + +import no.nav.paw.security.authorization.context.AuthorizationContext +import no.nav.paw.security.authorization.model.AccessResult + +interface AccessPolicy { + fun hasAccess(context: AuthorizationContext): AccessResult +} diff --git a/lib/security/src/main/kotlin/no/nav/paw/security/authorization/policy/AccessPolicyEvaluator.kt b/lib/security/src/main/kotlin/no/nav/paw/security/authorization/policy/AccessPolicyEvaluator.kt new file mode 100644 index 00000000..17d25e1c --- /dev/null +++ b/lib/security/src/main/kotlin/no/nav/paw/security/authorization/policy/AccessPolicyEvaluator.kt @@ -0,0 +1,21 @@ +package no.nav.paw.security.authorization.policy + +import no.nav.paw.security.authentication.exception.IngenTilgangException +import no.nav.paw.security.authorization.context.AuthorizationContext +import no.nav.paw.security.authorization.model.AccessDecision +import org.slf4j.LoggerFactory + +class AccessPolicyEvaluator(private val policies: List) { + + private val logger = LoggerFactory.getLogger("no.nav.paw.logger.security.authorization") + + fun checkAccess(context: AuthorizationContext) { + logger.debug("Sjekker {} tilgangsregler", policies.size) + val results = policies.map { it.hasAccess(context) } + val deny = results.filter { it.decision == AccessDecision.DENY } + if (deny.isNotEmpty()) { + logger.debug("Evaluering av tilgangsregler resulterte i {} DENY decisions [{}]", deny.size, deny) + throw IngenTilgangException("Ingen tilgang") + } + } +} \ No newline at end of file diff --git a/lib/security/src/test/kotlin/no/nav/paw/security/authorization/RouteAuthorizationTest.kt b/lib/security/src/test/kotlin/no/nav/paw/security/authorization/RouteAuthorizationTest.kt new file mode 100644 index 00000000..8930c7ae --- /dev/null +++ b/lib/security/src/test/kotlin/no/nav/paw/security/authorization/RouteAuthorizationTest.kt @@ -0,0 +1,61 @@ +package no.nav.paw.security.authorization + +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import io.ktor.client.request.bearerAuth +import io.ktor.client.request.get +import io.ktor.http.HttpStatusCode +import io.ktor.server.testing.testApplication +import no.nav.paw.security.authorization.policy.TestDenyPolicy +import no.nav.paw.security.authorization.policy.TestPermitPolicy +import no.nav.paw.security.test.TestApplicationContext +import no.nav.paw.security.test.issueTokenXToken + +class RouteAuthorizationTest : FreeSpec({ + + with(TestApplicationContext()) { + + beforeSpec { + mockOAuth2Server.start() + } + + afterSpec { + mockOAuth2Server.shutdown() + } + + "Skal få 401 Unauthorized uten Bearer Token header" { + testApplication { + application { + configureApplication() + } + + val testClient = configureTestClient() + + val response = testClient.get("/api/dummy") + + response.status shouldBe HttpStatusCode.Unauthorized + } + } + + "Skal få 403 Forbidden ved en DENY policy" { + testApplication { + application { + configureApplication( + listOf( + TestPermitPolicy(), + TestDenyPolicy() + ) + ) + } + + val testClient = configureTestClient() + + val response = testClient.get("/api/dummy") { + bearerAuth(mockOAuth2Server.issueTokenXToken(pid = "01017012345")) + } + + response.status shouldBe HttpStatusCode.Forbidden + } + } + } +}) \ No newline at end of file diff --git a/lib/security/src/test/kotlin/no/nav/paw/security/authorization/policy/TestPolicies.kt b/lib/security/src/test/kotlin/no/nav/paw/security/authorization/policy/TestPolicies.kt new file mode 100644 index 00000000..2b232f65 --- /dev/null +++ b/lib/security/src/test/kotlin/no/nav/paw/security/authorization/policy/TestPolicies.kt @@ -0,0 +1,19 @@ +package no.nav.paw.security.authorization.policy + +import no.nav.paw.security.authorization.context.AuthorizationContext +import no.nav.paw.security.authorization.model.AccessDecision +import no.nav.paw.security.authorization.model.AccessResult + +class TestPermitPolicy : AccessPolicy { + + override fun hasAccess(context: AuthorizationContext): AccessResult { + return AccessResult(AccessDecision.PERMIT, "FULL STEAM AHEAD!") + } +} + +class TestDenyPolicy : AccessPolicy { + + override fun hasAccess(context: AuthorizationContext): AccessResult { + return AccessResult(AccessDecision.DENY, "YOU SHALL NOT PASS!") + } +} \ No newline at end of file diff --git a/lib/security/src/test/kotlin/no/nav/paw/security/test/TestApplicationContext.kt b/lib/security/src/test/kotlin/no/nav/paw/security/test/TestApplicationContext.kt new file mode 100644 index 00000000..ccc66401 --- /dev/null +++ b/lib/security/src/test/kotlin/no/nav/paw/security/test/TestApplicationContext.kt @@ -0,0 +1,120 @@ +package no.nav.paw.security.test + +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import io.ktor.client.HttpClient +import io.ktor.serialization.jackson.jackson +import io.ktor.server.application.Application +import io.ktor.server.application.call +import io.ktor.server.application.install +import io.ktor.server.auth.authenticate +import io.ktor.server.auth.authentication +import io.ktor.server.plugins.statuspages.StatusPages +import io.ktor.server.response.respond +import io.ktor.server.routing.IgnoreTrailingSlash +import io.ktor.server.routing.get +import io.ktor.server.routing.post +import io.ktor.server.routing.routing +import io.ktor.server.testing.ApplicationTestBuilder +import no.nav.paw.error.handler.handleException +import no.nav.paw.security.authorization.interceptor.authorize +import no.nav.paw.security.authorization.policy.AccessPolicy +import no.nav.security.mock.oauth2.MockOAuth2Server +import no.nav.security.token.support.v2.IssuerConfig +import no.nav.security.token.support.v2.RequiredClaims +import no.nav.security.token.support.v2.TokenSupportConfig +import no.nav.security.token.support.v2.tokenValidationSupport +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation + +class TestApplicationContext { + + val mockOAuth2Server = MockOAuth2Server() + + fun ApplicationTestBuilder.configureTestClient(): HttpClient { + return createClient { + install(ClientContentNegotiation) { + jackson { + disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + registerModule(JavaTimeModule()) + registerKotlinModule() + } + } + } + } + + fun Application.configureApplication(policies: List = emptyList()) { + configureSerialization() + configureRequestHandling() + configureAuthentication() + configureRouting(policies) + } + + fun Application.configureRouting(policies: List = emptyList()) { + routing { + authenticate("tokenx") { + get("/api/dummy") { + authorize(policies) { + call.respond(TestResponse("All Quiet on the Western Front")) + } + } + + post("/api/dummy") { + authorize(policies) { + call.respond(TestResponse("All Quiet on the Western Front")) + } + } + } + } + } + + fun Application.configureSerialization() { + install(ServerContentNegotiation) { + jackson { + disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + registerModule(JavaTimeModule()) + registerKotlinModule() + } + } + } + + fun Application.configureRequestHandling() { + install(IgnoreTrailingSlash) + install(StatusPages) { + exception { call, cause -> + call.handleException(cause) + } + } + } + + + fun Application.configureAuthentication() { + val authProviders = mockOAuth2Server.getAuthProviders() + + authentication { + authProviders.forEach { authProvider -> + tokenValidationSupport( + name = authProvider.name, + config = TokenSupportConfig( + IssuerConfig( + name = authProvider.name, + discoveryUrl = authProvider.discoveryUrl, + acceptedAudience = authProvider.acceptedAudience + ) + ), + requiredClaims = RequiredClaims( + authProvider.name, + authProvider.claimMap, + authProvider.combineWithOr + ) + ) + } + } + } +} + +data class TestResponse(val message: String) diff --git a/lib/security/src/test/kotlin/no/nav/paw/security/test/TokenTestUtils.kt b/lib/security/src/test/kotlin/no/nav/paw/security/test/TokenTestUtils.kt new file mode 100644 index 00000000..25456410 --- /dev/null +++ b/lib/security/src/test/kotlin/no/nav/paw/security/test/TokenTestUtils.kt @@ -0,0 +1,80 @@ +package no.nav.paw.security.test + +import no.nav.security.mock.oauth2.MockOAuth2Server +import java.util.* + +fun MockOAuth2Server.issueIDPortenToken( + acr: String = "idporten-loa-high", + pid: String = "01017012345" +): String { + return issueToken( + claims = mapOf( + "acr" to acr, + "pid" to pid + ) + ).serialize() +} + +fun MockOAuth2Server.issueTokenXToken( + acr: String = "idporten-loa-high", + pid: String = "01017012345" +): String { + return issueToken( + claims = mapOf( + "acr" to acr, + "pid" to pid + ) + ).serialize() +} + +fun MockOAuth2Server.issueAzureADToken( + oid: UUID = UUID.randomUUID(), + name: String = "Kari Nordmann", + navIdent: String = "NAV12345" +): String { + return issueToken( + claims = mapOf( + "oid" to oid.toString(), + "name" to name, + "NAVident" to navIdent + ) + ).serialize() +} + +fun MockOAuth2Server.issueAzureM2MToken( + oid: UUID = UUID.randomUUID(), + roles: List = listOf("access_as_application"), +): String { + return issueToken( + claims = mapOf( + "oid" to oid.toString(), + "roles" to roles + ) + ).serialize() +} + +fun MockOAuth2Server.getAuthProviders(): List { + val issuerId = "default" + val wellKnownUrl = wellKnownUrl(issuerId).toString() + return listOf( + "idporten" to arrayOf("acr=idporten-loa-high"), + "tokenx" to arrayOf("acr=idporten-loa-high"), + "azure" to arrayOf("NAVident") + ).map { + AuthProvider( + name = it.first, + discoveryUrl = wellKnownUrl, + acceptedAudience = listOf(issuerId), + claimMap = it.second, + combineWithOr = true + ) + } +} + +data class AuthProvider( + val name: String, + val discoveryUrl: String, + val acceptedAudience: List, + val claimMap: Array, + val combineWithOr: Boolean +) diff --git a/settings.gradle.kts b/settings.gradle.kts index ae093c26..f3ebc404 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,6 +13,7 @@ rootProject.name = "paw-arbeidssoekerregisteret-monorepo-intern" include( "lib:hoplite-config", "lib:error-handling", + "lib:security", "lib:kafka", "lib:kafka-streams", "lib:kafka-key-generator-client",