Merge pull request #38 from usbharu/feature/mastodon-api

Feature/mastodon api
This commit is contained in:
usbharu 2023-09-22 23:35:55 +09:00 committed by GitHub
commit 887249c68d
26 changed files with 2208 additions and 292 deletions

1
.gitignore vendored
View File

@ -39,3 +39,4 @@ out/
/node_modules/ /node_modules/
/src/main/web/generated/ /src/main/web/generated/
/stats.html /stats.html
/tomcat/

View File

@ -15,9 +15,9 @@ plugins {
id("org.graalvm.buildtools.native") version "0.9.21" id("org.graalvm.buildtools.native") version "0.9.21"
id("io.gitlab.arturbosch.detekt") version "1.23.1" id("io.gitlab.arturbosch.detekt") version "1.23.1"
id("com.google.devtools.ksp") version "1.8.21-1.0.11" id("com.google.devtools.ksp") version "1.8.21-1.0.11"
id("org.springframework.boot") version "3.1.2" id("org.springframework.boot") version "3.1.3"
kotlin("plugin.spring") version "1.8.21" kotlin("plugin.spring") version "1.8.21"
id("org.openapi.generator") version "6.6.0" id("org.openapi.generator") version "7.0.1"
// id("org.jetbrains.kotlin.plugin.serialization") version "1.8.10" // id("org.jetbrains.kotlin.plugin.serialization") version "1.8.10"
} }
@ -47,8 +47,8 @@ tasks.withType<KotlinCompile> {
kotlinOptions { kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict" freeCompilerArgs += "-Xjsr305=strict"
} }
dependsOn("openApiGenerateServer") dependsOn("openApiGenerateMastodonCompatibleApi")
mustRunAfter("openApiGenerateServer") mustRunAfter("openApiGenerateMastodonCompatibleApi")
} }
tasks.withType<ShadowJar> { tasks.withType<ShadowJar> {
@ -63,30 +63,45 @@ tasks.clean {
delete += listOf("$rootDir/src/main/resources/static") delete += listOf("$rootDir/src/main/resources/static")
} }
tasks.create<GenerateTask>("openApiGenerateServer", GenerateTask::class) { //tasks.create<GenerateTask>("openApiGenerateServer", GenerateTask::class) {
// generatorName.set("kotlin-spring")
// inputSpec.set("$rootDir/src/main/resources/openapi/api.yaml")
// outputDir.set("$buildDir/generated/sources/openapi")
// apiPackage.set("dev.usbharu.hideout.controller.generated")
// modelPackage.set("dev.usbharu.hideout.domain.model.generated")
// configOptions.put("interfaceOnly", "true")
// configOptions.put("useSpringBoot3", "true")
// additionalProperties.put("useTags", "true")
// schemaMappings.putAll(
// mapOf(
// "ReactionResponse" to "dev.usbharu.hideout.domain.model.hideout.dto.ReactionResponse",
// "Account" to "dev.usbharu.hideout.domain.model.hideout.dto.Account",
// "JwtToken" to "dev.usbharu.hideout.domain.model.hideout.dto.JwtToken",
// "PostRequest" to "dev.usbharu.hideout.domain.model.hideout.form.Post",
// "PostResponse" to "dev.usbharu.hideout.domain.model.hideout.dto.PostResponse",
// "Reaction" to "dev.usbharu.hideout.domain.model.hideout.form.Reaction",
// "RefreshToken" to "dev.usbharu.hideout.domain.model.hideout.form.RefreshToken",
// "UserLogin" to "dev.usbharu.hideout.domain.model.hideout.form.UserLogin",
// "UserResponse" to "dev.usbharu.hideout.domain.model.hideout.dto.UserResponse",
// "UserCreate" to "dev.usbharu.hideout.domain.model.hideout.form.UserCreate",
// "Visibility" to "dev.usbharu.hideout.domain.model.hideout.entity.Visibility",
// )
// )
//
//// importMappings.putAll(mapOf("ReactionResponse" to "ReactionResponse"))
//// typeMappings.putAll(mapOf("ReactionResponse" to "ReactionResponse"))
//}
tasks.create<GenerateTask>("openApiGenerateMastodonCompatibleApi", GenerateTask::class) {
generatorName.set("kotlin-spring") generatorName.set("kotlin-spring")
inputSpec.set("$rootDir/src/main/resources/openapi/api.yaml") inputSpec.set("$rootDir/src/main/resources/openapi/mastodon.yaml")
outputDir.set("$buildDir/generated/sources/openapi") outputDir.set("$buildDir/generated/sources/mastodon")
apiPackage.set("dev.usbharu.hideout.controller.generated") apiPackage.set("dev.usbharu.hideout.controller.mastodon.generated")
modelPackage.set("dev.usbharu.hideout.domain.model.generated") modelPackage.set("dev.usbharu.hideout.domain.mastodon.model.generated")
configOptions.put("interfaceOnly", "true") configOptions.put("interfaceOnly", "true")
configOptions.put("useSpringBoot3", "true") configOptions.put("useSpringBoot3", "true")
configOptions.put("reactive", "true")
additionalProperties.put("useTags", "true") additionalProperties.put("useTags", "true")
schemaMappings.putAll(
mapOf(
"ReactionResponse" to "dev.usbharu.hideout.domain.model.hideout.dto.ReactionResponse",
"Account" to "dev.usbharu.hideout.domain.model.hideout.dto.Account",
"JwtToken" to "dev.usbharu.hideout.domain.model.hideout.dto.JwtToken",
"PostRequest" to "dev.usbharu.hideout.domain.model.hideout.form.Post",
"PostResponse" to "dev.usbharu.hideout.domain.model.hideout.dto.PostResponse",
"Reaction" to "dev.usbharu.hideout.domain.model.hideout.form.Reaction",
"RefreshToken" to "dev.usbharu.hideout.domain.model.hideout.form.RefreshToken",
"UserLogin" to "dev.usbharu.hideout.domain.model.hideout.form.UserLogin",
"UserResponse" to "dev.usbharu.hideout.domain.model.hideout.dto.UserResponse",
"UserCreate" to "dev.usbharu.hideout.domain.model.hideout.form.UserCreate",
"Visibility" to "dev.usbharu.hideout.domain.model.hideout.entity.Visibility",
)
)
// importMappings.putAll(mapOf("ReactionResponse" to "ReactionResponse")) // importMappings.putAll(mapOf("ReactionResponse" to "ReactionResponse"))
// typeMappings.putAll(mapOf("ReactionResponse" to "ReactionResponse")) // typeMappings.putAll(mapOf("ReactionResponse" to "ReactionResponse"))
@ -105,7 +120,11 @@ kotlin {
} }
sourceSets.main { sourceSets.main {
kotlin.srcDirs("$buildDir/generated/ksp/main", "$buildDir/generated/sources/openapi/src/main/kotlin") kotlin.srcDirs(
"$buildDir/generated/ksp/main",
"$buildDir/generated/sources/openapi/src/main/kotlin",
"$buildDir/generated/sources/mastodon/src/main/kotlin"
)
} }
dependencies { dependencies {
@ -134,6 +153,7 @@ dependencies {
implementation("io.insert-koin:koin-logger-slf4j:$koin_version") implementation("io.insert-koin:koin-logger-slf4j:$koin_version")
implementation("io.insert-koin:koin-annotations:1.2.0") implementation("io.insert-koin:koin-annotations:1.2.0")
implementation("io.ktor:ktor-server-compression-jvm:2.3.0") implementation("io.ktor:ktor-server-compression-jvm:2.3.0")
implementation("org.springframework.boot:spring-boot-starter-actuator")
ksp("io.insert-koin:koin-ksp-compiler:1.2.0") ksp("io.insert-koin:koin-ksp-compiler:1.2.0")
implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-web")
@ -153,6 +173,9 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
testImplementation("org.springframework.boot:spring-boot-test-autoconfigure") testImplementation("org.springframework.boot:spring-boot-test-autoconfigure")
testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.boot:spring-boot-starter-test")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.springframework.security:spring-security-oauth2-jose")
implementation("io.ktor:ktor-client-logging-jvm:$ktor_version") implementation("io.ktor:ktor-client-logging-jvm:$ktor_version")
implementation("io.ktor:ktor-server-host-common-jvm:$ktor_version") implementation("io.ktor:ktor-server-host-common-jvm:$ktor_version")

View File

@ -5,7 +5,9 @@ import com.nimbusds.jose.jwk.RSAKey
import com.nimbusds.jose.jwk.source.ImmutableJWKSet import com.nimbusds.jose.jwk.source.ImmutableJWKSet
import com.nimbusds.jose.jwk.source.JWKSource import com.nimbusds.jose.jwk.source.JWKSource
import com.nimbusds.jose.proc.SecurityContext import com.nimbusds.jose.proc.SecurityContext
import dev.usbharu.hideout.domain.model.UserDetailsImpl
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.autoconfigure.security.servlet.PathRequest
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
@ -14,26 +16,35 @@ import org.springframework.http.MediaType
import org.springframework.security.config.Customizer import org.springframework.security.config.Customizer
import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.core.Authentication
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.oauth2.jwt.JwtDecoder import org.springframework.security.oauth2.jwt.JwtDecoder
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer
import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher
import org.springframework.web.servlet.handler.HandlerMappingIntrospector
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey import java.security.interfaces.RSAPublicKey
import java.util.* import java.util.*
@EnableWebSecurity
@EnableWebSecurity(debug = true)
@Configuration @Configuration
class SecurityConfig { class SecurityConfig {
@Bean @Bean
@Order(1) @Order(1)
fun oauth2SecurityFilterChain(http: HttpSecurity): SecurityFilterChain { fun oauth2SecurityFilterChain(http: HttpSecurity, introspector: HandlerMappingIntrospector): SecurityFilterChain {
val builder = MvcRequestMatcher.Builder(introspector)
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http) OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
http http
.exceptionHandling { .exceptionHandling {
@ -45,31 +56,46 @@ class SecurityConfig {
.oauth2ResourceServer { .oauth2ResourceServer {
it.jwt(Customizer.withDefaults()) it.jwt(Customizer.withDefaults())
} }
.csrf {
it.disable()
}
return http.build() return http.build()
} }
@Bean @Bean
@Order(2) @Order(2)
fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain { fun defaultSecurityFilterChain(http: HttpSecurity, introspector: HandlerMappingIntrospector): SecurityFilterChain {
val builder = MvcRequestMatcher.Builder(introspector)
http.authorizeHttpRequests {
it.requestMatchers(builder.pattern("/api/v1/**")).hasAnyAuthority("SCOPE_read", "SCOPE_read:accounts")
}
http http
.authorizeHttpRequests { .authorizeHttpRequests {
it.requestMatchers( it.requestMatchers(
"/inbox", builder.pattern("/inbox"),
"/users/*/inbox", builder.pattern("/api/v1/apps"),
"/outbox", builder.pattern("/api/v1/instance/**")
"/users/*/outbox" ).permitAll()
)
.permitAll()
} }
http
.authorizeHttpRequests {
it.requestMatchers(PathRequest.toH2Console()).permitAll()
}
http
.authorizeHttpRequests { .authorizeHttpRequests {
it.anyRequest().authenticated() it.anyRequest().authenticated()
} }
http
.oauth2ResourceServer {
it.jwt(Customizer.withDefaults())
}
.formLogin(Customizer.withDefaults()) .formLogin(Customizer.withDefaults())
.csrf { .csrf {
it.disable() it.ignoringRequestMatchers(builder.pattern("/api/**"))
it.ignoringRequestMatchers(PathRequest.toH2Console())
}
.headers {
it.frameOptions {
it.sameOrigin()
}
} }
return http.build() return http.build()
} }
@ -119,6 +145,18 @@ class SecurityConfig {
.tokenRevocationEndpoint("/oauth/revoke") .tokenRevocationEndpoint("/oauth/revoke")
.build() .build()
} }
@Bean
fun jwtTokenCustomizer(): OAuth2TokenCustomizer<JwtEncodingContext> {
return OAuth2TokenCustomizer { context: JwtEncodingContext ->
if (OAuth2TokenType.ACCESS_TOKEN == context.tokenType) {
val userDetailsImpl = context.getPrincipal<Authentication>().principal as UserDetailsImpl
context.claims.claim("uid", userDetailsImpl.id.toString())
}
}
}
} }
@ConfigurationProperties("hideout.security.jwt") @ConfigurationProperties("hideout.security.jwt")

View File

@ -7,11 +7,14 @@ import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
@Configuration @Configuration
class DatabaseConfig { class SpringConfig {
@Autowired @Autowired
lateinit var dbConfig: DatabaseConnectConfig lateinit var dbConfig: DatabaseConnectConfig
@Autowired
lateinit var config: ApplicationConfig
@Bean @Bean
fun database(): Database { fun database(): Database {
return Database.connect( return Database.connect(
@ -23,6 +26,11 @@ class DatabaseConfig {
} }
} }
@ConfigurationProperties("hideout")
data class ApplicationConfig(
val url: String
)
@ConfigurationProperties("hideout.database") @ConfigurationProperties("hideout.database")
data class DatabaseConnectConfig( data class DatabaseConnectConfig(
val url: String, val url: String,

View File

@ -1,15 +0,0 @@
package dev.usbharu.hideout.controller
import dev.usbharu.hideout.controller.generated.DefaultApi
import dev.usbharu.hideout.domain.model.hideout.dto.JwtToken
import dev.usbharu.hideout.service.api.UserAuthApiService
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller
@Controller
class DefaultApiImpl(private val userAuthApiService: UserAuthApiService) : DefaultApi {
override fun refreshTokenPost(): ResponseEntity<JwtToken> {
return ResponseEntity(HttpStatus.OK)
}
}

View File

@ -0,0 +1,22 @@
package dev.usbharu.hideout.controller.mastodon
import dev.usbharu.hideout.controller.mastodon.generated.AccountApi
import dev.usbharu.hideout.domain.mastodon.model.generated.CredentialAccount
import dev.usbharu.hideout.service.api.mastodon.AccountApiService
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.stereotype.Controller
@Controller
class MastodonAccountApiController(private val accountApiService: AccountApiService) : AccountApi {
override suspend fun apiV1AccountsVerifyCredentialsGet(): ResponseEntity<CredentialAccount> {
val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt
return ResponseEntity(
accountApiService.verifyCredentials(principal.getClaim<String>("uid").toLong()),
HttpStatus.OK
)
}
}

View File

@ -0,0 +1,39 @@
package dev.usbharu.hideout.controller.mastodon
import dev.usbharu.hideout.controller.mastodon.generated.AppApi
import dev.usbharu.hideout.domain.mastodon.model.generated.Application
import dev.usbharu.hideout.domain.mastodon.model.generated.AppsRequest
import dev.usbharu.hideout.service.api.mastodon.AppApiService
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.RequestParam
@Controller
class MastodonAppsApiController(private val appApiService: AppApiService) : AppApi {
override suspend fun apiV1AppsPost(appsRequest: AppsRequest): ResponseEntity<Application> {
println(appsRequest)
return ResponseEntity(
appApiService.createApp(appsRequest),
HttpStatus.OK
)
}
@RequestMapping(
method = [RequestMethod.POST],
value = ["/api/v1/apps"],
produces = ["application/json"],
consumes = ["application/x-www-form-urlencoded"]
)
suspend fun apiV1AppsPost(@RequestParam map: Map<String, String>): ResponseEntity<Application> {
val appsRequest =
AppsRequest(map.getValue("client_name"), map.getValue("redirect_uris"), map["scopes"], map["website"])
return ResponseEntity(
appApiService.createApp(appsRequest),
HttpStatus.OK
)
}
}

View File

@ -0,0 +1,15 @@
package dev.usbharu.hideout.controller.mastodon
import dev.usbharu.hideout.controller.mastodon.generated.InstanceApi
import dev.usbharu.hideout.domain.mastodon.model.generated.V1Instance
import dev.usbharu.hideout.service.api.mastodon.InstanceApiService
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller
@Controller
class MastodonInstanceApiController(private val instanceApiService: InstanceApiService) : InstanceApi {
override suspend fun apiV1InstanceGet(): ResponseEntity<V1Instance> {
return ResponseEntity(instanceApiService.v1Instance(), HttpStatus.OK)
}
}

View File

@ -0,0 +1,20 @@
package dev.usbharu.hideout.controller.mastodon
import dev.usbharu.hideout.controller.mastodon.generated.StatusApi
import dev.usbharu.hideout.domain.mastodon.model.generated.Status
import dev.usbharu.hideout.domain.mastodon.model.generated.StatusesRequest
import dev.usbharu.hideout.domain.model.UserDetailsImpl
import dev.usbharu.hideout.service.api.mastodon.StatusesApiService
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Controller
@Controller
class MastodonStatusesApiContoller(private val statusesApiService: StatusesApiService) : StatusApi {
override suspend fun apiV1StatusesPost(statusesRequest: StatusesRequest): ResponseEntity<Status> {
val principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal()
require(principal is UserDetailsImpl)
return ResponseEntity(statusesApiService.postStatus(statusesRequest, principal), HttpStatus.OK)
}
}

View File

@ -0,0 +1,83 @@
package dev.usbharu.hideout.domain.model
import com.fasterxml.jackson.annotation.JsonAutoDetect
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.User
import java.io.Serial
class UserDetailsImpl(
val id: Long,
username: String?,
password: String?,
enabled: Boolean,
accountNonExpired: Boolean,
credentialsNonExpired: Boolean,
accountNonLocked: Boolean,
authorities: MutableCollection<out GrantedAuthority>?
) : User(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities) {
companion object {
@Serial
private const val serialVersionUID: Long = -899168205656607781L
}
}
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
@JsonDeserialize(using = UserDetailsDeserializer::class)
@JsonAutoDetect(
fieldVisibility = JsonAutoDetect.Visibility.ANY,
getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE,
creatorVisibility = JsonAutoDetect.Visibility.NONE
)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonSubTypes
abstract class UserDetailsMixin
class UserDetailsDeserializer : JsonDeserializer<UserDetailsImpl>() {
val SIMPLE_GRANTED_AUTHORITY_SET = object : TypeReference<Set<SimpleGrantedAuthority>>() {}
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): UserDetailsImpl {
val mapper = p.codec as ObjectMapper
val jsonNode: JsonNode = mapper.readTree(p)
println(jsonNode)
val authorities: Set<GrantedAuthority> = mapper.convertValue(
jsonNode["authorities"],
SIMPLE_GRANTED_AUTHORITY_SET
)
val password = jsonNode.readText("password")
return UserDetailsImpl(
jsonNode["id"].longValue(),
jsonNode.readText("username"),
password,
true,
true,
true,
true,
authorities.toMutableList(),
)
}
fun JsonNode.readText(field: String, defaultValue: String = ""): String {
return when {
has(field) -> get(field).asText(defaultValue)
else -> defaultValue
}
}
}

View File

@ -1,16 +1,22 @@
package dev.usbharu.hideout.repository package dev.usbharu.hideout.repository
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.repository.RegisteredClient.clientId import dev.usbharu.hideout.repository.RegisteredClient.clientId
import dev.usbharu.hideout.repository.RegisteredClient.clientSettings import dev.usbharu.hideout.repository.RegisteredClient.clientSettings
import dev.usbharu.hideout.repository.RegisteredClient.tokenSettings import dev.usbharu.hideout.repository.RegisteredClient.tokenSettings
import dev.usbharu.hideout.util.JsonUtil import dev.usbharu.hideout.service.auth.ExposedOAuth2AuthorizationService
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.javatime.CurrentTimestamp import org.jetbrains.exposed.sql.javatime.CurrentTimestamp
import org.jetbrains.exposed.sql.javatime.timestamp import org.jetbrains.exposed.sql.javatime.timestamp
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.slf4j.LoggerFactory
import org.springframework.security.jackson2.SecurityJackson2Modules
import org.springframework.security.oauth2.core.AuthorizationGrantType import org.springframework.security.oauth2.core.AuthorizationGrantType
import org.springframework.security.oauth2.core.ClientAuthenticationMethod import org.springframework.security.oauth2.core.ClientAuthenticationMethod
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings import org.springframework.security.oauth2.server.authorization.settings.ClientSettings
import org.springframework.security.oauth2.server.authorization.settings.ConfigurationSettingNames import org.springframework.security.oauth2.server.authorization.settings.ConfigurationSettingNames
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat
@ -41,13 +47,15 @@ class RegisteredClientRepositoryImpl(private val database: Database) : Registere
it[clientSecret] = registeredClient.clientSecret it[clientSecret] = registeredClient.clientSecret
it[clientSecretExpiresAt] = registeredClient.clientSecretExpiresAt it[clientSecretExpiresAt] = registeredClient.clientSecretExpiresAt
it[clientName] = registeredClient.clientName it[clientName] = registeredClient.clientName
it[clientAuthenticationMethods] = registeredClient.clientAuthenticationMethods.joinToString(",") it[clientAuthenticationMethods] =
it[authorizationGrantTypes] = registeredClient.authorizationGrantTypes.joinToString(",") registeredClient.clientAuthenticationMethods.map { it.value }.joinToString(",")
it[authorizationGrantTypes] =
registeredClient.authorizationGrantTypes.map { it.value }.joinToString(",")
it[redirectUris] = registeredClient.redirectUris.joinToString(",") it[redirectUris] = registeredClient.redirectUris.joinToString(",")
it[postLogoutRedirectUris] = registeredClient.postLogoutRedirectUris.joinToString(",") it[postLogoutRedirectUris] = registeredClient.postLogoutRedirectUris.joinToString(",")
it[scopes] = registeredClient.scopes.joinToString(",") it[scopes] = registeredClient.scopes.joinToString(",")
it[clientSettings] = JsonUtil.mapToJson(registeredClient.clientSettings.settings) it[clientSettings] = mapToJson(registeredClient.clientSettings.settings)
it[tokenSettings] = JsonUtil.mapToJson(registeredClient.tokenSettings.settings) it[tokenSettings] = mapToJson(registeredClient.tokenSettings.settings)
} }
} else { } else {
RegisteredClient.update({ RegisteredClient.id eq registeredClient.id }) { RegisteredClient.update({ RegisteredClient.id eq registeredClient.id }) {
@ -61,8 +69,8 @@ class RegisteredClientRepositoryImpl(private val database: Database) : Registere
it[redirectUris] = registeredClient.redirectUris.joinToString(",") it[redirectUris] = registeredClient.redirectUris.joinToString(",")
it[postLogoutRedirectUris] = registeredClient.postLogoutRedirectUris.joinToString(",") it[postLogoutRedirectUris] = registeredClient.postLogoutRedirectUris.joinToString(",")
it[scopes] = registeredClient.scopes.joinToString(",") it[scopes] = registeredClient.scopes.joinToString(",")
it[clientSettings] = JsonUtil.mapToJson(registeredClient.clientSettings.settings) it[clientSettings] = mapToJson(registeredClient.clientSettings.settings)
it[tokenSettings] = JsonUtil.mapToJson(registeredClient.tokenSettings.settings) it[tokenSettings] = mapToJson(registeredClient.tokenSettings.settings)
} }
} }
} }
@ -81,10 +89,93 @@ class RegisteredClientRepositoryImpl(private val database: Database) : Registere
if (clientId == null) { if (clientId == null) {
return null return null
} }
return RegisteredClient.select { val toRegisteredClient = RegisteredClient.select {
RegisteredClient.clientId eq clientId RegisteredClient.clientId eq clientId
}.singleOrNull()?.toRegisteredClient() }.singleOrNull()?.toRegisteredClient()
LOGGER.trace("findByClientId: $toRegisteredClient")
return toRegisteredClient
} }
private fun mapToJson(map: Map<*, *>): String = objectMapper.writeValueAsString(map)
private fun <T, U> jsonToMap(json: String): Map<T, U> = objectMapper.readValue(json)
companion object {
val objectMapper: ObjectMapper = ObjectMapper()
val LOGGER = LoggerFactory.getLogger(RegisteredClientRepositoryImpl::class.java)
init {
val classLoader = ExposedOAuth2AuthorizationService::class.java.classLoader
val modules = SecurityJackson2Modules.getModules(classLoader)
this.objectMapper.registerModules(JavaTimeModule())
this.objectMapper.registerModules(modules)
this.objectMapper.registerModules(OAuth2AuthorizationServerJackson2Module())
}
}
fun ResultRow.toRegisteredClient(): SpringRegisteredClient {
fun resolveClientAuthenticationMethods(string: String): ClientAuthenticationMethod {
return when (string) {
ClientAuthenticationMethod.CLIENT_SECRET_BASIC.value -> ClientAuthenticationMethod.CLIENT_SECRET_BASIC
ClientAuthenticationMethod.CLIENT_SECRET_JWT.value -> ClientAuthenticationMethod.CLIENT_SECRET_JWT
ClientAuthenticationMethod.CLIENT_SECRET_POST.value -> ClientAuthenticationMethod.CLIENT_SECRET_POST
ClientAuthenticationMethod.NONE.value -> ClientAuthenticationMethod.NONE
else -> {
ClientAuthenticationMethod(string)
}
}
}
fun resolveAuthorizationGrantType(string: String): AuthorizationGrantType {
return when (string) {
AuthorizationGrantType.AUTHORIZATION_CODE.value -> AuthorizationGrantType.AUTHORIZATION_CODE
AuthorizationGrantType.CLIENT_CREDENTIALS.value -> AuthorizationGrantType.CLIENT_CREDENTIALS
AuthorizationGrantType.REFRESH_TOKEN.value -> AuthorizationGrantType.REFRESH_TOKEN
else -> {
AuthorizationGrantType(string)
}
}
}
val clientAuthenticationMethods = this[RegisteredClient.clientAuthenticationMethods].split(",").toSet()
val authorizationGrantTypes = this[RegisteredClient.authorizationGrantTypes].split(",").toSet()
val redirectUris = this[RegisteredClient.redirectUris]?.split(",").orEmpty().toSet()
val postLogoutRedirectUris = this[RegisteredClient.postLogoutRedirectUris]?.split(",").orEmpty().toSet()
val clientScopes = this[RegisteredClient.scopes].split(",").toSet()
val builder = SpringRegisteredClient
.withId(this[RegisteredClient.id])
.clientId(this[clientId])
.clientIdIssuedAt(this[RegisteredClient.clientIdIssuedAt])
.clientSecret(this[RegisteredClient.clientSecret])
.clientSecretExpiresAt(this[RegisteredClient.clientSecretExpiresAt])
.clientName(this[RegisteredClient.clientName])
.clientAuthenticationMethods {
clientAuthenticationMethods.forEach { s ->
it.add(resolveClientAuthenticationMethods(s))
}
}
.authorizationGrantTypes {
authorizationGrantTypes.forEach { s ->
it.add(resolveAuthorizationGrantType(s))
}
}
.redirectUris { it.addAll(redirectUris) }
.postLogoutRedirectUris { it.addAll(postLogoutRedirectUris) }
.scopes { it.addAll(clientScopes) }
.clientSettings(ClientSettings.withSettings(jsonToMap(this[clientSettings])).build())
val tokenSettingsMap = jsonToMap<String, Any>(this[tokenSettings])
val withSettings = TokenSettings.withSettings(tokenSettingsMap)
if (tokenSettingsMap.containsKey(ConfigurationSettingNames.Token.ACCESS_TOKEN_FORMAT)) {
withSettings.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
}
builder.tokenSettings(withSettings.build())
return builder.build()
}
} }
// org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql // org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql
@ -105,65 +196,3 @@ object RegisteredClient : Table("registered_client") {
override val primaryKey = PrimaryKey(id) override val primaryKey = PrimaryKey(id)
} }
fun ResultRow.toRegisteredClient(): SpringRegisteredClient {
fun resolveClientAuthenticationMethods(string: String): ClientAuthenticationMethod {
return when (string) {
ClientAuthenticationMethod.CLIENT_SECRET_BASIC.value -> ClientAuthenticationMethod.CLIENT_SECRET_BASIC
ClientAuthenticationMethod.CLIENT_SECRET_JWT.value -> ClientAuthenticationMethod.CLIENT_SECRET_JWT
ClientAuthenticationMethod.CLIENT_SECRET_POST.value -> ClientAuthenticationMethod.CLIENT_SECRET_POST
ClientAuthenticationMethod.NONE.value -> ClientAuthenticationMethod.NONE
else -> {
ClientAuthenticationMethod(string)
}
}
}
fun resolveAuthorizationGrantType(string: String): AuthorizationGrantType {
return when (string) {
AuthorizationGrantType.AUTHORIZATION_CODE.value -> AuthorizationGrantType.AUTHORIZATION_CODE
AuthorizationGrantType.CLIENT_CREDENTIALS.value -> AuthorizationGrantType.CLIENT_CREDENTIALS
AuthorizationGrantType.REFRESH_TOKEN.value -> AuthorizationGrantType.REFRESH_TOKEN
else -> {
AuthorizationGrantType(string)
}
}
}
val clientAuthenticationMethods = this[RegisteredClient.clientAuthenticationMethods].split(",").toSet()
val authorizationGrantTypes = this[RegisteredClient.authorizationGrantTypes].split(",").toSet()
val redirectUris = this[RegisteredClient.redirectUris]?.split(",").orEmpty().toSet()
val postLogoutRedirectUris = this[RegisteredClient.postLogoutRedirectUris]?.split(",").orEmpty().toSet()
val clientScopes = this[RegisteredClient.scopes].split(",").toSet()
val builder = SpringRegisteredClient
.withId(this[RegisteredClient.id])
.clientId(this[clientId])
.clientIdIssuedAt(this[RegisteredClient.clientIdIssuedAt])
.clientSecret(this[RegisteredClient.clientSecret])
.clientSecretExpiresAt(this[RegisteredClient.clientSecretExpiresAt])
.clientName(this[RegisteredClient.clientName])
.clientAuthenticationMethods {
clientAuthenticationMethods.forEach { s ->
it.add(resolveClientAuthenticationMethods(s))
}
}
.authorizationGrantTypes {
authorizationGrantTypes.forEach { s ->
it.add(resolveAuthorizationGrantType(s))
}
}
.redirectUris { it.addAll(redirectUris) }
.postLogoutRedirectUris { it.addAll(postLogoutRedirectUris) }
.scopes { it.addAll(clientScopes) }
.clientSettings(ClientSettings.withSettings(JsonUtil.jsonToMap(this[clientSettings])).build())
val tokenSettingsMap = JsonUtil.jsonToMap<String, Any>(this[tokenSettings])
val withSettings = TokenSettings.withSettings(tokenSettingsMap)
if (tokenSettingsMap.containsKey(ConfigurationSettingNames.Token.ACCESS_TOKEN_FORMAT)) {
withSettings.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
}
builder.tokenSettings(withSettings.build())
return builder.build()
}

