mirror of https://github.com/usbharu/Hideout.git
Merge pull request #38 from usbharu/feature/mastodon-api
Feature/mastodon api
This commit is contained in:
commit
887249c68d
|
@ -39,3 +39,4 @@ out/
|
||||||
/node_modules/
|
/node_modules/
|
||||||
/src/main/web/generated/
|
/src/main/web/generated/
|
||||||
/stats.html
|
/stats.html
|
||||||
|
/tomcat/
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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,
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package dev.usbharu.hideout.service.auth
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
@Component
|
||||||
|
interface SecureTokenGenerator {
|
||||||
|
fun generate(): String
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue