diff --git a/build.gradle.kts b/build.gradle.kts index 0329620..46c3c54 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,17 +9,15 @@ plugins { dependencies { implementation(hlaeja.fasterxml.jackson.module.kotlin) - implementation(hlaeja.jjwt.api) implementation(hlaeja.kotlin.logging) implementation(hlaeja.kotlin.reflect) implementation(hlaeja.kotlinx.coroutines) implementation(hlaeja.library.hlaeja.common.messages) + implementation(hlaeja.library.hlaeja.jwt) implementation(hlaeja.springboot.starter.actuator) implementation(hlaeja.springboot.starter.r2dbc) implementation(hlaeja.springboot.starter.webflux) - runtimeOnly(hlaeja.jjwt.impl) - runtimeOnly(hlaeja.jjwt.jackson) runtimeOnly(hlaeja.postgresql) runtimeOnly(hlaeja.postgresql.r2dbc) diff --git a/gradle.properties b/gradle.properties index b1befca..fa1e40f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ kotlin.code.style=official version=0.4.0-SNAPSHOT -catalog=0.7.0 +catalog=0.8.0-SNAPSHOT container.port.host=9010 diff --git a/src/main/kotlin/ltd/hlaeja/controller/DeviceController.kt b/src/main/kotlin/ltd/hlaeja/controller/DeviceController.kt index db049d8..9794284 100644 --- a/src/main/kotlin/ltd/hlaeja/controller/DeviceController.kt +++ b/src/main/kotlin/ltd/hlaeja/controller/DeviceController.kt @@ -1,9 +1,9 @@ package ltd.hlaeja.controller import java.util.UUID +import ltd.hlaeja.jwt.service.PrivateJwtService import ltd.hlaeja.library.deviceRegistry.Device import ltd.hlaeja.service.DeviceService -import ltd.hlaeja.service.JwtService import ltd.hlaeja.util.toDeviceResponse import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable @@ -14,18 +14,18 @@ import org.springframework.web.bind.annotation.RestController @RestController class DeviceController( private val deviceService: DeviceService, - private val jwtService: JwtService, + private val privateJwtService: PrivateJwtService, ) { @PostMapping("/device") suspend fun addDevice( @RequestBody request: Device.Request, ): Device.Response = deviceService.addDevice(request.type) - .toDeviceResponse(jwtService) + .toDeviceResponse(privateJwtService) @GetMapping("/device-{device}") suspend fun getDevice( @PathVariable device: UUID, ): Device.Response = deviceService.getDevice(device) - .toDeviceResponse(jwtService) + .toDeviceResponse(privateJwtService) } diff --git a/src/main/kotlin/ltd/hlaeja/service/JwtService.kt b/src/main/kotlin/ltd/hlaeja/service/JwtService.kt deleted file mode 100644 index b09a952..0000000 --- a/src/main/kotlin/ltd/hlaeja/service/JwtService.kt +++ /dev/null @@ -1,25 +0,0 @@ -package ltd.hlaeja.service - -import io.jsonwebtoken.Jwts -import java.security.interfaces.RSAPrivateKey -import java.util.UUID -import ltd.hlaeja.property.JwtProperty -import ltd.hlaeja.util.PrivateKeyProvider -import org.springframework.stereotype.Service - -@Service -class JwtService( - jwtProperty: JwtProperty, -) { - - private var privateKey: RSAPrivateKey = PrivateKeyProvider.load(jwtProperty.privateKey) - - suspend fun makeIdentity(device: UUID): String { - return Jwts.builder() - .claims() - .add("device", device) - .and() - .signWith(privateKey) - .compact() - } -} diff --git a/src/main/kotlin/ltd/hlaeja/util/Mapping.kt b/src/main/kotlin/ltd/hlaeja/util/Mapping.kt index da23cd7..1503e2e 100644 --- a/src/main/kotlin/ltd/hlaeja/util/Mapping.kt +++ b/src/main/kotlin/ltd/hlaeja/util/Mapping.kt @@ -4,11 +4,11 @@ import java.time.ZonedDateTime import ltd.hlaeja.entity.DeviceEntity import ltd.hlaeja.entity.NodeEntity import ltd.hlaeja.entity.TypeEntity +import ltd.hlaeja.jwt.service.PrivateJwtService import ltd.hlaeja.library.deviceRegistry.Device import ltd.hlaeja.library.deviceRegistry.Identity import ltd.hlaeja.library.deviceRegistry.Node import ltd.hlaeja.library.deviceRegistry.Type -import ltd.hlaeja.service.JwtService import org.springframework.http.HttpStatus.EXPECTATION_FAILED import org.springframework.web.server.ResponseStatusException @@ -40,10 +40,10 @@ fun NodeEntity.toIdentityResponse(): Identity.Response = Identity.Response( device, ) -suspend fun DeviceEntity.toDeviceResponse( - jwtService: JwtService, +fun DeviceEntity.toDeviceResponse( + jwtService: PrivateJwtService, ): Device.Response = Device.Response( id ?: throw ResponseStatusException(EXPECTATION_FAILED), type, - jwtService.makeIdentity(id), + jwtService.sign("device" to id), ) diff --git a/src/main/kotlin/ltd/hlaeja/util/PrivateKeyProvider.kt b/src/main/kotlin/ltd/hlaeja/util/PrivateKeyProvider.kt deleted file mode 100644 index 4454af9..0000000 --- a/src/main/kotlin/ltd/hlaeja/util/PrivateKeyProvider.kt +++ /dev/null @@ -1,35 +0,0 @@ -package ltd.hlaeja.util - -import java.security.KeyFactory -import java.security.interfaces.RSAPrivateKey -import java.security.spec.PKCS8EncodedKeySpec -import java.util.Base64.getDecoder -import ltd.hlaeja.exception.KeyProviderException - -object PrivateKeyProvider { - - fun load( - pemFile: String, - ): RSAPrivateKey = readPrivatePemFile(pemFile) - .let(::makePrivateKey) - - private fun makePrivateKey( - privateKeyBytes: ByteArray, - ): RSAPrivateKey = KeyFactory.getInstance("RSA") - .generatePrivate(PKCS8EncodedKeySpec(privateKeyBytes)) as RSAPrivateKey - - private fun readPrivatePemFile( - privateKey: String, - ): ByteArray = javaClass.classLoader - .getResource(privateKey) - ?.readText() - ?.let(::getPrivateKeyByteArray) - ?: throw KeyProviderException("Could not load private key") - - private fun getPrivateKeyByteArray( - keyText: String, - ): ByteArray = keyText.replace(Regex("[\r\n]+"), "") - .removePrefix("-----BEGIN PRIVATE KEY-----") - .removeSuffix("-----END PRIVATE KEY-----") - .let { getDecoder().decode(it) } -} diff --git a/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 1eb3135..26fe8e1 100644 --- a/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -19,11 +19,6 @@ "name": "spring.application.build.os.version", "type": "java.lang.String", "description": "Application build os version." - }, - { - "name": "jwt.private-key", - "type": "java.lang.String", - "description": "Jwt private key file." } ] } diff --git a/src/test/kotlin/ltd/hlaeja/controller/DeviceControllerTest.kt b/src/test/kotlin/ltd/hlaeja/controller/DeviceControllerTest.kt index 798d4ae..0a749b3 100644 --- a/src/test/kotlin/ltd/hlaeja/controller/DeviceControllerTest.kt +++ b/src/test/kotlin/ltd/hlaeja/controller/DeviceControllerTest.kt @@ -9,9 +9,9 @@ import java.time.ZonedDateTime import java.util.UUID import kotlinx.coroutines.test.runTest import ltd.hlaeja.entity.DeviceEntity +import ltd.hlaeja.jwt.service.PrivateJwtService import ltd.hlaeja.library.deviceRegistry.Device import ltd.hlaeja.service.DeviceService -import ltd.hlaeja.service.JwtService import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested @@ -28,13 +28,13 @@ class DeviceControllerTest { } val deviceService: DeviceService = mockk() - val jwtService: JwtService = mockk() + val privateJwtService: PrivateJwtService = mockk() lateinit var controller: DeviceController @BeforeEach fun setUp() { - controller = DeviceController(deviceService, jwtService) + controller = DeviceController(deviceService, privateJwtService) } @Nested @@ -45,14 +45,14 @@ class DeviceControllerTest { // given val request = Device.Request(uuid) coEvery { deviceService.addDevice(any()) } returns DeviceEntity(uuid, timestamp, uuid) - coEvery { jwtService.makeIdentity(any()) } returns PAYLOAD + coEvery { privateJwtService.sign(any()) } returns PAYLOAD // when val response = controller.addDevice(request) // then coVerify(exactly = 1) { deviceService.addDevice(any()) } - coVerify(exactly = 1) { jwtService.makeIdentity(any()) } + coVerify(exactly = 1) { privateJwtService.sign(any()) } assertThat(response.identity).isEqualTo(PAYLOAD) } @@ -80,14 +80,14 @@ class DeviceControllerTest { fun `get device - success`() = runTest { // given coEvery { deviceService.getDevice(any()) } returns DeviceEntity(uuid, timestamp, uuid) - coEvery { jwtService.makeIdentity(any()) } returns PAYLOAD + coEvery { privateJwtService.sign(any()) } returns PAYLOAD // when val response = controller.getDevice(uuid) // then coVerify(exactly = 1) { deviceService.getDevice(any()) } - coVerify(exactly = 1) { jwtService.makeIdentity(any()) } + coVerify(exactly = 1) { privateJwtService.sign(any()) } assertThat(response.identity).isEqualTo(PAYLOAD) } diff --git a/src/test/kotlin/ltd/hlaeja/service/JwtServiceTest.kt b/src/test/kotlin/ltd/hlaeja/service/JwtServiceTest.kt deleted file mode 100644 index e3eb4b6..0000000 --- a/src/test/kotlin/ltd/hlaeja/service/JwtServiceTest.kt +++ /dev/null @@ -1,31 +0,0 @@ -package ltd.hlaeja.service - -import java.util.UUID -import kotlinx.coroutines.test.runTest -import ltd.hlaeja.property.JwtProperty -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test - -class JwtServiceTest { - - val property: JwtProperty = JwtProperty("cert/valid-private-key.pem") - lateinit var service: JwtService - - @BeforeEach - fun setUp() { - service = JwtService(property) - } - - @Test - fun `should generate a JWT successfully with a valid private key`() = runTest { - // given - val deviceId = UUID.fromString("00000000-0000-0000-0000-000000000000") - - // when - val jwt = service.makeIdentity(deviceId) - - // then - assertThat(jwt).contains("eyJkZXZpY2UiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAifQ") - } -} diff --git a/src/test/kotlin/ltd/hlaeja/util/PrivateKeyProviderTest.kt b/src/test/kotlin/ltd/hlaeja/util/PrivateKeyProviderTest.kt deleted file mode 100644 index e090ffd..0000000 --- a/src/test/kotlin/ltd/hlaeja/util/PrivateKeyProviderTest.kt +++ /dev/null @@ -1,51 +0,0 @@ -package ltd.hlaeja.util - -import java.security.interfaces.RSAPrivateKey -import ltd.hlaeja.exception.KeyProviderException -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows - -class PrivateKeyProviderTest { - - @Test - fun `load private key - success`() { - // given - val pemFilePath = "cert/valid-private-key.pem" - - // when - val privateKey: RSAPrivateKey = PrivateKeyProvider.load(pemFilePath) - - // then - assertThat(privateKey).isNotNull - assertThat(privateKey.algorithm).isEqualTo("RSA") - } - - @Test - fun `load private key - file does not exist`() { - // given - val nonExistentPemFilePath = "cert/non-existent.pem" - - // when exception - val exception = assertThrows { - PrivateKeyProvider.load(nonExistentPemFilePath) - } - - // then - assertThat(exception.message).isEqualTo("Could not load private key") - } - - @Test - fun `load private key - file is invalid`() { - // given - val invalidPemFilePath = "cert/invalid-private-key.pem" - - // when exception - val exception = assertThrows { - PrivateKeyProvider.load(invalidPemFilePath) - } - - // then - assertThat(exception.message).contains("Input byte array has wrong 4-byte ending unit") - } -} diff --git a/src/test/resources/cert/invalid-private-key.pem b/src/test/resources/cert/invalid-private-key.pem deleted file mode 100644 index 2eb0903..0000000 --- a/src/test/resources/cert/invalid-private-key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -VEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBK -VU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMg -SVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBU -SElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpV -TksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJ -UyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRI -SVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVO -SyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElT -IEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJ -UyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5L -IFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMg -SlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElT -IElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksg -VEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBK -VU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMg -SVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBU -SElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpV -TksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJ -UyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRI -SVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVO -SyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElT -IEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJ -UyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5L -IFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMg -SlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElT -IElTIEpVTksg== ------END PRIVATE KEY----- \ No newline at end of file