View File

@ -0,0 +1,63 @@
package dev.usbharu.hideout.service.api.mastodon
import dev.usbharu.hideout.domain.mastodon.model.generated.Account
import dev.usbharu.hideout.domain.mastodon.model.generated.CredentialAccount
import dev.usbharu.hideout.domain.mastodon.model.generated.CredentialAccountSource
import dev.usbharu.hideout.domain.mastodon.model.generated.Role
import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.service.mastodon.AccountService
import org.springframework.stereotype.Service
@Service
interface AccountApiService {
suspend fun verifyCredentials(userid: Long): CredentialAccount
}
@Service
class AccountApiServiceImpl(private val accountService: AccountService, private val transaction: Transaction) :
AccountApiService {
override suspend fun verifyCredentials(userid: Long): CredentialAccount = transaction.transaction {
val account = accountService.findById(userid)
of(account)
}
private fun of(account: Account): CredentialAccount {
return CredentialAccount(
id = account.id,
username = account.username,
acct = account.acct,
url = account.url,
displayName = account.displayName,
note = account.note,
avatar = account.avatar,
avatarStatic = account.avatarStatic,
header = account.header,
headerStatic = account.headerStatic,
locked = account.locked,
fields = account.fields,
emojis = account.emojis,
bot = account.bot,
group = account.group,
discoverable = account.discoverable,
createdAt = account.createdAt,
lastStatusAt = account.lastStatusAt,
statusesCount = account.statusesCount,
followersCount = account.followersCount,
noindex = account.noindex,
moved = account.moved,
suspendex = account.suspendex,
limited = account.limited,
followingCount = account.followingCount,
source = CredentialAccountSource(
account.note,
account.fields,
CredentialAccountSource.Privacy.public,
false,
0
),
role = Role(0, "Admin", "", 32)
)
}
}

