Skip to content

Commit

Permalink
Slår sammen alle auth til en.
Browse files Browse the repository at this point in the history
Co-authored-by: Robin Tordly <[email protected]>
Co-authored-by: Sturle Helland <[email protected]>
Co-authored-by: Vetle Hollund <[email protected]>
  • Loading branch information
4 people committed Feb 8, 2024
1 parent 3b0991d commit d827f79
Show file tree
Hide file tree
Showing 12 changed files with 317 additions and 0 deletions.
16 changes: 16 additions & 0 deletions ktor-auth/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
val ktorVersion = "2.3.8"

dependencies {
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-client-cio:$ktorVersion")
implementation("io.ktor:ktor-client-auth:$ktorVersion")

implementation("io.ktor:ktor-serialization-jackson:$ktorVersion")

implementation("com.fasterxml.jackson.core:jackson-databind:2.16.1")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.1")
implementation("com.nimbusds:nimbus-jose-jwt:9.37.3")

runtimeOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3")
runtimeOnly("org.slf4j:slf4j-api:2.0.12")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package no.nav.aap.ktor.client.auth.azure

import io.ktor.client.*
import no.nav.aap.ktor.client.auth.util.TokenClient
import no.nav.aap.ktor.client.auth.util.asUrlPart
import no.nav.aap.ktor.client.auth.util.defaultHttpClient

class AzureAdTokenProvider(
private val config: AzureConfig = AzureConfig(),
client: HttpClient = defaultHttpClient,
) {
private val tokenClient = TokenClient(client)

suspend fun getUsernamePasswordToken(scope: String, username: String, password: String) =
tokenClient.getAccessToken(config.tokenEndpoint, username) {
"""
client_id=${config.clientId}&
client_secret=${config.clientSecret}&
scope=$scope&
username=$username&
password=$password&
grant_type=password
""".asUrlPart()
}

suspend fun getClientCredentialToken(scope: String) =
tokenClient.getAccessToken(config.tokenEndpoint, scope) {
"""
client_id=${config.clientId}&
client_secret=${config.clientSecret}&
scope=$scope&
grant_type=client_credentials
""".asUrlPart()
}

suspend fun getOnBehalfOfToken(scope: String, accessToken: String) =
tokenClient.getAccessToken(config.tokenEndpoint, scope) {
"""
client_id=${config.clientId}&
client_secret=${config.clientSecret}&
assertion=$accessToken&
scope=$scope&
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&
requested_token_use=on_behalf_of
""".asUrlPart()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package no.nav.aap.ktor.client.auth.azure

import no.nav.aap.ktor.client.auth.util.getEnvVar

data class AzureConfig(
val tokenEndpoint: String = getEnvVar("AZURE_OPENID_CONFIG_TOKEN_ENDPOINT"),
val clientId: String = getEnvVar("AZURE_APP_CLIENT_ID"),
val clientSecret: String = getEnvVar("AZURE_APP_CLIENT_SECRET")
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package no.nav.aap.ktor.client.auth.maskinporten

import no.nav.aap.ktor.client.auth.util.JwtConfig
import java.time.Instant
import java.util.*

data class MaskinportenConfig(
val tokenEndpointUrl: String,
val clientId: String,
val privateKey: String,
val scope: String,
val resource: String,
val issuer: String
)

internal fun MaskinportenConfig.toJwtConfig() = JwtConfig(
privateKey = privateKey,
claimset = mapOf(
"scope" to scope,
"resource" to resource,
"aud" to issuer,
"iss" to clientId,
"iat" to Date(),
"exp" to Date.from(Instant.now().plusSeconds(120))
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package no.nav.aap.ktor.client.auth.maskinporten

import com.nimbusds.jwt.SignedJWT
import io.ktor.client.*
import no.nav.aap.ktor.client.auth.util.TokenClient
import no.nav.aap.ktor.client.auth.util.JwtGrantFactory
import no.nav.aap.ktor.client.auth.util.asUrlPart
import no.nav.aap.ktor.client.auth.util.defaultHttpClient

class HttpClientMaskinportenTokenProvider(
private val config: MaskinportenConfig,
client: HttpClient = defaultHttpClient
) {
private val grants = JwtGrantFactory(config.toJwtConfig())
private val tokenClient = TokenClient(client)

suspend fun getToken(): String {
val token = tokenClient.getAccessToken(config.tokenEndpointUrl, config.scope) {
"""
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&
assertion=${grants.jwt}
""".asUrlPart()
}
return token.let(SignedJWT::parse).parsedString
}
}
28 changes: 28 additions & 0 deletions ktor-auth/main/no/nav/aap/ktor/client/auth/tokenx/TokenXConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package no.nav.aap.ktor.client.auth.tokenx

import no.nav.aap.ktor.client.auth.util.JwtConfig
import no.nav.aap.ktor.client.auth.util.getEnvVar
import java.net.URL
import java.time.Instant
import java.util.*

data class TokenXConfig(
val clientId: String = getEnvVar("TOKEN_X_CLIENT_ID"),
val privateKey: String = getEnvVar("TOKEN_X_PRIVATE_JWK"),
val tokenEndpoint: String = getEnvVar("TOKEN_X_TOKEN_ENDPOINT"),
val jwksUrl: String = getEnvVar("TOKEN_X_JWKS_URI"),
val issuer: String = getEnvVar("TOKEN_X_ISSUER"),
)

internal fun TokenXConfig.toJwtConfig() = JwtConfig(
privateKey = privateKey,
claimset = mapOf(
"aud" to tokenEndpoint,
"iss" to clientId,
"iat" to Date(),
"exp" to Date.from(Instant.now().plusSeconds(120)),
"sub" to clientId,
"jti" to UUID.randomUUID().toString(),
"nbf" to Date()
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package no.nav.aap.ktor.client.auth.tokenx

import io.ktor.client.*
import no.nav.aap.ktor.client.auth.util.TokenClient
import no.nav.aap.ktor.client.auth.util.JwtGrantFactory
import no.nav.aap.ktor.client.auth.util.asUrlPart
import no.nav.aap.ktor.client.auth.util.defaultHttpClient

class TokenXTokenProvider(
private val config: TokenXConfig = TokenXConfig(),
client: HttpClient = defaultHttpClient,
) {
private val grants = JwtGrantFactory(config.toJwtConfig())
private val tokenClient = TokenClient(client)

suspend fun getOnBehalfOfToken(audience: String, token: String) =
tokenClient.getAccessToken(config.tokenEndpoint, token + audience) {
"""
grant_type=urn:ietf:params:oauth:grant-type:token-exchange&
client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&
subject_token_type=urn:ietf:params:oauth:token-type:jwt&
client_assertion=${grants.jwt}&
audience=$audience&
subject_token=$token
""".asUrlPart()
}
}
6 changes: 6 additions & 0 deletions ktor-auth/main/no/nav/aap/ktor/client/auth/util/Extensions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package no.nav.aap.ktor.client.auth.util

internal fun String.asUrlPart() =
this.trimIndent().replace("\n", "")

internal fun getEnvVar(envar: String) = System.getenv(envar) ?: error("missing envvar $envar")
37 changes: 37 additions & 0 deletions ktor-auth/main/no/nav/aap/ktor/client/auth/util/JwtGrantFactory.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package no.nav.aap.ktor.client.auth.util

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

internal data class JwtConfig(
val privateKey: String,
val claimset: Map<String, Any>
)

internal class JwtGrantFactory(private val config: JwtConfig) {
internal val jwt: String get() = signedJwt.serialize()

private val privateKey = RSAKey.parse(config.privateKey)

private val signedJwt get() = SignedJWT(jwsHeader, jwtClaimSet).apply {
sign(RSASSASigner(privateKey))
}

private val jwsHeader
get() = JWSHeader.Builder(JWSAlgorithm.RS256)
.keyID(privateKey.keyID)
.type(JOSEObjectType.JWT)
.build()

private val jwtClaimSet: JWTClaimsSet
get() = JWTClaimsSet.Builder().apply {
config.claimset.forEach { kv ->
claim(kv.key, kv.value)
}
}.build()
}
50 changes: 50 additions & 0 deletions ktor-auth/main/no/nav/aap/ktor/client/auth/util/Token.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package no.nav.aap.ktor.client.auth.util

import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.time.Instant

internal data class Token(val expires_in: Long, val access_token: String) {
private val expiry: Instant = Instant.now().plusSeconds(expires_in - LEEWAY_SECONDS)

internal fun expired() = Instant.now().isAfter(expiry)

private companion object {
const val LEEWAY_SECONDS = 60
}

override fun toString(): String {
return "($expires_in, $access_token)"
}
}

internal class TokenCache {
private val tokens: HashMap<String, Token> = hashMapOf()
private val mutex = Mutex()

internal suspend fun add(key: String, token: Token) {
mutex.withLock {
tokens[key] = token
}
}

internal suspend fun get(key: String): Token? {
mutex.withLock {
tokens[key]
}?.let {
if (it.expired()) {
rm(key)
}
}

return mutex.withLock {
tokens[key]
}
}

private suspend fun rm(key: String) {
mutex.withLock {
tokens.remove(key)
}
}
}
44 changes: 44 additions & 0 deletions ktor-auth/main/no/nav/aap/ktor/client/auth/util/TokenClient.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package no.nav.aap.ktor.client.auth.util

import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.jackson.*
import org.slf4j.LoggerFactory

internal val defaultHttpClient = HttpClient(CIO) {
install(ContentNegotiation) {
jackson {
registerModule(JavaTimeModule())
disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
}
}
}

internal class TokenClient(private val client: HttpClient) {
private val cache = TokenCache()
private val secureLog = LoggerFactory.getLogger("secureLog")

suspend fun getAccessToken(tokenEndpoint: String, cacheKey: String, body: () -> String): String {
val token = cache.get(cacheKey)
?: client.post(tokenEndpoint) {
accept(ContentType.Application.Json)
contentType(ContentType.Application.FormUrlEncoded)
setBody(body())
}.also {
if (!it.status.isSuccess()) {
secureLog.warn("Feilet token-kall {}: {}", it.status.value, it.bodyAsText())
}
}.body<Token>().also {
cache.add(cacheKey, it)
}

return token.access_token
}
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ include(
"kafka-interfaces",
"kafka-avroserde",
"kafka-test-2",
"ktor-auth",
"ktor-auth-azuread",
"ktor-auth-maskinporten",
"ktor-auth-tokenx",
Expand Down

0 comments on commit d827f79

Please sign in to comment.