From 834e40894bc7dc0e6183f7f2cc93874f9142f73a Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Thu, 21 Sep 2023 16:12:24 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20OAuth2=E3=81=8C=E5=8B=95=E3=81=8F?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + build.gradle.kts | 2 + .../usbharu/hideout/config/SecurityConfig.kt | 36 +- .../mastodon/MastodonAppsApiController.kt | 39 +++ .../mastodon/MastodonInstanceApiController.kt | 15 + ...ler.kt => MastodonStatusesApiContoller.kt} | 14 +- .../RegisteredClientRepositoryImpl.kt | 169 ++++++---- .../service/api/mastodon/AppApiService.kt | 61 ++++ ...xposedOAuth2AuthorizationConsentService.kt | 59 ++-- .../auth/ExposedOAuth2AuthorizationService.kt | 318 ++++++++++-------- .../service/auth/SecureTokenGenerator.kt | 8 + .../service/auth/SecureTokenGeneratorImpl.kt | 18 + .../service/auth/UserDetailsServiceImpl.kt | 10 +- .../service/user/UserAuthServiceImpl.kt | 5 +- .../dev/usbharu/hideout/util/JsonUtil.kt | 3 +- src/main/resources/application.yml | 21 +- src/main/resources/logback.xml | 2 +- src/main/resources/openapi/mastodon.yaml | 82 +++++ 18 files changed, 601 insertions(+), 262 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonAppsApiController.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonInstanceApiController.kt rename src/main/kotlin/dev/usbharu/hideout/controller/mastodon/{MastodonApiController.kt => MastodonStatusesApiContoller.kt} (63%) create mode 100644 src/main/kotlin/dev/usbharu/hideout/service/api/mastodon/AppApiService.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/service/auth/SecureTokenGenerator.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/service/auth/SecureTokenGeneratorImpl.kt diff --git a/.gitignore b/.gitignore index 2165d593..5f98beae 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ out/ /node_modules/ /src/main/web/generated/ /stats.html +/tomcat/ diff --git a/build.gradle.kts b/build.gradle.kts index 73104882..68602880 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -172,6 +172,8 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") testImplementation("org.springframework.boot:spring-boot-test-autoconfigure") 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("io.ktor:ktor-client-logging-jvm:$ktor_version") implementation("io.ktor:ktor-server-host-common-jvm:$ktor_version") diff --git a/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt b/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt index 75505da3..3421d12b 100644 --- a/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt @@ -6,11 +6,11 @@ import com.nimbusds.jose.jwk.source.ImmutableJWKSet import com.nimbusds.jose.jwk.source.JWKSource import com.nimbusds.jose.proc.SecurityContext 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.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.core.annotation.Order -import org.springframework.http.MediaType import org.springframework.security.config.Customizer import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity @@ -21,7 +21,8 @@ import org.springframework.security.oauth2.server.authorization.config.annotatio import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint -import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher +import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher +import org.springframework.web.servlet.handler.HandlerMappingIntrospector import java.security.KeyPairGenerator import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey @@ -37,10 +38,7 @@ class SecurityConfig { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http) http .exceptionHandling { - it.defaultAuthenticationEntryPointFor( - LoginUrlAuthenticationEntryPoint("/login"), - MediaTypeRequestMatcher(MediaType.TEXT_HTML) - ) + it.authenticationEntryPoint(LoginUrlAuthenticationEntryPoint("/login")) } .oauth2ResourceServer { it.jwt(Customizer.withDefaults()) @@ -53,23 +51,33 @@ class SecurityConfig { @Bean @Order(2) - fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain { + fun defaultSecurityFilterChain(http: HttpSecurity, introspector: HandlerMappingIntrospector): SecurityFilterChain { + val builder = MvcRequestMatcher.Builder(introspector) + + http .authorizeHttpRequests { it.requestMatchers( - "/inbox", - "/users/*/inbox", - "/outbox", - "/users/*/outbox" - ) - .permitAll() + builder.pattern("/inbox"), + builder.pattern("/api/v1/apps"), + builder.pattern("/api/v1/instance/**") + ).permitAll() + } + .authorizeHttpRequests { + it.requestMatchers(PathRequest.toH2Console()).permitAll() } .authorizeHttpRequests { it.anyRequest().authenticated() } .formLogin(Customizer.withDefaults()) .csrf { - it.disable() + it.ignoringRequestMatchers(builder.pattern("/api/**")) + it.ignoringRequestMatchers(PathRequest.toH2Console()) + } + .headers { + it.frameOptions { + it.sameOrigin() + } } return http.build() } diff --git a/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonAppsApiController.kt b/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonAppsApiController.kt new file mode 100644 index 00000000..9d538ce6 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonAppsApiController.kt @@ -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 { + 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): ResponseEntity { + val appsRequest = + AppsRequest(map.getValue("client_name"), map.getValue("redirect_uris"), map["scopes"], map["website"]) + return ResponseEntity( + appApiService.createApp(appsRequest), + HttpStatus.OK + ) + } + +} diff --git a/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonInstanceApiController.kt b/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonInstanceApiController.kt new file mode 100644 index 00000000..207d809c --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonInstanceApiController.kt @@ -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 { + return ResponseEntity(instanceApiService.v1Instance(), HttpStatus.OK) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonApiController.kt b/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonStatusesApiContoller.kt similarity index 63% rename from src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonApiController.kt rename to src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonStatusesApiContoller.kt index e04e803e..0e81d218 100644 --- a/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonApiController.kt +++ b/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonStatusesApiContoller.kt @@ -1,27 +1,17 @@ package dev.usbharu.hideout.controller.mastodon -import dev.usbharu.hideout.controller.mastodon.generated.DefaultApi +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.mastodon.model.generated.V1Instance import dev.usbharu.hideout.domain.model.UserDetailsImpl -import dev.usbharu.hideout.service.api.mastodon.InstanceApiService 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 MastodonApiController( - private val instanceApiService: InstanceApiService, - private val statusesApiService: StatusesApiService -) : DefaultApi { - override suspend fun apiV1InstanceGet(): ResponseEntity { - return ResponseEntity(instanceApiService.v1Instance(), HttpStatus.OK) - } - +class MastodonStatusesApiContoller(private val statusesApiService: StatusesApiService) : StatusApi { override suspend fun apiV1StatusesPost(statusesRequest: StatusesRequest): ResponseEntity { val principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal() require(principal is UserDetailsImpl) diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/RegisteredClientRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/repository/RegisteredClientRepositoryImpl.kt index 7199a949..c451639b 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/RegisteredClientRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/RegisteredClientRepositoryImpl.kt @@ -1,16 +1,22 @@ 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.clientSettings 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.javatime.CurrentTimestamp import org.jetbrains.exposed.sql.javatime.timestamp 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.ClientAuthenticationMethod 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.ConfigurationSettingNames 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[clientSecretExpiresAt] = registeredClient.clientSecretExpiresAt it[clientName] = registeredClient.clientName - it[clientAuthenticationMethods] = registeredClient.clientAuthenticationMethods.joinToString(",") - it[authorizationGrantTypes] = registeredClient.authorizationGrantTypes.joinToString(",") + it[clientAuthenticationMethods] = + registeredClient.clientAuthenticationMethods.map { it.value }.joinToString(",") + it[authorizationGrantTypes] = + registeredClient.authorizationGrantTypes.map { it.value }.joinToString(",") it[redirectUris] = registeredClient.redirectUris.joinToString(",") it[postLogoutRedirectUris] = registeredClient.postLogoutRedirectUris.joinToString(",") it[scopes] = registeredClient.scopes.joinToString(",") - it[clientSettings] = JsonUtil.mapToJson(registeredClient.clientSettings.settings) - it[tokenSettings] = JsonUtil.mapToJson(registeredClient.tokenSettings.settings) + it[clientSettings] = mapToJson(registeredClient.clientSettings.settings) + it[tokenSettings] = mapToJson(registeredClient.tokenSettings.settings) } } else { RegisteredClient.update({ RegisteredClient.id eq registeredClient.id }) { @@ -61,8 +69,8 @@ class RegisteredClientRepositoryImpl(private val database: Database) : Registere it[redirectUris] = registeredClient.redirectUris.joinToString(",") it[postLogoutRedirectUris] = registeredClient.postLogoutRedirectUris.joinToString(",") it[scopes] = registeredClient.scopes.joinToString(",") - it[clientSettings] = JsonUtil.mapToJson(registeredClient.clientSettings.settings) - it[tokenSettings] = JsonUtil.mapToJson(registeredClient.tokenSettings.settings) + it[clientSettings] = mapToJson(registeredClient.clientSettings.settings) + it[tokenSettings] = mapToJson(registeredClient.tokenSettings.settings) } } } @@ -81,10 +89,93 @@ class RegisteredClientRepositoryImpl(private val database: Database) : Registere if (clientId == null) { return null } - return RegisteredClient.select { + val toRegisteredClient = RegisteredClient.select { RegisteredClient.clientId eq clientId }.singleOrNull()?.toRegisteredClient() + LOGGER.trace("findByClientId: $toRegisteredClient") + return toRegisteredClient } + + private fun mapToJson(map: Map<*, *>): String = objectMapper.writeValueAsString(map) + + private fun jsonToMap(json: String): Map = 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(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 @@ -105,65 +196,3 @@ object RegisteredClient : Table("registered_client") { 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(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() -} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/api/mastodon/AppApiService.kt b/src/main/kotlin/dev/usbharu/hideout/service/api/mastodon/AppApiService.kt new file mode 100644 index 00000000..85dd1be1 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/api/mastodon/AppApiService.kt @@ -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 { + return string.split(" ").toSet() + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/auth/ExposedOAuth2AuthorizationConsentService.kt b/src/main/kotlin/dev/usbharu/hideout/service/auth/ExposedOAuth2AuthorizationConsentService.kt index aff981c3..9fb968e0 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/auth/ExposedOAuth2AuthorizationConsentService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/auth/ExposedOAuth2AuthorizationConsentService.kt @@ -1,7 +1,10 @@ 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.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.transactions.transaction import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService 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 @Service -class ExposedOAuth2AuthorizationConsentService(private val registeredClientRepository: RegisteredClientRepository) : +class ExposedOAuth2AuthorizationConsentService( + private val registeredClientRepository: RegisteredClientRepository, + private val transaction: Transaction, + private val database: Database +) : OAuth2AuthorizationConsentService { - override fun save(authorizationConsent: AuthorizationConsent?) { + + init { + transaction(database) { + SchemaUtils.create(OAuth2AuthorizationConsent) + SchemaUtils.createMissingTablesAndColumns(OAuth2AuthorizationConsent) + } + } + + + override fun save(authorizationConsent: AuthorizationConsent?) = runBlocking { requireNotNull(authorizationConsent) - val singleOrNull = - OAuth2AuthorizationConsent.select { - OAuth2AuthorizationConsent.registeredClientId - .eq(authorizationConsent.registeredClientId) - .and(OAuth2AuthorizationConsent.principalName.eq(authorizationConsent.principalName)) - } - .singleOrNull() - if (singleOrNull == null) { - OAuth2AuthorizationConsent.insert { - it[registeredClientId] = authorizationConsent.registeredClientId - it[principalName] = authorizationConsent.principalName - it[authorities] = authorizationConsent.authorities.joinToString(",") + transaction.transaction { + + val singleOrNull = + OAuth2AuthorizationConsent.select { + OAuth2AuthorizationConsent.registeredClientId + .eq(authorizationConsent.registeredClientId) + .and(OAuth2AuthorizationConsent.principalName.eq(authorizationConsent.principalName)) + } + .singleOrNull() + if (singleOrNull == null) { + OAuth2AuthorizationConsent.insert { + 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(principalName) + transaction.transaction { - return OAuth2AuthorizationConsent.select { - (OAuth2AuthorizationConsent.registeredClientId eq registeredClientId) - .and(OAuth2AuthorizationConsent.principalName eq principalName) + OAuth2AuthorizationConsent.select { + (OAuth2AuthorizationConsent.registeredClientId eq registeredClientId) + .and(OAuth2AuthorizationConsent.principalName eq principalName) + } + .singleOrNull()?.toAuthorizationConsent() } - .singleOrNull()?.toAuthorizationConsent() } fun ResultRow.toAuthorizationConsent(): AuthorizationConsent { diff --git a/src/main/kotlin/dev/usbharu/hideout/service/auth/ExposedOAuth2AuthorizationService.kt b/src/main/kotlin/dev/usbharu/hideout/service/auth/ExposedOAuth2AuthorizationService.kt index 9e25e117..1e3f7a7e 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/auth/ExposedOAuth2AuthorizationService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/auth/ExposedOAuth2AuthorizationService.kt @@ -1,9 +1,15 @@ 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.service.core.Transaction +import kotlinx.coroutines.runBlocking import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.javatime.timestamp +import org.jetbrains.exposed.sql.transactions.transaction +import org.springframework.security.jackson2.SecurityJackson2Modules import org.springframework.security.oauth2.core.* import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames import org.springframework.security.oauth2.core.oidc.OidcIdToken @@ -13,96 +19,113 @@ import org.springframework.security.oauth2.server.authorization.OAuth2Authorizat import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService import org.springframework.security.oauth2.server.authorization.OAuth2TokenType import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository +import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module import org.springframework.stereotype.Service @Service -class ExposedOAuth2AuthorizationService(private val registeredClientRepository: RegisteredClientRepository) : +class ExposedOAuth2AuthorizationService( + private val registeredClientRepository: RegisteredClientRepository, + private val transaction: Transaction, + private val database: Database +) : OAuth2AuthorizationService { - override fun save(authorization: OAuth2Authorization?) { + + init { + transaction(database) { + SchemaUtils.create(Authorization) + SchemaUtils.createMissingTablesAndColumns(Authorization) + } + } + + override fun save(authorization: OAuth2Authorization?): Unit = runBlocking { requireNotNull(authorization) - val singleOrNull = Authorization.select { Authorization.id eq authorization.id }.singleOrNull() - if (singleOrNull == null) { - val authorizationCodeToken = authorization.getToken(OAuth2AuthorizationCode::class.java) - val accessToken = authorization.getToken(OAuth2AccessToken::class.java) - val refreshToken = authorization.getToken(OAuth2RefreshToken::class.java) - val oidcIdToken = authorization.getToken(OidcIdToken::class.java) - val userCode = authorization.getToken(OAuth2UserCode::class.java) - val deviceCode = authorization.getToken(OAuth2DeviceCode::class.java) - Authorization.insert { - it[id] = authorization.id - it[registeredClientId] = authorization.registeredClientId - it[principalName] = authorization.principalName - it[authorizationGrantType] = authorization.authorizationGrantType.value - it[authorizedScopes] = authorization.authorizedScopes.joinToString(",").takeIf { it.isEmpty() } - it[attributes] = JsonUtil.mapToJson(authorization.attributes) - it[state] = authorization.getAttribute(OAuth2ParameterNames.STATE) - it[authorizationCodeValue] = authorizationCodeToken?.token?.tokenValue - it[authorizationCodeIssuedAt] = authorizationCodeToken?.token?.issuedAt - it[authorizationCodeExpiresAt] = authorizationCodeToken?.token?.expiresAt - it[authorizationCodeMetadata] = authorizationCodeToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } - it[accessTokenValue] = accessToken?.token?.tokenValue - it[accessTokenIssuedAt] = accessToken?.token?.issuedAt - it[accessTokenExpiresAt] = accessToken?.token?.expiresAt - it[accessTokenMetadata] = accessToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } - it[accessTokenType] = accessToken?.token?.tokenType?.value - it[accessTokenScopes] = accessToken?.run { token.scopes.joinToString(",").takeIf { it.isEmpty() } } - it[refreshTokenValue] = refreshToken?.token?.tokenValue - it[refreshTokenIssuedAt] = refreshToken?.token?.issuedAt - it[refreshTokenExpiresAt] = refreshToken?.token?.expiresAt - it[refreshTokenMetadata] = refreshToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } - it[oidcIdTokenValue] = oidcIdToken?.token?.tokenValue - it[oidcIdTokenIssuedAt] = oidcIdToken?.token?.issuedAt - it[oidcIdTokenExpiresAt] = oidcIdToken?.token?.expiresAt - it[oidcIdTokenMetadata] = oidcIdToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } - it[userCodeValue] = userCode?.token?.tokenValue - it[userCodeIssuedAt] = userCode?.token?.issuedAt - it[userCodeExpiresAt] = userCode?.token?.expiresAt - it[userCodeMetadata] = userCode?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } - it[deviceCodeValue] = deviceCode?.token?.tokenValue - it[deviceCodeIssuedAt] = deviceCode?.token?.issuedAt - it[deviceCodeExpiresAt] = deviceCode?.token?.expiresAt - it[deviceCodeMetadata] = deviceCode?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } - } - } else { - val authorizationCodeToken = authorization.getToken(OAuth2AuthorizationCode::class.java) - val accessToken = authorization.getToken(OAuth2AccessToken::class.java) - val refreshToken = authorization.getToken(OAuth2RefreshToken::class.java) - val oidcIdToken = authorization.getToken(OidcIdToken::class.java) - val userCode = authorization.getToken(OAuth2UserCode::class.java) - val deviceCode = authorization.getToken(OAuth2DeviceCode::class.java) - Authorization.update({ Authorization.id eq authorization.id }) { - it[registeredClientId] = authorization.registeredClientId - it[principalName] = authorization.principalName - it[authorizationGrantType] = authorization.authorizationGrantType.value - it[authorizedScopes] = authorization.authorizedScopes.joinToString(",").takeIf { it.isEmpty() } - it[attributes] = JsonUtil.mapToJson(authorization.attributes) - it[state] = authorization.getAttribute(OAuth2ParameterNames.STATE) - it[authorizationCodeValue] = authorizationCodeToken?.token?.tokenValue - it[authorizationCodeIssuedAt] = authorizationCodeToken?.token?.issuedAt - it[authorizationCodeExpiresAt] = authorizationCodeToken?.token?.expiresAt - it[authorizationCodeMetadata] = authorizationCodeToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } - it[accessTokenValue] = accessToken?.token?.tokenValue - it[accessTokenIssuedAt] = accessToken?.token?.issuedAt - it[accessTokenExpiresAt] = accessToken?.token?.expiresAt - it[accessTokenMetadata] = accessToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } - it[accessTokenType] = accessToken?.token?.tokenType?.value - it[accessTokenScopes] = accessToken?.token?.scopes?.joinToString(",")?.takeIf { it.isEmpty() } - it[refreshTokenValue] = refreshToken?.token?.tokenValue - it[refreshTokenIssuedAt] = refreshToken?.token?.issuedAt - it[refreshTokenExpiresAt] = refreshToken?.token?.expiresAt - it[refreshTokenMetadata] = refreshToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } - it[oidcIdTokenValue] = oidcIdToken?.token?.tokenValue - it[oidcIdTokenIssuedAt] = oidcIdToken?.token?.issuedAt - it[oidcIdTokenExpiresAt] = oidcIdToken?.token?.expiresAt - it[oidcIdTokenMetadata] = oidcIdToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } - it[userCodeValue] = userCode?.token?.tokenValue - it[userCodeIssuedAt] = userCode?.token?.issuedAt - it[userCodeExpiresAt] = userCode?.token?.expiresAt - it[userCodeMetadata] = userCode?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } - it[deviceCodeValue] = deviceCode?.token?.tokenValue - it[deviceCodeIssuedAt] = deviceCode?.token?.issuedAt - it[deviceCodeExpiresAt] = deviceCode?.token?.expiresAt - it[deviceCodeMetadata] = deviceCode?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } + transaction.transaction { + val singleOrNull = Authorization.select { Authorization.id eq authorization.id }.singleOrNull() + if (singleOrNull == null) { + val authorizationCodeToken = authorization.getToken(OAuth2AuthorizationCode::class.java) + val accessToken = authorization.getToken(OAuth2AccessToken::class.java) + val refreshToken = authorization.getToken(OAuth2RefreshToken::class.java) + val oidcIdToken = authorization.getToken(OidcIdToken::class.java) + val userCode = authorization.getToken(OAuth2UserCode::class.java) + val deviceCode = authorization.getToken(OAuth2DeviceCode::class.java) + Authorization.insert { + it[id] = authorization.id + it[registeredClientId] = authorization.registeredClientId + it[principalName] = authorization.principalName + it[authorizationGrantType] = authorization.authorizationGrantType.value + it[authorizedScopes] = authorization.authorizedScopes.joinToString(",").takeIf { it.isEmpty() } + it[attributes] = mapToJson(authorization.attributes) + it[state] = authorization.getAttribute(OAuth2ParameterNames.STATE) + it[authorizationCodeValue] = authorizationCodeToken?.token?.tokenValue + it[authorizationCodeIssuedAt] = authorizationCodeToken?.token?.issuedAt + it[authorizationCodeExpiresAt] = authorizationCodeToken?.token?.expiresAt + it[authorizationCodeMetadata] = + authorizationCodeToken?.metadata?.let { it1 -> mapToJson(it1) } + it[accessTokenValue] = accessToken?.token?.tokenValue + it[accessTokenIssuedAt] = accessToken?.token?.issuedAt + it[accessTokenExpiresAt] = accessToken?.token?.expiresAt + it[accessTokenMetadata] = accessToken?.metadata?.let { it1 -> mapToJson(it1) } + it[accessTokenType] = accessToken?.token?.tokenType?.value + it[accessTokenScopes] = accessToken?.run { token.scopes.joinToString(",").takeIf { it.isEmpty() } } + it[refreshTokenValue] = refreshToken?.token?.tokenValue + it[refreshTokenIssuedAt] = refreshToken?.token?.issuedAt + it[refreshTokenExpiresAt] = refreshToken?.token?.expiresAt + it[refreshTokenMetadata] = refreshToken?.metadata?.let { it1 -> mapToJson(it1) } + it[oidcIdTokenValue] = oidcIdToken?.token?.tokenValue + it[oidcIdTokenIssuedAt] = oidcIdToken?.token?.issuedAt + it[oidcIdTokenExpiresAt] = oidcIdToken?.token?.expiresAt + it[oidcIdTokenMetadata] = oidcIdToken?.metadata?.let { it1 -> mapToJson(it1) } + it[userCodeValue] = userCode?.token?.tokenValue + it[userCodeIssuedAt] = userCode?.token?.issuedAt + it[userCodeExpiresAt] = userCode?.token?.expiresAt + 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) } + } + } else { + val authorizationCodeToken = authorization.getToken(OAuth2AuthorizationCode::class.java) + val accessToken = authorization.getToken(OAuth2AccessToken::class.java) + val refreshToken = authorization.getToken(OAuth2RefreshToken::class.java) + val oidcIdToken = authorization.getToken(OidcIdToken::class.java) + val userCode = authorization.getToken(OAuth2UserCode::class.java) + val deviceCode = authorization.getToken(OAuth2DeviceCode::class.java) + Authorization.update({ Authorization.id eq authorization.id }) { + it[registeredClientId] = authorization.registeredClientId + it[principalName] = authorization.principalName + it[authorizationGrantType] = authorization.authorizationGrantType.value + it[authorizedScopes] = authorization.authorizedScopes.joinToString(",").takeIf { it.isEmpty() } + it[attributes] = mapToJson(authorization.attributes) + it[state] = authorization.getAttribute(OAuth2ParameterNames.STATE) + it[authorizationCodeValue] = authorizationCodeToken?.token?.tokenValue + it[authorizationCodeIssuedAt] = authorizationCodeToken?.token?.issuedAt + it[authorizationCodeExpiresAt] = authorizationCodeToken?.token?.expiresAt + it[authorizationCodeMetadata] = + authorizationCodeToken?.metadata?.let { it1 -> mapToJson(it1) } + it[accessTokenValue] = accessToken?.token?.tokenValue + it[accessTokenIssuedAt] = accessToken?.token?.issuedAt + it[accessTokenExpiresAt] = accessToken?.token?.expiresAt + it[accessTokenMetadata] = accessToken?.metadata?.let { it1 -> mapToJson(it1) } + it[accessTokenType] = accessToken?.token?.tokenType?.value + it[accessTokenScopes] = accessToken?.token?.scopes?.joinToString(",")?.takeIf { it.isEmpty() } + it[refreshTokenValue] = refreshToken?.token?.tokenValue + it[refreshTokenIssuedAt] = refreshToken?.token?.issuedAt + it[refreshTokenExpiresAt] = refreshToken?.token?.expiresAt + it[refreshTokenMetadata] = refreshToken?.metadata?.let { it1 -> mapToJson(it1) } + it[oidcIdTokenValue] = oidcIdToken?.token?.tokenValue + it[oidcIdTokenIssuedAt] = oidcIdToken?.token?.issuedAt + it[oidcIdTokenExpiresAt] = oidcIdToken?.token?.expiresAt + it[oidcIdTokenMetadata] = oidcIdToken?.metadata?.let { it1 -> mapToJson(it1) } + it[userCodeValue] = userCode?.token?.tokenValue + it[userCodeIssuedAt] = userCode?.token?.issuedAt + it[userCodeExpiresAt] = userCode?.token?.expiresAt + 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 +144,61 @@ class ExposedOAuth2AuthorizationService(private val registeredClientRepository: 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) - return when (tokenType?.value) { - null -> { - Authorization.select { - Authorization.authorizationCodeValue eq token - }.orWhere { - Authorization.accessTokenValue eq token - }.orWhere { - Authorization.oidcIdTokenValue eq token - }.orWhere { - Authorization.refreshTokenValue eq token - }.orWhere { - Authorization.userCodeValue eq token - }.orWhere { - Authorization.deviceCodeValue eq token + transaction.transaction { + + + when (tokenType?.value) { + null -> { + Authorization.select { + Authorization.authorizationCodeValue eq token + }.orWhere { + Authorization.accessTokenValue eq token + }.orWhere { + Authorization.oidcIdTokenValue eq token + }.orWhere { + Authorization.refreshTokenValue eq token + }.orWhere { + Authorization.userCodeValue eq token + }.orWhere { + Authorization.deviceCodeValue eq token + } } - } - OAuth2ParameterNames.STATE -> { - Authorization.select { Authorization.state eq token } - } + OAuth2ParameterNames.STATE -> { + Authorization.select { Authorization.state eq token } + } - OAuth2ParameterNames.CODE -> { - Authorization.select { Authorization.authorizationCodeValue eq token } - } + OAuth2ParameterNames.CODE -> { + Authorization.select { Authorization.authorizationCodeValue eq token } + } - OAuth2ParameterNames.ACCESS_TOKEN -> { - Authorization.select { Authorization.accessTokenValue eq token } - } + OAuth2ParameterNames.ACCESS_TOKEN -> { + Authorization.select { Authorization.accessTokenValue eq token } + } - OidcParameterNames.ID_TOKEN -> { - Authorization.select { Authorization.oidcIdTokenValue eq token } - } + OidcParameterNames.ID_TOKEN -> { + Authorization.select { Authorization.oidcIdTokenValue eq token } + } - OAuth2ParameterNames.REFRESH_TOKEN -> { - Authorization.select { Authorization.refreshTokenValue eq token } - } + OAuth2ParameterNames.REFRESH_TOKEN -> { + Authorization.select { Authorization.refreshTokenValue eq token } + } - OAuth2ParameterNames.USER_CODE -> { - Authorization.select { Authorization.userCodeValue eq token } - } + OAuth2ParameterNames.USER_CODE -> { + Authorization.select { Authorization.userCodeValue eq token } + } - OAuth2ParameterNames.DEVICE_CODE -> { - Authorization.select { Authorization.deviceCodeValue eq token } - } + OAuth2ParameterNames.DEVICE_CODE -> { + Authorization.select { Authorization.deviceCodeValue eq token } + } - else -> { - null - } - }?.singleOrNull()?.toAuthorization() + else -> { + null + } + }?.singleOrNull()?.toAuthorization() + } } fun ResultRow.toAuthorization(): OAuth2Authorization { @@ -184,7 +211,7 @@ class ExposedOAuth2AuthorizationService(private val registeredClientRepository: val principalName = this[Authorization.principalName] val authorizationGrantType = this[Authorization.authorizationGrantType] val authorizedScopes = this[Authorization.authorizedScopes]?.split(",").orEmpty().toSet() - val attributes = this[Authorization.attributes]?.let { JsonUtil.jsonToMap(it) }.orEmpty() + val attributes = this[Authorization.attributes]?.let { jsonToMap(it) }.orEmpty() builder.id(id).principalName(principalName) .authorizationGrantType(AuthorizationGrantType(authorizationGrantType)).authorizedScopes(authorizedScopes) @@ -195,12 +222,12 @@ class ExposedOAuth2AuthorizationService(private val registeredClientRepository: builder.attribute(OAuth2ParameterNames.STATE, state) } - val authorizationCodeValue = this[Authorization.authorizationCodeValue] - if (authorizationCodeValue.isNullOrBlank()) { + val authorizationCodeValue = this[Authorization.authorizationCodeValue].orEmpty() + if (authorizationCodeValue.isNotBlank()) { val authorizationCodeIssuedAt = this[Authorization.authorizationCodeIssuedAt] val authorizationCodeExpiresAt = this[Authorization.authorizationCodeExpiresAt] val authorizationCodeMetadata = this[Authorization.authorizationCodeMetadata]?.let { - JsonUtil.jsonToMap( + jsonToMap( it ) }.orEmpty() @@ -216,7 +243,7 @@ class ExposedOAuth2AuthorizationService(private val registeredClientRepository: val accessTokenIssuedAt = this[Authorization.accessTokenIssuedAt] val accessTokenExpiresAt = this[Authorization.accessTokenExpiresAt] val accessTokenMetadata = - this[Authorization.accessTokenMetadata]?.let { JsonUtil.jsonToMap(it) }.orEmpty() + this[Authorization.accessTokenMetadata]?.let { jsonToMap(it) }.orEmpty() val accessTokenType = if (this[Authorization.accessTokenType].equals(OAuth2AccessToken.TokenType.BEARER.value, true)) { OAuth2AccessToken.TokenType.BEARER @@ -242,7 +269,7 @@ class ExposedOAuth2AuthorizationService(private val registeredClientRepository: val oidcTokenIssuedAt = this[Authorization.oidcIdTokenIssuedAt] val oidcTokenExpiresAt = this[Authorization.oidcIdTokenExpiresAt] val oidcTokenMetadata = - this[Authorization.oidcIdTokenMetadata]?.let { JsonUtil.jsonToMap(it) }.orEmpty() + this[Authorization.oidcIdTokenMetadata]?.let { jsonToMap(it) }.orEmpty() val oidcIdToken = OidcIdToken( oidcIdTokenValue, @@ -259,7 +286,7 @@ class ExposedOAuth2AuthorizationService(private val registeredClientRepository: val refreshTokenIssuedAt = this[Authorization.refreshTokenIssuedAt] val refreshTokenExpiresAt = this[Authorization.refreshTokenExpiresAt] val refreshTokenMetadata = - this[Authorization.refreshTokenMetadata]?.let { JsonUtil.jsonToMap(it) }.orEmpty() + this[Authorization.refreshTokenMetadata]?.let { jsonToMap(it) }.orEmpty() val oAuth2RefreshToken = OAuth2RefreshToken(refreshTokenValue, refreshTokenIssuedAt, refreshTokenExpiresAt) @@ -271,7 +298,7 @@ class ExposedOAuth2AuthorizationService(private val registeredClientRepository: val userCodeIssuedAt = this[Authorization.userCodeIssuedAt] val userCodeExpiresAt = this[Authorization.userCodeExpiresAt] val userCodeMetadata = - this[Authorization.userCodeMetadata]?.let { JsonUtil.jsonToMap(it) }.orEmpty() + this[Authorization.userCodeMetadata]?.let { jsonToMap(it) }.orEmpty() val oAuth2UserCode = OAuth2UserCode(userCodeValue, userCodeIssuedAt, userCodeExpiresAt) builder.token(oAuth2UserCode) { it.putAll(userCodeMetadata) } } @@ -281,7 +308,7 @@ class ExposedOAuth2AuthorizationService(private val registeredClientRepository: val deviceCodeIssuedAt = this[Authorization.deviceCodeIssuedAt] val deviceCodeExpiresAt = this[Authorization.deviceCodeExpiresAt] val deviceCodeMetadata = - this[Authorization.deviceCodeMetadata]?.let { JsonUtil.jsonToMap(it) }.orEmpty() + this[Authorization.deviceCodeMetadata]?.let { jsonToMap(it) }.orEmpty() val oAuth2DeviceCode = OAuth2DeviceCode(deviceCodeValue, deviceCodeIssuedAt, deviceCodeExpiresAt) builder.token(oAuth2DeviceCode) { it.putAll(deviceCodeMetadata) } @@ -289,9 +316,26 @@ class ExposedOAuth2AuthorizationService(private val registeredClientRepository: return builder.build() } + + private fun mapToJson(map: Map<*, *>): String = objectMapper.writeValueAsString(map) + + private fun jsonToMap(json: String): Map = 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()) + } + } } -object Authorization : Table("authorization") { +object Authorization : Table("application_authorization") { val id = varchar("id", 255) val registeredClientId = varchar("registered_client_id", 255) val principalName = varchar("principal_name", 255) diff --git a/src/main/kotlin/dev/usbharu/hideout/service/auth/SecureTokenGenerator.kt b/src/main/kotlin/dev/usbharu/hideout/service/auth/SecureTokenGenerator.kt new file mode 100644 index 00000000..81b7a793 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/auth/SecureTokenGenerator.kt @@ -0,0 +1,8 @@ +package dev.usbharu.hideout.service.auth + +import org.springframework.stereotype.Component + +@Component +interface SecureTokenGenerator { + fun generate(): String +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/auth/SecureTokenGeneratorImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/auth/SecureTokenGeneratorImpl.kt new file mode 100644 index 00000000..9a36f5e7 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/auth/SecureTokenGeneratorImpl.kt @@ -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) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/auth/UserDetailsServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/auth/UserDetailsServiceImpl.kt index 34bd9b60..f896209f 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/auth/UserDetailsServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/auth/UserDetailsServiceImpl.kt @@ -1,5 +1,6 @@ package dev.usbharu.hideout.service.auth +import dev.usbharu.hideout.config.ApplicationConfig import dev.usbharu.hideout.query.UserQueryService import dev.usbharu.hideout.service.core.Transaction import kotlinx.coroutines.runBlocking @@ -8,16 +9,21 @@ import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.stereotype.Service +import java.net.URL @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 { override fun loadUserByUsername(username: String?): UserDetails = runBlocking { if (username == null) { throw UsernameNotFoundException("$username not found") } transaction.transaction { - val findById = userQueryService.findByNameAndDomain(username, "") + val findById = userQueryService.findByNameAndDomain(username, URL(applicationConfig.url).host) User( findById.name, findById.password, diff --git a/src/main/kotlin/dev/usbharu/hideout/service/user/UserAuthServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/user/UserAuthServiceImpl.kt index a4e0bba6..547aa367 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/user/UserAuthServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/user/UserAuthServiceImpl.kt @@ -2,8 +2,8 @@ package dev.usbharu.hideout.service.user import dev.usbharu.hideout.config.Config import dev.usbharu.hideout.query.UserQueryService -import io.ktor.util.* import org.koin.core.annotation.Single +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.stereotype.Service import java.security.* import java.util.* @@ -15,8 +15,7 @@ class UserAuthServiceImpl( ) : UserAuthService { override fun hash(password: String): String { - val digest = sha256.digest(password.toByteArray(Charsets.UTF_8)) - return hex(digest) + return BCryptPasswordEncoder().encode(password) } override suspend fun usernameAlreadyUse(username: String): Boolean { diff --git a/src/main/kotlin/dev/usbharu/hideout/util/JsonUtil.kt b/src/main/kotlin/dev/usbharu/hideout/util/JsonUtil.kt index 25d282bc..958d0a90 100644 --- a/src/main/kotlin/dev/usbharu/hideout/util/JsonUtil.kt +++ b/src/main/kotlin/dev/usbharu/hideout/util/JsonUtil.kt @@ -1,11 +1,12 @@ package dev.usbharu.hideout.util 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.readValue object JsonUtil { - val objectMapper = jacksonObjectMapper() + val objectMapper = jacksonObjectMapper().registerModule(JavaTimeModule()) fun mapToJson(map: Map<*, *>, objectMapper: ObjectMapper = this.objectMapper): String = objectMapper.writeValueAsString(map) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f3e47635..37a09f1e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,13 +1,28 @@ hideout: - url: "http://localhost:8080" + url: "https://test-hideout.usbharu.dev" database: - url: "jdbc:h2:./test;MODE=POSTGRESQL" + url: "jdbc:h2:./test-dev2;MODE=POSTGRESQL" driver: "org.h2.Driver" user: "" password: "" spring: + jackson: + serialization: + WRITE_DATES_AS_TIMESTAMPS: false datasource: driver-class-name: org.h2.Driver - url: "jdbc:h2:./test;MODE=POSTGRESQL" + url: "jdbc:h2:./test-dev2;MODE=POSTGRESQL" username: "" password: "" + + h2: + console: + enabled: true +server: + + tomcat: + basedir: tomcat + accesslog: + enabled: true + + port: 8081 diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 4593b633..ad457f2b 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -4,7 +4,7 @@ %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - + diff --git a/src/main/resources/openapi/mastodon.yaml b/src/main/resources/openapi/mastodon.yaml index 4c9755ad..3df85bd4 100644 --- a/src/main/resources/openapi/mastodon.yaml +++ b/src/main/resources/openapi/mastodon.yaml @@ -5,9 +5,22 @@ info: version: 1.0.0 servers: - url: 'https://test-hideout.usbharu.dev' + +tags: + - name: status + description: status + - name: account + description: account + - name: app + description: app + - name: instance + description: instance + paths: /api/v2/instance: get: + tags: + - instance security: - { } responses: @@ -20,6 +33,8 @@ paths: /api/v1/instance/peers: get: + tags: + - instance security: - { } responses: @@ -34,6 +49,8 @@ paths: /api/v1/instance/activity: get: + tags: + - instance security: - { } responses: @@ -48,6 +65,8 @@ paths: /api/v1/instance/rules: get: + tags: + - instance security: - { } responses: @@ -62,6 +81,8 @@ paths: /api/v1/instance/domain_blocks: get: + tags: + - instance security: - { } responses: @@ -76,6 +97,8 @@ paths: /api/v1/instance/extended_description: get: + tags: + - instance security: - { } responses: @@ -88,6 +111,8 @@ paths: /api/v1/instance: get: + tags: + - instance security: - { } responses: @@ -100,6 +125,8 @@ paths: /api/v1/statuses: post: + tags: + - status security: - OAuth2: - "write:statuses" @@ -118,6 +145,28 @@ paths: schema: $ref: "#/components/schemas/Status" + /api/v1/apps: + post: + tags: + - app + security: + - { } + requestBody: + description: 作成するApp + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AppsRequest" + + responses: + 200: + description: 成功 + content: + application/json: + schema: + $ref: "#/components/schemas/Application" + components: schemas: Account: @@ -931,6 +980,39 @@ components: hide_totals: type: boolean + Application: + type: object + properties: + name: + type: string + website: + type: string + nullable: true + vapid_key: + type: string + client_id: + type: string + client_secret: + type: string + required: + - name + - vapid_key + + AppsRequest: + type: object + properties: + client_name: + type: string + redirect_uris: + type: string + scopes: + type: string + website: + type: string + required: + - client_name + - redirect_uris + securitySchemes: OAuth2: type: oauth2