View File

@ -0,0 +1,61 @@
package dev.usbharu.hideout.service.api.mastodon
import dev.usbharu.hideout.domain.mastodon.model.generated.Application
import dev.usbharu.hideout.domain.mastodon.model.generated.AppsRequest
import dev.usbharu.hideout.service.auth.SecureTokenGenerator
import dev.usbharu.hideout.service.core.Transaction
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.oauth2.core.AuthorizationGrantType
import org.springframework.security.oauth2.core.ClientAuthenticationMethod
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings
import org.springframework.stereotype.Service
import java.util.*
@Service
interface AppApiService {
suspend fun createApp(appsRequest: AppsRequest): Application
}
@Service
class AppApiServiceImpl(
private val registeredClientRepository: RegisteredClientRepository,
private val secureTokenGenerator: SecureTokenGenerator,
private val passwordEncoder: PasswordEncoder,
private val transaction: Transaction
) : AppApiService {
override suspend fun createApp(appsRequest: AppsRequest): Application {
return transaction.transaction {
val id = UUID.randomUUID().toString()
val clientSecret = secureTokenGenerator.generate()
val registeredClient = RegisteredClient.withId(id)
.clientId(id)
.clientSecret(passwordEncoder.encode(clientSecret))
.clientName(appsRequest.clientName)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri(appsRequest.redirectUris)
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.scopes { it.addAll(parseScope(appsRequest.scopes.orEmpty())) }
.build()
registeredClientRepository.save(registeredClient)
Application(
appsRequest.clientName,
"invalid-vapid-key",
appsRequest.website,
id,
clientSecret
)
}
}
private fun parseScope(string: String): Set<String> {
return string.split(" ").toSet()
}
}

