diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt index 674b228a..939fe5ba 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt @@ -35,7 +35,7 @@ fun Application.configureSecurity( acceptLeeway(3) } validate { jwtCredential -> - if (jwtCredential.payload.getClaim("username").asString().isNotEmpty()) { + if (jwtCredential.payload.getClaim("username")?.asString().isNullOrBlank().not()) { JWTPrincipal(jwtCredential.payload) } else { null diff --git a/src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt b/src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt index d5f6a9ea..32473173 100644 --- a/src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt @@ -11,7 +11,9 @@ import dev.usbharu.hideout.config.ConfigData import dev.usbharu.hideout.domain.model.hideout.dto.JwtToken import dev.usbharu.hideout.domain.model.hideout.entity.Jwt import dev.usbharu.hideout.domain.model.hideout.entity.User +import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken import dev.usbharu.hideout.domain.model.hideout.form.UserLogin +import dev.usbharu.hideout.exception.InvalidRefreshTokenException import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.service.IJwtService import dev.usbharu.hideout.service.IMetaService @@ -21,17 +23,11 @@ import dev.usbharu.hideout.util.JsonWebKeyUtil import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* -import io.ktor.server.application.* import io.ktor.server.config.* -import io.ktor.server.response.* -import io.ktor.server.routing.* import io.ktor.server.testing.* import org.junit.jupiter.api.Test import org.mockito.ArgumentMatchers.anyString -import org.mockito.kotlin.any -import org.mockito.kotlin.doReturn -import org.mockito.kotlin.eq -import org.mockito.kotlin.mock +import org.mockito.kotlin.* import java.security.KeyPairGenerator import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey @@ -246,7 +242,7 @@ class SecurityKtTest { val jwkProvider = mock { onBlocking { get(anyString()) }.doReturn( Jwk.fromValues( - (readValue["keys"] as List>)[0] + (readValue["keys"] as List>)[0] ) ) } @@ -256,27 +252,294 @@ class SecurityKtTest { configureSerialization() configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider) } - externalServices { - hosts("http://localhost:8080") { - routing { - get("/.well-known/jwks.json") { - call.application.log.info("aaaaaaaaaaaaaa") - println("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") - call.respondText( - contentType = ContentType.Application.Json, - text = JsonWebKeyUtil.publicKeyToJwk(rsaPublicKey, kid.toString()) - ) - } - } - } - } - client.get("/auth-check") { header("Authorization", "Bearer $token") }.apply { assertEquals(HttpStatusCode.OK, call.response.status) - assertEquals("Hello \"test\"",call.response.bodyAsText()) + assertEquals("Hello \"test\"", call.response.bodyAsText()) + } + } + + @Test + fun `auth-check 期限切れのトークンではアクセスできない`() = testApplication { + environment { + config = ApplicationConfig("empty.conf") + } + val keyPairGenerator = KeyPairGenerator.getInstance("RSA") + keyPairGenerator.initialize(2048) + val keyPair = keyPairGenerator.generateKeyPair() + val rsaPublicKey = keyPair.public as RSAPublicKey + + Config.configData = ConfigData(url = "https://localhost", objectMapper = jacksonObjectMapper()) + + + val now = Instant.now() + val kid = UUID.randomUUID() + val token = JWT.create() + .withAudience("${Config.configData.url}/users/test") + .withIssuer(Config.configData.url) + .withKeyId(kid.toString()) + .withClaim("username", "test") + .withExpiresAt(now.minus(30, ChronoUnit.MINUTES)) + .sign(Algorithm.RSA256(rsaPublicKey, keyPair.private as RSAPrivateKey)) + val metaService = mock { + onBlocking { getJwtMeta() }.doReturn( + Jwt( + kid, + Base64Util.encode(keyPair.private.encoded), + Base64Util.encode(rsaPublicKey.encoded) + ) + ) + } + + + val readValue = Config.configData.objectMapper.readerFor(Map::class.java) + .readValue?>( + JsonWebKeyUtil.publicKeyToJwk( + rsaPublicKey, + kid.toString() + ) + ) + val jwkProvider = mock { + onBlocking { get(anyString()) }.doReturn( + Jwk.fromValues( + (readValue["keys"] as List>)[0] + ) + ) + } + val userRepository = mock() + val jwtService = mock() + application { + configureSerialization() + configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider) + } + client.get("/auth-check") { + header("Authorization", "Bearer $token") + }.apply { + assertEquals(HttpStatusCode.Unauthorized, call.response.status) + } + } + + @Test + fun `auth-check issuerが間違っているとアクセスできない`() = testApplication { + environment { + config = ApplicationConfig("empty.conf") + } + val keyPairGenerator = KeyPairGenerator.getInstance("RSA") + keyPairGenerator.initialize(2048) + val keyPair = keyPairGenerator.generateKeyPair() + val rsaPublicKey = keyPair.public as RSAPublicKey + + Config.configData = ConfigData(url = "https://localhost", objectMapper = jacksonObjectMapper()) + + + val now = Instant.now() + val kid = UUID.randomUUID() + val token = JWT.create() + .withAudience("${Config.configData.url}/users/test") + .withIssuer("https://example.com") + .withKeyId(kid.toString()) + .withClaim("username", "test") + .withExpiresAt(now.plus(30, ChronoUnit.MINUTES)) + .sign(Algorithm.RSA256(rsaPublicKey, keyPair.private as RSAPrivateKey)) + val metaService = mock { + onBlocking { getJwtMeta() }.doReturn( + Jwt( + kid, + Base64Util.encode(keyPair.private.encoded), + Base64Util.encode(rsaPublicKey.encoded) + ) + ) + } + + + val readValue = Config.configData.objectMapper.readerFor(Map::class.java) + .readValue?>( + JsonWebKeyUtil.publicKeyToJwk( + rsaPublicKey, + kid.toString() + ) + ) + val jwkProvider = mock { + onBlocking { get(anyString()) }.doReturn( + Jwk.fromValues( + (readValue["keys"] as List>)[0] + ) + ) + } + val userRepository = mock() + val jwtService = mock() + application { + configureSerialization() + configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider) + } + client.get("/auth-check") { + header("Authorization", "Bearer $token") + }.apply { + assertEquals(HttpStatusCode.Unauthorized, call.response.status) + } + } + + @Test + fun `auth-check usernameが空だと失敗する`() = testApplication { + environment { + config = ApplicationConfig("empty.conf") + } + val keyPairGenerator = KeyPairGenerator.getInstance("RSA") + keyPairGenerator.initialize(2048) + val keyPair = keyPairGenerator.generateKeyPair() + val rsaPublicKey = keyPair.public as RSAPublicKey + + Config.configData = ConfigData(url = "https://localhost", objectMapper = jacksonObjectMapper()) + + + val now = Instant.now() + val kid = UUID.randomUUID() + val token = JWT.create() + .withAudience("${Config.configData.url}/users/test") + .withIssuer(Config.configData.url) + .withKeyId(kid.toString()) + .withClaim("username", "") + .withExpiresAt(now.plus(30, ChronoUnit.MINUTES)) + .sign(Algorithm.RSA256(rsaPublicKey, keyPair.private as RSAPrivateKey)) + val metaService = mock { + onBlocking { getJwtMeta() }.doReturn( + Jwt( + kid, + Base64Util.encode(keyPair.private.encoded), + Base64Util.encode(rsaPublicKey.encoded) + ) + ) + } + + + val readValue = Config.configData.objectMapper.readerFor(Map::class.java) + .readValue?>( + JsonWebKeyUtil.publicKeyToJwk( + rsaPublicKey, + kid.toString() + ) + ) + val jwkProvider = mock { + onBlocking { get(anyString()) }.doReturn( + Jwk.fromValues( + (readValue["keys"] as List>)[0] + ) + ) + } + val userRepository = mock() + val jwtService = mock() + application { + configureSerialization() + configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider) + } + client.get("/auth-check") { + header("Authorization", "Bearer $token") + }.apply { + assertEquals(HttpStatusCode.Unauthorized, call.response.status) + } + } + + @Test + fun `auth-check usernameが存在しないと失敗する`() = testApplication { + environment { + config = ApplicationConfig("empty.conf") + } + val keyPairGenerator = KeyPairGenerator.getInstance("RSA") + keyPairGenerator.initialize(2048) + val keyPair = keyPairGenerator.generateKeyPair() + val rsaPublicKey = keyPair.public as RSAPublicKey + + Config.configData = ConfigData(url = "https://localhost", objectMapper = jacksonObjectMapper()) + + + val now = Instant.now() + val kid = UUID.randomUUID() + val token = JWT.create() + .withAudience("${Config.configData.url}/users/test") + .withIssuer(Config.configData.url) + .withKeyId(kid.toString()) + .withExpiresAt(now.plus(30, ChronoUnit.MINUTES)) + .sign(Algorithm.RSA256(rsaPublicKey, keyPair.private as RSAPrivateKey)) + val metaService = mock { + onBlocking { getJwtMeta() }.doReturn( + Jwt( + kid, + Base64Util.encode(keyPair.private.encoded), + Base64Util.encode(rsaPublicKey.encoded) + ) + ) + } + + + val readValue = Config.configData.objectMapper.readerFor(Map::class.java) + .readValue?>( + JsonWebKeyUtil.publicKeyToJwk( + rsaPublicKey, + kid.toString() + ) + ) + val jwkProvider = mock { + onBlocking { get(anyString()) }.doReturn( + Jwk.fromValues( + (readValue["keys"] as List>)[0] + ) + ) + } + val userRepository = mock() + val jwtService = mock() + application { + configureSerialization() + configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider) + } + client.get("/auth-check") { + header("Authorization", "Bearer $token") + }.apply { + assertEquals(HttpStatusCode.Unauthorized, call.response.status) + } + } + + @Test + fun `refresh-token リフレッシュトークンが正当だとトークンを発行する`() = testApplication { + Config.configData = ConfigData(url = "https://localhost", objectMapper = jacksonObjectMapper()) + environment { + config = ApplicationConfig("empty.conf") + } + val jwtService = mock { + onBlocking { refreshToken(any()) }.doReturn(JwtToken("token", "refreshToken2")) + } + application { + configureSerialization() + configureSecurity(mock(), mock(), mock(), jwtService, mock()) + } + client.post("/refresh-token") { + header("Content-Type", "application/json") + setBody(Config.configData.objectMapper.writeValueAsString(RefreshToken("refreshToken"))) + }.apply { + assertEquals(HttpStatusCode.OK, call.response.status) + } + } + + @Test + fun `refresh-token リフレッシュトークンが不正だと失敗する`() = testApplication { + Config.configData = ConfigData(url = "https://localhost", objectMapper = jacksonObjectMapper()) + environment { + config = ApplicationConfig("empty.conf") + } + val jwtService = mock { + onBlocking { refreshToken(any()) } doThrow InvalidRefreshTokenException("Invalid Refresh Token") + } + application { + configureStatusPages() + configureSerialization() + configureSecurity(mock(), mock(), mock(), jwtService, mock()) + } + client.post("/refresh-token") { + header("Content-Type", "application/json") + setBody(Config.configData.objectMapper.writeValueAsString(RefreshToken("InvalidRefreshToken"))) + }.apply { + assertEquals(HttpStatusCode.BadRequest, call.response.status) } } }