mirror of https://github.com/usbharu/Hideout.git
commit
85bbe55628
|
@ -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,18 @@ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask<*>>().con
|
|||
compilerOptions.apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_8)
|
||||
}
|
||||
|
||||
tasks.withType<ShadowJar> {
|
||||
manifest {
|
||||
attributes(
|
||||
"Implementation-Version" to project.version.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.clean {
|
||||
delete += listOf("$rootDir/src/main/resources/static")
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
@ -45,7 +59,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")
|
||||
|
|
|
@ -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
|
||||
|
@ -8,15 +10,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,8 +28,10 @@ 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
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
fun main(args: Array<String>): Unit = io.ktor.server.cio.EngineMain.main(args)
|
||||
|
||||
|
@ -43,7 +41,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"),
|
||||
|
@ -89,14 +87,36 @@ fun Application.parent() {
|
|||
single<IPostService> { PostService(get(), get()) }
|
||||
single<IPostRepository> { PostRepositoryImpl(get(), get()) }
|
||||
single<IdGenerateService> { TwitterSnowflakeIdGenerateService }
|
||||
single<IMetaRepository> { MetaRepositoryImpl(get()) }
|
||||
single<IServerInitialiseService> { ServerInitialiseServiceImpl(get()) }
|
||||
single<IJwtRefreshTokenRepository> { JwtRefreshTokenRepositoryImpl(get(), get()) }
|
||||
single<IMetaService> { MetaServiceImpl(get()) }
|
||||
single<IJwtService> { JwtServiceImpl(get(), get(), get()) }
|
||||
single<JwkProvider> {
|
||||
JwkProviderBuilder(Config.configData.url).cached(
|
||||
10,
|
||||
24,
|
||||
TimeUnit.HOURS
|
||||
)
|
||||
.rateLimited(10, 1, TimeUnit.MINUTES).build()
|
||||
}
|
||||
}
|
||||
|
||||
configureKoin(module)
|
||||
runBlocking {
|
||||
inject<IServerInitialiseService>().value.init()
|
||||
}
|
||||
configureHTTP()
|
||||
configureStaticRouting()
|
||||
configureMonitoring()
|
||||
configureSerialization()
|
||||
register(inject<IUserService>().value)
|
||||
configureSecurity(
|
||||
inject<IUserAuthService>().value,
|
||||
inject<IMetaService>().value,
|
||||
inject<IUserRepository>().value,
|
||||
inject<IJwtService>().value,
|
||||
inject<JwkProvider>().value,
|
||||
)
|
||||
configureRouting(
|
||||
inject<HttpSignatureVerifyService>().value,
|
||||
inject<ActivityPubService>().value,
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
package dev.usbharu.hideout.domain.model.hideout.dto
|
||||
|
||||
data class JwtToken(val token: String, val refreshToken: String)
|
|
@ -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)
|
|
@ -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
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
package dev.usbharu.hideout.domain.model.hideout.entity
|
||||
|
||||
data class Meta(val version: String, val jwt: Jwt)
|
|
@ -0,0 +1,3 @@
|
|||
package dev.usbharu.hideout.domain.model.hideout.form
|
||||
|
||||
data class RefreshToken(val refreshToken: String)
|
|
@ -0,0 +1,3 @@
|
|||
package dev.usbharu.hideout.domain.model.hideout.form
|
||||
|
||||
data class UserLogin(val username: String, val password: String)
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1,25 +1,82 @@
|
|||
@file:Suppress("UnusedPrivateMember")
|
||||
|
||||
package dev.usbharu.hideout.plugins
|
||||
|
||||
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.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.JsonWebKeyUtil
|
||||
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.*
|
||||
|
||||
const val TOKEN_AUTH = "token-auth"
|
||||
const val TOKEN_AUTH = "jwt-auth"
|
||||
|
||||
fun Application.configureSecurity(userAuthService: IUserAuthService) {
|
||||
@Suppress("MagicNumber")
|
||||
fun Application.configureSecurity(
|
||||
userAuthService: IUserAuthService,
|
||||
metaService: IMetaService,
|
||||
userRepository: IUserRepository,
|
||||
jwtService: IJwtService,
|
||||
jwkProvider: JwkProvider
|
||||
) {
|
||||
val issuer = Config.configData.url
|
||||
install(Authentication) {
|
||||
bearer(TOKEN_AUTH) {
|
||||
authenticate { bearerTokenCredential ->
|
||||
UserIdPrincipal(bearerTokenCredential.token)
|
||||
jwt(TOKEN_AUTH) {
|
||||
verifier(jwkProvider, issuer) {
|
||||
acceptLeeway(3)
|
||||
}
|
||||
validate { jwtCredential ->
|
||||
if (jwtCredential.payload.getClaim("username")?.asString().isNullOrBlank().not()) {
|
||||
JWTPrincipal(jwtCredential.payload)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
routing {
|
||||
post("/login") {
|
||||
val loginUser = call.receive<UserLogin>()
|
||||
val check = userAuthService.verifyAccount(loginUser.username, loginUser.password)
|
||||
if (check.not()) {
|
||||
return@post call.respond(HttpStatusCode.Unauthorized)
|
||||
}
|
||||
|
||||
val user = userRepository.findByNameAndDomain(loginUser.username, Config.configData.domain)
|
||||
?: throw UserNotFoundException("${loginUser.username} was not found.")
|
||||
|
||||
return@post call.respond(jwtService.createToken(user))
|
||||
}
|
||||
|
||||
post("/refresh-token") {
|
||||
val refreshToken = call.receive<RefreshToken>()
|
||||
return@post call.respond(jwtService.refreshToken(refreshToken))
|
||||
}
|
||||
|
||||
get("/.well-known/jwks.json") {
|
||||
//language=JSON
|
||||
val jwt = metaService.getJwtMeta()
|
||||
call.respondText(
|
||||
contentType = ContentType.Application.Json,
|
||||
text = JsonWebKeyUtil.publicKeyToJwk(jwt.publicKey, jwt.kid.toString())
|
||||
)
|
||||
}
|
||||
authenticate(TOKEN_AUTH) {
|
||||
get("/auth-check") {
|
||||
val principal = call.principal<JWTPrincipal>()
|
||||
val username = principal!!.payload.getClaim("username")
|
||||
call.respondText("Hello $username")
|
||||
}
|
||||
skipWhen { true }
|
||||
}
|
||||
}
|
||||
// install(Sessions) {
|
||||
// cookie<UserSession>("MY_SESSION") {
|
||||
// cookie.extensions["SameSite"] = "lax"
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -7,11 +7,11 @@ import io.ktor.server.response.*
|
|||
|
||||
fun Application.configureStatusPages() {
|
||||
install(StatusPages) {
|
||||
exception<Throwable> { call, cause ->
|
||||
call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError)
|
||||
}
|
||||
exception<IllegalArgumentException> { call, cause ->
|
||||
call.respondText(text = "400: $cause", status = HttpStatusCode.BadRequest)
|
||||
}
|
||||
exception<Throwable> { call, cause ->
|
||||
call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
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()
|
||||
}
|
|
@ -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?
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
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,
|
||||
private val idGenerateService: IdGenerateService
|
||||
) :
|
||||
IJwtRefreshTokenRepository {
|
||||
|
||||
init {
|
||||
transaction(database) {
|
||||
SchemaUtils.create(JwtRefreshTokens)
|
||||
SchemaUtils.createMissingTablesAndColumns(JwtRefreshTokens)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("InjectDispatcher")
|
||||
suspend fun <T> 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()) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("InjectDispatcher")
|
||||
suspend fun <T> 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)
|
||||
}
|
|
@ -20,6 +20,7 @@ class PostRepositoryImpl(database: Database, private val idGenerateService: IdGe
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("InjectDispatcher")
|
||||
suspend fun <T> query(block: suspend () -> T): T =
|
||||
newSuspendedTransaction(Dispatchers.IO) { block() }
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ class UserRepository(private val database: Database, private val idGenerateServi
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("InjectDispatcher")
|
||||
suspend fun <T> query(block: suspend () -> T): T =
|
||||
newSuspendedTransaction(Dispatchers.IO) { block() }
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package dev.usbharu.hideout.service
|
||||
|
||||
interface IServerInitialiseService {
|
||||
suspend fun init()
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
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.*
|
||||
|
||||
@Suppress("InjectDispatcher")
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override suspend fun createToken(user: User): JwtToken {
|
||||
val now = Instant.now()
|
||||
val token = JWT.create()
|
||||
.withAudience("${Config.configData.url}/users/${user.name}")
|
||||
.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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
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.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.security.KeyPairGenerator
|
||||
import java.util.*
|
||||
|
||||
class ServerInitialiseServiceImpl(private val metaRepository: IMetaRepository) : IServerInitialiseService {
|
||||
|
||||
val logger: 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.private.encoded),
|
||||
Base64.getEncoder().encodeToString(generateKeyPair.public.encoded)
|
||||
)
|
||||
val meta = Meta(implementationVersion, jwt)
|
||||
metaRepository.save(meta)
|
||||
}
|
||||
|
||||
private suspend fun updateVersion(meta: Meta, version: String) {
|
||||
metaRepository.save(meta.copy(version = version))
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
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)
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
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, kid: String): String {
|
||||
val x509EncodedKeySpec = X509EncodedKeySpec(Base64.getDecoder().decode(publicKey))
|
||||
val generatePublic = KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec)
|
||||
return publicKeyToJwk(generatePublic as RSAPublicKey, kid)
|
||||
}
|
||||
|
||||
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 {
|
||||
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 (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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
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 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))
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package dev.usbharu.hideout.util
|
||||
|
||||
object ServerUtil {
|
||||
fun getImplementationVersion(): String =
|
||||
ServerUtil.javaClass.`package`.implementationVersion ?: "DEVELOPMENT-VERSION"
|
||||
}
|
|
@ -54,6 +54,7 @@ class ExposedJobRepository(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("InjectDispatcher")
|
||||
suspend fun <T> 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 }
|
||||
|
@ -289,7 +290,7 @@ class ExposedJobRepository(
|
|||
try {
|
||||
@Suppress("SwallowedException")
|
||||
UUID.fromString(it)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
} catch (ignored: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
},
|
||||
|
|
|
@ -33,6 +33,7 @@ class ExposedLockRepository(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("InjectDispatcher")
|
||||
suspend fun <T> query(block: suspend () -> T): T = newSuspendedTransaction(Dispatchers.IO) { block() }
|
||||
|
||||
override suspend fun exists(id: UUID): Boolean {
|
||||
|
|
|
@ -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'"
|
||||
}
|
||||
|
|
|
@ -1,5 +1,58 @@
|
|||
import {Component} from "solid-js";
|
||||
import {Component, createSignal} from "solid-js";
|
||||
|
||||
export const App: Component = () => {
|
||||
return (<p>aaa</p>)
|
||||
|
||||
const fn = (form: HTMLButtonElement) => {
|
||||
console.log(form)
|
||||
}
|
||||
|
||||
const [username, setUsername] = createSignal("")
|
||||
const [password, setPassword] = createSignal("")
|
||||
|
||||
return (
|
||||
<form onSubmit={function (e: SubmitEvent) {
|
||||
e.preventDefault()
|
||||
fetch("/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({username: username(), password: password()}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).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: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({refreshToken: res.refreshToken}),
|
||||
}).then(res=> res.json()).then(res => console.log(res.token))
|
||||
})
|
||||
}
|
||||
|
||||
}>
|
||||
<input name="username" type="text" placeholder="Username" required
|
||||
onChange={(e) => setUsername(e.currentTarget.value)}/>
|
||||
<input name="password" type="password" placeholder="Password" required
|
||||
onChange={(e) => setPassword(e.currentTarget.value)}/>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
declare module 'solid-js' {
|
||||
namespace JSX {
|
||||
interface Directives {
|
||||
fn: (form: HTMLFormElement) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,535 @@
|
|||
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.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
|
||||
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.config.*
|
||||
import io.ktor.server.testing.*
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.ArgumentMatchers.anyString
|
||||
import org.mockito.kotlin.*
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.interfaces.RSAPrivateKey
|
||||
import java.security.interfaces.RSAPublicKey
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class SecurityKtTest {
|
||||
@Test
|
||||
fun `login ログイン出来るか`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
|
||||
val userAuthService = mock<IUserAuthService> {
|
||||
onBlocking { verifyAccount(eq("testUser"), eq("password")) } doReturn true
|
||||
}
|
||||
val metaService = mock<IMetaService>()
|
||||
val userRepository = mock<IUserRepository> {
|
||||
onBlocking { findByNameAndDomain(eq("testUser"), eq("example.com")) } doReturn User(
|
||||
id = 1L,
|
||||
name = "testUser",
|
||||
domain = "example.com",
|
||||
screenName = "test",
|
||||
description = "",
|
||||
password = "hashedPassword",
|
||||
inbox = "https://example.com/inbox",
|
||||
outbox = "https://example.com/outbox",
|
||||
url = "https://example.com/profile",
|
||||
publicKey = "",
|
||||
privateKey = "",
|
||||
createdAt = Instant.now()
|
||||
)
|
||||
}
|
||||
val jwtToken = JwtToken("Token", "RefreshToken")
|
||||
val jwtService = mock<IJwtService> {
|
||||
onBlocking { createToken(any()) } doReturn jwtToken
|
||||
}
|
||||
val jwkProvider = mock<JwkProvider>()
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(userAuthService, metaService, userRepository, jwtService, jwkProvider)
|
||||
}
|
||||
|
||||
client.post("/login") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(Config.configData.objectMapper.writeValueAsString(UserLogin("testUser", "password")))
|
||||
}.apply {
|
||||
assertEquals(HttpStatusCode.OK, call.response.status)
|
||||
assertEquals(jwtToken, Config.configData.objectMapper.readValue(call.response.bodyAsText()))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login 存在しないユーザーのログインに失敗する`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
|
||||
val userAuthService = mock<IUserAuthService> {
|
||||
onBlocking { verifyAccount(anyString(), anyString()) }.doReturn(false)
|
||||
}
|
||||
val metaService = mock<IMetaService>()
|
||||
val userRepository = mock<IUserRepository>()
|
||||
val jwtService = mock<IJwtService>()
|
||||
val jwkProvider = mock<JwkProvider>()
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(userAuthService, metaService, userRepository, jwtService, jwkProvider)
|
||||
}
|
||||
client.post("/login") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(Config.configData.objectMapper.writeValueAsString(UserLogin("InvalidTtestUser", "password")))
|
||||
}.apply {
|
||||
assertEquals(HttpStatusCode.Unauthorized, call.response.status)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login 不正なパスワードのログインに失敗する`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
|
||||
val userAuthService = mock<IUserAuthService> {
|
||||
onBlocking { verifyAccount(anyString(), eq("InvalidPassword")) } doReturn false
|
||||
}
|
||||
val metaService = mock<IMetaService>()
|
||||
val userRepository = mock<IUserRepository>()
|
||||
val jwtService = mock<IJwtService>()
|
||||
val jwkProvider = mock<JwkProvider>()
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(userAuthService, metaService, userRepository, jwtService, jwkProvider)
|
||||
}
|
||||
client.post("/login") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(Config.configData.objectMapper.writeValueAsString(UserLogin("TestUser", "InvalidPassword")))
|
||||
}.apply {
|
||||
assertEquals(HttpStatusCode.Unauthorized, call.response.status)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `auth-check Authorizedヘッダーが無いと401が帰ってくる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
}
|
||||
client.get("/auth-check").apply {
|
||||
assertEquals(HttpStatusCode.Unauthorized, call.response.status)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `auth-check Authorizedヘッダーの形式が間違っていると401が帰ってくる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
}
|
||||
client.get("/auth-check") {
|
||||
header("Authorization", "Digest dsfjjhogalkjdfmlhaog")
|
||||
}.apply {
|
||||
assertEquals(HttpStatusCode.Unauthorized, call.response.status)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `auth-check Authorizedヘッダーが空だと401が帰ってくる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
}
|
||||
client.get("/auth-check") {
|
||||
header("Authorization", "")
|
||||
}.apply {
|
||||
assertEquals(HttpStatusCode.Unauthorized, call.response.status)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `auth-check AuthorizedヘッダーがBeararで空だと401が帰ってくる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
Config.configData = ConfigData(url = "http://example.com", objectMapper = jacksonObjectMapper())
|
||||
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), mock(), mock(), mock(), mock())
|
||||
}
|
||||
client.get("/auth-check") {
|
||||
header("Authorization", "Bearer ")
|
||||
}.apply {
|
||||
assertEquals(HttpStatusCode.Unauthorized, call.response.status)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `auth-check 正当なJWTだとアクセスできる`() = testApplication {
|
||||
environment {
|
||||
config = ApplicationConfig("empty.conf")
|
||||
}
|
||||
val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
|
||||
keyPairGenerator.initialize(2048)
|
||||
val keyPair = keyPairGenerator.generateKeyPair()
|
||||
val rsaPublicKey = keyPair.public as RSAPublicKey
|
||||
|
||||
Config.configData = ConfigData(url = "https://localhost", objectMapper = jacksonObjectMapper())
|
||||
|
||||
val now = Instant.now()
|
||||
val kid = UUID.randomUUID()
|
||||
val token = JWT.create()
|
||||
.withAudience("${Config.configData.url}/users/test")
|
||||
.withIssuer(Config.configData.url)
|
||||
.withKeyId(kid.toString())
|
||||
.withClaim("username", "test")
|
||||
.withExpiresAt(now.plus(30, ChronoUnit.MINUTES))
|
||||
.sign(Algorithm.RSA256(rsaPublicKey, keyPair.private as RSAPrivateKey))
|
||||
val metaService = mock<IMetaService> {
|
||||
onBlocking { getJwtMeta() }.doReturn(
|
||||
Jwt(
|
||||
kid,
|
||||
Base64Util.encode(keyPair.private.encoded),
|
||||
Base64Util.encode(rsaPublicKey.encoded)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val readValue = Config.configData.objectMapper.readerFor(Map::class.java)
|
||||
.readValue<MutableMap<String, Any>?>(
|
||||
JsonWebKeyUtil.publicKeyToJwk(
|
||||
rsaPublicKey,
|
||||
kid.toString()
|
||||
)
|
||||
)
|
||||
val jwkProvider = mock<JwkProvider> {
|
||||
onBlocking { get(anyString()) }.doReturn(
|
||||
Jwk.fromValues(
|
||||
(readValue["keys"] as List<Map<String, Any>>)[0]
|
||||
)
|
||||
)
|
||||
}
|
||||
val userRepository = mock<IUserRepository>()
|
||||
val jwtService = mock<IJwtService>()
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider)
|
||||
}
|
||||
|
||||
client.get("/auth-check") {
|
||||
header("Authorization", "Bearer $token")
|
||||
}.apply {
|
||||
assertEquals(HttpStatusCode.OK, call.response.status)
|
||||
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<IMetaService> {
|
||||
onBlocking { getJwtMeta() }.doReturn(
|
||||
Jwt(
|
||||
kid,
|
||||
Base64Util.encode(keyPair.private.encoded),
|
||||
Base64Util.encode(rsaPublicKey.encoded)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val readValue = Config.configData.objectMapper.readerFor(Map::class.java)
|
||||
.readValue<MutableMap<String, Any>?>(
|
||||
JsonWebKeyUtil.publicKeyToJwk(
|
||||
rsaPublicKey,
|
||||
kid.toString()
|
||||
)
|
||||
)
|
||||
val jwkProvider = mock<JwkProvider> {
|
||||
onBlocking { get(anyString()) }.doReturn(
|
||||
Jwk.fromValues(
|
||||
(readValue["keys"] as List<Map<String, Any>>)[0]
|
||||
)
|
||||
)
|
||||
}
|
||||
val userRepository = mock<IUserRepository>()
|
||||
val jwtService = mock<IJwtService>()
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider)
|
||||
}
|
||||
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<IMetaService> {
|
||||
onBlocking { getJwtMeta() }.doReturn(
|
||||
Jwt(
|
||||
kid,
|
||||
Base64Util.encode(keyPair.private.encoded),
|
||||
Base64Util.encode(rsaPublicKey.encoded)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val readValue = Config.configData.objectMapper.readerFor(Map::class.java)
|
||||
.readValue<MutableMap<String, Any>?>(
|
||||
JsonWebKeyUtil.publicKeyToJwk(
|
||||
rsaPublicKey,
|
||||
kid.toString()
|
||||
)
|
||||
)
|
||||
val jwkProvider = mock<JwkProvider> {
|
||||
onBlocking { get(anyString()) }.doReturn(
|
||||
Jwk.fromValues(
|
||||
(readValue["keys"] as List<Map<String, Any>>)[0]
|
||||
)
|
||||
)
|
||||
}
|
||||
val userRepository = mock<IUserRepository>()
|
||||
val jwtService = mock<IJwtService>()
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider)
|
||||
}
|
||||
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<IMetaService> {
|
||||
onBlocking { getJwtMeta() }.doReturn(
|
||||
Jwt(
|
||||
kid,
|
||||
Base64Util.encode(keyPair.private.encoded),
|
||||
Base64Util.encode(rsaPublicKey.encoded)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val readValue = Config.configData.objectMapper.readerFor(Map::class.java)
|
||||
.readValue<MutableMap<String, Any>?>(
|
||||
JsonWebKeyUtil.publicKeyToJwk(
|
||||
rsaPublicKey,
|
||||
kid.toString()
|
||||
)
|
||||
)
|
||||
val jwkProvider = mock<JwkProvider> {
|
||||
onBlocking { get(anyString()) }.doReturn(
|
||||
Jwk.fromValues(
|
||||
(readValue["keys"] as List<Map<String, Any>>)[0]
|
||||
)
|
||||
)
|
||||
}
|
||||
val userRepository = mock<IUserRepository>()
|
||||
val jwtService = mock<IJwtService>()
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider)
|
||||
}
|
||||
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<IMetaService> {
|
||||
onBlocking { getJwtMeta() }.doReturn(
|
||||
Jwt(
|
||||
kid,
|
||||
Base64Util.encode(keyPair.private.encoded),
|
||||
Base64Util.encode(rsaPublicKey.encoded)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val readValue = Config.configData.objectMapper.readerFor(Map::class.java)
|
||||
.readValue<MutableMap<String, Any>?>(
|
||||
JsonWebKeyUtil.publicKeyToJwk(
|
||||
rsaPublicKey,
|
||||
kid.toString()
|
||||
)
|
||||
)
|
||||
val jwkProvider = mock<JwkProvider> {
|
||||
onBlocking { get(anyString()) }.doReturn(
|
||||
Jwk.fromValues(
|
||||
(readValue["keys"] as List<Map<String, Any>>)[0]
|
||||
)
|
||||
)
|
||||
}
|
||||
val userRepository = mock<IUserRepository>()
|
||||
val jwtService = mock<IJwtService>()
|
||||
application {
|
||||
configureSerialization()
|
||||
configureSecurity(mock(), metaService, userRepository, jwtService, jwkProvider)
|
||||
}
|
||||
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<IJwtService> {
|
||||
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<IJwtService> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
@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<IMetaService> {
|
||||
onBlocking { getJwtMeta() } doReturn Jwt(
|
||||
kid,
|
||||
Base64Util.encode(generateKeyPair.private.encoded),
|
||||
Base64Util.encode(generateKeyPair.public.encoded)
|
||||
)
|
||||
}
|
||||
val refreshTokenRepository = mock<IJwtRefreshTokenRepository> {
|
||||
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<IJwtRefreshTokenRepository> {
|
||||
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<IUserService> {
|
||||
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<IMetaService> {
|
||||
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<IJwtRefreshTokenRepository> {
|
||||
onBlocking { findByToken("InvalidRefreshToken") } doReturn null
|
||||
}
|
||||
val jwtService = JwtServiceImpl(mock(), refreshTokenRepository, mock())
|
||||
assertThrows<InvalidRefreshTokenException> { jwtService.refreshToken(RefreshToken("InvalidRefreshToken")) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refreshToken 未来に作成されたリフレッシュトークンは失敗する`() = runTest {
|
||||
val refreshTokenRepository = mock<IJwtRefreshTokenRepository> {
|
||||
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<InvalidRefreshTokenException> { jwtService.refreshToken(RefreshToken("refreshToken")) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refreshToken 期限切れのリフレッシュトークンでは失敗する`() = runTest {
|
||||
val refreshTokenRepository = mock<IJwtRefreshTokenRepository> {
|
||||
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<InvalidRefreshTokenException> { jwtService.refreshToken(RefreshToken("refreshToken")) }
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -7,7 +7,10 @@ export default defineConfig({
|
|||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': 'http://localhost:8080'
|
||||
'/api': 'http://localhost:8080',
|
||||
'/login': 'http://localhost:8080',
|
||||
'/auth-check': 'http://localhost:8080',
|
||||
'/refresh-token': 'http://localhost:8080',
|
||||
}
|
||||
},
|
||||
root: './src/main/web',
|
||||
|
|
Loading…
Reference in New Issue