View File

@ -0,0 +1,83 @@
package dev.usbharu.hideout.service.api.mastodon
import dev.usbharu.hideout.config.ApplicationConfig
import dev.usbharu.hideout.domain.mastodon.model.generated.*
import org.springframework.stereotype.Service
import java.net.URL
@Service
interface InstanceApiService {
suspend fun v1Instance(): V1Instance
}
@Service
class InstanceApiServiceImpl(private val applicationConfig: ApplicationConfig) : InstanceApiService {
override suspend fun v1Instance(): V1Instance {
val url = applicationConfig.url
val url1 = URL(url)
return V1Instance(
uri = url1.host,
title = "Hideout Server",
shortDescription = "Hideout test server",
description = "This server is operated for testing of Hideout. We are not responsible for any events that occur when associating with this server",
email = "i@usbharu.dev",
version = "0.0.1",
urls = V1InstanceUrls("wss://${url1.host}"),
stats = V1InstanceStats(1, 0, 0),
thumbnail = null,
languages = listOf("ja-JP"),
registrations = false,
approvalRequired = false,
invitesEnabled = false,
configuration = V1InstanceConfiguration(
accounts = V1InstanceConfigurationAccounts(1),
statuses = V1InstanceConfigurationStatuses(
300,
4,
23
),
mediaAttachments = V1InstanceConfigurationMediaAttachments(
listOf(),
0,
0,
0,
0
),
polls = V1InstanceConfigurationPolls(
0,
0,
0,
0
)
),
contactAccount = Account(
id = "0",
username = "",
acct = "",
url = "",
displayName = "",
note = "",
avatar = "",
avatarStatic = "",
header = "",
headerStatic = "",
locked = false,
fields = emptyList(),
emojis = emptyList(),
bot = false,
group = false,
discoverable = false,
createdAt = "0",
lastStatusAt = "0",
statusesCount = 1,
followersCount = 0,
noindex = false,
moved = false,
suspendex = false,
limited = false,
followingCount = 0
),
rules = emptyList()
)
}
}

View File

