From 19dc00236f343fe5af48b2bc64111e9a9f092ca5 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 30 Apr 2023 13:47:59 +0900 Subject: [PATCH 01/17] =?UTF-8?q?chore:=20=E4=BE=9D=E5=AD=98=E3=81=ABktor-?= =?UTF-8?q?auth-jwt=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 25d07a41..d347a77a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -45,7 +45,8 @@ kotlin { dependencies { implementation("io.ktor:ktor-server-core-jvm:$ktor_version") - implementation("io.ktor:ktor-server-auth-jvm:$ktor_version") + implementation("io.ktor:ktor-server-auth:$ktor_version") + implementation("io.ktor:ktor-server-auth-jwt:$ktor_version") implementation("io.ktor:ktor-server-sessions-jvm:$ktor_version") implementation("io.ktor:ktor-server-auto-head-response-jvm:$ktor_version") implementation("io.ktor:ktor-server-cors-jvm:$ktor_version") From aad86a31a56efc6a151dc751a9cfe1b1b0ed854b Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sun, 30 Apr 2023 17:05:10 +0900 Subject: [PATCH 02/17] =?UTF-8?q?feat:=20=E3=82=B5=E3=83=B3=E3=83=97?= =?UTF-8?q?=E3=83=AB=E3=81=AE=E5=80=A4=E3=82=92=E4=BD=BF=E3=81=A3=E3=81=A6?= =?UTF-8?q?JWT=E3=81=A7=E3=83=AD=E3=82=B0=E3=82=A4=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=83=BC=E3=82=AF=E3=83=B3=E3=82=92=E7=99=BA=E8=A1=8C=E3=81=A7?= =?UTF-8?q?=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/dev/usbharu/hideout/Application.kt | 1 + .../domain/model/hideout/form/UserLogin.kt | 3 + .../dev/usbharu/hideout/plugins/Security.kt | 82 ++++++++++++++++--- .../usbharu/hideout/routing/LoginRouting.kt | 8 ++ src/main/resources/application.conf | 7 ++ 5 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/UserLogin.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/routing/LoginRouting.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/Application.kt b/src/main/kotlin/dev/usbharu/hideout/Application.kt index de723c6b..9fb82629 100644 --- a/src/main/kotlin/dev/usbharu/hideout/Application.kt +++ b/src/main/kotlin/dev/usbharu/hideout/Application.kt @@ -97,6 +97,7 @@ fun Application.parent() { configureMonitoring() configureSerialization() register(inject().value) + configureSecurity(inject().value) configureRouting( inject().value, inject().value, diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/UserLogin.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/UserLogin.kt new file mode 100644 index 00000000..d9d5fc4d --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/UserLogin.kt @@ -0,0 +1,3 @@ +package dev.usbharu.hideout.domain.model.hideout.form + +data class UserLogin(val username: String, val password: String) diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt index da66a042..e5a3766f 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt @@ -2,24 +2,86 @@ package dev.usbharu.hideout.plugins +import com.auth0.jwk.JwkProviderBuilder +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import dev.usbharu.hideout.domain.model.hideout.form.UserLogin +import dev.usbharu.hideout.property import dev.usbharu.hideout.service.IUserAuthService +import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* +import io.ktor.server.auth.jwt.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import java.security.KeyFactory +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey +import java.security.spec.PKCS8EncodedKeySpec +import java.util.* +import java.util.concurrent.TimeUnit -const val TOKEN_AUTH = "token-auth" +const val TOKEN_AUTH = "jwt-auth" fun Application.configureSecurity(userAuthService: IUserAuthService) { + + val privateKeyString = property("jwt.privateKey") + val issuer = property("jwt.issuer") +// val audience = property("jwt.audience") + val myRealm = property("jwt.realm") + val jwkProvider = JwkProviderBuilder(issuer) + .cached(10, 24, TimeUnit.HOURS) + .rateLimited(10, 1, TimeUnit.MINUTES) + .build() install(Authentication) { - bearer(TOKEN_AUTH) { - authenticate { bearerTokenCredential -> - UserIdPrincipal(bearerTokenCredential.token) + jwt(TOKEN_AUTH) { + realm = myRealm + verifier(jwkProvider, issuer) { + acceptLeeway(3) + } + validate { jwtCredential -> + if (jwtCredential.payload.getClaim("username").asString().isNotEmpty()) { + JWTPrincipal(jwtCredential.payload) + } else { + null + } } - skipWhen { true } } } -// install(Sessions) { -// cookie("MY_SESSION") { -// cookie.extensions["SameSite"] = "lax" -// } -// } + + routing { + post("/login") { + val user = call.receive() + val check = userAuthService.verifyAccount(user.username, user.password) + if (check.not()) { + return@post call.respond(HttpStatusCode.Unauthorized) + } + + val publicKey = jwkProvider.get("6f8856ed-9189-488f-9011-0ff4b6c08edc").publicKey + val keySpecPKCS8 = PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyString)) + val privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpecPKCS8) + val token = JWT.create() +// .withAudience(audience) +// .withIssuer(issuer) + .withClaim("username", user.username) + .withExpiresAt(Date(System.currentTimeMillis() + 60000)) + .sign(Algorithm.RSA256(publicKey as RSAPublicKey, privateKey as RSAPrivateKey)) + return@post call.respond(hashSetOf("token" to token)) + } + + get("/.well-known/jwks.json"){ + //language=JSON + call.respondText(contentType = ContentType.Application.Json,text = """{ + "keys": [ + { + "kty": "RSA", + "e": "AQAB", + "kid": "6f8856ed-9189-488f-9011-0ff4b6c08edc", + "n":"tfJaLrzXILUg1U3N1KV8yJr92GHn5OtYZR7qWk1Mc4cy4JGjklYup7weMjBD9f3bBVoIsiUVX6xNcYIr0Ie0AQ" + } + ] +}""") + } + } } diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/LoginRouting.kt b/src/main/kotlin/dev/usbharu/hideout/routing/LoginRouting.kt new file mode 100644 index 00000000..e0db266b --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/routing/LoginRouting.kt @@ -0,0 +1,8 @@ +package dev.usbharu.hideout.routing + +import dev.usbharu.hideout.service.IUserAuthService +import io.ktor.server.routing.* + +fun Routing.login(userAuthService: IUserAuthService){ + +} diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index beb8fa3b..db3cc28b 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -20,3 +20,10 @@ hideout { password = "" } } + +jwt { + privateKey = "MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAtfJaLrzXILUg1U3N1KV8yJr92GHn5OtYZR7qWk1Mc4cy4JGjklYup7weMjBD9f3bBVoIsiUVX6xNcYIr0Ie0AQIDAQABAkEAg+FBquToDeYcAWBe1EaLVyC45HG60zwfG1S4S3IB+y4INz1FHuZppDjBh09jptQNd+kSMlG1LkAc/3znKTPJ7QIhANpyB0OfTK44lpH4ScJmCxjZV52mIrQcmnS3QzkxWQCDAiEA1Tn7qyoh+0rOO/9vJHP8U/beo51SiQMw0880a1UaiisCIQDNwY46EbhGeiLJR1cidr+JHl86rRwPDsolmeEF5AdzRQIgK3KXL3d0WSoS//K6iOkBX3KMRzaFXNnDl0U/XyeGMuUCIHaXv+n+Brz5BDnRbWS+2vkgIe9bUNlkiArpjWvX+2we" + issuer = "http://0.0.0.0:8080/" + audience = "http://0.0.0.0:8080/hello" + realm = "Access to 'hello'" +} From 3e151f5a57a3742b37c5163e046a98591e548a32 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Mon, 1 May 2023 06:38:30 +0900 Subject: [PATCH 03/17] =?UTF-8?q?feat:=20=E5=88=9D=E5=9B=9E=E8=B5=B7?= =?UTF-8?q?=E5=8B=95=E3=83=81=E3=82=A7=E3=83=83=E3=82=AF=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 10 +++ .../kotlin/dev/usbharu/hideout/Application.kt | 19 +++--- .../domain/model/hideout/entity/Meta.kt | 3 + .../hideout/repository/IMetaRepository.kt | 10 +++ .../hideout/repository/MetaRepositoryImpl.kt | 62 +++++++++++++++++++ .../service/IServerInitialiseService.kt | 5 ++ .../service/ServerInitialiseServiceImpl.kt | 56 +++++++++++++++++ .../dev/usbharu/hideout/util/ServerUtil.kt | 5 ++ 8 files changed, 160 insertions(+), 10 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Meta.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/repository/IMetaRepository.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/repository/MetaRepositoryImpl.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/service/IServerInitialiseService.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/service/ServerInitialiseServiceImpl.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/util/ServerUtil.kt diff --git a/build.gradle.kts b/build.gradle.kts index d347a77a..b7cca720 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,5 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + val ktor_version: String by project val kotlin_version: String by project val logback_version: String by project @@ -31,6 +33,14 @@ tasks.withType>().con compilerOptions.apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_8) } +tasks.withType { + manifest { + attributes( + "Implementation-Version" to project.version.toString() + ) + } +} + repositories { mavenCentral() } diff --git a/src/main/kotlin/dev/usbharu/hideout/Application.kt b/src/main/kotlin/dev/usbharu/hideout/Application.kt index 9fb82629..4d3180a9 100644 --- a/src/main/kotlin/dev/usbharu/hideout/Application.kt +++ b/src/main/kotlin/dev/usbharu/hideout/Application.kt @@ -8,15 +8,9 @@ import dev.usbharu.hideout.config.ConfigData import dev.usbharu.hideout.domain.model.job.DeliverPostJob import dev.usbharu.hideout.domain.model.job.ReceiveFollowJob import dev.usbharu.hideout.plugins.* -import dev.usbharu.hideout.repository.IPostRepository -import dev.usbharu.hideout.repository.IUserRepository -import dev.usbharu.hideout.repository.PostRepositoryImpl -import dev.usbharu.hideout.repository.UserRepository +import dev.usbharu.hideout.repository.* import dev.usbharu.hideout.routing.register -import dev.usbharu.hideout.service.IPostService -import dev.usbharu.hideout.service.IUserAuthService -import dev.usbharu.hideout.service.IdGenerateService -import dev.usbharu.hideout.service.TwitterSnowflakeIdGenerateService +import dev.usbharu.hideout.service.* import dev.usbharu.hideout.service.activitypub.* import dev.usbharu.hideout.service.impl.IUserService import dev.usbharu.hideout.service.impl.PostService @@ -32,6 +26,7 @@ import io.ktor.client.engine.cio.* import io.ktor.client.plugins.logging.* import io.ktor.server.application.* import kjob.core.kjob +import kotlinx.coroutines.runBlocking import org.jetbrains.exposed.sql.Database import org.koin.ktor.ext.inject @@ -89,15 +84,19 @@ fun Application.parent() { single { PostService(get(), get()) } single { PostRepositoryImpl(get(), get()) } single { TwitterSnowflakeIdGenerateService } + single{ MetaRepositoryImpl(get()) } + single { ServerInitialiseServiceImpl(get()) } } - configureKoin(module) + runBlocking { + inject().value.init() + } configureHTTP() configureStaticRouting() configureMonitoring() configureSerialization() register(inject().value) - configureSecurity(inject().value) + configureSecurity(inject().value,inject().value) configureRouting( inject().value, inject().value, diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Meta.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Meta.kt new file mode 100644 index 00000000..f1fa6d5f --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Meta.kt @@ -0,0 +1,3 @@ +package dev.usbharu.hideout.domain.model.hideout.entity + +data class Meta(val version:String,val jwt:Jwt) diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/IMetaRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/IMetaRepository.kt new file mode 100644 index 00000000..b3ed940f --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/repository/IMetaRepository.kt @@ -0,0 +1,10 @@ +package dev.usbharu.hideout.repository + +import dev.usbharu.hideout.domain.model.hideout.entity.Meta + +interface IMetaRepository { + + suspend fun save(meta: Meta) + + suspend fun get():Meta? +} diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/MetaRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/repository/MetaRepositoryImpl.kt new file mode 100644 index 00000000..5a537ae7 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/repository/MetaRepositoryImpl.kt @@ -0,0 +1,62 @@ +package dev.usbharu.hideout.repository + +import dev.usbharu.hideout.domain.model.hideout.entity.Jwt +import kotlinx.coroutines.Dispatchers +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction +import org.jetbrains.exposed.sql.transactions.transaction +import java.util.* + +class MetaRepositoryImpl(private val database: Database) : IMetaRepository { + + init { + transaction(database) { + SchemaUtils.create(Meta) + SchemaUtils.createMissingTablesAndColumns(Meta) + } + } + + suspend fun query(block: suspend () -> T): T = + newSuspendedTransaction(Dispatchers.IO) { block() } + + override suspend fun save(meta: dev.usbharu.hideout.domain.model.hideout.entity.Meta) { + return query { + if (Meta.select { Meta.id eq 1 }.empty()) { + Meta.insert { + it[id] = 1 + it[this.version] = meta.version + it[kid] = UUID.randomUUID().toString() + it[this.jwtPrivateKey] = meta.jwt.privateKey + it[this.jwtPublicKey] = meta.jwt.publicKey + } + }else { + Meta.update({ Meta.id eq 1 }) { + it[this.version] = meta.version + it[kid] = UUID.randomUUID().toString() + it[this.jwtPrivateKey] = meta.jwt.privateKey + it[this.jwtPublicKey] = meta.jwt.publicKey + } + } + } + } + + override suspend fun get(): dev.usbharu.hideout.domain.model.hideout.entity.Meta? { + return query { + Meta.select { Meta.id eq 1 }.singleOrNull()?.let { + dev.usbharu.hideout.domain.model.hideout.entity.Meta( + it[Meta.version], + Jwt(UUID.fromString(it[Meta.kid]), it[Meta.jwtPrivateKey], it[Meta.jwtPublicKey]) + ) + } + } + } +} + +object Meta : Table("meta_info") { + val id = long("id") + val version = varchar("version", 1000) + val kid = varchar("kid", 1000) + val jwtPrivateKey = varchar("jwt_private_key", 100000) + val jwtPublicKey = varchar("jwt_public_key", 100000) + override val primaryKey: PrimaryKey = PrimaryKey(id) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/IServerInitialiseService.kt b/src/main/kotlin/dev/usbharu/hideout/service/IServerInitialiseService.kt new file mode 100644 index 00000000..49a613fd --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/IServerInitialiseService.kt @@ -0,0 +1,5 @@ +package dev.usbharu.hideout.service + +interface IServerInitialiseService { + suspend fun init() +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ServerInitialiseServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/ServerInitialiseServiceImpl.kt new file mode 100644 index 00000000..39ea048e --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/ServerInitialiseServiceImpl.kt @@ -0,0 +1,56 @@ +package dev.usbharu.hideout.service + +import dev.usbharu.hideout.domain.model.hideout.entity.Jwt +import dev.usbharu.hideout.domain.model.hideout.entity.Meta +import dev.usbharu.hideout.repository.IMetaRepository +import dev.usbharu.hideout.util.ServerUtil +import org.slf4j.LoggerFactory +import java.security.KeyPairGenerator +import java.util.* + +class ServerInitialiseServiceImpl(private val metaRepository: IMetaRepository) : IServerInitialiseService { + + val logger = LoggerFactory.getLogger(ServerInitialiseServiceImpl::class.java) + + override suspend fun init() { + + val savedMeta = metaRepository.get() + val implementationVersion = ServerUtil.getImplementationVersion() + if (wasInitialised(savedMeta).not()) { + logger.info("Start Initialise") + initialise(implementationVersion) + logger.info("Finish Initialise") + return + } + + if (isVersionChanged(savedMeta!!)) { + logger.info("Version changed!! (${savedMeta.version} -> $implementationVersion)") + updateVersion(savedMeta, implementationVersion) + } + + } + + private fun wasInitialised(meta: Meta?): Boolean { + logger.debug("Initialise checking...") + return meta != null + } + + private fun isVersionChanged(meta: Meta): Boolean = meta.version != ServerUtil.getImplementationVersion() + + private suspend fun initialise(implementationVersion: String) { + val keyPairGenerator = KeyPairGenerator.getInstance("RSA") + keyPairGenerator.initialize(2048) + val generateKeyPair = keyPairGenerator.generateKeyPair() + val jwt = Jwt( + UUID.randomUUID(), + Base64.getEncoder().encodeToString(generateKeyPair.public.encoded), + Base64.getEncoder().encodeToString(generateKeyPair.private.encoded) + ) + val meta = Meta(implementationVersion, jwt) + metaRepository.save(meta) + } + + private suspend fun updateVersion(meta: Meta, version: String) { + metaRepository.save(meta.copy(version = version)) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/util/ServerUtil.kt b/src/main/kotlin/dev/usbharu/hideout/util/ServerUtil.kt new file mode 100644 index 00000000..438f7e33 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/util/ServerUtil.kt @@ -0,0 +1,5 @@ +package dev.usbharu.hideout.util + +object ServerUtil { + fun getImplementationVersion():String = ServerUtil.javaClass.`package`.implementationVersion ?: "DEVELOPMENT-VERSION" +} From 7dd3c185bb08a4d0358f84a7e19bb08813cf47f2 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Mon, 1 May 2023 07:09:26 +0900 Subject: [PATCH 04/17] =?UTF-8?q?feat:=20=E8=87=AA=E5=8B=95=E3=81=A7?= =?UTF-8?q?=E9=8D=B5=E3=82=92=E4=BD=9C=E6=88=90=E3=81=99=E3=82=8B=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/model/hideout/entity/Jwt.kt | 5 +++ .../dev/usbharu/hideout/plugins/Security.kt | 39 ++++++++++-------- .../service/ServerInitialiseServiceImpl.kt | 4 +- .../usbharu/hideout/util/JsonWebKeyUtil.kt | 41 +++++++++++++++++++ .../dev/usbharu/hideout/util/RsaUtil.kt | 19 +++++++++ 5 files changed, 88 insertions(+), 20 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Jwt.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/util/JsonWebKeyUtil.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/util/RsaUtil.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Jwt.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Jwt.kt new file mode 100644 index 00000000..07f3ad55 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Jwt.kt @@ -0,0 +1,5 @@ +package dev.usbharu.hideout.domain.model.hideout.entity + +import java.util.* + +data class Jwt(val kid: UUID, val privateKey: String, val publicKey: String) diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt index e5a3766f..bb487c6f 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt @@ -7,7 +7,10 @@ import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm import dev.usbharu.hideout.domain.model.hideout.form.UserLogin import dev.usbharu.hideout.property +import dev.usbharu.hideout.repository.IMetaRepository import dev.usbharu.hideout.service.IUserAuthService +import dev.usbharu.hideout.util.JsonWebKeyUtil +import dev.usbharu.hideout.util.RsaUtil import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* @@ -15,19 +18,27 @@ import io.ktor.server.auth.jwt.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* +import kotlinx.coroutines.runBlocking import java.security.KeyFactory import java.security.interfaces.RSAPrivateKey -import java.security.interfaces.RSAPublicKey import java.security.spec.PKCS8EncodedKeySpec import java.util.* import java.util.concurrent.TimeUnit const val TOKEN_AUTH = "jwt-auth" -fun Application.configureSecurity(userAuthService: IUserAuthService) { +fun Application.configureSecurity(userAuthService: IUserAuthService, metaRepository: IMetaRepository) { - val privateKeyString = property("jwt.privateKey") - val issuer = property("jwt.issuer") + val privateKeyString = runBlocking { + requireNotNull(metaRepository.get()).jwt.privateKey + } + val publicKey = runBlocking { + val publicKey = requireNotNull(metaRepository.get()).jwt.publicKey + println(publicKey) + RsaUtil.decodeRsaPublicKey(Base64.getDecoder().decode(publicKey)) + } + println(privateKeyString) + val issuer = property("hideout.url") // val audience = property("jwt.audience") val myRealm = property("jwt.realm") val jwkProvider = JwkProviderBuilder(issuer) @@ -57,8 +68,6 @@ fun Application.configureSecurity(userAuthService: IUserAuthService) { if (check.not()) { return@post call.respond(HttpStatusCode.Unauthorized) } - - val publicKey = jwkProvider.get("6f8856ed-9189-488f-9011-0ff4b6c08edc").publicKey val keySpecPKCS8 = PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyString)) val privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpecPKCS8) val token = JWT.create() @@ -66,22 +75,16 @@ fun Application.configureSecurity(userAuthService: IUserAuthService) { // .withIssuer(issuer) .withClaim("username", user.username) .withExpiresAt(Date(System.currentTimeMillis() + 60000)) - .sign(Algorithm.RSA256(publicKey as RSAPublicKey, privateKey as RSAPrivateKey)) + .sign(Algorithm.RSA256(publicKey, privateKey as RSAPrivateKey)) return@post call.respond(hashSetOf("token" to token)) } - get("/.well-known/jwks.json"){ + get("/.well-known/jwks.json") { //language=JSON - call.respondText(contentType = ContentType.Application.Json,text = """{ - "keys": [ - { - "kty": "RSA", - "e": "AQAB", - "kid": "6f8856ed-9189-488f-9011-0ff4b6c08edc", - "n":"tfJaLrzXILUg1U3N1KV8yJr92GHn5OtYZR7qWk1Mc4cy4JGjklYup7weMjBD9f3bBVoIsiUVX6xNcYIr0Ie0AQ" - } - ] -}""") + call.respondText( + contentType = ContentType.Application.Json, + text = JsonWebKeyUtil.publicKeyToJwk(requireNotNull(metaRepository.get()).jwt.publicKey) + ) } } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ServerInitialiseServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/ServerInitialiseServiceImpl.kt index 39ea048e..f18a090a 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ServerInitialiseServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ServerInitialiseServiceImpl.kt @@ -43,8 +43,8 @@ class ServerInitialiseServiceImpl(private val metaRepository: IMetaRepository) : val generateKeyPair = keyPairGenerator.generateKeyPair() val jwt = Jwt( UUID.randomUUID(), - Base64.getEncoder().encodeToString(generateKeyPair.public.encoded), - Base64.getEncoder().encodeToString(generateKeyPair.private.encoded) + Base64.getEncoder().encodeToString(generateKeyPair.private.encoded), + Base64.getEncoder().encodeToString(generateKeyPair.public.encoded) ) val meta = Meta(implementationVersion, jwt) metaRepository.save(meta) diff --git a/src/main/kotlin/dev/usbharu/hideout/util/JsonWebKeyUtil.kt b/src/main/kotlin/dev/usbharu/hideout/util/JsonWebKeyUtil.kt new file mode 100644 index 00000000..73eb47a1 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/util/JsonWebKeyUtil.kt @@ -0,0 +1,41 @@ +package dev.usbharu.hideout.util + +import java.math.BigInteger +import java.security.KeyFactory +import java.security.interfaces.RSAPublicKey +import java.security.spec.X509EncodedKeySpec +import java.util.* + +object JsonWebKeyUtil { + + fun publicKeyToJwk(publicKey: String): String { + val x509EncodedKeySpec = X509EncodedKeySpec(Base64.getDecoder().decode(publicKey)) + val generatePublic = KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec) + return publicKeyToJwk(generatePublic as RSAPublicKey) + } + + fun publicKeyToJwk(publicKey: RSAPublicKey): String { + val e = encodeBase64UInt(publicKey.publicExponent) + val n = encodeBase64UInt(publicKey.modulus) + return """{"e":"$e","n":"$n","use":"sig","kty":"RSA"}""" + } + + private fun encodeBase64UInt(bigInteger: BigInteger, minLength: Int = -1): String { + if(bigInteger.signum() < 0){ + throw IllegalArgumentException("Cannot encode negative numbers") + } + + var bytes = bigInteger.toByteArray() + if (bigInteger.bitLength() % 8 == 0 && (bytes[0] == 0.toByte()) && bytes.size > 1){ + bytes = Arrays.copyOfRange(bytes, 1, bytes.size) + } + if (minLength != -1){ + if (bytes.size < minLength){ + val array = ByteArray(minLength) + System.arraycopy(bytes, 0, array, minLength - bytes.size, bytes.size) + bytes = array + } + } + return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/util/RsaUtil.kt b/src/main/kotlin/dev/usbharu/hideout/util/RsaUtil.kt new file mode 100644 index 00000000..e7f9aef4 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/util/RsaUtil.kt @@ -0,0 +1,19 @@ +package dev.usbharu.hideout.util + +import java.security.KeyFactory +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey +import java.security.spec.PKCS8EncodedKeySpec +import java.security.spec.X509EncodedKeySpec + +object RsaUtil { + fun decodeRsaPublicKey(byteArray: ByteArray):RSAPublicKey{ + val x509EncodedKeySpec = X509EncodedKeySpec(byteArray) + return KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec) as RSAPublicKey + } + + fun decodeRsaPrivateKey(byteArray: ByteArray):RSAPrivateKey{ + val pkcS8EncodedKeySpec = PKCS8EncodedKeySpec(byteArray) + return KeyFactory.getInstance("RSA").generatePrivate(pkcS8EncodedKeySpec) as RSAPrivateKey + } +} From ed00d741ff076dedb03817545022b04ab63f8218 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Mon, 1 May 2023 07:19:30 +0900 Subject: [PATCH 05/17] =?UTF-8?q?fix:=20jwk=E3=81=AE=E5=BD=A2=E5=BC=8F?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/dev/usbharu/hideout/util/JsonWebKeyUtil.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/util/JsonWebKeyUtil.kt b/src/main/kotlin/dev/usbharu/hideout/util/JsonWebKeyUtil.kt index 73eb47a1..444e1039 100644 --- a/src/main/kotlin/dev/usbharu/hideout/util/JsonWebKeyUtil.kt +++ b/src/main/kotlin/dev/usbharu/hideout/util/JsonWebKeyUtil.kt @@ -17,7 +17,7 @@ object JsonWebKeyUtil { fun publicKeyToJwk(publicKey: RSAPublicKey): String { val e = encodeBase64UInt(publicKey.publicExponent) val n = encodeBase64UInt(publicKey.modulus) - return """{"e":"$e","n":"$n","use":"sig","kty":"RSA"}""" + return """{"keys":[{"e":"$e","n":"$n","use":"sig","kty":"RSA"}]}""" } private fun encodeBase64UInt(bigInteger: BigInteger, minLength: Int = -1): String { From 5d85eb0ca66823aac25260d2665ada0ec90ca352 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Mon, 1 May 2023 07:27:28 +0900 Subject: [PATCH 06/17] =?UTF-8?q?feat:=20kid,issuer,aud=E3=82=92=E6=8C=87?= =?UTF-8?q?=E5=AE=9A=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt | 9 ++++++--- .../kotlin/dev/usbharu/hideout/util/JsonWebKeyUtil.kt | 8 ++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt index bb487c6f..014e5cb8 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt @@ -5,6 +5,7 @@ package dev.usbharu.hideout.plugins import com.auth0.jwk.JwkProviderBuilder import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm +import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.domain.model.hideout.form.UserLogin import dev.usbharu.hideout.property import dev.usbharu.hideout.repository.IMetaRepository @@ -71,8 +72,9 @@ fun Application.configureSecurity(userAuthService: IUserAuthService, metaReposit val keySpecPKCS8 = PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyString)) val privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpecPKCS8) val token = JWT.create() -// .withAudience(audience) -// .withIssuer(issuer) + .withAudience("${Config.configData.url}/users/${user.username}") + .withIssuer(issuer) + .withKeyId(metaRepository.get()?.jwt?.kid.toString()) .withClaim("username", user.username) .withExpiresAt(Date(System.currentTimeMillis() + 60000)) .sign(Algorithm.RSA256(publicKey, privateKey as RSAPrivateKey)) @@ -81,9 +83,10 @@ fun Application.configureSecurity(userAuthService: IUserAuthService, metaReposit get("/.well-known/jwks.json") { //language=JSON + val meta = requireNotNull(metaRepository.get()) call.respondText( contentType = ContentType.Application.Json, - text = JsonWebKeyUtil.publicKeyToJwk(requireNotNull(metaRepository.get()).jwt.publicKey) + text = JsonWebKeyUtil.publicKeyToJwk(meta.jwt.publicKey,meta.jwt.kid.toString()) ) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/util/JsonWebKeyUtil.kt b/src/main/kotlin/dev/usbharu/hideout/util/JsonWebKeyUtil.kt index 444e1039..4b73a8f5 100644 --- a/src/main/kotlin/dev/usbharu/hideout/util/JsonWebKeyUtil.kt +++ b/src/main/kotlin/dev/usbharu/hideout/util/JsonWebKeyUtil.kt @@ -8,16 +8,16 @@ import java.util.* object JsonWebKeyUtil { - fun publicKeyToJwk(publicKey: String): String { + fun publicKeyToJwk(publicKey: String,kid:String): String { val x509EncodedKeySpec = X509EncodedKeySpec(Base64.getDecoder().decode(publicKey)) val generatePublic = KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec) - return publicKeyToJwk(generatePublic as RSAPublicKey) + return publicKeyToJwk(generatePublic as RSAPublicKey,kid) } - fun publicKeyToJwk(publicKey: RSAPublicKey): String { + fun publicKeyToJwk(publicKey: RSAPublicKey,kid:String): String { val e = encodeBase64UInt(publicKey.publicExponent) val n = encodeBase64UInt(publicKey.modulus) - return """{"keys":[{"e":"$e","n":"$n","use":"sig","kty":"RSA"}]}""" + return """{"keys":[{"e":"$e","n":"$n","use":"sig","kid":"$kid","kty":"RSA"}]}""" } private fun encodeBase64UInt(bigInteger: BigInteger, minLength: Int = -1): String { From f9cf7152fcc14bea4e16cad2a88ceec54ada0481 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Mon, 1 May 2023 09:12:15 +0900 Subject: [PATCH 07/17] =?UTF-8?q?feat:=20=E8=AA=8D=E8=A8=BC=E3=81=AE?= =?UTF-8?q?=E7=A2=BA=E8=AA=8D=E3=81=AE=E5=AE=9F=E8=A3=85=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/usbharu/hideout/plugins/HTTP.kt | 21 ++++----- .../dev/usbharu/hideout/plugins/Routing.kt | 3 ++ .../dev/usbharu/hideout/plugins/Security.kt | 6 ++- .../hideout/routing/AuthTestRouting.kt | 19 ++++++++ src/main/web/App.tsx | 46 ++++++++++++++++++- vite.config.ts | 4 +- 6 files changed, 84 insertions(+), 15 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/routing/AuthTestRouting.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/HTTP.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/HTTP.kt index 234130ad..98e5e259 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/HTTP.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/HTTP.kt @@ -1,21 +1,20 @@ package dev.usbharu.hideout.plugins -import io.ktor.http.* import io.ktor.server.application.* -import io.ktor.server.plugins.cors.routing.* import io.ktor.server.plugins.defaultheaders.* import io.ktor.server.plugins.forwardedheaders.* fun Application.configureHTTP() { - install(CORS) { - allowMethod(HttpMethod.Options) - allowMethod(HttpMethod.Put) - allowMethod(HttpMethod.Delete) - allowMethod(HttpMethod.Patch) - allowHeader(HttpHeaders.Authorization) - allowHeader("MyCustomHeader") - anyHost() // @TODO: Don't do this in production if possible. Try to limit it. - } +// install(CORS) { +// allowMethod(HttpMethod.Options) +// allowMethod(HttpMethod.Put) +// allowMethod(HttpMethod.Delete) +// allowMethod(HttpMethod.Patch) +// allowHeader(HttpHeaders.Authorization) +// allow +// allowHeader("MyCustomHeader") +// anyHost() // @TODO: Don't do this in production if possible. Try to limit it. +// } install(DefaultHeaders) { header("X-Engine", "Ktor") // will send this header with each response } diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt index fed45736..7c843ce9 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt @@ -4,6 +4,7 @@ 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 @@ -31,5 +32,7 @@ fun Application.configureRouting( route("/api/v1") { statuses(postService) } + + authTestRouting() } } diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt index 014e5cb8..79a5b90d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt @@ -51,6 +51,7 @@ fun Application.configureSecurity(userAuthService: IUserAuthService, metaReposit realm = myRealm verifier(jwkProvider, issuer) { acceptLeeway(3) + } validate { jwtCredential -> if (jwtCredential.payload.getClaim("username").asString().isNotEmpty()) { @@ -59,6 +60,9 @@ fun Application.configureSecurity(userAuthService: IUserAuthService, metaReposit null } } + challenge { defaultScheme, realm -> + call.respondRedirect("/login") + } } } @@ -78,7 +82,7 @@ fun Application.configureSecurity(userAuthService: IUserAuthService, metaReposit .withClaim("username", user.username) .withExpiresAt(Date(System.currentTimeMillis() + 60000)) .sign(Algorithm.RSA256(publicKey, privateKey as RSAPrivateKey)) - return@post call.respond(hashSetOf("token" to token)) + return@post call.respond(token) } get("/.well-known/jwks.json") { diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/AuthTestRouting.kt b/src/main/kotlin/dev/usbharu/hideout/routing/AuthTestRouting.kt new file mode 100644 index 00000000..79946c46 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/routing/AuthTestRouting.kt @@ -0,0 +1,19 @@ +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() + val username = principal!!.payload.getClaim("username") + call.respondText("Hello $username") + } + } +} diff --git a/src/main/web/App.tsx b/src/main/web/App.tsx index d1b57e50..62047d22 100644 --- a/src/main/web/App.tsx +++ b/src/main/web/App.tsx @@ -1,5 +1,47 @@ -import {Component} from "solid-js"; +import {Component, createSignal} from "solid-js"; export const App: Component = () => { - return (

aaa

) + + const fn = (form: HTMLButtonElement) => { + console.log(form) + } + + const [username, setUsername] = createSignal("") + const [password, setPassword] = createSignal("") + + return ( +
res.text()) + .then(res => fetch("/auth-check", { + method: "GET", + headers: { + 'Authorization': 'Bearer ' + res + } + })).then(res => console.log(res)) + } + + }> + setUsername(e.currentTarget.value)}/> + setPassword(e.currentTarget.value)}/> + +
+ ) +} + + +declare module 'solid-js' { + namespace JSX { + interface Directives { + fn: (form: HTMLFormElement) => void + } + } } diff --git a/vite.config.ts b/vite.config.ts index 391fa37d..4ae5194e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,7 +7,9 @@ export default defineConfig({ server: { port: 3000, proxy: { - '/api': 'http://localhost:8080' + '/api': 'http://localhost:8080', + '/login': 'http://localhost:8080', + '/auth-check': 'http://localhost:8080', } }, root: './src/main/web', From 65dd694eedce2f658c394e8aab0670bcd7cabbbd Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Mon, 1 May 2023 10:48:59 +0900 Subject: [PATCH 08/17] =?UTF-8?q?feat:=20=E3=83=91=E3=82=B9=E3=83=AF?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=80=81=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC?= =?UTF-8?q?=E5=90=8D=E3=81=8C=E9=96=93=E9=81=95=E3=81=A3=E3=81=A6=E3=81=84?= =?UTF-8?q?=E3=82=8B=E3=81=A8=E3=81=8D=E3=81=AB=E6=AD=A3=E5=B8=B8=E3=81=AA?= =?UTF-8?q?HTTP=20Status=20Code=E3=82=92=E8=BF=94=E3=81=99=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 4 ++++ .../usbharu/hideout/exception/UserNotFoundException.kt | 10 ++-------- .../kotlin/dev/usbharu/hideout/plugins/StatusPages.kt | 6 +++--- .../usbharu/hideout/service/impl/UserAuthService.kt | 3 +-- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index b7cca720..e28896a3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -41,6 +41,10 @@ tasks.withType { } } +tasks.clean { + delete += listOf("$rootDir/src/main/resources/static") +} + repositories { mavenCentral() } diff --git a/src/main/kotlin/dev/usbharu/hideout/exception/UserNotFoundException.kt b/src/main/kotlin/dev/usbharu/hideout/exception/UserNotFoundException.kt index 4634e141..0c8ca15e 100644 --- a/src/main/kotlin/dev/usbharu/hideout/exception/UserNotFoundException.kt +++ b/src/main/kotlin/dev/usbharu/hideout/exception/UserNotFoundException.kt @@ -1,14 +1,8 @@ package dev.usbharu.hideout.exception -class UserNotFoundException : Exception { +class UserNotFoundException : IllegalArgumentException { constructor() : super() - constructor(message: String?) : super(message) + constructor(s: String?) : super(s) constructor(message: String?, cause: Throwable?) : super(message, cause) constructor(cause: Throwable?) : super(cause) - constructor( - message: String?, - cause: Throwable?, - enableSuppression: Boolean, - writableStackTrace: Boolean - ) : super(message, cause, enableSuppression, writableStackTrace) } diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/StatusPages.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/StatusPages.kt index cd078e28..67ffdb1b 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/StatusPages.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/StatusPages.kt @@ -7,11 +7,11 @@ import io.ktor.server.response.* fun Application.configureStatusPages() { install(StatusPages) { - exception { call, cause -> - call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError) - } exception { call, cause -> call.respondText(text = "400: $cause", status = HttpStatusCode.BadRequest) } + exception { call, cause -> + call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError) + } } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserAuthService.kt b/src/main/kotlin/dev/usbharu/hideout/service/impl/UserAuthService.kt index 2d1ef29b..51356cff 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/impl/UserAuthService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/impl/UserAuthService.kt @@ -1,7 +1,6 @@ package dev.usbharu.hideout.service.impl import dev.usbharu.hideout.config.Config -import dev.usbharu.hideout.exception.UserNotFoundException import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.service.IUserAuthService import io.ktor.util.* @@ -24,7 +23,7 @@ class UserAuthService( override suspend fun verifyAccount(username: String, password: String): Boolean { val userEntity = userRepository.findByNameAndDomain(username, Config.configData.domain) - ?: throw UserNotFoundException("$username was not found") + ?: return false return userEntity.password == hash(password) } From 749f0c891dfaf7064c7a79daedd8b23bb1a66e1d Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Mon, 1 May 2023 16:07:22 +0900 Subject: [PATCH 09/17] =?UTF-8?q?feat:=20=E3=83=AA=E3=83=95=E3=83=AC?= =?UTF-8?q?=E3=83=83=E3=82=B7=E3=83=A5=E3=83=88=E3=83=BC=E3=82=AF=E3=83=B3?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/dev/usbharu/hideout/Application.kt | 10 ++- .../domain/model/hideout/dto/JwtToken.kt | 3 + .../model/hideout/entity/JwtRefreshToken.kt | 11 +++ .../domain/model/hideout/form/RefreshToken.kt | 3 + .../dev/usbharu/hideout/plugins/Security.kt | 79 +++++++++++++++---- .../repository/IJwtRefreshTokenRepository.kt | 11 +++ .../JwtRefreshTokenRepositoryImpl.kt | 73 +++++++++++++++++ .../dev/usbharu/hideout/util/Base64Util.kt | 10 +++ 8 files changed, 184 insertions(+), 16 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/JwtToken.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/JwtRefreshToken.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/RefreshToken.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/repository/IJwtRefreshTokenRepository.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/repository/JwtRefreshTokenRepositoryImpl.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/util/Base64Util.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/Application.kt b/src/main/kotlin/dev/usbharu/hideout/Application.kt index 4d3180a9..438a49ed 100644 --- a/src/main/kotlin/dev/usbharu/hideout/Application.kt +++ b/src/main/kotlin/dev/usbharu/hideout/Application.kt @@ -84,8 +84,9 @@ fun Application.parent() { single { PostService(get(), get()) } single { PostRepositoryImpl(get(), get()) } single { TwitterSnowflakeIdGenerateService } - single{ MetaRepositoryImpl(get()) } + single { MetaRepositoryImpl(get()) } single { ServerInitialiseServiceImpl(get()) } + single { JwtRefreshTokenRepositoryImpl(get()) } } configureKoin(module) runBlocking { @@ -96,7 +97,12 @@ fun Application.parent() { configureMonitoring() configureSerialization() register(inject().value) - configureSecurity(inject().value,inject().value) + configureSecurity( + inject().value, + inject().value, + inject().value, + inject().value + ) configureRouting( inject().value, inject().value, diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/JwtToken.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/JwtToken.kt new file mode 100644 index 00000000..fe25b3b0 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/JwtToken.kt @@ -0,0 +1,3 @@ +package dev.usbharu.hideout.domain.model.hideout.dto + +data class JwtToken(val token:String,val refreshToken:String) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/JwtRefreshToken.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/JwtRefreshToken.kt new file mode 100644 index 00000000..a7b54817 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/JwtRefreshToken.kt @@ -0,0 +1,11 @@ +package dev.usbharu.hideout.domain.model.hideout.entity + +import java.time.Instant + +data class JwtRefreshToken( + val id: Long, + val userId: Long, + val refreshToken: String, + val createdAt: Instant, + val expiresAt: Instant +) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/RefreshToken.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/RefreshToken.kt new file mode 100644 index 00000000..3d35cf21 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/RefreshToken.kt @@ -0,0 +1,3 @@ +package dev.usbharu.hideout.domain.model.hideout.form + +data class RefreshToken(val refreshToken:String) diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt index 79a5b90d..f2b70cf7 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt @@ -6,10 +6,18 @@ import com.auth0.jwk.JwkProviderBuilder import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm import dev.usbharu.hideout.config.Config +import dev.usbharu.hideout.domain.model.hideout.dto.JwtToken +import dev.usbharu.hideout.domain.model.hideout.entity.JwtRefreshToken +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.IJwtRefreshTokenRepository import dev.usbharu.hideout.repository.IMetaRepository +import dev.usbharu.hideout.repository.IUserRepository import dev.usbharu.hideout.service.IUserAuthService +import dev.usbharu.hideout.service.IdGenerateService +import dev.usbharu.hideout.util.Base64Util import dev.usbharu.hideout.util.JsonWebKeyUtil import dev.usbharu.hideout.util.RsaUtil import io.ktor.http.* @@ -20,25 +28,27 @@ import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import kotlinx.coroutines.runBlocking -import java.security.KeyFactory -import java.security.interfaces.RSAPrivateKey -import java.security.spec.PKCS8EncodedKeySpec +import java.time.Instant import java.util.* import java.util.concurrent.TimeUnit const val TOKEN_AUTH = "jwt-auth" -fun Application.configureSecurity(userAuthService: IUserAuthService, metaRepository: IMetaRepository) { +fun Application.configureSecurity( + userAuthService: IUserAuthService, + metaRepository: IMetaRepository, + refreshTokenRepository: IJwtRefreshTokenRepository, + userRepository: IUserRepository, + idGenerateService: IdGenerateService +) { - val privateKeyString = runBlocking { - requireNotNull(metaRepository.get()).jwt.privateKey + val privateKey = runBlocking { + RsaUtil.decodeRsaPrivateKey(Base64Util.decode(requireNotNull(metaRepository.get()).jwt.privateKey)) } val publicKey = runBlocking { val publicKey = requireNotNull(metaRepository.get()).jwt.publicKey - println(publicKey) - RsaUtil.decodeRsaPublicKey(Base64.getDecoder().decode(publicKey)) + RsaUtil.decodeRsaPublicKey(Base64Util.decode(publicKey)) } - println(privateKeyString) val issuer = property("hideout.url") // val audience = property("jwt.audience") val myRealm = property("jwt.realm") @@ -73,16 +83,57 @@ fun Application.configureSecurity(userAuthService: IUserAuthService, metaReposit if (check.not()) { return@post call.respond(HttpStatusCode.Unauthorized) } - val keySpecPKCS8 = PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyString)) - val privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpecPKCS8) + + val findByNameAndDomain = userRepository.findByNameAndDomain(user.username, Config.configData.domain) + ?: throw UserNotFoundException("${user.username} was not found.") + val token = JWT.create() .withAudience("${Config.configData.url}/users/${user.username}") .withIssuer(issuer) .withKeyId(metaRepository.get()?.jwt?.kid.toString()) .withClaim("username", user.username) .withExpiresAt(Date(System.currentTimeMillis() + 60000)) - .sign(Algorithm.RSA256(publicKey, privateKey as RSAPrivateKey)) - return@post call.respond(token) + .sign(Algorithm.RSA256(publicKey, privateKey)) + val refreshToken = UUID.randomUUID().toString() + refreshTokenRepository.save( + JwtRefreshToken( + idGenerateService.generateId(), findByNameAndDomain.id, refreshToken, Instant.now(), + Instant.ofEpochMilli(Instant.now().toEpochMilli() + 1209600033) + ) + ) + return@post call.respond(JwtToken(token, refreshToken)) + } + + post("/refresh-token") { + val refreshToken = call.receive() + val findByToken = refreshTokenRepository.findByToken(refreshToken.refreshToken) + ?: return@post call.respond(HttpStatusCode.Forbidden) + + if (findByToken.createdAt.isAfter(Instant.now())) { + return@post call.respond(HttpStatusCode.Forbidden) + } + + if (findByToken.expiresAt.isAfter(Instant.now())) { + return@post call.respond(HttpStatusCode.Forbidden) + } + + val user = userRepository.findById(findByToken.userId) + ?: throw UserNotFoundException("${findByToken.userId} was not found.") + val token = JWT.create() + .withAudience("${Config.configData.url}/users/${user.name}") + .withIssuer(issuer) + .withKeyId(metaRepository.get()?.jwt?.kid.toString()) + .withClaim("username", user.name) + .withExpiresAt(Date(System.currentTimeMillis() + 60000)) + .sign(Algorithm.RSA256(publicKey, privateKey)) + val newRefreshToken = UUID.randomUUID().toString() + refreshTokenRepository.save( + JwtRefreshToken( + idGenerateService.generateId(), user.id, newRefreshToken, Instant.now(), + Instant.ofEpochMilli(Instant.now().toEpochMilli() + 1209600033) + ) + ) + return@post call.respond(JwtToken(token, newRefreshToken)) } get("/.well-known/jwks.json") { @@ -90,7 +141,7 @@ fun Application.configureSecurity(userAuthService: IUserAuthService, metaReposit val meta = requireNotNull(metaRepository.get()) call.respondText( contentType = ContentType.Application.Json, - text = JsonWebKeyUtil.publicKeyToJwk(meta.jwt.publicKey,meta.jwt.kid.toString()) + text = JsonWebKeyUtil.publicKeyToJwk(meta.jwt.publicKey, meta.jwt.kid.toString()) ) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/IJwtRefreshTokenRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/IJwtRefreshTokenRepository.kt new file mode 100644 index 00000000..c268a406 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/repository/IJwtRefreshTokenRepository.kt @@ -0,0 +1,11 @@ +package dev.usbharu.hideout.repository + +import dev.usbharu.hideout.domain.model.hideout.entity.JwtRefreshToken + +interface IJwtRefreshTokenRepository { + suspend fun save(token: JwtRefreshToken) + + suspend fun findById(id:Long):JwtRefreshToken? + suspend fun findByToken(token:String):JwtRefreshToken? + +} diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/JwtRefreshTokenRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/repository/JwtRefreshTokenRepositoryImpl.kt new file mode 100644 index 00000000..78cc525a --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/repository/JwtRefreshTokenRepositoryImpl.kt @@ -0,0 +1,73 @@ +package dev.usbharu.hideout.repository + +import dev.usbharu.hideout.domain.model.hideout.entity.JwtRefreshToken +import kotlinx.coroutines.Dispatchers +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction +import org.jetbrains.exposed.sql.transactions.transaction +import java.time.Instant + +class JwtRefreshTokenRepositoryImpl(private val database: Database) : IJwtRefreshTokenRepository { + + init { + transaction(database){ + SchemaUtils.create(JwtRefreshTokens) + SchemaUtils.createMissingTablesAndColumns(JwtRefreshTokens) + } + } + + suspend fun query(block: suspend () -> T): T = + newSuspendedTransaction(Dispatchers.IO) { block() } + + override suspend fun save(token: JwtRefreshToken) { + query { + if (JwtRefreshTokens.select { JwtRefreshTokens.id.eq(token.id) }.empty()) { + JwtRefreshTokens.insert { + it[id] = token.id + it[userId] = token.userId + it[refreshToken] = token.refreshToken + it[createdAt] = token.createdAt.toEpochMilli() + it[expiresAt] = token.expiresAt.toEpochMilli() + } + } else { + JwtRefreshTokens.update({ JwtRefreshTokens.id eq token.id }) { + it[userId] = token.userId + it[refreshToken] = token.refreshToken + it[createdAt] = token.createdAt.toEpochMilli() + it[expiresAt] = token.expiresAt.toEpochMilli() + } + } + } + } + + override suspend fun findById(id: Long): JwtRefreshToken? { + return query { + JwtRefreshTokens.select { JwtRefreshTokens.id.eq(id) }.singleOrNull()?.toJwtRefreshToken() + } + } + + override suspend fun findByToken(token: String): JwtRefreshToken? { + return query { + JwtRefreshTokens.select { JwtRefreshTokens.refreshToken.eq(token) }.singleOrNull()?.toJwtRefreshToken() + } + } +} + +fun ResultRow.toJwtRefreshToken(): JwtRefreshToken { + return JwtRefreshToken( + this[JwtRefreshTokens.id], + this[JwtRefreshTokens.userId], + this[JwtRefreshTokens.refreshToken], + Instant.ofEpochMilli(this[JwtRefreshTokens.createdAt]), + Instant.ofEpochMilli(this[JwtRefreshTokens.expiresAt]) + ) +} + +object JwtRefreshTokens : Table("jwt_refresh_tokens") { + val id = long("id") + val userId = long("user_id") + val refreshToken = varchar("refresh_token", 1000) + val createdAt = long("created_at") + val expiresAt = long("expires_at") + override val primaryKey = PrimaryKey(id) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/util/Base64Util.kt b/src/main/kotlin/dev/usbharu/hideout/util/Base64Util.kt new file mode 100644 index 00000000..07ae9284 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/util/Base64Util.kt @@ -0,0 +1,10 @@ +package dev.usbharu.hideout.util + +import java.util.* + +object Base64Util { + fun decode(str: String): ByteArray = Base64.getDecoder().decode(str) + + fun encode(bytes: ByteArray): String = Base64.getEncoder().encodeToString(bytes) + +} From 5c82bfd532e1bfa79f0762bb3d954b8925d63f35 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Tue, 2 May 2023 08:48:23 +0900 Subject: [PATCH 10/17] =?UTF-8?q?feat:=20=E3=83=88=E3=83=BC=E3=82=AF?= =?UTF-8?q?=E3=83=B3=E3=80=81=E3=83=AA=E3=83=95=E3=83=AC=E3=83=83=E3=82=B7?= =?UTF-8?q?=E3=83=A5=E3=83=88=E3=83=BC=E3=82=AF=E3=83=B3=E3=81=AE=E7=99=BA?= =?UTF-8?q?=E8=A1=8C=E3=81=A8=E3=83=AA=E3=83=95=E3=83=AC=E3=83=83=E3=82=B7?= =?UTF-8?q?=E3=83=A5=E3=83=88=E3=83=BC=E3=82=AF=E3=83=B3=E3=81=8B=E3=82=89?= =?UTF-8?q?=E3=83=88=E3=83=BC=E3=82=AF=E3=83=B3=E3=81=AE=E5=86=8D=E7=94=9F?= =?UTF-8?q?=E6=88=90=E3=81=8C=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86?= =?UTF-8?q?=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/dev/usbharu/hideout/Application.kt | 3 ++- .../dev/usbharu/hideout/plugins/Security.kt | 8 +++---- src/main/web/App.tsx | 23 ++++++++++++++----- vite.config.ts | 1 + 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/Application.kt b/src/main/kotlin/dev/usbharu/hideout/Application.kt index 438a49ed..92ec9ee3 100644 --- a/src/main/kotlin/dev/usbharu/hideout/Application.kt +++ b/src/main/kotlin/dev/usbharu/hideout/Application.kt @@ -101,7 +101,8 @@ fun Application.parent() { inject().value, inject().value, inject().value, - inject().value + inject().value, + inject().value ) configureRouting( inject().value, diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt index f2b70cf7..19c3bb60 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt @@ -107,14 +107,14 @@ fun Application.configureSecurity( post("/refresh-token") { val refreshToken = call.receive() val findByToken = refreshTokenRepository.findByToken(refreshToken.refreshToken) - ?: return@post call.respond(HttpStatusCode.Forbidden) + ?: return@post call.respondText("token not found",status = HttpStatusCode.Forbidden) if (findByToken.createdAt.isAfter(Instant.now())) { - return@post call.respond(HttpStatusCode.Forbidden) + return@post call.respondText("created_at", status = HttpStatusCode.Forbidden) } - if (findByToken.expiresAt.isAfter(Instant.now())) { - return@post call.respond(HttpStatusCode.Forbidden) + if (findByToken.expiresAt.isBefore(Instant.now())) { + return@post call.respondText( "expires_at", status = HttpStatusCode.Forbidden) } val user = userRepository.findById(findByToken.userId) diff --git a/src/main/web/App.tsx b/src/main/web/App.tsx index 62047d22..0da03fa6 100644 --- a/src/main/web/App.tsx +++ b/src/main/web/App.tsx @@ -18,13 +18,24 @@ export const App: Component = () => { headers: { 'Content-Type': 'application/json' } - }).then(res => res.text()) - .then(res => fetch("/auth-check", { - method: "GET", + }).then(res => res.json()) + // .then(res => fetch("/auth-check", { + // method: "GET", + // headers: { + // 'Authorization': 'Bearer ' + res.token + // } + // })) + // .then(res => res.json()) + .then(res => { + console.log(res.token); + fetch("/refresh-token", { + method: "POST", headers: { - 'Authorization': 'Bearer ' + res - } - })).then(res => console.log(res)) + 'Content-Type': 'application/json', + }, + body: JSON.stringify({refreshToken: res.refreshToken}), + }).then(res=> res.json()).then(res => console.log(res.token)) + }) } }> diff --git a/vite.config.ts b/vite.config.ts index 4ae5194e..3f3c0c58 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -10,6 +10,7 @@ export default defineConfig({ '/api': 'http://localhost:8080', '/login': 'http://localhost:8080', '/auth-check': 'http://localhost:8080', + '/refresh-token': 'http://localhost:8080', } }, root: './src/main/web', From dd30728548127b804bcdfb20a0171adafb4364f3 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Tue, 2 May 2023 15:53:04 +0900 Subject: [PATCH 11/17] =?UTF-8?q?refactor:=20JWT=E9=96=A2=E4=BF=82?= =?UTF-8?q?=E3=81=AE=E3=82=B3=E3=83=BC=E3=83=89=E3=82=92=E3=83=AA=E3=83=95?= =?UTF-8?q?=E3=82=A1=E3=82=AF=E3=82=BF=E3=83=AA=E3=83=B3=E3=82=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/dev/usbharu/hideout/Application.kt | 9 +- .../exception/InvalidRefreshTokenException.kt | 8 ++ .../hideout/exception/NotInitException.kt | 14 +++ .../dev/usbharu/hideout/plugins/Security.kt | 90 +++--------------- .../repository/IJwtRefreshTokenRepository.kt | 9 ++ .../JwtRefreshTokenRepositoryImpl.kt | 48 +++++++++- .../usbharu/hideout/service/IJwtService.kt | 14 +++ .../usbharu/hideout/service/IMetaService.kt | 10 ++ .../usbharu/hideout/service/JwtServiceImpl.kt | 95 +++++++++++++++++++ .../hideout/service/MetaServiceImpl.kt | 16 ++++ .../dev/usbharu/hideout/util/RsaUtil.kt | 4 + 11 files changed, 233 insertions(+), 84 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/exception/InvalidRefreshTokenException.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/exception/NotInitException.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/service/IJwtService.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/service/IMetaService.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/service/JwtServiceImpl.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/service/MetaServiceImpl.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/Application.kt b/src/main/kotlin/dev/usbharu/hideout/Application.kt index 92ec9ee3..10f8596b 100644 --- a/src/main/kotlin/dev/usbharu/hideout/Application.kt +++ b/src/main/kotlin/dev/usbharu/hideout/Application.kt @@ -86,7 +86,9 @@ fun Application.parent() { single { TwitterSnowflakeIdGenerateService } single { MetaRepositoryImpl(get()) } single { ServerInitialiseServiceImpl(get()) } - single { JwtRefreshTokenRepositoryImpl(get()) } + single { JwtRefreshTokenRepositoryImpl(get(),get()) } + single { MetaServiceImpl(get()) } + single { JwtServiceImpl(get(),get(),get()) } } configureKoin(module) runBlocking { @@ -99,10 +101,9 @@ fun Application.parent() { register(inject().value) configureSecurity( inject().value, - inject().value, - inject().value, + inject().value, inject().value, - inject().value + inject().value ) configureRouting( inject().value, diff --git a/src/main/kotlin/dev/usbharu/hideout/exception/InvalidRefreshTokenException.kt b/src/main/kotlin/dev/usbharu/hideout/exception/InvalidRefreshTokenException.kt new file mode 100644 index 00000000..e9b780d5 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/exception/InvalidRefreshTokenException.kt @@ -0,0 +1,8 @@ +package dev.usbharu.hideout.exception + +class InvalidRefreshTokenException : IllegalArgumentException{ + constructor() : super() + constructor(s: String?) : super(s) + constructor(message: String?, cause: Throwable?) : super(message, cause) + constructor(cause: Throwable?) : super(cause) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/exception/NotInitException.kt b/src/main/kotlin/dev/usbharu/hideout/exception/NotInitException.kt new file mode 100644 index 00000000..10ccdf29 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/exception/NotInitException.kt @@ -0,0 +1,14 @@ +package dev.usbharu.hideout.exception + +class NotInitException : Exception { + constructor() : super() + constructor(message: String?) : super(message) + constructor(message: String?, cause: Throwable?) : super(message, cause) + constructor(cause: Throwable?) : super(cause) + constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super( + message, + cause, + enableSuppression, + writableStackTrace + ) +} diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt index 19c3bb60..468419c2 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt @@ -3,23 +3,16 @@ package dev.usbharu.hideout.plugins import com.auth0.jwk.JwkProviderBuilder -import com.auth0.jwt.JWT -import com.auth0.jwt.algorithms.Algorithm import dev.usbharu.hideout.config.Config -import dev.usbharu.hideout.domain.model.hideout.dto.JwtToken -import dev.usbharu.hideout.domain.model.hideout.entity.JwtRefreshToken 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.IJwtRefreshTokenRepository -import dev.usbharu.hideout.repository.IMetaRepository 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.service.IdGenerateService -import dev.usbharu.hideout.util.Base64Util import dev.usbharu.hideout.util.JsonWebKeyUtil -import dev.usbharu.hideout.util.RsaUtil import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* @@ -27,38 +20,23 @@ import io.ktor.server.auth.jwt.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* -import kotlinx.coroutines.runBlocking -import java.time.Instant -import java.util.* import java.util.concurrent.TimeUnit const val TOKEN_AUTH = "jwt-auth" fun Application.configureSecurity( userAuthService: IUserAuthService, - metaRepository: IMetaRepository, - refreshTokenRepository: IJwtRefreshTokenRepository, + metaService: IMetaService, userRepository: IUserRepository, - idGenerateService: IdGenerateService + jwtService: IJwtService ) { - - val privateKey = runBlocking { - RsaUtil.decodeRsaPrivateKey(Base64Util.decode(requireNotNull(metaRepository.get()).jwt.privateKey)) - } - val publicKey = runBlocking { - val publicKey = requireNotNull(metaRepository.get()).jwt.publicKey - RsaUtil.decodeRsaPublicKey(Base64Util.decode(publicKey)) - } val issuer = property("hideout.url") -// val audience = property("jwt.audience") - val myRealm = property("jwt.realm") val jwkProvider = JwkProviderBuilder(issuer) .cached(10, 24, TimeUnit.HOURS) .rateLimited(10, 1, TimeUnit.MINUTES) .build() install(Authentication) { jwt(TOKEN_AUTH) { - realm = myRealm verifier(jwkProvider, issuer) { acceptLeeway(3) @@ -70,78 +48,34 @@ fun Application.configureSecurity( null } } - challenge { defaultScheme, realm -> - call.respondRedirect("/login") - } } } routing { post("/login") { - val user = call.receive() - val check = userAuthService.verifyAccount(user.username, user.password) + val loginUser = call.receive() + val check = userAuthService.verifyAccount(loginUser.username, loginUser.password) if (check.not()) { return@post call.respond(HttpStatusCode.Unauthorized) } - val findByNameAndDomain = userRepository.findByNameAndDomain(user.username, Config.configData.domain) - ?: throw UserNotFoundException("${user.username} was not found.") + val user = userRepository.findByNameAndDomain(loginUser.username, Config.configData.domain) + ?: throw UserNotFoundException("${loginUser.username} was not found.") - val token = JWT.create() - .withAudience("${Config.configData.url}/users/${user.username}") - .withIssuer(issuer) - .withKeyId(metaRepository.get()?.jwt?.kid.toString()) - .withClaim("username", user.username) - .withExpiresAt(Date(System.currentTimeMillis() + 60000)) - .sign(Algorithm.RSA256(publicKey, privateKey)) - val refreshToken = UUID.randomUUID().toString() - refreshTokenRepository.save( - JwtRefreshToken( - idGenerateService.generateId(), findByNameAndDomain.id, refreshToken, Instant.now(), - Instant.ofEpochMilli(Instant.now().toEpochMilli() + 1209600033) - ) - ) - return@post call.respond(JwtToken(token, refreshToken)) + return@post call.respond(jwtService.createToken(user)) } post("/refresh-token") { val refreshToken = call.receive() - val findByToken = refreshTokenRepository.findByToken(refreshToken.refreshToken) - ?: return@post call.respondText("token not found",status = HttpStatusCode.Forbidden) - - if (findByToken.createdAt.isAfter(Instant.now())) { - return@post call.respondText("created_at", status = HttpStatusCode.Forbidden) - } - - if (findByToken.expiresAt.isBefore(Instant.now())) { - return@post call.respondText( "expires_at", status = HttpStatusCode.Forbidden) - } - - val user = userRepository.findById(findByToken.userId) - ?: throw UserNotFoundException("${findByToken.userId} was not found.") - val token = JWT.create() - .withAudience("${Config.configData.url}/users/${user.name}") - .withIssuer(issuer) - .withKeyId(metaRepository.get()?.jwt?.kid.toString()) - .withClaim("username", user.name) - .withExpiresAt(Date(System.currentTimeMillis() + 60000)) - .sign(Algorithm.RSA256(publicKey, privateKey)) - val newRefreshToken = UUID.randomUUID().toString() - refreshTokenRepository.save( - JwtRefreshToken( - idGenerateService.generateId(), user.id, newRefreshToken, Instant.now(), - Instant.ofEpochMilli(Instant.now().toEpochMilli() + 1209600033) - ) - ) - return@post call.respond(JwtToken(token, newRefreshToken)) + return@post call.respond(jwtService.refreshToken(refreshToken)) } get("/.well-known/jwks.json") { //language=JSON - val meta = requireNotNull(metaRepository.get()) + val jwt = metaService.getJwtMeta() call.respondText( contentType = ContentType.Application.Json, - text = JsonWebKeyUtil.publicKeyToJwk(meta.jwt.publicKey, meta.jwt.kid.toString()) + text = JsonWebKeyUtil.publicKeyToJwk(jwt.publicKey, jwt.kid.toString()) ) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/IJwtRefreshTokenRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/IJwtRefreshTokenRepository.kt index c268a406..851fa5bc 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/IJwtRefreshTokenRepository.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/IJwtRefreshTokenRepository.kt @@ -3,9 +3,18 @@ package dev.usbharu.hideout.repository import dev.usbharu.hideout.domain.model.hideout.entity.JwtRefreshToken interface IJwtRefreshTokenRepository { + suspend fun generateId():Long + suspend fun save(token: JwtRefreshToken) suspend fun findById(id:Long):JwtRefreshToken? suspend fun findByToken(token:String):JwtRefreshToken? + suspend fun findByUserId(userId:Long):JwtRefreshToken? + suspend fun delete(token:JwtRefreshToken) + suspend fun deleteById(id:Long) + suspend fun deleteByToken(token:String) + suspend fun deleteByUserId(userId:Long) + + suspend fun deleteAll() } diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/JwtRefreshTokenRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/repository/JwtRefreshTokenRepositoryImpl.kt index 78cc525a..8796c9fc 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/JwtRefreshTokenRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/JwtRefreshTokenRepositoryImpl.kt @@ -1,16 +1,22 @@ package dev.usbharu.hideout.repository import dev.usbharu.hideout.domain.model.hideout.entity.JwtRefreshToken +import dev.usbharu.hideout.service.IdGenerateService import kotlinx.coroutines.Dispatchers import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction import org.jetbrains.exposed.sql.transactions.transaction import java.time.Instant -class JwtRefreshTokenRepositoryImpl(private val database: Database) : IJwtRefreshTokenRepository { +class JwtRefreshTokenRepositoryImpl( + private val database: Database, + private val idGenerateService: IdGenerateService +) : + IJwtRefreshTokenRepository { init { - transaction(database){ + transaction(database) { SchemaUtils.create(JwtRefreshTokens) SchemaUtils.createMissingTablesAndColumns(JwtRefreshTokens) } @@ -19,6 +25,8 @@ class JwtRefreshTokenRepositoryImpl(private val database: Database) : IJwtRefres suspend fun query(block: suspend () -> T): T = newSuspendedTransaction(Dispatchers.IO) { block() } + override suspend fun generateId(): Long = idGenerateService.generateId() + override suspend fun save(token: JwtRefreshToken) { query { if (JwtRefreshTokens.select { JwtRefreshTokens.id.eq(token.id) }.empty()) { @@ -51,6 +59,42 @@ class JwtRefreshTokenRepositoryImpl(private val database: Database) : IJwtRefres JwtRefreshTokens.select { JwtRefreshTokens.refreshToken.eq(token) }.singleOrNull()?.toJwtRefreshToken() } } + + override suspend fun findByUserId(userId: Long): JwtRefreshToken? { + return query { + JwtRefreshTokens.select { JwtRefreshTokens.userId.eq(userId) }.singleOrNull()?.toJwtRefreshToken() + } + } + + override suspend fun delete(token: JwtRefreshToken) { + return query { + JwtRefreshTokens.deleteWhere { JwtRefreshTokens.id eq token.id } + } + } + + override suspend fun deleteById(id: Long) { + return query { + JwtRefreshTokens.deleteWhere { JwtRefreshTokens.id eq id } + } + } + + override suspend fun deleteByToken(token: String) { + return query { + JwtRefreshTokens.deleteWhere { JwtRefreshTokens.refreshToken eq token } + } + } + + override suspend fun deleteByUserId(userId: Long) { + return query { + JwtRefreshTokens.deleteWhere { JwtRefreshTokens.userId eq userId } + } + } + + override suspend fun deleteAll() { + return query { + JwtRefreshTokens.deleteAll() + } + } } fun ResultRow.toJwtRefreshToken(): JwtRefreshToken { diff --git a/src/main/kotlin/dev/usbharu/hideout/service/IJwtService.kt b/src/main/kotlin/dev/usbharu/hideout/service/IJwtService.kt new file mode 100644 index 00000000..0eece5a7 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/IJwtService.kt @@ -0,0 +1,14 @@ +package dev.usbharu.hideout.service + +import dev.usbharu.hideout.domain.model.hideout.dto.JwtToken +import dev.usbharu.hideout.domain.model.hideout.entity.User +import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken + +interface IJwtService { + suspend fun createToken(user:User):JwtToken + suspend fun refreshToken(refreshToken: RefreshToken):JwtToken + + suspend fun revokeToken(refreshToken: RefreshToken) + suspend fun revokeToken(user:User) + suspend fun revokeAll() +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/IMetaService.kt b/src/main/kotlin/dev/usbharu/hideout/service/IMetaService.kt new file mode 100644 index 00000000..84c59d60 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/IMetaService.kt @@ -0,0 +1,10 @@ +package dev.usbharu.hideout.service + +import dev.usbharu.hideout.domain.model.hideout.entity.Jwt +import dev.usbharu.hideout.domain.model.hideout.entity.Meta + +interface IMetaService { + suspend fun getMeta(): Meta + suspend fun updateMeta(meta: Meta) + suspend fun getJwtMeta(): Jwt +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/JwtServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/JwtServiceImpl.kt new file mode 100644 index 00000000..5b036522 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/JwtServiceImpl.kt @@ -0,0 +1,95 @@ +package dev.usbharu.hideout.service + +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import dev.usbharu.hideout.config.Config +import dev.usbharu.hideout.domain.model.hideout.dto.JwtToken +import dev.usbharu.hideout.domain.model.hideout.entity.JwtRefreshToken +import dev.usbharu.hideout.domain.model.hideout.entity.User +import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken +import dev.usbharu.hideout.exception.InvalidRefreshTokenException +import dev.usbharu.hideout.repository.IJwtRefreshTokenRepository +import dev.usbharu.hideout.service.impl.IUserService +import dev.usbharu.hideout.util.RsaUtil +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import java.time.Instant +import java.time.temporal.ChronoUnit +import java.util.* + +class JwtServiceImpl( + private val metaService: IMetaService, + private val refreshTokenRepository: IJwtRefreshTokenRepository, + private val userService: IUserService +) : IJwtService { + + private val privateKey by lazy { + CoroutineScope(Dispatchers.IO).async { + RsaUtil.decodeRsaPrivateKey(metaService.getJwtMeta().privateKey) + } + } + + private val publicKey by lazy { + CoroutineScope(Dispatchers.IO).async { + RsaUtil.decodeRsaPublicKey(metaService.getJwtMeta().publicKey) + } + } + + private val keyId by lazy { + CoroutineScope(Dispatchers.IO).async { + metaService.getJwtMeta().kid + } + } + + override suspend fun createToken(user: User): JwtToken { + val now = Instant.now() + val token = JWT.create() + .withAudience("${Config.configData.url}/users/${user.id}") + .withIssuer(Config.configData.url) + .withKeyId(keyId.await().toString()) + .withClaim("username", user.name) + .withExpiresAt(now.plus(30, ChronoUnit.MINUTES)) + .sign(Algorithm.RSA256(publicKey.await(), privateKey.await())) + + val jwtRefreshToken = JwtRefreshToken( + id = refreshTokenRepository.generateId(), + userId = user.id, + refreshToken = UUID.randomUUID().toString(), + createdAt = now, + expiresAt = now.plus(14, ChronoUnit.DAYS) + ) + refreshTokenRepository.save(jwtRefreshToken) + return JwtToken(token, jwtRefreshToken.refreshToken) + } + + override suspend fun refreshToken(refreshToken: RefreshToken): JwtToken { + val token = refreshTokenRepository.findByToken(refreshToken.refreshToken) + ?: throw InvalidRefreshTokenException("Invalid Refresh Token") + + val user = userService.findById(token.userId) + + val now = Instant.now() + if (token.createdAt.isAfter(now)) { + throw InvalidRefreshTokenException("Invalid Refresh Token") + } + + if (token.expiresAt.isBefore(now)) { + throw InvalidRefreshTokenException("Refresh Token Expired") + } + + return createToken(user) + } + + override suspend fun revokeToken(refreshToken: RefreshToken) { + refreshTokenRepository.deleteByToken(refreshToken.refreshToken) + } + + override suspend fun revokeToken(user: User) { + refreshTokenRepository.deleteByUserId(user.id) + } + + override suspend fun revokeAll() { + refreshTokenRepository.deleteAll() + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/MetaServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/MetaServiceImpl.kt new file mode 100644 index 00000000..4e5eb17a --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/MetaServiceImpl.kt @@ -0,0 +1,16 @@ +package dev.usbharu.hideout.service + +import dev.usbharu.hideout.domain.model.hideout.entity.Jwt +import dev.usbharu.hideout.domain.model.hideout.entity.Meta +import dev.usbharu.hideout.exception.NotInitException +import dev.usbharu.hideout.repository.IMetaRepository + +class MetaServiceImpl(private val metaRepository: IMetaRepository) : IMetaService { + override suspend fun getMeta(): Meta = metaRepository.get() ?: throw NotInitException("Meta is null") + + override suspend fun updateMeta(meta: Meta) { + metaRepository.save(meta) + } + + override suspend fun getJwtMeta(): Jwt = getMeta().jwt +} diff --git a/src/main/kotlin/dev/usbharu/hideout/util/RsaUtil.kt b/src/main/kotlin/dev/usbharu/hideout/util/RsaUtil.kt index e7f9aef4..db912596 100644 --- a/src/main/kotlin/dev/usbharu/hideout/util/RsaUtil.kt +++ b/src/main/kotlin/dev/usbharu/hideout/util/RsaUtil.kt @@ -12,8 +12,12 @@ object RsaUtil { return KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec) as RSAPublicKey } + fun decodeRsaPublicKey(encoded: String): RSAPublicKey = decodeRsaPublicKey(Base64Util.decode(encoded)) + fun decodeRsaPrivateKey(byteArray: ByteArray):RSAPrivateKey{ val pkcS8EncodedKeySpec = PKCS8EncodedKeySpec(byteArray) return KeyFactory.getInstance("RSA").generatePrivate(pkcS8EncodedKeySpec) as RSAPrivateKey } + + fun decodeRsaPrivateKey(encoded: String):RSAPrivateKey = decodeRsaPrivateKey(Base64Util.decode(encoded)) } From 4e702d679332f732c61f6ed2f33a6d0ff3cc1aa2 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Tue, 2 May 2023 16:21:12 +0900 Subject: [PATCH 12/17] =?UTF-8?q?style:=20=E3=82=B9=E3=82=BF=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/dev/usbharu/hideout/Application.kt | 6 +++--- .../domain/model/hideout/dto/JwtToken.kt | 2 +- .../domain/model/hideout/entity/Meta.kt | 2 +- .../domain/model/hideout/form/RefreshToken.kt | 2 +- .../exception/InvalidRefreshTokenException.kt | 2 +- .../dev/usbharu/hideout/plugins/Security.kt | 2 +- .../repository/IJwtRefreshTokenRepository.kt | 16 ++++++++-------- .../hideout/repository/IMetaRepository.kt | 2 +- .../JwtRefreshTokenRepositoryImpl.kt | 1 + .../hideout/repository/MetaRepositoryImpl.kt | 3 ++- .../hideout/repository/PostRepositoryImpl.kt | 1 + .../hideout/repository/UserRepository.kt | 1 + .../usbharu/hideout/routing/AuthTestRouting.kt | 7 +++---- .../usbharu/hideout/routing/LoginRouting.kt | 3 +-- .../dev/usbharu/hideout/service/IJwtService.kt | 6 +++--- .../usbharu/hideout/service/JwtServiceImpl.kt | 2 ++ .../service/ServerInitialiseServiceImpl.kt | 5 ++--- .../activitypub/ActivityPubUserServiceImpl.kt | 2 +- .../dev/usbharu/hideout/util/Base64Util.kt | 1 - .../dev/usbharu/hideout/util/JsonWebKeyUtil.kt | 18 ++++++++---------- .../kotlin/dev/usbharu/hideout/util/RsaUtil.kt | 6 +++--- .../dev/usbharu/hideout/util/ServerUtil.kt | 3 ++- .../kjob/exposed/ExposedJobRepository.kt | 3 ++- .../kjob/exposed/ExposedLockRepository.kt | 1 + 24 files changed, 50 insertions(+), 47 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/Application.kt b/src/main/kotlin/dev/usbharu/hideout/Application.kt index 10f8596b..b6e8d924 100644 --- a/src/main/kotlin/dev/usbharu/hideout/Application.kt +++ b/src/main/kotlin/dev/usbharu/hideout/Application.kt @@ -38,7 +38,7 @@ val Application.property: Application.(propertyName: String) -> String } // application.conf references the main function. This annotation prevents the IDE from marking it as unused. -@Suppress("unused") +@Suppress("unused", "LongMethod") fun Application.parent() { Config.configData = ConfigData( url = property("hideout.url"), @@ -86,9 +86,9 @@ fun Application.parent() { single { TwitterSnowflakeIdGenerateService } single { MetaRepositoryImpl(get()) } single { ServerInitialiseServiceImpl(get()) } - single { JwtRefreshTokenRepositoryImpl(get(),get()) } + single { JwtRefreshTokenRepositoryImpl(get(), get()) } single { MetaServiceImpl(get()) } - single { JwtServiceImpl(get(),get(),get()) } + single { JwtServiceImpl(get(), get(), get()) } } configureKoin(module) runBlocking { diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/JwtToken.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/JwtToken.kt index fe25b3b0..9c232f82 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/JwtToken.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/JwtToken.kt @@ -1,3 +1,3 @@ package dev.usbharu.hideout.domain.model.hideout.dto -data class JwtToken(val token:String,val refreshToken:String) +data class JwtToken(val token: String, val refreshToken: String) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Meta.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Meta.kt index f1fa6d5f..770b04d1 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Meta.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Meta.kt @@ -1,3 +1,3 @@ package dev.usbharu.hideout.domain.model.hideout.entity -data class Meta(val version:String,val jwt:Jwt) +data class Meta(val version: String, val jwt: Jwt) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/RefreshToken.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/RefreshToken.kt index 3d35cf21..5d7898a1 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/RefreshToken.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/form/RefreshToken.kt @@ -1,3 +1,3 @@ package dev.usbharu.hideout.domain.model.hideout.form -data class RefreshToken(val refreshToken:String) +data class RefreshToken(val refreshToken: String) diff --git a/src/main/kotlin/dev/usbharu/hideout/exception/InvalidRefreshTokenException.kt b/src/main/kotlin/dev/usbharu/hideout/exception/InvalidRefreshTokenException.kt index e9b780d5..08c8ab7d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/exception/InvalidRefreshTokenException.kt +++ b/src/main/kotlin/dev/usbharu/hideout/exception/InvalidRefreshTokenException.kt @@ -1,6 +1,6 @@ package dev.usbharu.hideout.exception -class InvalidRefreshTokenException : IllegalArgumentException{ +class InvalidRefreshTokenException : IllegalArgumentException { constructor() : super() constructor(s: String?) : super(s) constructor(message: String?, cause: Throwable?) : super(message, cause) diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt index 468419c2..42210def 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt @@ -24,6 +24,7 @@ import java.util.concurrent.TimeUnit const val TOKEN_AUTH = "jwt-auth" +@Suppress("MagicNumber") fun Application.configureSecurity( userAuthService: IUserAuthService, metaService: IMetaService, @@ -39,7 +40,6 @@ fun Application.configureSecurity( jwt(TOKEN_AUTH) { verifier(jwkProvider, issuer) { acceptLeeway(3) - } validate { jwtCredential -> if (jwtCredential.payload.getClaim("username").asString().isNotEmpty()) { diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/IJwtRefreshTokenRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/IJwtRefreshTokenRepository.kt index 851fa5bc..9e6a3c96 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/IJwtRefreshTokenRepository.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/IJwtRefreshTokenRepository.kt @@ -3,18 +3,18 @@ package dev.usbharu.hideout.repository import dev.usbharu.hideout.domain.model.hideout.entity.JwtRefreshToken interface IJwtRefreshTokenRepository { - suspend fun generateId():Long + suspend fun generateId(): Long suspend fun save(token: JwtRefreshToken) - suspend fun findById(id:Long):JwtRefreshToken? - suspend fun findByToken(token:String):JwtRefreshToken? - suspend fun findByUserId(userId:Long):JwtRefreshToken? + suspend fun findById(id: Long): JwtRefreshToken? + suspend fun findByToken(token: String): JwtRefreshToken? + suspend fun findByUserId(userId: Long): JwtRefreshToken? - suspend fun delete(token:JwtRefreshToken) - suspend fun deleteById(id:Long) - suspend fun deleteByToken(token:String) - suspend fun deleteByUserId(userId:Long) + suspend fun delete(token: JwtRefreshToken) + suspend fun deleteById(id: Long) + suspend fun deleteByToken(token: String) + suspend fun deleteByUserId(userId: Long) suspend fun deleteAll() } diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/IMetaRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/IMetaRepository.kt index b3ed940f..b90be212 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/IMetaRepository.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/IMetaRepository.kt @@ -6,5 +6,5 @@ interface IMetaRepository { suspend fun save(meta: Meta) - suspend fun get():Meta? + suspend fun get(): Meta? } diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/JwtRefreshTokenRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/repository/JwtRefreshTokenRepositoryImpl.kt index 8796c9fc..74bf019c 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/JwtRefreshTokenRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/JwtRefreshTokenRepositoryImpl.kt @@ -22,6 +22,7 @@ class JwtRefreshTokenRepositoryImpl( } } + @Suppress("InjectDispatcher") suspend fun query(block: suspend () -> T): T = newSuspendedTransaction(Dispatchers.IO) { block() } diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/MetaRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/repository/MetaRepositoryImpl.kt index 5a537ae7..f58e555e 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/MetaRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/MetaRepositoryImpl.kt @@ -16,6 +16,7 @@ class MetaRepositoryImpl(private val database: Database) : IMetaRepository { } } + @Suppress("InjectDispatcher") suspend fun query(block: suspend () -> T): T = newSuspendedTransaction(Dispatchers.IO) { block() } @@ -29,7 +30,7 @@ class MetaRepositoryImpl(private val database: Database) : IMetaRepository { it[this.jwtPrivateKey] = meta.jwt.privateKey it[this.jwtPublicKey] = meta.jwt.publicKey } - }else { + } else { Meta.update({ Meta.id eq 1 }) { it[this.version] = meta.version it[kid] = UUID.randomUUID().toString() diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt index 237acb6b..669a0df7 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt @@ -20,6 +20,7 @@ class PostRepositoryImpl(database: Database, private val idGenerateService: IdGe } } + @Suppress("InjectDispatcher") suspend fun query(block: suspend () -> T): T = newSuspendedTransaction(Dispatchers.IO) { block() } diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/UserRepository.kt b/src/main/kotlin/dev/usbharu/hideout/repository/UserRepository.kt index 2f2905d8..8dc92ba0 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/UserRepository.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/UserRepository.kt @@ -21,6 +21,7 @@ class UserRepository(private val database: Database, private val idGenerateServi } } + @Suppress("InjectDispatcher") suspend fun query(block: suspend () -> T): T = newSuspendedTransaction(Dispatchers.IO) { block() } diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/AuthTestRouting.kt b/src/main/kotlin/dev/usbharu/hideout/routing/AuthTestRouting.kt index 79946c46..0667a7a7 100644 --- a/src/main/kotlin/dev/usbharu/hideout/routing/AuthTestRouting.kt +++ b/src/main/kotlin/dev/usbharu/hideout/routing/AuthTestRouting.kt @@ -7,10 +7,9 @@ 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"){ +fun Routing.authTestRouting() { + authenticate(TOKEN_AUTH) { + get("/auth-check") { val principal = call.principal() val username = principal!!.payload.getClaim("username") call.respondText("Hello $username") diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/LoginRouting.kt b/src/main/kotlin/dev/usbharu/hideout/routing/LoginRouting.kt index e0db266b..0a8b9a26 100644 --- a/src/main/kotlin/dev/usbharu/hideout/routing/LoginRouting.kt +++ b/src/main/kotlin/dev/usbharu/hideout/routing/LoginRouting.kt @@ -3,6 +3,5 @@ package dev.usbharu.hideout.routing import dev.usbharu.hideout.service.IUserAuthService import io.ktor.server.routing.* -fun Routing.login(userAuthService: IUserAuthService){ - +fun Routing.login(userAuthService: IUserAuthService) { } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/IJwtService.kt b/src/main/kotlin/dev/usbharu/hideout/service/IJwtService.kt index 0eece5a7..f722517f 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/IJwtService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/IJwtService.kt @@ -5,10 +5,10 @@ import dev.usbharu.hideout.domain.model.hideout.entity.User import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken interface IJwtService { - suspend fun createToken(user:User):JwtToken - suspend fun refreshToken(refreshToken: RefreshToken):JwtToken + suspend fun createToken(user: User): JwtToken + suspend fun refreshToken(refreshToken: RefreshToken): JwtToken suspend fun revokeToken(refreshToken: RefreshToken) - suspend fun revokeToken(user:User) + suspend fun revokeToken(user: User) suspend fun revokeAll() } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/JwtServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/JwtServiceImpl.kt index 5b036522..116b6088 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/JwtServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/JwtServiceImpl.kt @@ -18,6 +18,7 @@ import java.time.Instant import java.time.temporal.ChronoUnit import java.util.* +@Suppress("InjectDispatcher") class JwtServiceImpl( private val metaService: IMetaService, private val refreshTokenRepository: IJwtRefreshTokenRepository, @@ -42,6 +43,7 @@ class JwtServiceImpl( } } + @Suppress("MagicNumber") override suspend fun createToken(user: User): JwtToken { val now = Instant.now() val token = JWT.create() diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ServerInitialiseServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/ServerInitialiseServiceImpl.kt index f18a090a..35f53843 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ServerInitialiseServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ServerInitialiseServiceImpl.kt @@ -4,16 +4,16 @@ import dev.usbharu.hideout.domain.model.hideout.entity.Jwt import dev.usbharu.hideout.domain.model.hideout.entity.Meta import dev.usbharu.hideout.repository.IMetaRepository import dev.usbharu.hideout.util.ServerUtil +import org.slf4j.Logger import org.slf4j.LoggerFactory import java.security.KeyPairGenerator import java.util.* class ServerInitialiseServiceImpl(private val metaRepository: IMetaRepository) : IServerInitialiseService { - val logger = LoggerFactory.getLogger(ServerInitialiseServiceImpl::class.java) + val logger: Logger = LoggerFactory.getLogger(ServerInitialiseServiceImpl::class.java) override suspend fun init() { - val savedMeta = metaRepository.get() val implementationVersion = ServerUtil.getImplementationVersion() if (wasInitialised(savedMeta).not()) { @@ -27,7 +27,6 @@ class ServerInitialiseServiceImpl(private val metaRepository: IMetaRepository) : logger.info("Version changed!! (${savedMeta.version} -> $implementationVersion)") updateVersion(savedMeta, implementationVersion) } - } private fun wasInitialised(meta: Meta?): Boolean { diff --git a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserServiceImpl.kt index 18811258..767a6bdf 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/activitypub/ActivityPubUserServiceImpl.kt @@ -77,7 +77,7 @@ class ActivityPubUserServiceImpl( publicKeyPem = userEntity.publicKey ) ) - } catch (e: UserNotFoundException) { + } catch (ignore: UserNotFoundException) { val httpResponse = if (targetActor != null) { httpClient.getAp(url, "$targetActor#pubkey") } else { diff --git a/src/main/kotlin/dev/usbharu/hideout/util/Base64Util.kt b/src/main/kotlin/dev/usbharu/hideout/util/Base64Util.kt index 07ae9284..451d4ade 100644 --- a/src/main/kotlin/dev/usbharu/hideout/util/Base64Util.kt +++ b/src/main/kotlin/dev/usbharu/hideout/util/Base64Util.kt @@ -6,5 +6,4 @@ object Base64Util { fun decode(str: String): ByteArray = Base64.getDecoder().decode(str) fun encode(bytes: ByteArray): String = Base64.getEncoder().encodeToString(bytes) - } diff --git a/src/main/kotlin/dev/usbharu/hideout/util/JsonWebKeyUtil.kt b/src/main/kotlin/dev/usbharu/hideout/util/JsonWebKeyUtil.kt index 4b73a8f5..c733388e 100644 --- a/src/main/kotlin/dev/usbharu/hideout/util/JsonWebKeyUtil.kt +++ b/src/main/kotlin/dev/usbharu/hideout/util/JsonWebKeyUtil.kt @@ -8,29 +8,27 @@ import java.util.* object JsonWebKeyUtil { - fun publicKeyToJwk(publicKey: String,kid:String): String { + fun publicKeyToJwk(publicKey: String, kid: String): String { val x509EncodedKeySpec = X509EncodedKeySpec(Base64.getDecoder().decode(publicKey)) val generatePublic = KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec) - return publicKeyToJwk(generatePublic as RSAPublicKey,kid) + return publicKeyToJwk(generatePublic as RSAPublicKey, kid) } - fun publicKeyToJwk(publicKey: RSAPublicKey,kid:String): String { + fun publicKeyToJwk(publicKey: RSAPublicKey, kid: String): String { val e = encodeBase64UInt(publicKey.publicExponent) val n = encodeBase64UInt(publicKey.modulus) return """{"keys":[{"e":"$e","n":"$n","use":"sig","kid":"$kid","kty":"RSA"}]}""" } private fun encodeBase64UInt(bigInteger: BigInteger, minLength: Int = -1): String { - if(bigInteger.signum() < 0){ - throw IllegalArgumentException("Cannot encode negative numbers") - } + require(bigInteger.signum() >= 0) { "Cannot encode negative numbers" } var bytes = bigInteger.toByteArray() - if (bigInteger.bitLength() % 8 == 0 && (bytes[0] == 0.toByte()) && bytes.size > 1){ - bytes = Arrays.copyOfRange(bytes, 1, bytes.size) + if (bigInteger.bitLength() % 8 == 0 && (bytes[0] == 0.toByte()) && bytes.size > 1) { + bytes = Arrays.copyOfRange(bytes, 1, bytes.size) } - if (minLength != -1){ - if (bytes.size < minLength){ + if (minLength != -1) { + if (bytes.size < minLength) { val array = ByteArray(minLength) System.arraycopy(bytes, 0, array, minLength - bytes.size, bytes.size) bytes = array diff --git a/src/main/kotlin/dev/usbharu/hideout/util/RsaUtil.kt b/src/main/kotlin/dev/usbharu/hideout/util/RsaUtil.kt index db912596..e0ebbfc8 100644 --- a/src/main/kotlin/dev/usbharu/hideout/util/RsaUtil.kt +++ b/src/main/kotlin/dev/usbharu/hideout/util/RsaUtil.kt @@ -7,17 +7,17 @@ import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.X509EncodedKeySpec object RsaUtil { - fun decodeRsaPublicKey(byteArray: ByteArray):RSAPublicKey{ + fun decodeRsaPublicKey(byteArray: ByteArray): RSAPublicKey { val x509EncodedKeySpec = X509EncodedKeySpec(byteArray) return KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec) as RSAPublicKey } fun decodeRsaPublicKey(encoded: String): RSAPublicKey = decodeRsaPublicKey(Base64Util.decode(encoded)) - fun decodeRsaPrivateKey(byteArray: ByteArray):RSAPrivateKey{ + fun decodeRsaPrivateKey(byteArray: ByteArray): RSAPrivateKey { val pkcS8EncodedKeySpec = PKCS8EncodedKeySpec(byteArray) return KeyFactory.getInstance("RSA").generatePrivate(pkcS8EncodedKeySpec) as RSAPrivateKey } - fun decodeRsaPrivateKey(encoded: String):RSAPrivateKey = decodeRsaPrivateKey(Base64Util.decode(encoded)) + fun decodeRsaPrivateKey(encoded: String): RSAPrivateKey = decodeRsaPrivateKey(Base64Util.decode(encoded)) } diff --git a/src/main/kotlin/dev/usbharu/hideout/util/ServerUtil.kt b/src/main/kotlin/dev/usbharu/hideout/util/ServerUtil.kt index 438f7e33..e8264487 100644 --- a/src/main/kotlin/dev/usbharu/hideout/util/ServerUtil.kt +++ b/src/main/kotlin/dev/usbharu/hideout/util/ServerUtil.kt @@ -1,5 +1,6 @@ package dev.usbharu.hideout.util object ServerUtil { - fun getImplementationVersion():String = ServerUtil.javaClass.`package`.implementationVersion ?: "DEVELOPMENT-VERSION" + fun getImplementationVersion(): String = + ServerUtil.javaClass.`package`.implementationVersion ?: "DEVELOPMENT-VERSION" } diff --git a/src/main/kotlin/dev/usbharu/kjob/exposed/ExposedJobRepository.kt b/src/main/kotlin/dev/usbharu/kjob/exposed/ExposedJobRepository.kt index a9f5e5bb..c2ba778e 100644 --- a/src/main/kotlin/dev/usbharu/kjob/exposed/ExposedJobRepository.kt +++ b/src/main/kotlin/dev/usbharu/kjob/exposed/ExposedJobRepository.kt @@ -54,6 +54,7 @@ class ExposedJobRepository( } } + @Suppress("InjectDispatcher") suspend fun query(block: suspend () -> T): T = newSuspendedTransaction(Dispatchers.IO) { block() } override suspend fun completeProgress(id: String): Boolean { @@ -204,7 +205,7 @@ class ExposedJobRepository( this ?: return emptyMap() return json.parseToJsonElement(this).jsonObject.mapValues { (_, el) -> if (el is JsonObject) { - val t = el["t"]?.jsonPrimitive?.content ?: error("Cannot get jsonPrimitive") + val t = el["t"]?.run { jsonPrimitive.content } ?: error("Cannot get jsonPrimitive") val value = el["v"]?.jsonArray ?: error("Cannot get jsonArray") when (t) { "s" -> value.map { it.jsonPrimitive.content } diff --git a/src/main/kotlin/dev/usbharu/kjob/exposed/ExposedLockRepository.kt b/src/main/kotlin/dev/usbharu/kjob/exposed/ExposedLockRepository.kt index 9ed2146f..e29341ba 100644 --- a/src/main/kotlin/dev/usbharu/kjob/exposed/ExposedLockRepository.kt +++ b/src/main/kotlin/dev/usbharu/kjob/exposed/ExposedLockRepository.kt @@ -33,6 +33,7 @@ class ExposedLockRepository( } } + @Suppress("InjectDispatcher") suspend fun query(block: suspend () -> T): T = newSuspendedTransaction(Dispatchers.IO) { block() } override suspend fun exists(id: UUID): Boolean { From d9ce28d094e0b68a0890103bca9fe4bb95459c5f Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Tue, 2 May 2023 16:23:36 +0900 Subject: [PATCH 13/17] =?UTF-8?q?style:=20=E3=82=B9=E3=82=BF=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/dev/usbharu/kjob/exposed/ExposedJobRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/dev/usbharu/kjob/exposed/ExposedJobRepository.kt b/src/main/kotlin/dev/usbharu/kjob/exposed/ExposedJobRepository.kt index c2ba778e..af71d07f 100644 --- a/src/main/kotlin/dev/usbharu/kjob/exposed/ExposedJobRepository.kt +++ b/src/main/kotlin/dev/usbharu/kjob/exposed/ExposedJobRepository.kt @@ -290,7 +290,7 @@ class ExposedJobRepository( try { @Suppress("SwallowedException") UUID.fromString(it) - } catch (e: IllegalArgumentException) { + } catch (ignored: IllegalArgumentException) { null } }, From 295b6b15728170d1bd18079a4e37cda7a4dc1e4a Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Wed, 3 May 2023 14:53:35 +0900 Subject: [PATCH 14/17] =?UTF-8?q?test:=20JWT=E3=81=A7=E3=83=AD=E3=82=B0?= =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/dev/usbharu/hideout/Application.kt | 14 +- .../dev/usbharu/hideout/plugins/Routing.kt | 3 - .../dev/usbharu/hideout/plugins/Security.kt | 22 +- .../hideout/routing/AuthTestRouting.kt | 18 -- .../usbharu/hideout/plugins/SecurityKtTest.kt | 282 ++++++++++++++++++ src/test/resources/empty.conf | 3 +- 6 files changed, 307 insertions(+), 35 deletions(-) delete mode 100644 src/main/kotlin/dev/usbharu/hideout/routing/AuthTestRouting.kt create mode 100644 src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/Application.kt b/src/main/kotlin/dev/usbharu/hideout/Application.kt index b6e8d924..1bc2747e 100644 --- a/src/main/kotlin/dev/usbharu/hideout/Application.kt +++ b/src/main/kotlin/dev/usbharu/hideout/Application.kt @@ -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): Unit = io.ktor.server.cio.EngineMain.main(args) @@ -89,6 +92,14 @@ fun Application.parent() { single { JwtRefreshTokenRepositoryImpl(get(), get()) } single { MetaServiceImpl(get()) } single { JwtServiceImpl(get(), get(), get()) } + single { + 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().value, inject().value, inject().value, - inject().value + inject().value, + inject().value, ) configureRouting( inject().value, diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt index 7c843ce9..fed45736 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Routing.kt @@ -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() } } diff --git a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt index 42210def..674b228a 100644 --- a/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt +++ b/src/main/kotlin/dev/usbharu/hideout/plugins/Security.kt @@ -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() + val username = principal!!.payload.getClaim("username") + call.respondText("Hello $username") + } + } } } diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/AuthTestRouting.kt b/src/main/kotlin/dev/usbharu/hideout/routing/AuthTestRouting.kt deleted file mode 100644 index 0667a7a7..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/routing/AuthTestRouting.kt +++ /dev/null @@ -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() - val username = principal!!.payload.getClaim("username") - call.respondText("Hello $username") - } - } -} diff --git a/src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt b/src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt new file mode 100644 index 00000000..d5f6a9ea --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt @@ -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 { + onBlocking { verifyAccount(eq("testUser"), eq("password")) } doReturn true + } + val metaService = mock() + val userRepository = mock { + 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 { + onBlocking { createToken(any()) } doReturn jwtToken + } + val jwkProvider = mock() + 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 { + onBlocking { verifyAccount(anyString(), anyString()) }.doReturn(false) + } + val metaService = mock() + val userRepository = mock() + val jwtService = mock() + val jwkProvider = mock() + 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 { + onBlocking { verifyAccount(anyString(), eq("InvalidPassword")) } doReturn false + } + val metaService = mock() + val userRepository = mock() + val jwtService = mock() + val jwkProvider = mock() + 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 { + 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) + } + 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()) + } + } +} diff --git a/src/test/resources/empty.conf b/src/test/resources/empty.conf index ba691e1c..3c142bff 100644 --- a/src/test/resources/empty.conf +++ b/src/test/resources/empty.conf @@ -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" From c4ae2d16f1eb39e2bc141490b46f5d6f5bcb0a93 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Wed, 3 May 2023 15:32:13 +0900 Subject: [PATCH 15/17] =?UTF-8?q?test:=20=E3=83=AA=E3=83=95=E3=83=AC?= =?UTF-8?q?=E3=83=83=E3=82=B7=E3=83=A5=E3=83=88=E3=83=BC=E3=82=AF=E3=83=B3?= =?UTF-8?q?=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/usbharu/hideout/plugins/Security.kt | 2 +- .../usbharu/hideout/plugins/SecurityKtTest.kt | 311 ++++++++++++++++-- 2 files changed, 288 insertions(+), 25 deletions(-) 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) } } } From ad26369346a9da7b249bc3c8ee31afb6fac5321b Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Wed, 3 May 2023 16:44:46 +0900 Subject: [PATCH 16/17] =?UTF-8?q?test:=20JWT=E3=81=AE=E7=99=BA=E8=A1=8C?= =?UTF-8?q?=E3=80=81=E3=83=AA=E3=83=95=E3=83=AC=E3=83=83=E3=82=B7=E3=83=A5?= =?UTF-8?q?=E3=83=88=E3=83=BC=E3=82=AF=E3=83=B3=E3=81=AE=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../usbharu/hideout/service/JwtServiceImpl.kt | 2 +- .../hideout/service/JwtServiceImplTest.kt | 183 ++++++++++++++++++ 2 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 src/test/kotlin/dev/usbharu/hideout/service/JwtServiceImplTest.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/service/JwtServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/JwtServiceImpl.kt index 116b6088..3548c84d 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/JwtServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/JwtServiceImpl.kt @@ -47,7 +47,7 @@ class JwtServiceImpl( override suspend fun createToken(user: User): JwtToken { val now = Instant.now() val token = JWT.create() - .withAudience("${Config.configData.url}/users/${user.id}") + .withAudience("${Config.configData.url}/users/${user.name}") .withIssuer(Config.configData.url) .withKeyId(keyId.await().toString()) .withClaim("username", user.name) diff --git a/src/test/kotlin/dev/usbharu/hideout/service/JwtServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/JwtServiceImplTest.kt new file mode 100644 index 00000000..6faa4416 --- /dev/null +++ b/src/test/kotlin/dev/usbharu/hideout/service/JwtServiceImplTest.kt @@ -0,0 +1,183 @@ +@file:OptIn(ExperimentalCoroutinesApi::class) + +package dev.usbharu.hideout.service + +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import dev.usbharu.hideout.config.Config +import dev.usbharu.hideout.config.ConfigData +import dev.usbharu.hideout.domain.model.hideout.entity.Jwt +import dev.usbharu.hideout.domain.model.hideout.entity.JwtRefreshToken +import dev.usbharu.hideout.domain.model.hideout.entity.User +import dev.usbharu.hideout.domain.model.hideout.form.RefreshToken +import dev.usbharu.hideout.exception.InvalidRefreshTokenException +import dev.usbharu.hideout.repository.IJwtRefreshTokenRepository +import dev.usbharu.hideout.service.impl.IUserService +import dev.usbharu.hideout.util.Base64Util +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.kotlin.doReturn +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 +import kotlin.test.assertNotEquals + +class JwtServiceImplTest { + @Test + fun `createToken トークンを作成できる`() = runTest { + Config.configData = ConfigData(url = "https://example.com", objectMapper = jacksonObjectMapper()) + val kid = UUID.randomUUID() + val keyPairGenerator = KeyPairGenerator.getInstance("RSA") + keyPairGenerator.initialize(2048) + val generateKeyPair = keyPairGenerator.generateKeyPair() + + val metaService = mock { + onBlocking { getJwtMeta() } doReturn Jwt( + kid, + Base64Util.encode(generateKeyPair.private.encoded), Base64Util.encode(generateKeyPair.public.encoded) + ) + } + val refreshTokenRepository = mock { + onBlocking { generateId() } doReturn 1L + } + val jwtService = JwtServiceImpl(metaService, refreshTokenRepository, mock()) + val token = jwtService.createToken( + User( + id = 1L, + name = "test", + domain = "example.com", + screenName = "testUser", + description = "", + password = "hashedPassword", + inbox = "https://example.com/inbox", + outbox = "https://example.com/outbox", + url = "https://example.com", + publicKey = "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----", + privateKey = "-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----", + createdAt = Instant.now() + ) + ) + assertNotEquals("", token.token) + assertNotEquals("", token.refreshToken) + val verify = JWT.require( + Algorithm.RSA256( + generateKeyPair.public as RSAPublicKey, + generateKeyPair.private as RSAPrivateKey + ) + ) + .withAudience("https://example.com/users/test") + .withIssuer("https://example.com") + .acceptLeeway(3L) + .build() + .verify(token.token) + + assertEquals(kid.toString(), verify.keyId) + } + + @Test + fun `refreshToken リフレッシュトークンからトークンを作成できる`() = runTest { + Config.configData = ConfigData(url = "https://example.com", objectMapper = jacksonObjectMapper()) + val kid = UUID.randomUUID() + val keyPairGenerator = KeyPairGenerator.getInstance("RSA") + keyPairGenerator.initialize(2048) + val generateKeyPair = keyPairGenerator.generateKeyPair() + + val refreshTokenRepository = mock { + onBlocking { findByToken("refreshToken") } doReturn JwtRefreshToken( + id = 1L, + userId = 1L, + refreshToken = "refreshToken", + createdAt = Instant.now().minus(60, ChronoUnit.MINUTES), + expiresAt = Instant.now().plus(14, ChronoUnit.DAYS).minus(60, ChronoUnit.MINUTES) + ) + onBlocking { generateId() } doReturn 2L + } + val userService = mock { + onBlocking { findById(1L) } doReturn User( + id = 1L, + name = "test", + domain = "example.com", + screenName = "testUser", + description = "", + password = "hashedPassword", + inbox = "https://example.com/inbox", + outbox = "https://example.com/outbox", + url = "https://example.com", + publicKey = "-----BEGIN PUBLIC KEY-----...-----BEGIN PUBLIC KEY-----", + privateKey = "-----BEGIN PRIVATE KEY-----...-----BEGIN PRIVATE KEY-----", + createdAt = Instant.now() + ) + } + val metaService = mock { + onBlocking { getJwtMeta() } doReturn Jwt( + kid, + Base64Util.encode(generateKeyPair.private.encoded), Base64Util.encode(generateKeyPair.public.encoded) + ) + } + val jwtService = JwtServiceImpl(metaService, refreshTokenRepository, userService) + val refreshToken = jwtService.refreshToken(RefreshToken("refreshToken")) + assertNotEquals("", refreshToken.token) + assertNotEquals("", refreshToken.refreshToken) + + val verify = JWT.require( + Algorithm.RSA256( + generateKeyPair.public as RSAPublicKey, + generateKeyPair.private as RSAPrivateKey + ) + ) + .withAudience("https://example.com/users/test") + .withIssuer("https://example.com") + .acceptLeeway(3L) + .build() + .verify(refreshToken.token) + + assertEquals(kid.toString(), verify.keyId) + } + + @Test + fun `refreshToken 無効なリフレッシュトークンは失敗する`() = runTest { + val refreshTokenRepository = mock { + onBlocking { findByToken("InvalidRefreshToken") } doReturn null + } + val jwtService = JwtServiceImpl(mock(), refreshTokenRepository, mock()) + assertThrows { jwtService.refreshToken(RefreshToken("InvalidRefreshToken")) } + } + + @Test + fun `refreshToken 未来に作成されたリフレッシュトークンは失敗する`() = runTest { + val refreshTokenRepository = mock { + onBlocking { findByToken("refreshToken") } doReturn JwtRefreshToken( + id = 1L, + userId = 1L, + refreshToken = "refreshToken", + createdAt = Instant.now().plus(10, ChronoUnit.MINUTES), + expiresAt = Instant.now().plus(10, ChronoUnit.MINUTES).plus(14, ChronoUnit.DAYS) + ) + } + val jwtService = JwtServiceImpl(mock(), refreshTokenRepository, mock()) + assertThrows { jwtService.refreshToken(RefreshToken("refreshToken")) } + } + + @Test + fun `refreshToken 期限切れのリフレッシュトークンでは失敗する`() = runTest { + val refreshTokenRepository = mock { + onBlocking { findByToken("refreshToken") } doReturn JwtRefreshToken( + id = 1L, + userId = 1L, + refreshToken = "refreshToken", + createdAt = Instant.now().minus(30, ChronoUnit.DAYS), + expiresAt = Instant.now().minus(16, ChronoUnit.DAYS) + ) + } + val jwtService = JwtServiceImpl(mock(), refreshTokenRepository, mock()) + assertThrows { jwtService.refreshToken(RefreshToken("refreshToken")) } + } +} From 17e574feca87c111e5998bffa91af646bb041b33 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Wed, 3 May 2023 16:53:26 +0900 Subject: [PATCH 17/17] =?UTF-8?q?style:=20=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=81=AE=E3=82=B9=E3=82=BF=E3=82=A4=E3=83=AB=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/dev/usbharu/hideout/routing/LoginRouting.kt | 7 ------- .../dev/usbharu/hideout/plugins/SecurityKtTest.kt | 10 ---------- .../dev/usbharu/hideout/service/JwtServiceImplTest.kt | 6 ++++-- 3 files changed, 4 insertions(+), 19 deletions(-) delete mode 100644 src/main/kotlin/dev/usbharu/hideout/routing/LoginRouting.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/routing/LoginRouting.kt b/src/main/kotlin/dev/usbharu/hideout/routing/LoginRouting.kt deleted file mode 100644 index 0a8b9a26..00000000 --- a/src/main/kotlin/dev/usbharu/hideout/routing/LoginRouting.kt +++ /dev/null @@ -1,7 +0,0 @@ -package dev.usbharu.hideout.routing - -import dev.usbharu.hideout.service.IUserAuthService -import io.ktor.server.routing.* - -fun Routing.login(userAuthService: IUserAuthService) { -} diff --git a/src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt b/src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt index 32473173..9c558f36 100644 --- a/src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/plugins/SecurityKtTest.kt @@ -211,7 +211,6 @@ class SecurityKtTest { Config.configData = ConfigData(url = "https://localhost", objectMapper = jacksonObjectMapper()) - val now = Instant.now() val kid = UUID.randomUUID() val token = JWT.create() @@ -231,7 +230,6 @@ class SecurityKtTest { ) } - val readValue = Config.configData.objectMapper.readerFor(Map::class.java) .readValue?>( JsonWebKeyUtil.publicKeyToJwk( @@ -273,7 +271,6 @@ class SecurityKtTest { Config.configData = ConfigData(url = "https://localhost", objectMapper = jacksonObjectMapper()) - val now = Instant.now() val kid = UUID.randomUUID() val token = JWT.create() @@ -293,7 +290,6 @@ class SecurityKtTest { ) } - val readValue = Config.configData.objectMapper.readerFor(Map::class.java) .readValue?>( JsonWebKeyUtil.publicKeyToJwk( @@ -333,7 +329,6 @@ class SecurityKtTest { Config.configData = ConfigData(url = "https://localhost", objectMapper = jacksonObjectMapper()) - val now = Instant.now() val kid = UUID.randomUUID() val token = JWT.create() @@ -353,7 +348,6 @@ class SecurityKtTest { ) } - val readValue = Config.configData.objectMapper.readerFor(Map::class.java) .readValue?>( JsonWebKeyUtil.publicKeyToJwk( @@ -393,7 +387,6 @@ class SecurityKtTest { Config.configData = ConfigData(url = "https://localhost", objectMapper = jacksonObjectMapper()) - val now = Instant.now() val kid = UUID.randomUUID() val token = JWT.create() @@ -413,7 +406,6 @@ class SecurityKtTest { ) } - val readValue = Config.configData.objectMapper.readerFor(Map::class.java) .readValue?>( JsonWebKeyUtil.publicKeyToJwk( @@ -453,7 +445,6 @@ class SecurityKtTest { Config.configData = ConfigData(url = "https://localhost", objectMapper = jacksonObjectMapper()) - val now = Instant.now() val kid = UUID.randomUUID() val token = JWT.create() @@ -472,7 +463,6 @@ class SecurityKtTest { ) } - val readValue = Config.configData.objectMapper.readerFor(Map::class.java) .readValue?>( JsonWebKeyUtil.publicKeyToJwk( diff --git a/src/test/kotlin/dev/usbharu/hideout/service/JwtServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/JwtServiceImplTest.kt index 6faa4416..7c4f90bb 100644 --- a/src/test/kotlin/dev/usbharu/hideout/service/JwtServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/service/JwtServiceImplTest.kt @@ -42,7 +42,8 @@ class JwtServiceImplTest { val metaService = mock { onBlocking { getJwtMeta() } doReturn Jwt( kid, - Base64Util.encode(generateKeyPair.private.encoded), Base64Util.encode(generateKeyPair.public.encoded) + Base64Util.encode(generateKeyPair.private.encoded), + Base64Util.encode(generateKeyPair.public.encoded) ) } val refreshTokenRepository = mock { @@ -119,7 +120,8 @@ class JwtServiceImplTest { val metaService = mock { onBlocking { getJwtMeta() } doReturn Jwt( kid, - Base64Util.encode(generateKeyPair.private.encoded), Base64Util.encode(generateKeyPair.public.encoded) + Base64Util.encode(generateKeyPair.private.encoded), + Base64Util.encode(generateKeyPair.public.encoded) ) } val jwtService = JwtServiceImpl(metaService, refreshTokenRepository, userService)