mirror of https://github.com/usbharu/Hideout.git
test: JWTでログインのテストを追加
This commit is contained in:
parent
a053a17924
commit
c50f8a0594
|
@ -1,5 +1,7 @@
|
|||
package dev.usbharu.hideout
|
||||
|
||||
import com.auth0.jwk.JwkProvider
|
||||
import com.auth0.jwk.JwkProviderBuilder
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
|
@ -29,6 +31,7 @@ import kjob.core.kjob
|
|||
import kotlinx.coroutines.runBlocking
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.koin.ktor.ext.inject
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
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<IMetaService> { MetaServiceImpl(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)
|
||||
runBlocking {
|
||||
|
@ -103,7 +114,8 @@ fun Application.parent() {
|
|||
inject<IUserAuthService>().value,
|
||||
inject<IMetaService>().value,
|
||||
inject<IUserRepository>().value,
|
||||
inject<IJwtService>().value
|
||||
inject<IJwtService>().value,
|
||||
inject<JwkProvider>().value,
|
||||
)
|
||||
configureRouting(
|
||||
inject<HttpSignatureVerifyService>().value,
|
||||
|
|
|
@ -4,7 +4,6 @@ import dev.usbharu.hideout.routing.activitypub.inbox
|
|||
import dev.usbharu.hideout.routing.activitypub.outbox
|
||||
import dev.usbharu.hideout.routing.activitypub.usersAP
|
||||
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.service.IPostService
|
||||
import dev.usbharu.hideout.service.activitypub.ActivityPubService
|
||||
|
@ -32,7 +31,5 @@ fun Application.configureRouting(
|
|||
route("/api/v1") {
|
||||
statuses(postService)
|
||||
}
|
||||
|
||||
authTestRouting()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
@file:Suppress("UnusedPrivateMember")
|
||||
|
||||
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.domain.model.hideout.form.RefreshToken
|
||||
import dev.usbharu.hideout.domain.model.hideout.form.UserLogin
|
||||
import dev.usbharu.hideout.exception.UserNotFoundException
|
||||
import dev.usbharu.hideout.property
|
||||
import dev.usbharu.hideout.repository.IUserRepository
|
||||
import dev.usbharu.hideout.service.IJwtService
|
||||
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.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
const val TOKEN_AUTH = "jwt-auth"
|
||||
|
||||
|
@ -29,13 +25,10 @@ fun Application.configureSecurity(
|
|||
userAuthService: IUserAuthService,
|
||||
metaService: IMetaService,
|
||||
userRepository: IUserRepository,
|
||||
jwtService: IJwtService
|
||||
jwtService: IJwtService,
|
||||
jwkProvider: JwkProvider
|
||||
) {
|
||||
val issuer = property("hideout.url")
|
||||
val jwkProvider = JwkProviderBuilder(issuer)
|
||||
.cached(10, 24, TimeUnit.HOURS)
|
||||
.rateLimited(10, 1, TimeUnit.MINUTES)
|
||||
.build()
|
||||
val issuer = Config.configData.url
|
||||
install(Authentication) {
|
||||
jwt(TOKEN_AUTH) {
|
||||
verifier(jwkProvider, issuer) {
|
||||
|
@ -78,5 +71,12 @@ fun Application.configureSecurity(
|
|||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,8 +10,7 @@ ktor {
|
|||
}
|
||||
|
||||
hideout {
|
||||
hostname = "https://localhost:8080"
|
||||
hostname = ${?HOSTNAME}
|
||||
url = "http://localhost:8080"
|
||||
database {
|
||||
url = "jdbc:h2:./test;MODE=POSTGRESQL"
|
||||
driver = "org.h2.Driver"
|
||||
|
|
Loading…
Reference in New Issue