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 97b5084a..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 @@ -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 @@ -33,14 +32,15 @@ import java.util.Locale class HelseIdTokenValidator( private val issuer: String = config().helseId.issuerUrl, - private val allowedClockSkewInMs: Long = 0, + 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() ) { - 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) @@ -79,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, 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, messageGenerationDate: Date) { + claims.issueTime?.let { 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 + allowedMessageGenerationGapInMs) { + error("Message generation time should be within ${allowedMessageGenerationGapInMs / 1000} seconds after token issued time") } } claims.expirationTime?.let { exp -> - if (now.time > exp.time + allowedClockSkewInMs) { - error("${timePrefix(now)} 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 (now.time < nbf.time - allowedClockSkewInMs) { - error("${timePrefix(now)} 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 (now.time < at.time - allowedClockSkewInMs) { - error("${timePrefix(now)} is before auth-time ${timePrefix(at)}") + if (messageGenerationDate.time < at.time - allowedClockSkewInMs) { + error("${timePrefix(messageGenerationDate)} is before auth-time ${timePrefix(at)}") } } } @@ -126,9 +131,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/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 = [ 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..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 @@ -168,6 +168,17 @@ internal class HelseIDValidatorTest { ) } + @Test + fun `validate helseID with long 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, 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"))