Skip to content

Commit

Permalink
lagt til tokenxProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
VHollund committed Dec 19, 2023
1 parent f816679 commit 82bca65
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 1 deletion.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ subprojects {

tasks {
withType<KotlinCompile> {
kotlinOptions.jvmTarget = "20"
kotlinOptions.jvmTarget = "21"
}
withType<Jar> {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
Expand Down
15 changes: 15 additions & 0 deletions ktor-auth-tokenx/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
val ktorVersion = "2.3.6"

dependencies {
// implementation(project(":cache"))

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.15.3")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.3")
implementation("com.nimbusds:nimbus-jose-jwt:9.31")
}
43 changes: 43 additions & 0 deletions ktor-auth-tokenx/main/no/nav/aap/ktor/client/Token.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package no.nav.aap.ktor.client

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
}
}

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

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

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

private suspend fun rm(key: K) {
mutex.withLock {
tokens.remove(key)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package no.nav.aap.ktor.client

import java.net.URL

data class TokenXProviderConfig(
val clientId: String,
val privateKey: String,
val tokenEndpoint: String,
val jwksUrl: URL,
val issuer: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package no.nav.aap.ktor.client

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 io.ktor.client.call.body
import io.ktor.client.request.accept
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.contentType
import io.ktor.http.isSuccess
import io.ktor.serialization.jackson.jackson
import org.slf4j.LoggerFactory
import java.time.Instant
import java.util.*

private val secureLog = LoggerFactory.getLogger("secureLog")
class TokenXTokenProvider(
private val config: TokenXProviderConfig,
private val audience: String,
private val client: io.ktor.client.HttpClient = defaultHttpClient,
) {

private val jwtFactory = JwtGrantFactory(config)
suspend fun getOnBehalfOfToken(tokenx_token: String) = getAccessToken(tokenx_token) {
"""
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=${jwtFactory.jwt}
&audience=$audience
&subject_token=$tokenx_token
""".trimIndent()
.replace("\n", "")
}

private val cache = TokenCache<String>()

private suspend fun getAccessToken(cacheKey: String, body: () -> String): String {
val token = cache.get(cacheKey)
?: client.post(config.tokenEndpoint) {
accept(io.ktor.http.ContentType.Application.Json)
contentType(io.ktor.http.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
}

private companion object {
private val defaultHttpClient = io.ktor.client.HttpClient(io.ktor.client.engine.cio.CIO) {
install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) {
jackson {
registerModule(com.fasterxml.jackson.datatype.jsr310.JavaTimeModule())
disable(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
}
}
}
}
}

internal class JwtGrantFactory(private val config: TokenXProviderConfig) {
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 {
subject(config.clientId)
issuer(config.clientId)
audience(config.tokenEndpoint)
jwtID(UUID.randomUUID().toString())
notBeforeTime(Date())
issueTime(Date())
expirationTime(Date.from(Instant.now().plusSeconds(120)))
}.build()
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ include(
"kafka-test-2",
"ktor-auth-azuread",
"ktor-auth-maskinporten",
"ktor-auth-tokenx",
"ktor-utils",
)

0 comments on commit 82bca65

Please sign in to comment.