test: JWTでログインのテストを追加

This commit is contained in:
usbharu 2023-05-03 14:53:35 +09:00
parent a053a17924
commit c50f8a0594
6 changed files with 307 additions and 35 deletions

View File

@ -1,5 +1,7 @@
package dev.usbharu.hideout package dev.usbharu.hideout
import com.auth0.jwk.JwkProvider
import com.auth0.jwk.JwkProviderBuilder
import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
@ -29,6 +31,7 @@ import kjob.core.kjob
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.koin.ktor.ext.inject import org.koin.ktor.ext.inject
import java.util.concurrent.TimeUnit
fun main(args: Array<String>): Unit = io.ktor.server.cio.EngineMain.main(args) fun main(args: Array<String>): Unit = io.ktor.server.cio.EngineMain.main(args)
@ -89,6 +92,14 @@ fun Application.parent() {
single<IJwtRefreshTokenRepository> { JwtRefreshTokenRepositoryImpl(get(), get()) } single<IJwtRefreshTokenRepository> { JwtRefreshTokenRepositoryImpl(get(), get()) }
single<IMetaService> { MetaServiceImpl(get()) } single<IMetaService> { MetaServiceImpl(get()) }
single<IJwtService> { JwtServiceImpl(get(), get(), get()) } single<IJwtService> { JwtServiceImpl(get(), get(), get()) }
single<JwkProvider> {
JwkProviderBuilder(Config.configData.url).cached(
10,
24,
TimeUnit.HOURS
)
.rateLimited(10, 1, TimeUnit.MINUTES).build()
}
} }
configureKoin(module) configureKoin(module)
runBlocking { runBlocking {
@ -103,7 +114,8 @@ fun Application.parent() {
inject<IUserAuthService>().value, inject<IUserAuthService>().value,
inject<IMetaService>().value, inject<IMetaService>().value,
inject<IUserRepository>().value, inject<IUserRepository>().value,
inject<IJwtService>().value inject<IJwtService>().value,
inject<JwkProvider>().value,
) )
configureRouting( configureRouting(
inject<HttpSignatureVerifyService>().value, inject<HttpSignatureVerifyService>().value,

View File

@ -4,7 +4,6 @@ import dev.usbharu.hideout.routing.activitypub.inbox
import dev.usbharu.hideout.routing.activitypub.outbox import dev.usbharu.hideout.routing.activitypub.outbox
import dev.usbharu.hideout.routing.activitypub.usersAP import dev.usbharu.hideout.routing.activitypub.usersAP
import dev.usbharu.hideout.routing.api.v1.statuses import dev.usbharu.hideout.routing.api.v1.statuses
import dev.usbharu.hideout.routing.authTestRouting
import dev.usbharu.hideout.routing.wellknown.webfinger import dev.usbharu.hideout.routing.wellknown.webfinger
import dev.usbharu.hideout.service.IPostService import dev.usbharu.hideout.service.IPostService
import dev.usbharu.hideout.service.activitypub.ActivityPubService import dev.usbharu.hideout.service.activitypub.ActivityPubService
@ -32,7 +31,5 @@ fun Application.configureRouting(
route("/api/v1") { route("/api/v1") {
statuses(postService) statuses(postService)
} }
authTestRouting()
} }
} }

View File

