Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<SecurityContext> = JWKSourceBuilder<SecurityContext>
.create<SecurityContext>(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)

Expand Down Expand Up @@ -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)}")
}
}
}
Expand All @@ -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()) }

Expand Down
2 changes: 2 additions & 0 deletions ebms-payload/src/main/resources/application_dev.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
2 changes: 2 additions & 0 deletions ebms-payload/src/main/resources/application_local.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
2 changes: 2 additions & 0 deletions ebms-payload/src/main/resources/application_prod.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) {
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand Down Expand Up @@ -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"))
Expand Down