@ -0,0 +1,105 @@
package dev.usbharu.hideout.service.api.mastodon
import dev.usbharu.hideout.domain.mastodon.model.generated.Status
import dev.usbharu.hideout.domain.mastodon.model.generated.StatusesRequest
import dev.usbharu.hideout.domain.model.UserDetailsImpl
import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
import dev.usbharu.hideout.exception.FailedToGetResourcesException
import dev.usbharu.hideout.query.PostQueryService
import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.mastodon.AccountService
import dev.usbharu.hideout.service.post.PostService
import org.springframework.stereotype.Service
import java.time.Instant
@Service
interface StatusesApiService {
suspend fun postStatus(statusesRequest: StatusesRequest, user: UserDetailsImpl): Status
}
@Service
class StatsesApiServiceImpl(
private val postService: PostService,
private val accountService: AccountService,
private val postQueryService: PostQueryService,
private val userQueryService: UserQueryService
) :
StatusesApiService {
override suspend fun postStatus(statusesRequest: StatusesRequest, user: UserDetailsImpl): Status {
val visibility = when (statusesRequest.visibility) {
StatusesRequest.Visibility.public -> Visibility.PUBLIC
StatusesRequest.Visibility.unlisted -> Visibility.UNLISTED
StatusesRequest.Visibility.private -> Visibility.FOLLOWERS
StatusesRequest.Visibility.direct -> Visibility.DIRECT
null -> Visibility.PUBLIC
}
val post = postService.createLocal(
PostCreateDto(
statusesRequest.status.orEmpty(),
statusesRequest.spoilerText,
visibility,
null,
statusesRequest.inReplyToId?.toLongOrNull(),
user.id
)
)
val account = accountService.findById(user.id)
val postVisibility = when (statusesRequest.visibility) {
StatusesRequest.Visibility.public -> Status.Visibility.public
StatusesRequest.Visibility.unlisted -> Status.Visibility.unlisted
StatusesRequest.Visibility.private -> Status.Visibility.private
StatusesRequest.Visibility.direct -> Status.Visibility.direct
null -> Status.Visibility.public
}
val replyUser = if (post.replyId != null) {
try {
userQueryService.findById(postQueryService.findById(post.replyId).userId).id
} catch (e: FailedToGetResourcesException) {
null
}
} else {
null
}
return Status(
id = post.id.toString(),
uri = post.apId,
createdAt = Instant.ofEpochMilli(post.createdAt).toString(),
account = account,
content = post.text,
visibility = postVisibility,
sensitive = post.sensitive,
spoilerText = post.overview.orEmpty(),
mediaAttachments = emptyList(),
mentions = emptyList(),
tags = emptyList(),
emojis = emptyList(),
reblogsCount = 0,
favouritesCount = 0,
repliesCount = 0,
url = post.url,
post.replyId?.toString(),
inReplyToAccountId = replyUser?.toString(),
reblog = null,
language = null,
text = post.text,
editedAt = null,
application = null,
poll = null,
card = null,
favourited = null,
reblogged = null,
muted = null,
bookmarked = null,
pinned = null,
filtered = null
)
}
}

View File

