From 21383af9eb3a77946a2b843d856587fc73a60ceb Mon Sep 17 00:00:00 2001 From: Oleksandr Chmyr Date: Thu, 23 Oct 2025 15:15:09 +0200 Subject: [PATCH 1/4] Fikset lesin av "iat" claim --- .../payload/helseid/HelseIdTokenValidator.kt | 24 ++++++++----------- .../helseid/testutils/HelseIDCreator.kt | 5 ++-- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/ebms-payload/src/main/kotlin/no/nav/emottak/payload/helseid/HelseIdTokenValidator.kt b/ebms-payload/src/main/kotlin/no/nav/emottak/payload/helseid/HelseIdTokenValidator.kt index 97b5084a..82e252e5 100644 --- a/ebms-payload/src/main/kotlin/no/nav/emottak/payload/helseid/HelseIdTokenValidator.kt +++ b/ebms-payload/src/main/kotlin/no/nav/emottak/payload/helseid/HelseIdTokenValidator.kt @@ -14,7 +14,6 @@ import com.nimbusds.jose.jwk.source.JWKSource import com.nimbusds.jose.jwk.source.JWKSourceBuilder import com.nimbusds.jose.proc.SecurityContext import com.nimbusds.jose.proc.SimpleSecurityContext -import com.nimbusds.jwt.JWTClaimNames import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.JWTParser import com.nimbusds.jwt.SignedJWT @@ -89,25 +88,25 @@ class HelseIdTokenValidator( validateEssentialClaims(claims) } - private fun validateTimestamps(claims: JWTClaimsSet, now: Date) { - issuedAt(claims)?.let { iat -> - if (now.time < iat.time - allowedClockSkewInMs) { - error("${timePrefix(now)} is before issued time ${timePrefix(iat)}") + private fun validateTimestamps(claims: JWTClaimsSet, timestamp: Date) { + claims.issueTime?.let { iat -> + if (timestamp.time < iat.time - allowedClockSkewInMs) { + error("${timePrefix(timestamp)} is before issued time ${timePrefix(iat)}") } } claims.expirationTime?.let { exp -> - if (now.time > exp.time + allowedClockSkewInMs) { - error("${timePrefix(now)} is after expiry time ${timePrefix(exp)}") + if (timestamp.time > exp.time + allowedClockSkewInMs) { + error("${timePrefix(timestamp)} is after expiry time ${timePrefix(exp)}") } } claims.notBeforeTime?.let { nbf -> - if (now.time < nbf.time - allowedClockSkewInMs) { - error("${timePrefix(now)} is before not-before time ${timePrefix(nbf)}") + if (timestamp.time < nbf.time - allowedClockSkewInMs) { + error("${timePrefix(timestamp)} is before not-before time ${timePrefix(nbf)}") } } authTime(claims)?.let { at -> - if (now.time < at.time - allowedClockSkewInMs) { - error("${timePrefix(now)} is before auth-time ${timePrefix(at)}") + if (timestamp.time < at.time - allowedClockSkewInMs) { + error("${timePrefix(timestamp)} is before auth-time ${timePrefix(at)}") } } } @@ -126,9 +125,6 @@ class HelseIdTokenValidator( private fun extractNin(jwt: SignedJWT): String = getString(jwt.jwtClaimsSet, PID_CLAIM) - private fun issuedAt(claims: JWTClaimsSet): Date? = - (claims.getClaim(JWTClaimNames.ISSUED_AT) as? Number)?.let { Date(it.toLong() * 1000) } - private fun authTime(claims: JWTClaimsSet): Date? = (claims.getClaim("auth_time") as? Number)?.let { Date(it.toLong()) } diff --git a/ebms-payload/src/test/kotlin/no/nav/emottak/payload/helseid/testutils/HelseIDCreator.kt b/ebms-payload/src/test/kotlin/no/nav/emottak/payload/helseid/testutils/HelseIDCreator.kt index db2e54e6..e3656126 100644 --- a/ebms-payload/src/test/kotlin/no/nav/emottak/payload/helseid/testutils/HelseIDCreator.kt +++ b/ebms-payload/src/test/kotlin/no/nav/emottak/payload/helseid/testutils/HelseIDCreator.kt @@ -21,6 +21,7 @@ import java.security.NoSuchAlgorithmException import java.util.Base64 import java.util.Date import java.util.UUID +import kotlin.math.floor class HelseIDCreator(pathToKeystore: String, keystoreType: String = "jks", private val password: CharArray) { @@ -69,8 +70,8 @@ class HelseIDCreator(pathToKeystore: String, keystoreType: String = "jks", priva .issueTime(now) .notBeforeTime(now) .claim("client_id", clientId) - .claim("auth_time", now.time) - .claim("iat", now.time) + .claim("auth_time", floor(now.time / 1000.0)) + .claim("iat", floor(now.time / 1000.0)) .claim("idp", "testidp-oidc") .claim("helseid://claims/identity/security_level", "4") .claim("helseid://claims/hpr/hpr_number", listOf("431001110")) From a02c94c90537bfc51eb00da5f1ddb1898f2cd0cf Mon Sep 17 00:00:00 2001 From: Oleksandr Chmyr Date: Thu, 23 Oct 2025 16:12:33 +0200 Subject: [PATCH 2/4] Lagt til en sjekk for tidsperiode mellom generering av HelseID token og generering av selve meldingen --- .../payload/helseid/HelseIdTokenValidator.kt | 32 +++++++++++-------- .../payload/helseid/HelseIDValidatorTest.kt | 16 ++++++++-- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/ebms-payload/src/main/kotlin/no/nav/emottak/payload/helseid/HelseIdTokenValidator.kt b/ebms-payload/src/main/kotlin/no/nav/emottak/payload/helseid/HelseIdTokenValidator.kt index 82e252e5..aa01981f 100644 --- a/ebms-payload/src/main/kotlin/no/nav/emottak/payload/helseid/HelseIdTokenValidator.kt +++ b/ebms-payload/src/main/kotlin/no/nav/emottak/payload/helseid/HelseIdTokenValidator.kt @@ -33,13 +33,14 @@ import java.util.Locale class HelseIdTokenValidator( private val issuer: String = config().helseId.issuerUrl, private val allowedClockSkewInMs: Long = 0, + private val allowedMessageGenerationGap: Long = 10000, private val helseIdJwkSource: JWKSource = JWKSourceBuilder .create(URI.create(config().helseId.jwksUrl).toURL()).build() ) { - fun getValidatedNin(base64Token: String, timestamp: Instant): String? = parseSignedJwt(base64Token) + fun getValidatedNin(base64Token: String, messageGenerationDate: Instant): String? = parseSignedJwt(base64Token) .also { validateHeader(it) - validateClaims(it, Date.from(timestamp)) + validateClaims(it, Date.from(messageGenerationDate)) it.verify(helseIdJwkSource) }.let(::extractNin) @@ -78,35 +79,40 @@ class HelseIdTokenValidator( if (jwt.header.type !in SUPPORTED_JWT_TYPES) error("Unsupported token type ${jwt.header.type}") } - private fun validateClaims(jwt: SignedJWT, timestamp: Date) = try { + private fun validateClaims(jwt: SignedJWT, messageGenerationDate: Date) = try { jwt.jwtClaimsSet } catch (ex: ParseException) { throw RuntimeException("Failed to parse claims", ex) }.also { claims -> - validateTimestamps(claims, timestamp) + validateTimestamps(claims, messageGenerationDate) if (claims.issuer != issuer) error("Invalid issuer ${claims.issuer}") validateEssentialClaims(claims) } - private fun validateTimestamps(claims: JWTClaimsSet, timestamp: Date) { + private fun validateTimestamps(claims: JWTClaimsSet, messageGenerationDate: Date) { claims.issueTime?.let { iat -> - if (timestamp.time < iat.time - allowedClockSkewInMs) { - error("${timePrefix(timestamp)} is before issued time ${timePrefix(iat)}") + if (messageGenerationDate.time < iat.time - allowedClockSkewInMs) { + error("${timePrefix(messageGenerationDate)} is before issued time ${timePrefix(iat)}") + } + } + claims.issueTime?.let { iat -> + if (messageGenerationDate.time > iat.time - allowedClockSkewInMs + allowedMessageGenerationGap) { + error("Message generation time should be within ${allowedMessageGenerationGap / 1000} seconds after token issued time") } } claims.expirationTime?.let { exp -> - if (timestamp.time > exp.time + allowedClockSkewInMs) { - error("${timePrefix(timestamp)} is after expiry time ${timePrefix(exp)}") + if (messageGenerationDate.time > exp.time + allowedClockSkewInMs) { + error("${timePrefix(messageGenerationDate)} is after expiry time ${timePrefix(exp)}") } } claims.notBeforeTime?.let { nbf -> - if (timestamp.time < nbf.time - allowedClockSkewInMs) { - error("${timePrefix(timestamp)} is before not-before time ${timePrefix(nbf)}") + if (messageGenerationDate.time < nbf.time - allowedClockSkewInMs) { + error("${timePrefix(messageGenerationDate)} is before not-before time ${timePrefix(nbf)}") } } authTime(claims)?.let { at -> - if (timestamp.time < at.time - allowedClockSkewInMs) { - error("${timePrefix(timestamp)} is before auth-time ${timePrefix(at)}") + if (messageGenerationDate.time < at.time - allowedClockSkewInMs) { + error("${timePrefix(messageGenerationDate)} is before auth-time ${timePrefix(at)}") } } } diff --git a/ebms-payload/src/test/kotlin/no/nav/emottak/payload/helseid/HelseIDValidatorTest.kt b/ebms-payload/src/test/kotlin/no/nav/emottak/payload/helseid/HelseIDValidatorTest.kt index 4d7d6489..b3350349 100644 --- a/ebms-payload/src/test/kotlin/no/nav/emottak/payload/helseid/HelseIDValidatorTest.kt +++ b/ebms-payload/src/test/kotlin/no/nav/emottak/payload/helseid/HelseIDValidatorTest.kt @@ -168,6 +168,17 @@ internal class HelseIDValidatorTest { ) } + @Test + fun `validate helseID with lang message generation lag`() { + validateHomeMadeHelseId( + validator, + scope = HelseIdTokenValidator.SUPPORTED_SCOPES.first(), + audience = HelseIdTokenValidator.SUPPORTED_AUDIENCE.first(), + messageGenerationLagSec = 20, + errMsg = "Message generation time should be within 10 seconds after token issued time" + ) + } + @Suppress("LongParameterList") private fun validateHomeMadeHelseId( validator: HelseIdTokenValidator, @@ -176,7 +187,8 @@ internal class HelseIDValidatorTest { errMsg: String? = null, algo: JWSAlgorithm = JWSAlgorithm.RS256, type: JOSEObjectType = JOSEObjectType.JWT, - jwk: JWK? = null + jwk: JWK? = null, + messageGenerationLagSec: Long = 0L ) { val b64 = Base64.getEncoder().encodeToString( if (jwk == null) { @@ -200,7 +212,7 @@ internal class HelseIDValidatorTest { } ) TimeUnit.MILLISECONDS.sleep(20) - val timeStamp = Instant.now() + val timeStamp = Instant.now().plusSeconds(messageGenerationLagSec) val func: () -> Unit = { validator.getValidatedNin( b64, From 39342f6531d71a4df4f705410a715f6d97a573b9 Mon Sep 17 00:00:00 2001 From: Oleksandr Chmyr Date: Thu, 23 Oct 2025 16:28:47 +0200 Subject: [PATCH 3/4] Fikset skrivefeil --- .../no/nav/emottak/payload/helseid/HelseIDValidatorTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ebms-payload/src/test/kotlin/no/nav/emottak/payload/helseid/HelseIDValidatorTest.kt b/ebms-payload/src/test/kotlin/no/nav/emottak/payload/helseid/HelseIDValidatorTest.kt index b3350349..4430f3ae 100644 --- a/ebms-payload/src/test/kotlin/no/nav/emottak/payload/helseid/HelseIDValidatorTest.kt +++ b/ebms-payload/src/test/kotlin/no/nav/emottak/payload/helseid/HelseIDValidatorTest.kt @@ -169,7 +169,7 @@ internal class HelseIDValidatorTest { } @Test - fun `validate helseID with lang message generation lag`() { + fun `validate helseID with long message generation lag`() { validateHomeMadeHelseId( validator, scope = HelseIdTokenValidator.SUPPORTED_SCOPES.first(), From 1a5d0a8ae3254efc585ccd472c37b26aeb6dd745 Mon Sep 17 00:00:00 2001 From: Oleksandr Chmyr Date: Fri, 24 Oct 2025 14:54:24 +0200 Subject: [PATCH 4/4] Flyttet allowedClockSkewInMs og allowedMessageGenerationGapInMs innstillinger inn i konfigurasjon fil --- .../kotlin/no/nav/emottak/payload/configuration/Config.kt | 4 +++- .../nav/emottak/payload/helseid/HelseIdTokenValidator.kt | 8 ++++---- ebms-payload/src/main/resources/application_dev.conf | 2 ++ ebms-payload/src/main/resources/application_local.conf | 2 ++ ebms-payload/src/main/resources/application_prod.conf | 2 ++ 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/ebms-payload/src/main/kotlin/no/nav/emottak/payload/configuration/Config.kt b/ebms-payload/src/main/kotlin/no/nav/emottak/payload/configuration/Config.kt index 05aea3cc..6721ccbd 100644 --- a/ebms-payload/src/main/kotlin/no/nav/emottak/payload/configuration/Config.kt +++ b/ebms-payload/src/main/kotlin/no/nav/emottak/payload/configuration/Config.kt @@ -20,5 +20,7 @@ data class CertificateAuthority( data class HelseId( val issuerUrl: String, - val jwksUrl: String + val jwksUrl: String, + val allowedClockSkewInMs: Long, + val allowedMessageGenerationGapInMs: Long ) diff --git a/ebms-payload/src/main/kotlin/no/nav/emottak/payload/helseid/HelseIdTokenValidator.kt b/ebms-payload/src/main/kotlin/no/nav/emottak/payload/helseid/HelseIdTokenValidator.kt index aa01981f..d5c62059 100644 --- a/ebms-payload/src/main/kotlin/no/nav/emottak/payload/helseid/HelseIdTokenValidator.kt +++ b/ebms-payload/src/main/kotlin/no/nav/emottak/payload/helseid/HelseIdTokenValidator.kt @@ -32,8 +32,8 @@ import java.util.Locale class HelseIdTokenValidator( private val issuer: String = config().helseId.issuerUrl, - private val allowedClockSkewInMs: Long = 0, - private val allowedMessageGenerationGap: Long = 10000, + private val allowedClockSkewInMs: Long = config().helseId.allowedClockSkewInMs, + private val allowedMessageGenerationGapInMs: Long = config().helseId.allowedMessageGenerationGapInMs, private val helseIdJwkSource: JWKSource = JWKSourceBuilder .create(URI.create(config().helseId.jwksUrl).toURL()).build() ) { @@ -96,8 +96,8 @@ class HelseIdTokenValidator( } } claims.issueTime?.let { iat -> - if (messageGenerationDate.time > iat.time - allowedClockSkewInMs + allowedMessageGenerationGap) { - error("Message generation time should be within ${allowedMessageGenerationGap / 1000} seconds after token issued time") + if (messageGenerationDate.time > iat.time - allowedClockSkewInMs + allowedMessageGenerationGapInMs) { + error("Message generation time should be within ${allowedMessageGenerationGapInMs / 1000} seconds after token issued time") } } claims.expirationTime?.let { exp -> diff --git a/ebms-payload/src/main/resources/application_dev.conf b/ebms-payload/src/main/resources/application_dev.conf index 4f3ea7e2..f2d14b29 100644 --- a/ebms-payload/src/main/resources/application_dev.conf +++ b/ebms-payload/src/main/resources/application_dev.conf @@ -20,6 +20,8 @@ kafka { helseId { issuerUrl = "https://helseid-sts.test.nhn.no" jwksUrl = "https://helseid-sts.test.nhn.no/.well-known/openid-configuration/jwks" + allowedClockSkewInMs = 0 + allowedMessageGenerationGapInMs = 10000 } signering = [ diff --git a/ebms-payload/src/main/resources/application_local.conf b/ebms-payload/src/main/resources/application_local.conf index 565a0e37..06148298 100644 --- a/ebms-payload/src/main/resources/application_local.conf +++ b/ebms-payload/src/main/resources/application_local.conf @@ -20,6 +20,8 @@ kafka { helseId { issuerUrl = "https://helseid-sts.test.nhn.no" jwksUrl = "https://helseid-sts.test.nhn.no/.well-known/openid-configuration/jwks" + allowedClockSkewInMs = 0 + allowedMessageGenerationGapInMs = 10000 } signering = [ diff --git a/ebms-payload/src/main/resources/application_prod.conf b/ebms-payload/src/main/resources/application_prod.conf index 1fc5b54c..5462aa29 100644 --- a/ebms-payload/src/main/resources/application_prod.conf +++ b/ebms-payload/src/main/resources/application_prod.conf @@ -18,6 +18,8 @@ kafka { helseId { issuerUrl = "https://helseid-sts.nhn.no" jwksUrl = "https://helseid-sts.nhn.no/.well-known/openid-configuration/jwks" + allowedClockSkewInMs = 0 + allowedMessageGenerationGapInMs = 10000 } signering = [