@ -1,13 +1,10 @@
@file:Suppress("UnusedPrivateMember")
package dev.usbharu.hideout.plugins package dev.usbharu.hideout.plugins
import com.auth0.jwk.JwkProviderBuilder import com.auth0.jwk.JwkProvider
import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken
import dev.usbharu.hideout.domain.model.hideout.form.UserLogin import dev.usbharu.hideout.domain.model.hideout.form.UserLogin
import dev.usbharu.hideout.exception.UserNotFoundException import dev.usbharu.hideout.exception.UserNotFoundException
import dev.usbharu.hideout.property
import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.service.IJwtService import dev.usbharu.hideout.service.IJwtService
import dev.usbharu.hideout.service.IMetaService import dev.usbharu.hideout.service.IMetaService
@ -20,7 +17,6 @@ import io.ktor.server.auth.jwt.*
import io.ktor.server.request.* import io.ktor.server.request.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import java.util.concurrent.TimeUnit
const val TOKEN_AUTH = "jwt-auth" const val TOKEN_AUTH = "jwt-auth"
@ -29,13 +25,10 @@ fun Application.configureSecurity(
userAuthService: IUserAuthService, userAuthService: IUserAuthService,
metaService: IMetaService, metaService: IMetaService,
userRepository: IUserRepository, userRepository: IUserRepository,
jwtService: IJwtService jwtService: IJwtService,
jwkProvider: JwkProvider
) { ) {
val issuer = property("hideout.url") val issuer = Config.configData.url
val jwkProvider = JwkProviderBuilder(issuer)
.cached(10, 24, TimeUnit.HOURS)
.rateLimited(10, 1, TimeUnit.MINUTES)
.build()
install(Authentication) { install(Authentication) {
jwt(TOKEN_AUTH) { jwt(TOKEN_AUTH) {
verifier(jwkProvider, issuer) { verifier(jwkProvider, issuer) {
@ -78,5 +71,12 @@ fun Application.configureSecurity(
text = JsonWebKeyUtil.publicKeyToJwk(jwt.publicKey, jwt.kid.toString()) text = JsonWebKeyUtil.publicKeyToJwk(jwt.publicKey, jwt.kid.toString())
) )
} }
authenticate(TOKEN_AUTH) {
get("/auth-check") {
val principal = call.principal<JWTPrincipal>()
val username = principal!!.payload.getClaim("username")
call.respondText("Hello $username")
}
}
} }
} }

View File

@ -1,18 +0,0 @@
package dev.usbharu.hideout.routing
import dev.usbharu.hideout.plugins.TOKEN_AUTH
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Routing.authTestRouting() {
authenticate(TOKEN_AUTH) {
get("/auth-check") {
val principal = call.principal<JWTPrincipal>()
val username = principal!!.payload.getClaim("username")
call.respondText("Hello $username")
}
}
}

View File

