Skip to content

Commit

Permalink
Maskinporten client (#3)
Browse files Browse the repository at this point in the history
* Maskinporten client
  • Loading branch information
mettok authored Jul 3, 2024
1 parent f5698f0 commit 23534db
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 133 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @navikt/helsearbeidsgiver
12 changes: 9 additions & 3 deletions .github/workflows/publish-snapshot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,16 @@ jobs:

- name: Setup Gradle
uses: gradle/gradle-build-action@v2

- name: Ensure Gradle Wrapper is Downloaded
run: ./gradlew --version

- name: Get Version
run: |
echo "CURRENT_VERSION=$(./gradlew :printVersion --quiet)"
echo "CURRENT_VERSION=$(./gradlew :printVersion --quiet)" >> $GITHUB_ENV
run: echo "CURRENT_VERSION=$(./gradlew :printVersion --quiet)" >> $GITHUB_ENV

- name: Print Project Version
run: echo "Project version is ${{ env.CURRENT_VERSION }}"

- name: Publish artifact
if: ${{ endsWith(env.CURRENT_VERSION, '-SNAPSHOT') }}
run: ./gradlew publish
15 changes: 9 additions & 6 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
kotlin("jvm")
kotlin("plugin.serialization")
id("org.jlleitschuh.gradle.ktlint")
id("maven-publish")
}
group = "no.nav.helsearbeidsgiver"
version = "0.1.6"

version = "0.1.8"

