diff --git a/README.md b/README.md index e871c6a..e3aaf73 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,16 @@ In quiet chambers of learning, where minds are aglow, A ledger of endorsements, The following properties can be used to configure the deployment of your application. If specified, these properties will load their respective services. -| name | info | -|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------| -| jwt.private-key | Location of the private key file. If specified, the `PrivateJwtService` will be loaded with the provided private key for signing purposes. | +| name | info | +|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------| +| jwt.private-key | Location of the private key file. If specified, the `PrivateJwtService` will be loaded with the provided private key for signing purposes. | +| jwt.public-key | Location of the public key file. If specified, the `PublicJwtService` service will be loaded with the provided public key for verification purposes. | -**Note:** The `jwt.private-key` property is optional and corresponds to the `PrivateJwtService`. If specified, this service will be loaded for signing purposes. +**Note:** The `jwt.private-key` and `jwt.public-key` properties are optional and correspond to separate services: `PrivateJwtService` and `PublicJwtService`, respectively. If either property is specified, its corresponding service will be loaded. For example: + +- Specifying only `jwt.private-key` will load the `PrivateJwtService` for signing purposes. +- Specifying only `jwt.public-key` will load the `PublicJwtService` for verification purposes. +- Specifying both properties will enable both services, allowing for full JWT functionality with authentication and authorization. ## Releasing library diff --git a/src/main/kotlin/ltd/hlaeja/jwt/service/PublicJwtService.kt b/src/main/kotlin/ltd/hlaeja/jwt/service/PublicJwtService.kt new file mode 100644 index 0000000..457c828 --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/jwt/service/PublicJwtService.kt @@ -0,0 +1,28 @@ +package ltd.hlaeja.jwt.service + +import io.jsonwebtoken.Claims +import io.jsonwebtoken.Jws +import io.jsonwebtoken.JwtParser +import io.jsonwebtoken.Jwts +import ltd.hlaeja.jwt.util.PublicKeyProvider +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.stereotype.Service + +@Service +@ConditionalOnProperty(prefix = "jwt", name = ["public-key"]) +class PublicJwtService( + @Value("\${jwt.public-key}") + jwtPublicKey: String, +) { + + private val parser: JwtParser = Jwts.parser() + .verifyWith(PublicKeyProvider.load(jwtPublicKey)) + .build() + + fun verify( + token: String, + block: (claims: Jws) -> T, + ): T = parser.parseSignedClaims(token) + .let(block) +} diff --git a/src/test/kotlin/ltd/hlaeja/jwt/service/PublicJwtServiceTest.kt b/src/test/kotlin/ltd/hlaeja/jwt/service/PublicJwtServiceTest.kt new file mode 100644 index 0000000..1ea94e6 --- /dev/null +++ b/src/test/kotlin/ltd/hlaeja/jwt/service/PublicJwtServiceTest.kt @@ -0,0 +1,36 @@ +package ltd.hlaeja.jwt.service + +import java.nio.charset.StandardCharsets.UTF_8 +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.core.io.ClassPathResource +import org.springframework.util.FileCopyUtils + +class PublicJwtServiceTest { + + private lateinit var service: PublicJwtService + + @BeforeEach + fun setup() { + service = PublicJwtService("cert/valid-public-key.pem") + } + + @Test + fun `parse token with claims`() { + // given + val token = String(FileCopyUtils.copyToByteArray(ClassPathResource("jwt.token").inputStream), UTF_8).trim() + + // when + val result = service.verify(token) { claims -> + mapOf( + "claim1" to claims.payload["claim1"] as String, + "claim2" to claims.payload["claim2"] as Int, + ) + } + + // then + assertThat(result["claim1"]).isEqualTo("value1") + assertThat(result["claim2"]).isEqualTo(123) + } +}