@ -0,0 +1,282 @@
package dev.usbharu.hideout.plugins
import com.auth0.jwk.Jwk
import com.auth0.jwk.JwkProvider
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.config.Config
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.UserLogin
import dev.usbharu.hideout.repository.IUserRepository
import dev.usbharu.hideout.service.IJwtService
import dev.usbharu.hideout.service.IMetaService
import dev.usbharu.hideout.service.IUserAuthService
import dev.usbharu.hideout.util.Base64Util
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 java.security.KeyPairGenerator
import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.*
import kotlin.test.assertEquals
class SecurityKtTest {
@Test
fun `login ログイン出来るか`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
val userAuthService = mock<IUserAuthService> {
onBlocking { verifyAccount(eq("testUser"), eq("password")) } doReturn true
}
val metaService = mock<IMetaService>()
val userRepository = mock<IUserRepository> {
onBlocking { findByNameAndDomain(eq("testUser"), eq("example.com")) } doReturn User(
id = 1L,
name = "testUser",
domain = "example.com",
screenName = "test",
description = "",
password = "hashedPassword",
inbox = "https://example.com/inbox",
outbox = "https://example.com/outbox",
url = "https://example.com/profile",
publicKey = "",
privateKey = "",
createdAt = Instant.now()
)
}
val jwtToken = JwtToken("Token", "RefreshToken")
val jwtService = mock<IJwtService> {
onBlocking { createToken(any()) } doReturn jwtToken
}
val jwkProvider = mock<JwkProvider>()
application {
configureSerialization()
configureSecurity(userAuthService, metaService, userRepository, jwtService, jwkProvider)
}
client.post("/login") {
contentType(ContentType.Application.Json)
setBody(Config.configData.objectMapper.writeValueAsString(UserLogin("testUser", "password")))
}.apply {
assertEquals(HttpStatusCode.OK, call.response.status)
assertEquals(jwtToken, Config.configData.objectMapper.readValue(call.response.bodyAsText()))
}
}
@Test
fun `login 存在しないユーザーのログインに失敗する`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
val userAuthService = mock<IUserAuthService> {
onBlocking { verifyAccount(anyString(), anyString()) }.doReturn(false)
}
val metaService = mock<IMetaService>()
val userRepository = mock<IUserRepository>()
val jwtService = mock<IJwtService>()
val jwkProvider = mock<JwkProvider>()
application {
configureSerialization()
configureSecurity(userAuthService, metaService, userRepository, jwtService, jwkProvider)
}
client.post("/login") {
contentType(ContentType.Application.Json)
setBody(Config.configData.objectMapper.writeValueAsString(UserLogin("InvalidTtestUser", "password")))
}.apply {
assertEquals(HttpStatusCode.Unauthorized, call.response.status)
}
}
@Test
fun `login 不正なパスワードのログインに失敗する`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
val userAuthService = mock<IUserAuthService> {
onBlocking { verifyAccount(anyString(), eq("InvalidPassword")) } doReturn false
}
val metaService = mock<IMetaService>()
val userRepository = mock<IUserRepository>()
val jwtService = mock<IJwtService>()
val jwkProvider = mock<JwkProvider>()
application {
configureSerialization()
configureSecurity(userAuthService, metaService, userRepository, jwtService, jwkProvider)
}
client.post("/login") {
contentType(ContentType.Application.Json)
setBody(Config.configData.objectMapper.writeValueAsString(UserLogin("TestUser", "InvalidPassword")))
}.apply {
assertEquals(HttpStatusCode.Unauthorized, call.response.status)
}
}
@Test
fun `auth-check Authorizedヘッダーが無いと401が帰ってくる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
application {
configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock())
}
client.get("/auth-check").apply {
assertEquals(HttpStatusCode.Unauthorized, call.response.status)
}
}
@Test
fun `auth-check Authorizedヘッダーの形式が間違っていると401が帰ってくる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
application {
configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock())
}
client.get("/auth-check") {
header("Authorization", "Digest dsfjjhogalkjdfmlhaog")
}.apply {
assertEquals(HttpStatusCode.Unauthorized, call.response.status)
}
}
@Test
fun `auth-check Authorizedヘッダーが空だと401が帰ってくる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
application {
configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock())
}
client.get("/auth-check") {
header("Authorization", "")
}.apply {
assertEquals(HttpStatusCode.Unauthorized, call.response.status)
}
}
@Test
fun `auth-check AuthorizedヘッダーがBeararで空だと401が帰ってくる`() = testApplication {
environment {
config = ApplicationConfig("empty.conf")
}
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
application {
configureSerialization()
configureSecurity(mock(), mock(), mock(), mock(), mock())
}
client.get("/auth-check") {
header("Authorization", "Bearer ")
}.apply {
assertEquals(HttpStatusCode.Unauthorized, call.response.status)
}
}
@Test
fun `auth-check 正当なJWTだとアクセスできる`() = 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.plus(30, ChronoUnit.MINUTES))
.sign(Algorithm.RSA256(rsaPublicKey, keyPair.private as RSAPrivateKey))
val metaService = mock<IMetaService> {
onBlocking { getJwtMeta() }.doReturn(
Jwt(
kid,
Base64Util.encode(keyPair.private.encoded),
Base64Util.encode(rsaPublicKey.encoded)
)
)
}
val readValue = Config.configData.objectMapper.readerFor(Map::class.java)
.readValue<MutableMap<String, Any>?>(
JsonWebKeyUtil.publicKeyToJwk(
rsaPublicKey,
kid.toString()
)
)
val jwkProvider = mock<JwkProvider> {
onBlocking { get(anyString()) }.doReturn(
Jwk.fromValues(
(readValue["keys"] as List<Map<String,Any>>)[0]
)
)
}
val userRepository = mock<IUserRepository>()
val jwtService = mock<IJwtService>()
application {
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())
}
}
}

View File

@ -10,8 +10,7 @@ ktor {
} }
hideout { hideout {
hostname = "https://localhost:8080" url = "http://localhost:8080"
hostname = ${?HOSTNAME}
database { database {
url = "jdbc:h2:./test;MODE=POSTGRESQL" url = "jdbc:h2:./test;MODE=POSTGRESQL"
driver = "org.h2.Driver" driver = "org.h2.Driver"