kotlin {
compilerOptions {
Expand All @@ -19,6 +20,10 @@ repositories {
mavenNav("*")
}

tasks.register("printVersion") {
println(project.version)
}

publishing {
publications {
create<MavenPublication>("mavenJava") {
Expand All @@ -35,7 +40,6 @@ dependencies {
val ktorVersion: String by project
val utilsVersion: String by project


api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
implementation("no.nav.helsearbeidsgiver:utils:$utilsVersion")
implementation("io.ktor:ktor-client-apache5:$ktorVersion")
Expand All @@ -45,13 +49,12 @@ dependencies {
implementation("com.nimbusds:nimbus-jose-jwt:9.40")

testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
testImplementation(testFixtures("no.nav.helsearbeidsgiver:utils:$utilsVersion"))
testImplementation("io.ktor:ktor-client-mock:$ktorVersion")
testImplementation("io.mockk:mockk:1.13.11")
testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
testImplementation(kotlin("test"))
testRuntimeOnly("org.slf4j:slf4j-simple:2.0.7")


}

tasks.test {
Expand All @@ -72,4 +75,4 @@ fun RepositoryHandler.mavenNav(repo: String): MavenArtifactRepository {
password = githubPassword
}
}
}
}
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
kotlin.code.style=official
ktlintVersion=11.5.1
kotlinVersion= 1.9.24
ktorVersion=2.3.11
utilsVersion=0.7.0
4 changes: 3 additions & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ rootProject.name = "maskinporten-client"

pluginManagement {
val kotlinVersion: String by settings
val ktlintVersion: String by settings

plugins {
kotlin("jvm") version kotlinVersion
kotlin("plugin.serialization") version kotlinVersion
id("org.jlleitschuh.gradle.ktlint") version ktlintVersion
id("maven-publish")
}
}
}
11 changes: 6 additions & 5 deletions src/main/kotlin/MaskinPortenHttpClient.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package no.nav.helsearbeidsgiver.maskinporten

import io.ktor.client.*
import io.ktor.client.engine.apache5.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.client.HttpClient
import io.ktor.client.HttpClientConfig
import io.ktor.client.engine.apache5.Apache5
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.serialization.kotlinx.json.json

internal fun createHttpClient(): HttpClient =
HttpClient(Apache5) { configure() }
Expand All @@ -14,4 +15,4 @@ internal fun HttpClientConfig<*>.configure() {
install(ContentNegotiation) {
json()
}
}
}
46 changes: 18 additions & 28 deletions src/main/kotlin/MaskinportenClient.kt
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
package no.nav.helsearbeidsgiver.maskinporten

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import no.nav.helsearbeidsgiver.utils.log.logger

import io.ktor.client.call.body
import io.ktor.client.plugins.ClientRequestException
import io.ktor.client.plugins.ServerResponseException
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.http.formUrlEncode
import no.nav.helsearbeidsgiver.utils.log.sikkerLogger

private const val GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"

class MaskinportenClient(scope: String) {

private var httpClient = createHttpClient()
private var maskinportenClientConfig = MaskinportenClientConfig(scope)
private var logger = this.logger()
class MaskinportenClient(private val maskinportenClientConfig: MaskinportenClientConfig) {

private val httpClient = createHttpClient()

suspend fun fetchNewAccessToken(): TokenResponseWrapper {
logger.info("Henter ny access token fra Maskinporten")
sikkerLogger().info("Henter ny access token fra Maskinporten")

val result = runCatching {
val response: HttpResponse = httpClient.post(maskinportenClientConfig.getEndpoint()) {
val response: HttpResponse = httpClient.post(maskinportenClientConfig.endpoint) {
contentType(ContentType.Application.FormUrlEncoded)
setBody(
listOf(
Expand All @@ -36,34 +35,25 @@ class MaskinportenClient(scope: String) {
return result.fold(
onSuccess = { tokenResponse ->
TokenResponseWrapper(tokenResponse).also {
logger.info("Hentet ny access token. Expires in ${it.remainingTimeInSeconds} seconds.")
sikkerLogger().info("Hentet ny access token. Expires in ${it.remainingTimeInSeconds} seconds.")
}
},
onFailure = { e ->
when (e) {
is ClientRequestException -> {
logger.error("ClientRequestException: Feilet å hente ny access token fra Maskinporten. Status: ${e.response.status}, Message: ${e.message} Exception: $e")
sikkerLogger().error("ClientRequestException: Feilet å hente ny access token fra Maskinporten. Status: ${e.response.status}, Message: ${e.message} Exception: $e")
}

is ServerResponseException -> {
logger.error("ServerResponseException: Feilet å hente ny access token fra Maskinporten. Status: ${e.response.status}, Message: ${e.message} Exception: $e")
sikkerLogger().error("ServerResponseException: Feilet å hente ny access token fra Maskinporten. Status: ${e.response.status}, Message: ${e.message} Exception: $e")
}

else -> {
logger.error("Feilet å hente ny access token fra Maskinporten: $e")
sikkerLogger().error("Feilet å hente ny access token fra Maskinporten: $e")
}
}
throw e
}
)
}

fun setHttpClient(httpClient: HttpClient) {
this.httpClient = httpClient
}
}





82 changes: 42 additions & 40 deletions src/main/kotlin/MaskinportenClientConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,47 +8,49 @@ import com.nimbusds.jose.jwk.RSAKey
import com.nimbusds.jwt.JWTClaimsSet
import com.nimbusds.jwt.SignedJWT
import java.time.Instant
import java.util.*

class MaskinportenClientConfig(scope: String) {

private val clientId: String = EnvWrapper.getEnv("MASKINPORTEN_CLIENT_ID") ?: throw IllegalStateException("Fant ikke MASKINPORTEN_CLIENT_ID")
private val clientJwk: String = EnvWrapper.getEnv("MASKINPORTEN_CLIENT_JWK") ?: throw IllegalStateException("Fant ikke MASKINPORTEN_CLIENT_JWK")
private val issuer: String = EnvWrapper.getEnv("MASKINPORTEN_ISSUER") ?: throw IllegalStateException("Fant ikke MASKINPORTEN_ISSUER")
// Denne er foreløig ikke i bruk, men kan verifisere at scopet er riktig ved et senere tidspunkt.
private val scopes: String = EnvWrapper.getEnv("MASKINPORTEN_SCOPES") ?: throw IllegalStateException("Fant ikke MASKINPORTEN_SCOPES")
private val ENDPOINT: String = EnvWrapper.getEnv("MASKINPORTEN_TOKEN_ENDPOINT") ?: throw IllegalStateException("Fant ikke MASKINPORTEN_TOKEN_ENDPOINT")

private val rsaKey: RSAKey = RSAKey.parse(clientJwk)
private val signer: RSASSASigner = RSASSASigner(rsaKey.toPrivateKey())
private val header: JWSHeader = JWSHeader.Builder(JWSAlgorithm.RS256)
.keyID(rsaKey.keyID)
.type(JOSEObjectType.JWT)
.build()

private val now: Date = Date.from(Instant.now())
private val expiration: Date = Date.from(Instant.now().plusSeconds(60))
private val claims: JWTClaimsSet = JWTClaimsSet.Builder()
.issuer(clientId)
.audience(issuer)
.issueTime(now)
.claim("scope", scope)
.expirationTime(expiration)
.jwtID(UUID.randomUUID().toString())
.build()

fun getJwtAssertion(): String = SignedJWT(header, claims)
.apply { sign(signer) }
.serialize()

fun getEndpoint(): String {
return ENDPOINT
import java.util.Date
import java.util.UUID

data class MaskinportenClientConfig(
val scope: String,
val clientId: String,
val clientJwk: String,
val issuer: String,
val endpoint: String
) {

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 val claims: JWTClaimsSet by lazy {
val now = currentTime()
JWTClaimsSet.Builder()
.issuer(clientId)
.audience(issuer)
.issueTime(now)
.claim("scope", scope)
.expirationTime(Date.from(now.toInstant().plusSeconds(60)))
.jwtID(UUID.randomUUID().toString())
.build()
}

object EnvWrapper {
fun getEnv(key: String): String? {
return System.getenv(key)
fun getJwtAssertion(): String {
return SignedJWT(header, claims).apply { sign(signer) }.serialize()
}
}
}
2 changes: 1 addition & 1 deletion src/main/kotlin/TokenResponse.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ data class TokenResponse(
@SerialName("access_token") val accessToken: String,
@SerialName("token_type") val tokenType: String,
@SerialName("expires_in") val expiresInSeconds: Long,
val scope: String,
val scope: String
)
class TokenResponseWrapper(val tokenResponse: TokenResponse) {

Expand Down
Loading

0 comments on commit 23534db

Please sign in to comment.