Skip to content

Commit

Permalink
Lagt til felles sikkerhetsmodul
Browse files Browse the repository at this point in the history
  • Loading branch information
naviktthomas committed Oct 30, 2024
1 parent 094d38d commit 33d6f7a
Show file tree
Hide file tree
Showing 23 changed files with 625 additions and 0 deletions.
3 changes: 3 additions & 0 deletions lib/security/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Sikkerhetsmodul

Felles sikkerhetsmodul for autentisering og autorisering.
24 changes: 24 additions & 0 deletions lib/security/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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<Test>().configureEach {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package no.nav.paw.security.authentication.model

import java.util.*

sealed class Bruker<ID : Any>(
open val ident: ID
)

data class Sluttbruker(override val ident: Identitetsnummer) : Bruker<Identitetsnummer>(ident)
data class NavAnsatt(val oid: UUID, override val ident: String) : Bruker<String>(ident)
data class M2MToken(val oid: UUID) : Bruker<String>("N/A")
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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<Claim<*>, Any>) {
@Suppress("UNCHECKED_CAST")
fun <T : Any> getOrNull(claim: Claim<T>): T? = claims[claim] as T?

@Suppress("UNCHECKED_CAST")
fun <T : Any> getOrThrow(claim: Claim<T>): 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<A : Any>(
open val name: String,
open val resolve: (Any) -> A
)

sealed class SingleClaim<A : Any>(
override val name: String,
override val resolve: (Any) -> A
) : Claim<A>(name, resolve)

sealed class ListClaim<A : Any>(
override val name: String,
override val resolve: (Any) -> List<A>
) : Claim<List<A>>(name, resolve)

data object PID : SingleClaim<Identitetsnummer>("pid", { Identitetsnummer(it.toString()) })
data object OID : SingleClaim<UUID>("oid", { UUID.fromString(it.toString()) })
data object Name : SingleClaim<String>("name", { it.toString() })
data object NavIdent : SingleClaim<String>("NAVident", { it.toString() })
data object Roles : ListClaim<String>("roles", { value -> (value as List<*>).map { it.toString() } })

sealed class ResolveToken(val issuer: Issuer, val claims: List<Claim<*>>)

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<ResolveToken> = listOf(IdPortenToken, TokenXToken, AzureAdToken)

fun TokenValidationContext.resolveTokens(): List<AccessToken> {
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)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package no.nav.paw.security.authorization.context

data class AuthorizationContext(
val requestContext: RequestContext,
val securityContext: SecurityContext
)
Original file line number Diff line number Diff line change
@@ -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<Unit, ApplicationCall>.resolveRequestContext(): RequestContext {
return RequestContext(
request = call.request,
headers = resolveRequestHeaders(),
principal = call.principal<TokenValidationContextPrincipal>()
)
}

fun PipelineContext<Unit, ApplicationCall>.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],
)
}
Original file line number Diff line number Diff line change
@@ -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
)
}
Original file line number Diff line number Diff line change
@@ -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<Unit, ApplicationCall>.authorize(
policies: List<AccessPolicy> = emptyList(),
body: suspend PipelineContext<Unit, ApplicationCall>.(AuthorizationContext) -> Unit
): PipelineContext<Unit, ApplicationCall> {
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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package no.nav.paw.security.authorization.model

enum class AccessDecision {
PERMIT,
DENY,
DEFER
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package no.nav.paw.security.authorization.model

enum class AccessOperation { READ, WRITE }
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package no.nav.paw.security.authorization.model

data class AccessResult(
val decision: AccessDecision,
val details: String
)
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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<AccessPolicy>) {

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")
}
}
}
Loading

0 comments on commit 33d6f7a

Please sign in to comment.