From 561c5ba6de2dcd8c23efce9abbc21142c58ef761 Mon Sep 17 00:00:00 2001 From: Mehdi Zare <32900443+mettok@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:35:39 +0100 Subject: [PATCH] systembruker (#6) Systembruker konfigurasjon --- build.gradle.kts | 2 +- src/main/kotlin/MaskinportenClientConfig.kt | 147 ++---------------- .../kotlin/MaskinportenClientConfigJwk.kt | 59 +++++++ .../kotlin/MaskinportenClientConfigPkey.kt | 85 ++++++++++ src/test/kotlin/MaskinportenClientTest.kt | 5 +- 5 files changed, 162 insertions(+), 136 deletions(-) create mode 100644 src/main/kotlin/MaskinportenClientConfigJwk.kt create mode 100644 src/main/kotlin/MaskinportenClientConfigPkey.kt diff --git a/build.gradle.kts b/build.gradle.kts index 45bf6bb..801f315 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { id("maven-publish") } group = "no.nav.helsearbeidsgiver" -version = "0.2.0.9" +version = "0.2.1" kotlin { compilerOptions { diff --git a/src/main/kotlin/MaskinportenClientConfig.kt b/src/main/kotlin/MaskinportenClientConfig.kt index 3053c2e..9409b12 100644 --- a/src/main/kotlin/MaskinportenClientConfig.kt +++ b/src/main/kotlin/MaskinportenClientConfig.kt @@ -1,145 +1,28 @@ package no.nav.helsearbeidsgiver.maskinporten -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 java.security.KeyFactory -import java.security.PrivateKey -import java.security.spec.PKCS8EncodedKeySpec import java.time.Instant -import java.util.Base64 import java.util.Date -import java.util.UUID interface MaskinportenClientConfig { val scope: String val clientId: String val issuer: String val endpoint: String + val additionalClaims: Map? fun getJwtAssertion(): String + fun currentTime(): Date = Date.from(Instant.now()) } -/** - * MaskinportenClientConfigPkey er en implementasjon av MaskinportenClientConfig med privatekey som autentiseringsmetode for maskinporten - * - * @param kid Det er id-en til nøkkelen key-id (kid). - * @param privateKey Det er privatekey som skal brukes til å signere JWT tokenet - * @param clientId Din egen client_id. Sendes videre til Maskinporten som en issuer claim. - * @param issuer Issuer-identifikatoren til Maskinporten. 'https://maskinporten.no/' i produksjon. Sendes videre til Maskinporten som en audience claim. - * @param consumerOrgNr Det er organisasjonsnummeret til virksomheten som skal bruke maskinporten på vegne av - * @param scope Space-separert liste over scopes som klienten forespør. - * @param endpoint Det er endepunktet til maskinporten - * - */ -class MaskinportenClientConfigPkey( - val kid: String, - val privateKey: String, - override val clientId: String, - override val issuer: String, - val consumerOrgNr: String, - override val scope: String, - override val endpoint: String -) : MaskinportenClientConfig { - - private fun loadPrivateKey(key: String): PrivateKey { - val keyText = - key - .replace("-----BEGIN PRIVATE KEY-----", "") - .replace("-----END PRIVATE KEY-----", "") - .replace("\n", "") - .replace("\\s".toRegex(), "") - - val encoded = Base64.getDecoder().decode(keyText) - return KeyFactory - .getInstance("RSA") - .generatePrivate(PKCS8EncodedKeySpec(encoded)) - } - - override fun getJwtAssertion(): String { - val currentTimestamp = System.currentTimeMillis() / 1000 - - val header = - JWSHeader - .Builder(JWSAlgorithm.RS256) - .keyID(kid) - .type(JOSEObjectType.JWT) - .build() - - val claims = - JWTClaimsSet - .Builder() - .issuer(clientId) - .audience(issuer) - .issueTime(Date(currentTimestamp * 1000)) - .expirationTime(Date((currentTimestamp + 60) * 1000)) - .claim("scope", scope) - .claim("consumer_org", consumerOrgNr) - .jwtID(UUID.randomUUID().toString()) - .build() - - val signer = RSASSASigner(loadPrivateKey(privateKey)) - val signedJWT = SignedJWT(header, claims) - signedJWT.sign(signer) - - return signedJWT.serialize() - } -} - -/** - * MaskinportenSimpleAssertion er en implementasjon av MaskinportenClientConfig med assertion som autentiseringsmetode - * for maskinporten Denne brukes for å authentisere mot maskinporten for eksample for Altinn - * - * @param scope Space-separert liste over scopes som klienten forespør. - * @param clientId issuer - Din egen client_id. - * @param issuer Audience - issuer-identifikatoren til Maskinporten. Verdi for aktuelt miljø finner du på .well-known-endpunkt. - * @param endpoint Endepunktet til maskinporten - * @param clientJwk JWK som skal brukes til å signere JWT tokenet - * - */ -class MaskinportenClientConfigSimpleAssertion( - override val scope: String, - override val clientId: String, - override val issuer: String, - override val endpoint: String, - val clientJwk: String -) : MaskinportenClientConfig { - - private val rsaKey: RSAKey by lazy { - try { - RSAKey.parse(clientJwk) - } catch (e: Exception) { - throw IllegalArgumentException("Invalid JWK format", e) - } - } - private val signer: RSASSASigner by lazy { - RSASSASigner(rsaKey.toPrivateKey()) - } - private val header: JWSHeader by lazy { - JWSHeader.Builder(JWSAlgorithm.RS256) - .keyID(rsaKey.keyID) - .type(JOSEObjectType.JWT) - .build() - } - - private fun currentTime(): Date = Date.from(Instant.now()) - - private fun claims(): JWTClaimsSet { - val now = currentTime() - return JWTClaimsSet.Builder() - .issuer(clientId) - .audience(issuer) - .issueTime(now) - .claim("scope", scope) - .expirationTime(Date.from(now.toInstant().plusSeconds(60))) - .jwtID(UUID.randomUUID().toString()) - .build() - } - - override fun getJwtAssertion(): String { - return SignedJWT(header, claims()).apply { sign(signer) }.serialize() - } -} +fun getConsumerOrgClaim(orgnr: String) = mapOf("consumer_orgno" to orgnr) + +fun getSystembrukerClaim(orgNr: String) = mapOf( + "authorization_details" to listOf( + mapOf( + "type" to "urn:altinn:systemuser", + "systemuser_org" to mapOf( + "authority" to "iso6523-actorid-upis", + "ID" to "0192:$orgNr" + ) + ) + ) +) diff --git a/src/main/kotlin/MaskinportenClientConfigJwk.kt b/src/main/kotlin/MaskinportenClientConfigJwk.kt new file mode 100644 index 0000000..546bcd7 --- /dev/null +++ b/src/main/kotlin/MaskinportenClientConfigJwk.kt @@ -0,0 +1,59 @@ +package no.nav.helsearbeidsgiver.maskinporten + +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 java.util.Date +import java.util.UUID + +class MaskinportenClientConfigJwk( + override val scope: String, + override val clientId: String, + override val issuer: String, + override val endpoint: String, + val clientJwk: String, + override val additionalClaims: Map? = null +) : MaskinportenClientConfig { + + private val rsaKey: RSAKey by lazy { + try { + RSAKey.parse(clientJwk) + } catch (e: Exception) { + throw IllegalArgumentException("Invalid JWK format", e) + } + } + private val signer: RSASSASigner by lazy { + RSASSASigner(rsaKey.toPrivateKey()) + } + private val header: JWSHeader by lazy { + JWSHeader.Builder(JWSAlgorithm.RS256) + .keyID(rsaKey.keyID) + .type(JOSEObjectType.JWT) + .build() + } + + private fun claims(): JWTClaimsSet { + val now = currentTime() + val builder = JWTClaimsSet.Builder() + .issuer(clientId) + .audience(issuer) + .issueTime(now) + .claim("scope", scope) + .expirationTime(Date.from(now.toInstant().plusSeconds(60))) + .jwtID(UUID.randomUUID().toString()) + + additionalClaims?.forEach { (key, value) -> + builder.claim(key, value) + } + + return builder.build() + } + + override fun getJwtAssertion(): String { + return SignedJWT(header, claims()).apply { sign(signer) }.serialize() + } +} diff --git a/src/main/kotlin/MaskinportenClientConfigPkey.kt b/src/main/kotlin/MaskinportenClientConfigPkey.kt new file mode 100644 index 0000000..c54052e --- /dev/null +++ b/src/main/kotlin/MaskinportenClientConfigPkey.kt @@ -0,0 +1,85 @@ +package no.nav.helsearbeidsgiver.maskinporten + +import com.nimbusds.jose.JOSEObjectType +import com.nimbusds.jose.JWSAlgorithm +import com.nimbusds.jose.JWSHeader +import com.nimbusds.jose.crypto.RSASSASigner +import com.nimbusds.jwt.JWTClaimsSet +import com.nimbusds.jwt.SignedJWT +import java.security.KeyFactory +import java.security.PrivateKey +import java.security.spec.PKCS8EncodedKeySpec +import java.util.Base64 +import java.util.Date +import java.util.UUID + +/** + * MaskinportenClientConfigPkey er en implementasjon av MaskinportenClientConfig med privatekey som autentiseringsmetode for maskinporten + * + * @param kid Det er id-en til Nøkkelen key-id (kid) + * @param privateKey Det er privatekey som skal brukes til å signere JWT tokenet + * @param clientId issuer - Din egen client_id. + * @param issuer Audience - issuer-identifikatoren til Maskinporten. Verdi for aktuelt miljø finner du på .well-known-endpunkt. + * @param consumerOrgNr Det er organisasjonsnummeret til virksomheten som skal bruke maskinporten på vegne av + * @param scope Space-separert liste over scopes som klienten forespør. + * @param endpoint Det er endepunktet til maskinporten + * + */ +class MaskinportenClientConfigPkey( + val kid: String, + val privateKey: String, + override val clientId: String, + override val issuer: String, + override val scope: String, + override val endpoint: String, + override val additionalClaims: Map? = null +) : MaskinportenClientConfig { + + val currentTimestamp = System.currentTimeMillis() / 1000 + override fun getJwtAssertion(): String { + val header = + JWSHeader + .Builder(JWSAlgorithm.RS256) + .keyID(kid) + .type(JOSEObjectType.JWT) + .build() + + val signer = RSASSASigner(loadPrivateKey(privateKey)) + val signedJWT = SignedJWT(header, claims()) + signedJWT.sign(signer) + + return signedJWT.serialize() + } + + private fun claims(): JWTClaimsSet { + val now = currentTime() + val builder = JWTClaimsSet + .Builder() + .issuer(clientId) + .audience(issuer) + .issueTime(now) + .expirationTime(Date.from(now.toInstant().plusSeconds(60))) + .claim("scope", scope) + .jwtID(UUID.randomUUID().toString()) + + additionalClaims?.forEach { (key, value) -> + builder.claim(key, value) + } + + return builder.build() + } +} + +private fun loadPrivateKey(key: String): PrivateKey { + val keyText = + key + .replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replace("\n", "") + .replace("\\s".toRegex(), "") + + val encoded = Base64.getDecoder().decode(keyText) + return KeyFactory + .getInstance("RSA") + .generatePrivate(PKCS8EncodedKeySpec(encoded)) +} diff --git a/src/test/kotlin/MaskinportenClientTest.kt b/src/test/kotlin/MaskinportenClientTest.kt index f703f30..51c4014 100644 --- a/src/test/kotlin/MaskinportenClientTest.kt +++ b/src/test/kotlin/MaskinportenClientTest.kt @@ -11,8 +11,8 @@ import io.ktor.serialization.kotlinx.json.json import io.mockk.every import kotlinx.coroutines.runBlocking import no.nav.helsearbeidsgiver.maskinporten.MaskinportenClient +import no.nav.helsearbeidsgiver.maskinporten.MaskinportenClientConfigJwk import no.nav.helsearbeidsgiver.maskinporten.MaskinportenClientConfigPkey -import no.nav.helsearbeidsgiver.maskinporten.MaskinportenClientConfigSimpleAssertion import no.nav.helsearbeidsgiver.maskinporten.createHttpClient import no.nav.helsearbeidsgiver.utils.test.mock.mockStatic import org.junit.jupiter.api.Test @@ -84,7 +84,6 @@ class MaskinportenClientTest { kid = "test_kid", privateKey = generatePkey(), clientId = "test_issuer", - consumerOrgNr = "test_consumer_org_nr", scope = "test_scope", issuer = "https://test.test.no/", endpoint = "https://test.test.no/token" @@ -99,7 +98,7 @@ class MaskinportenClientTest { } } - private fun getMaskinportenClientConfig() = MaskinportenClientConfigSimpleAssertion( + private fun getMaskinportenClientConfig() = MaskinportenClientConfigJwk( scope = "test_scope", clientId = "test_client_id", clientJwk = generateJWK(),