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] =?UTF-8?q?feat:=20=E3=83=AA=E3=83=95=E3=83=AC=E3=83=83?= =?UTF-8?q?=E3=82=B7=E3=83=A5=E3=83=88=E3=83=BC=E3=82=AF=E3=83=B3=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 | 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) + +}