@ -1,7 +1,10 @@
package dev.usbharu.hideout.service.auth package dev.usbharu.hideout.service.auth
import dev.usbharu.hideout.service.core.Transaction
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.transaction
import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository
@ -9,22 +12,38 @@ import org.springframework.stereotype.Service
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent as AuthorizationConsent import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent as AuthorizationConsent
@Service @Service
class ExposedOAuth2AuthorizationConsentService(private val registeredClientRepository: RegisteredClientRepository) : class ExposedOAuth2AuthorizationConsentService(
private val registeredClientRepository: RegisteredClientRepository,
private val transaction: Transaction,
private val database: Database
) :
OAuth2AuthorizationConsentService { OAuth2AuthorizationConsentService {
override fun save(authorizationConsent: AuthorizationConsent?) {
init {
transaction(database) {
SchemaUtils.create(OAuth2AuthorizationConsent)
SchemaUtils.createMissingTablesAndColumns(OAuth2AuthorizationConsent)
}
}
override fun save(authorizationConsent: AuthorizationConsent?) = runBlocking {
requireNotNull(authorizationConsent) requireNotNull(authorizationConsent)
val singleOrNull = transaction.transaction {
OAuth2AuthorizationConsent.select {
OAuth2AuthorizationConsent.registeredClientId val singleOrNull =
.eq(authorizationConsent.registeredClientId) OAuth2AuthorizationConsent.select {
.and(OAuth2AuthorizationConsent.principalName.eq(authorizationConsent.principalName)) OAuth2AuthorizationConsent.registeredClientId
} .eq(authorizationConsent.registeredClientId)
.singleOrNull() .and(OAuth2AuthorizationConsent.principalName.eq(authorizationConsent.principalName))
if (singleOrNull == null) { }
OAuth2AuthorizationConsent.insert { .singleOrNull()
it[registeredClientId] = authorizationConsent.registeredClientId if (singleOrNull == null) {
it[principalName] = authorizationConsent.principalName OAuth2AuthorizationConsent.insert {
it[authorities] = authorizationConsent.authorities.joinToString(",") it[registeredClientId] = authorizationConsent.registeredClientId
it[principalName] = authorizationConsent.principalName
it[authorities] = authorizationConsent.authorities.joinToString(",")
}
} }
} }
} }
@ -38,15 +57,17 @@ class ExposedOAuth2AuthorizationConsentService(private val registeredClientRepos
} }
} }
override fun findById(registeredClientId: String?, principalName: String?): AuthorizationConsent? { override fun findById(registeredClientId: String?, principalName: String?): AuthorizationConsent? = runBlocking {
requireNotNull(registeredClientId) requireNotNull(registeredClientId)
requireNotNull(principalName) requireNotNull(principalName)
transaction.transaction {
return OAuth2AuthorizationConsent.select { OAuth2AuthorizationConsent.select {
(OAuth2AuthorizationConsent.registeredClientId eq registeredClientId) (OAuth2AuthorizationConsent.registeredClientId eq registeredClientId)
.and(OAuth2AuthorizationConsent.principalName eq principalName) .and(OAuth2AuthorizationConsent.principalName eq principalName)
}
.singleOrNull()?.toAuthorizationConsent()
} }
.singleOrNull()?.toAuthorizationConsent()
} }
fun ResultRow.toAuthorizationConsent(): AuthorizationConsent { fun ResultRow.toAuthorizationConsent(): AuthorizationConsent {

View File

@ -1,9 +1,18 @@
package dev.usbharu.hideout.service.auth package dev.usbharu.hideout.service.auth
import dev.usbharu.hideout.util.JsonUtil import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.readValue
import dev.usbharu.hideout.domain.model.UserDetailsImpl
import dev.usbharu.hideout.domain.model.UserDetailsMixin
import dev.usbharu.hideout.service.core.Transaction
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.javatime.timestamp import org.jetbrains.exposed.sql.javatime.timestamp
import org.jetbrains.exposed.sql.transactions.transaction
import org.springframework.security.jackson2.CoreJackson2Module
import org.springframework.security.jackson2.SecurityJackson2Modules
import org.springframework.security.oauth2.core.* import org.springframework.security.oauth2.core.*
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames
import org.springframework.security.oauth2.core.oidc.OidcIdToken import org.springframework.security.oauth2.core.oidc.OidcIdToken
@ -13,96 +22,114 @@ import org.springframework.security.oauth2.server.authorization.OAuth2Authorizat
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType import org.springframework.security.oauth2.server.authorization.OAuth2TokenType
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@Service @Service
class ExposedOAuth2AuthorizationService(private val registeredClientRepository: RegisteredClientRepository) : class ExposedOAuth2AuthorizationService(
private val registeredClientRepository: RegisteredClientRepository,
private val transaction: Transaction,
private val database: Database
) :
OAuth2AuthorizationService { OAuth2AuthorizationService {
override fun save(authorization: OAuth2Authorization?) {
init {
transaction(database) {
SchemaUtils.create(Authorization)
SchemaUtils.createMissingTablesAndColumns(Authorization)
}
}
override fun save(authorization: OAuth2Authorization?): Unit = runBlocking {
requireNotNull(authorization) requireNotNull(authorization)
val singleOrNull = Authorization.select { Authorization.id eq authorization.id }.singleOrNull() transaction.transaction {
if (singleOrNull == null) { val singleOrNull = Authorization.select { Authorization.id eq authorization.id }.singleOrNull()
val authorizationCodeToken = authorization.getToken(OAuth2AuthorizationCode::class.java) if (singleOrNull == null) {
val accessToken = authorization.getToken(OAuth2AccessToken::class.java) val authorizationCodeToken = authorization.getToken(OAuth2AuthorizationCode::class.java)
val refreshToken = authorization.getToken(OAuth2RefreshToken::class.java) val accessToken = authorization.getToken(OAuth2AccessToken::class.java)
val oidcIdToken = authorization.getToken(OidcIdToken::class.java) val refreshToken = authorization.getToken(OAuth2RefreshToken::class.java)
val userCode = authorization.getToken(OAuth2UserCode::class.java) val oidcIdToken = authorization.getToken(OidcIdToken::class.java)
val deviceCode = authorization.getToken(OAuth2DeviceCode::class.java) val userCode = authorization.getToken(OAuth2UserCode::class.java)
Authorization.insert { val deviceCode = authorization.getToken(OAuth2DeviceCode::class.java)
it[id] = authorization.id Authorization.insert {
it[registeredClientId] = authorization.registeredClientId it[id] = authorization.id
it[principalName] = authorization.principalName it[registeredClientId] = authorization.registeredClientId
it[authorizationGrantType] = authorization.authorizationGrantType.value it[principalName] = authorization.principalName
it[authorizedScopes] = authorization.authorizedScopes.joinToString(",").takeIf { it.isEmpty() } it[authorizationGrantType] = authorization.authorizationGrantType.value
it[attributes] = JsonUtil.mapToJson(authorization.attributes) it[authorizedScopes] = authorization.authorizedScopes.joinToString(",").takeIf { it.isNotEmpty() }
it[state] = authorization.getAttribute(OAuth2ParameterNames.STATE) it[attributes] = mapToJson(authorization.attributes)
it[authorizationCodeValue] = authorizationCodeToken?.token?.tokenValue it[state] = authorization.getAttribute(OAuth2ParameterNames.STATE)
it[authorizationCodeIssuedAt] = authorizationCodeToken?.token?.issuedAt it[authorizationCodeValue] = authorizationCodeToken?.token?.tokenValue
it[authorizationCodeExpiresAt] = authorizationCodeToken?.token?.expiresAt it[authorizationCodeIssuedAt] = authorizationCodeToken?.token?.issuedAt
it[authorizationCodeMetadata] = authorizationCodeToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } it[authorizationCodeExpiresAt] = authorizationCodeToken?.token?.expiresAt
it[accessTokenValue] = accessToken?.token?.tokenValue it[authorizationCodeMetadata] =
it[accessTokenIssuedAt] = accessToken?.token?.issuedAt authorizationCodeToken?.metadata?.let { it1 -> mapToJson(it1) }
it[accessTokenExpiresAt] = accessToken?.token?.expiresAt it[accessTokenValue] = accessToken?.token?.tokenValue
it[accessTokenMetadata] = accessToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } it[accessTokenIssuedAt] = accessToken?.token?.issuedAt
it[accessTokenType] = accessToken?.token?.tokenType?.value it[accessTokenExpiresAt] = accessToken?.token?.expiresAt
it[accessTokenScopes] = accessToken?.run { token.scopes.joinToString(",").takeIf { it.isEmpty() } } it[accessTokenMetadata] = accessToken?.metadata?.let { it1 -> mapToJson(it1) }
it[refreshTokenValue] = refreshToken?.token?.tokenValue it[accessTokenType] = accessToken?.token?.tokenType?.value
it[refreshTokenIssuedAt] = refreshToken?.token?.issuedAt it[accessTokenScopes] =
it[refreshTokenExpiresAt] = refreshToken?.token?.expiresAt accessToken?.run { token.scopes.joinToString(",").takeIf { it.isNotEmpty() } }
it[refreshTokenMetadata] = refreshToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } it[refreshTokenValue] = refreshToken?.token?.tokenValue
it[oidcIdTokenValue] = oidcIdToken?.token?.tokenValue it[refreshTokenIssuedAt] = refreshToken?.token?.issuedAt
it[oidcIdTokenIssuedAt] = oidcIdToken?.token?.issuedAt it[refreshTokenExpiresAt] = refreshToken?.token?.expiresAt
it[oidcIdTokenExpiresAt] = oidcIdToken?.token?.expiresAt it[refreshTokenMetadata] = refreshToken?.metadata?.let { it1 -> mapToJson(it1) }
it[oidcIdTokenMetadata] = oidcIdToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } it[oidcIdTokenValue] = oidcIdToken?.token?.tokenValue
it[userCodeValue] = userCode?.token?.tokenValue it[oidcIdTokenIssuedAt] = oidcIdToken?.token?.issuedAt
it[userCodeIssuedAt] = userCode?.token?.issuedAt it[oidcIdTokenExpiresAt] = oidcIdToken?.token?.expiresAt
it[userCodeExpiresAt] = userCode?.token?.expiresAt it[oidcIdTokenMetadata] = oidcIdToken?.metadata?.let { it1 -> mapToJson(it1) }
it[userCodeMetadata] = userCode?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } it[userCodeValue] = userCode?.token?.tokenValue
it[deviceCodeValue] = deviceCode?.token?.tokenValue it[userCodeIssuedAt] = userCode?.token?.issuedAt
it[deviceCodeIssuedAt] = deviceCode?.token?.issuedAt it[userCodeExpiresAt] = userCode?.token?.expiresAt
it[deviceCodeExpiresAt] = deviceCode?.token?.expiresAt it[userCodeMetadata] = userCode?.metadata?.let { it1 -> mapToJson(it1) }
it[deviceCodeMetadata] = deviceCode?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } it[deviceCodeValue] = deviceCode?.token?.tokenValue
} it[deviceCodeIssuedAt] = deviceCode?.token?.issuedAt
} else { it[deviceCodeExpiresAt] = deviceCode?.token?.expiresAt
val authorizationCodeToken = authorization.getToken(OAuth2AuthorizationCode::class.java) it[deviceCodeMetadata] = deviceCode?.metadata?.let { it1 -> mapToJson(it1) }
val accessToken = authorization.getToken(OAuth2AccessToken::class.java) }
val refreshToken = authorization.getToken(OAuth2RefreshToken::class.java) } else {
val oidcIdToken = authorization.getToken(OidcIdToken::class.java) val authorizationCodeToken = authorization.getToken(OAuth2AuthorizationCode::class.java)
val userCode = authorization.getToken(OAuth2UserCode::class.java) val accessToken = authorization.getToken(OAuth2AccessToken::class.java)
val deviceCode = authorization.getToken(OAuth2DeviceCode::class.java) val refreshToken = authorization.getToken(OAuth2RefreshToken::class.java)
Authorization.update({ Authorization.id eq authorization.id }) { val oidcIdToken = authorization.getToken(OidcIdToken::class.java)
it[registeredClientId] = authorization.registeredClientId val userCode = authorization.getToken(OAuth2UserCode::class.java)
it[principalName] = authorization.principalName val deviceCode = authorization.getToken(OAuth2DeviceCode::class.java)
it[authorizationGrantType] = authorization.authorizationGrantType.value Authorization.update({ Authorization.id eq authorization.id }) {
it[authorizedScopes] = authorization.authorizedScopes.joinToString(",").takeIf { it.isEmpty() } it[registeredClientId] = authorization.registeredClientId
it[attributes] = JsonUtil.mapToJson(authorization.attributes) it[principalName] = authorization.principalName
it[state] = authorization.getAttribute(OAuth2ParameterNames.STATE) it[authorizationGrantType] = authorization.authorizationGrantType.value
it[authorizationCodeValue] = authorizationCodeToken?.token?.tokenValue it[authorizedScopes] = authorization.authorizedScopes.joinToString(",").takeIf { it.isNotEmpty() }
it[authorizationCodeIssuedAt] = authorizationCodeToken?.token?.issuedAt it[attributes] = mapToJson(authorization.attributes)
it[authorizationCodeExpiresAt] = authorizationCodeToken?.token?.expiresAt it[state] = authorization.getAttribute(OAuth2ParameterNames.STATE)
it[authorizationCodeMetadata] = authorizationCodeToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } it[authorizationCodeValue] = authorizationCodeToken?.token?.tokenValue
it[accessTokenValue] = accessToken?.token?.tokenValue it[authorizationCodeIssuedAt] = authorizationCodeToken?.token?.issuedAt
it[accessTokenIssuedAt] = accessToken?.token?.issuedAt it[authorizationCodeExpiresAt] = authorizationCodeToken?.token?.expiresAt
it[accessTokenExpiresAt] = accessToken?.token?.expiresAt it[authorizationCodeMetadata] =
it[accessTokenMetadata] = accessToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } authorizationCodeToken?.metadata?.let { it1 -> mapToJson(it1) }
it[accessTokenType] = accessToken?.token?.tokenType?.value it[accessTokenValue] = accessToken?.token?.tokenValue
it[accessTokenScopes] = accessToken?.token?.scopes?.joinToString(",")?.takeIf { it.isEmpty() } it[accessTokenIssuedAt] = accessToken?.token?.issuedAt
it[refreshTokenValue] = refreshToken?.token?.tokenValue it[accessTokenExpiresAt] = accessToken?.token?.expiresAt
it[refreshTokenIssuedAt] = refreshToken?.token?.issuedAt it[accessTokenMetadata] = accessToken?.metadata?.let { it1 -> mapToJson(it1) }
it[refreshTokenExpiresAt] = refreshToken?.token?.expiresAt it[accessTokenType] = accessToken?.token?.tokenType?.value
it[refreshTokenMetadata] = refreshToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } it[accessTokenScopes] = accessToken?.token?.scopes?.joinToString(",")?.takeIf { it.isNotEmpty() }
it[oidcIdTokenValue] = oidcIdToken?.token?.tokenValue it[refreshTokenValue] = refreshToken?.token?.tokenValue
it[oidcIdTokenIssuedAt] = oidcIdToken?.token?.issuedAt it[refreshTokenIssuedAt] = refreshToken?.token?.issuedAt
it[oidcIdTokenExpiresAt] = oidcIdToken?.token?.expiresAt it[refreshTokenExpiresAt] = refreshToken?.token?.expiresAt
it[oidcIdTokenMetadata] = oidcIdToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } it[refreshTokenMetadata] = refreshToken?.metadata?.let { it1 -> mapToJson(it1) }
it[userCodeValue] = userCode?.token?.tokenValue it[oidcIdTokenValue] = oidcIdToken?.token?.tokenValue
it[userCodeIssuedAt] = userCode?.token?.issuedAt it[oidcIdTokenIssuedAt] = oidcIdToken?.token?.issuedAt
it[userCodeExpiresAt] = userCode?.token?.expiresAt it[oidcIdTokenExpiresAt] = oidcIdToken?.token?.expiresAt
it[userCodeMetadata] = userCode?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } it[oidcIdTokenMetadata] = oidcIdToken?.metadata?.let { it1 -> mapToJson(it1) }
it[deviceCodeValue] = deviceCode?.token?.tokenValue it[userCodeValue] = userCode?.token?.tokenValue
it[deviceCodeIssuedAt] = deviceCode?.token?.issuedAt it[userCodeIssuedAt] = userCode?.token?.issuedAt
it[deviceCodeExpiresAt] = deviceCode?.token?.expiresAt it[userCodeExpiresAt] = userCode?.token?.expiresAt
it[deviceCodeMetadata] = deviceCode?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } it[userCodeMetadata] = userCode?.metadata?.let { it1 -> mapToJson(it1) }
it[deviceCodeValue] = deviceCode?.token?.tokenValue
it[deviceCodeIssuedAt] = deviceCode?.token?.issuedAt
it[deviceCodeExpiresAt] = deviceCode?.token?.expiresAt
it[deviceCodeMetadata] = deviceCode?.metadata?.let { it1 -> mapToJson(it1) }
}
} }
} }
} }
@ -121,57 +148,61 @@ class ExposedOAuth2AuthorizationService(private val registeredClientRepository:
return Authorization.select { Authorization.id eq id }.singleOrNull()?.toAuthorization() return Authorization.select { Authorization.id eq id }.singleOrNull()?.toAuthorization()
} }
override fun findByToken(token: String?, tokenType: OAuth2TokenType?): OAuth2Authorization? { override fun findByToken(token: String?, tokenType: OAuth2TokenType?): OAuth2Authorization? = runBlocking {
requireNotNull(token) requireNotNull(token)
return when (tokenType?.value) { transaction.transaction {
null -> {
Authorization.select {
Authorization.authorizationCodeValue eq token when (tokenType?.value) {
}.orWhere { null -> {
Authorization.accessTokenValue eq token Authorization.select {
}.orWhere { Authorization.authorizationCodeValue eq token
Authorization.oidcIdTokenValue eq token }.orWhere {
}.orWhere { Authorization.accessTokenValue eq token
Authorization.refreshTokenValue eq token }.orWhere {
}.orWhere { Authorization.oidcIdTokenValue eq token
Authorization.userCodeValue eq token }.orWhere {
}.orWhere { Authorization.refreshTokenValue eq token
Authorization.deviceCodeValue eq token }.orWhere {
Authorization.userCodeValue eq token
}.orWhere {
Authorization.deviceCodeValue eq token
}
} }
}
OAuth2ParameterNames.STATE -> { OAuth2ParameterNames.STATE -> {
Authorization.select { Authorization.state eq token } Authorization.select { Authorization.state eq token }
} }
OAuth2ParameterNames.CODE -> { OAuth2ParameterNames.CODE -> {
Authorization.select { Authorization.authorizationCodeValue eq token } Authorization.select { Authorization.authorizationCodeValue eq token }
} }
OAuth2ParameterNames.ACCESS_TOKEN -> { OAuth2ParameterNames.ACCESS_TOKEN -> {
Authorization.select { Authorization.accessTokenValue eq token } Authorization.select { Authorization.accessTokenValue eq token }
} }
OidcParameterNames.ID_TOKEN -> { OidcParameterNames.ID_TOKEN -> {
Authorization.select { Authorization.oidcIdTokenValue eq token } Authorization.select { Authorization.oidcIdTokenValue eq token }
} }
OAuth2ParameterNames.REFRESH_TOKEN -> { OAuth2ParameterNames.REFRESH_TOKEN -> {
Authorization.select { Authorization.refreshTokenValue eq token } Authorization.select { Authorization.refreshTokenValue eq token }
} }
OAuth2ParameterNames.USER_CODE -> { OAuth2ParameterNames.USER_CODE -> {
Authorization.select { Authorization.userCodeValue eq token } Authorization.select { Authorization.userCodeValue eq token }
} }
OAuth2ParameterNames.DEVICE_CODE -> { OAuth2ParameterNames.DEVICE_CODE -> {
Authorization.select { Authorization.deviceCodeValue eq token } Authorization.select { Authorization.deviceCodeValue eq token }
} }
else -> { else -> {
null null
} }
}?.singleOrNull()?.toAuthorization() }?.singleOrNull()?.toAuthorization()
}
} }
fun ResultRow.toAuthorization(): OAuth2Authorization { fun ResultRow.toAuthorization(): OAuth2Authorization {
@ -184,7 +215,7 @@ class ExposedOAuth2AuthorizationService(private val registeredClientRepository:
val principalName = this[Authorization.principalName] val principalName = this[Authorization.principalName]
val authorizationGrantType = this[Authorization.authorizationGrantType] val authorizationGrantType = this[Authorization.authorizationGrantType]
val authorizedScopes = this[Authorization.authorizedScopes]?.split(",").orEmpty().toSet() val authorizedScopes = this[Authorization.authorizedScopes]?.split(",").orEmpty().toSet()
val attributes = this[Authorization.attributes]?.let { JsonUtil.jsonToMap<String, Any>(it) }.orEmpty() val attributes = this[Authorization.attributes]?.let { jsonToMap<String, Any>(it) }.orEmpty()
builder.id(id).principalName(principalName) builder.id(id).principalName(principalName)
.authorizationGrantType(AuthorizationGrantType(authorizationGrantType)).authorizedScopes(authorizedScopes) .authorizationGrantType(AuthorizationGrantType(authorizationGrantType)).authorizedScopes(authorizedScopes)
@ -195,12 +226,12 @@ class ExposedOAuth2AuthorizationService(private val registeredClientRepository:
builder.attribute(OAuth2ParameterNames.STATE, state) builder.attribute(OAuth2ParameterNames.STATE, state)
} }
val authorizationCodeValue = this[Authorization.authorizationCodeValue] val authorizationCodeValue = this[Authorization.authorizationCodeValue].orEmpty()
if (authorizationCodeValue.isNullOrBlank()) { if (authorizationCodeValue.isNotBlank()) {
val authorizationCodeIssuedAt = this[Authorization.authorizationCodeIssuedAt] val authorizationCodeIssuedAt = this[Authorization.authorizationCodeIssuedAt]
val authorizationCodeExpiresAt = this[Authorization.authorizationCodeExpiresAt] val authorizationCodeExpiresAt = this[Authorization.authorizationCodeExpiresAt]
val authorizationCodeMetadata = this[Authorization.authorizationCodeMetadata]?.let { val authorizationCodeMetadata = this[Authorization.authorizationCodeMetadata]?.let {
JsonUtil.jsonToMap<String, Any>( jsonToMap<String, Any>(
it it
) )
}.orEmpty() }.orEmpty()
@ -216,7 +247,7 @@ class ExposedOAuth2AuthorizationService(private val registeredClientRepository:
val accessTokenIssuedAt = this[Authorization.accessTokenIssuedAt] val accessTokenIssuedAt = this[Authorization.accessTokenIssuedAt]
val accessTokenExpiresAt = this[Authorization.accessTokenExpiresAt] val accessTokenExpiresAt = this[Authorization.accessTokenExpiresAt]
val accessTokenMetadata = val accessTokenMetadata =
this[Authorization.accessTokenMetadata]?.let { JsonUtil.jsonToMap<String, Any>(it) }.orEmpty() this[Authorization.accessTokenMetadata]?.let { jsonToMap<String, Any>(it) }.orEmpty()
val accessTokenType = val accessTokenType =
if (this[Authorization.accessTokenType].equals(OAuth2AccessToken.TokenType.BEARER.value, true)) { if (this[Authorization.accessTokenType].equals(OAuth2AccessToken.TokenType.BEARER.value, true)) {
OAuth2AccessToken.TokenType.BEARER OAuth2AccessToken.TokenType.BEARER
@ -242,7 +273,7 @@ class ExposedOAuth2AuthorizationService(private val registeredClientRepository:
val oidcTokenIssuedAt = this[Authorization.oidcIdTokenIssuedAt] val oidcTokenIssuedAt = this[Authorization.oidcIdTokenIssuedAt]
val oidcTokenExpiresAt = this[Authorization.oidcIdTokenExpiresAt] val oidcTokenExpiresAt = this[Authorization.oidcIdTokenExpiresAt]
val oidcTokenMetadata = val oidcTokenMetadata =
this[Authorization.oidcIdTokenMetadata]?.let { JsonUtil.jsonToMap<String, Any>(it) }.orEmpty() this[Authorization.oidcIdTokenMetadata]?.let { jsonToMap<String, Any>(it) }.orEmpty()
val oidcIdToken = OidcIdToken( val oidcIdToken = OidcIdToken(
oidcIdTokenValue, oidcIdTokenValue,
@ -259,7 +290,7 @@ class ExposedOAuth2AuthorizationService(private val registeredClientRepository:
val refreshTokenIssuedAt = this[Authorization.refreshTokenIssuedAt] val refreshTokenIssuedAt = this[Authorization.refreshTokenIssuedAt]
val refreshTokenExpiresAt = this[Authorization.refreshTokenExpiresAt] val refreshTokenExpiresAt = this[Authorization.refreshTokenExpiresAt]
val refreshTokenMetadata = val refreshTokenMetadata =
this[Authorization.refreshTokenMetadata]?.let { JsonUtil.jsonToMap<String, Any>(it) }.orEmpty() this[Authorization.refreshTokenMetadata]?.let { jsonToMap<String, Any>(it) }.orEmpty()
val oAuth2RefreshToken = OAuth2RefreshToken(refreshTokenValue, refreshTokenIssuedAt, refreshTokenExpiresAt) val oAuth2RefreshToken = OAuth2RefreshToken(refreshTokenValue, refreshTokenIssuedAt, refreshTokenExpiresAt)
@ -271,7 +302,7 @@ class ExposedOAuth2AuthorizationService(private val registeredClientRepository:
val userCodeIssuedAt = this[Authorization.userCodeIssuedAt] val userCodeIssuedAt = this[Authorization.userCodeIssuedAt]
val userCodeExpiresAt = this[Authorization.userCodeExpiresAt] val userCodeExpiresAt = this[Authorization.userCodeExpiresAt]
val userCodeMetadata = val userCodeMetadata =
this[Authorization.userCodeMetadata]?.let { JsonUtil.jsonToMap<String, Any>(it) }.orEmpty() this[Authorization.userCodeMetadata]?.let { jsonToMap<String, Any>(it) }.orEmpty()
val oAuth2UserCode = OAuth2UserCode(userCodeValue, userCodeIssuedAt, userCodeExpiresAt) val oAuth2UserCode = OAuth2UserCode(userCodeValue, userCodeIssuedAt, userCodeExpiresAt)
builder.token(oAuth2UserCode) { it.putAll(userCodeMetadata) } builder.token(oAuth2UserCode) { it.putAll(userCodeMetadata) }
} }
@ -281,7 +312,7 @@ class ExposedOAuth2AuthorizationService(private val registeredClientRepository:
val deviceCodeIssuedAt = this[Authorization.deviceCodeIssuedAt] val deviceCodeIssuedAt = this[Authorization.deviceCodeIssuedAt]
val deviceCodeExpiresAt = this[Authorization.deviceCodeExpiresAt] val deviceCodeExpiresAt = this[Authorization.deviceCodeExpiresAt]
val deviceCodeMetadata = val deviceCodeMetadata =
this[Authorization.deviceCodeMetadata]?.let { JsonUtil.jsonToMap<String, Any>(it) }.orEmpty() this[Authorization.deviceCodeMetadata]?.let { jsonToMap<String, Any>(it) }.orEmpty()
val oAuth2DeviceCode = OAuth2DeviceCode(deviceCodeValue, deviceCodeIssuedAt, deviceCodeExpiresAt) val oAuth2DeviceCode = OAuth2DeviceCode(deviceCodeValue, deviceCodeIssuedAt, deviceCodeExpiresAt)
builder.token(oAuth2DeviceCode) { it.putAll(deviceCodeMetadata) } builder.token(oAuth2DeviceCode) { it.putAll(deviceCodeMetadata) }
@ -289,9 +320,28 @@ class ExposedOAuth2AuthorizationService(private val registeredClientRepository:
return builder.build() return builder.build()
} }
private fun mapToJson(map: Map<*, *>): String = objectMapper.writeValueAsString(map)
private fun <T, U> jsonToMap(json: String): Map<T, U> = objectMapper.readValue(json)
companion object {
val objectMapper: ObjectMapper = ObjectMapper()
init {
val classLoader = ExposedOAuth2AuthorizationService::class.java.classLoader
val modules = SecurityJackson2Modules.getModules(classLoader)
this.objectMapper.registerModules(JavaTimeModule())
this.objectMapper.registerModules(modules)
this.objectMapper.registerModules(OAuth2AuthorizationServerJackson2Module())
this.objectMapper.registerModules(CoreJackson2Module())
this.objectMapper.addMixIn(UserDetailsImpl::class.java, UserDetailsMixin::class.java)
}
}
} }
object Authorization : Table("authorization") { object Authorization : Table("application_authorization") {
val id = varchar("id", 255) val id = varchar("id", 255)
val registeredClientId = varchar("registered_client_id", 255) val registeredClientId = varchar("registered_client_id", 255)
val principalName = varchar("principal_name", 255) val principalName = varchar("principal_name", 255)

View File

@ -0,0 +1,8 @@
package dev.usbharu.hideout.service.auth
import org.springframework.stereotype.Component
@Component
interface SecureTokenGenerator {
fun generate(): String
}

View File

@ -0,0 +1,18 @@
package dev.usbharu.hideout.service.auth
import org.springframework.stereotype.Component
import java.security.SecureRandom
import java.util.*
@Component
class SecureTokenGeneratorImpl : SecureTokenGenerator {
override fun generate(): String {
val byteArray = ByteArray(16)
val secureRandom = SecureRandom()
secureRandom.nextBytes(byteArray)
return Base64.getUrlEncoder().encodeToString(byteArray)
}
}

View File

@ -1,27 +1,38 @@
package dev.usbharu.hideout.service.auth package dev.usbharu.hideout.service.auth
import dev.usbharu.hideout.config.ApplicationConfig
import dev.usbharu.hideout.domain.model.UserDetailsImpl
import dev.usbharu.hideout.query.UserQueryService import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.core.Transaction import dev.usbharu.hideout.service.core.Transaction
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.net.URL
@Service @Service
class UserDetailsServiceImpl(private val userQueryService: UserQueryService, private val transaction: Transaction) : class UserDetailsServiceImpl(
private val userQueryService: UserQueryService,
private val applicationConfig: ApplicationConfig,
private val transaction: Transaction
) :
UserDetailsService { UserDetailsService {
override fun loadUserByUsername(username: String?): UserDetails = runBlocking { override fun loadUserByUsername(username: String?): UserDetails = runBlocking {
if (username == null) { if (username == null) {
throw UsernameNotFoundException("$username not found") throw UsernameNotFoundException("$username not found")
} }
transaction.transaction { transaction.transaction {
val findById = userQueryService.findByNameAndDomain(username, "") val findById = userQueryService.findByNameAndDomain(username, URL(applicationConfig.url).host)
User( UserDetailsImpl(
findById.id,
findById.name, findById.name,
findById.password, findById.password,
emptyList() true,
true,
true,
true,
mutableListOf()
) )
} }
} }

View File

@ -0,0 +1,39 @@
package dev.usbharu.hideout.service.mastodon
import dev.usbharu.hideout.domain.mastodon.model.generated.Account
import dev.usbharu.hideout.query.UserQueryService
import org.springframework.stereotype.Service
@Service
interface AccountService {
suspend fun findById(id: Long): Account
}
@Service
class AccountServiceImpl(private val userQueryService: UserQueryService) : AccountService {
override suspend fun findById(id: Long): Account {
val findById = userQueryService.findById(id)
return Account(
id = findById.id.toString(),
username = findById.name,
acct = "${findById.name}@${findById.domain}",
url = findById.url,
displayName = findById.screenName,
note = findById.description,
avatar = findById.url + "/icon.jpg",
avatarStatic = findById.url + "/icon.jpg",
header = findById.url + "/header.jpg",
headerStatic = findById.url + "/header.jpg",
locked = false,
emptyList(),
emptyList(),
false,
false,
false,
findById.createdAt.toString(),
findById.createdAt.toString(),
0,
0,
)
}
}

View File

@ -2,8 +2,8 @@ package dev.usbharu.hideout.service.user
import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.config.Config
import dev.usbharu.hideout.query.UserQueryService import dev.usbharu.hideout.query.UserQueryService
import io.ktor.util.*
import org.koin.core.annotation.Single import org.koin.core.annotation.Single
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.security.* import java.security.*
import java.util.* import java.util.*
@ -15,8 +15,7 @@ class UserAuthServiceImpl(
) : UserAuthService { ) : UserAuthService {
override fun hash(password: String): String { override fun hash(password: String): String {
val digest = sha256.digest(password.toByteArray(Charsets.UTF_8)) return BCryptPasswordEncoder().encode(password)
return hex(digest)
} }
override suspend fun usernameAlreadyUse(username: String): Boolean { override suspend fun usernameAlreadyUse(username: String): Boolean {

View File

@ -1,11 +1,12 @@
package dev.usbharu.hideout.util package dev.usbharu.hideout.util
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
object JsonUtil { object JsonUtil {
val objectMapper = jacksonObjectMapper() val objectMapper = jacksonObjectMapper().registerModule(JavaTimeModule())
fun mapToJson(map: Map<*, *>, objectMapper: ObjectMapper = this.objectMapper): String = fun mapToJson(map: Map<*, *>, objectMapper: ObjectMapper = this.objectMapper): String =
objectMapper.writeValueAsString(map) objectMapper.writeValueAsString(map)

View File

@ -1,12 +1,28 @@
hideout: hideout:
url: "https://test-hideout.usbharu.dev"
database: database:
url: "jdbc:h2:./test;MODE=POSTGRESQL" url: "jdbc:h2:./test-dev2;MODE=POSTGRESQL"
driver: "org.h2.Driver" driver: "org.h2.Driver"
user: "" user: ""
password: "" password: ""
spring: spring:
jackson:
serialization:
WRITE_DATES_AS_TIMESTAMPS: false
datasource: datasource:
driver-class-name: org.h2.Driver driver-class-name: org.h2.Driver
url: "jdbc:h2:./test;MODE=POSTGRESQL" url: "jdbc:h2:./test-dev2;MODE=POSTGRESQL"
username: "" username: ""
password: "" password: ""
h2:
console:
enabled: true
server:
tomcat:
basedir: tomcat
accesslog:
enabled: true
port: 8081

View File

@ -4,7 +4,7 @@
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder> </encoder>
</appender> </appender>
<root level="DEBUG"> <root level="TRACE">
<appender-ref ref="STDOUT"/> <appender-ref ref="STDOUT"/>
</root> </root>
<logger name="org.eclipse.jetty" level="INFO"/> <logger name="org.eclipse.jetty" level="INFO"/>
@ -12,4 +12,5 @@
<logger name="kjob.core.internal.scheduler.JobServiceImpl" level="INFO"/> <logger name="kjob.core.internal.scheduler.JobServiceImpl" level="INFO"/>
<logger name="Exposed" level="INFO"/> <logger name="Exposed" level="INFO"/>
<logger name="io.ktor.server.plugins.contentnegotiation" level="INFO"/> <logger name="io.ktor.server.plugins.contentnegotiation" level="INFO"/>
<logger name="org.springframework.security" level="trace"/>
</configuration> </configuration>

File diff suppressed because it is too large Load Diff