feat: OAuth2が動くように

This commit is contained in:
usbharu 2023-09-21 16:12:24 +09:00
parent 0e00e9526d
commit 834e40894b
18 changed files with 601 additions and 262 deletions

1
.gitignore vendored
View File

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

View File

@ -172,6 +172,8 @@ 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("io.ktor:ktor-client-logging-jvm:$ktor_version") implementation("io.ktor:ktor-client-logging-jvm:$ktor_version")
implementation("io.ktor:ktor-server-host-common-jvm:$ktor_version") implementation("io.ktor:ktor-server-host-common-jvm:$ktor_version")

View File

@ -6,11 +6,11 @@ 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 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
import org.springframework.core.annotation.Order import org.springframework.core.annotation.Order
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
@ -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.oauth2.server.authorization.settings.AuthorizationServerSettings
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.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.KeyPairGenerator
import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey import java.security.interfaces.RSAPublicKey
@ -37,10 +38,7 @@ class SecurityConfig {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http) OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
http http
.exceptionHandling { .exceptionHandling {
it.defaultAuthenticationEntryPointFor( it.authenticationEntryPoint(LoginUrlAuthenticationEntryPoint("/login"))
LoginUrlAuthenticationEntryPoint("/login"),
MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
} }
.oauth2ResourceServer { .oauth2ResourceServer {
it.jwt(Customizer.withDefaults()) it.jwt(Customizer.withDefaults())
@ -53,23 +51,33 @@ class SecurityConfig {
@Bean @Bean
@Order(2) @Order(2)
fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain { fun defaultSecurityFilterChain(http: HttpSecurity, introspector: HandlerMappingIntrospector): SecurityFilterChain {
val builder = MvcRequestMatcher.Builder(introspector)
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() .authorizeHttpRequests {
it.requestMatchers(PathRequest.toH2Console()).permitAll()
} }
.authorizeHttpRequests { .authorizeHttpRequests {
it.anyRequest().authenticated() it.anyRequest().authenticated()
} }
.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()
} }

View File

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

View File

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

View File

@ -1,27 +1,17 @@
package dev.usbharu.hideout.controller.mastodon 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.Status
import dev.usbharu.hideout.domain.mastodon.model.generated.StatusesRequest 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.domain.model.UserDetailsImpl
import dev.usbharu.hideout.service.api.mastodon.InstanceApiService
import dev.usbharu.hideout.service.api.mastodon.StatusesApiService import dev.usbharu.hideout.service.api.mastodon.StatusesApiService
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Controller import org.springframework.stereotype.Controller
@Controller @Controller
class MastodonApiController( class MastodonStatusesApiContoller(private val statusesApiService: StatusesApiService) : StatusApi {
private val instanceApiService: InstanceApiService,
private val statusesApiService: StatusesApiService
) : DefaultApi {
override suspend fun apiV1InstanceGet(): ResponseEntity<V1Instance> {
return ResponseEntity(instanceApiService.v1Instance(), HttpStatus.OK)
}
override suspend fun apiV1StatusesPost(statusesRequest: StatusesRequest): ResponseEntity<Status> { override suspend fun apiV1StatusesPost(statusesRequest: StatusesRequest): ResponseEntity<Status> {
val principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal() val principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal()
require(principal is UserDetailsImpl) require(principal is UserDetailsImpl)

View File

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

View File

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

View File

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

View File

@ -1,9 +1,15 @@
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.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.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 +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.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.isEmpty() }
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] = accessToken?.run { token.scopes.joinToString(",").takeIf { it.isEmpty() } }
it[refreshTokenExpiresAt] = refreshToken?.token?.expiresAt it[refreshTokenValue] = refreshToken?.token?.tokenValue
it[refreshTokenMetadata] = refreshToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } it[refreshTokenIssuedAt] = refreshToken?.token?.issuedAt
it[oidcIdTokenValue] = oidcIdToken?.token?.tokenValue it[refreshTokenExpiresAt] = refreshToken?.token?.expiresAt
it[oidcIdTokenIssuedAt] = oidcIdToken?.token?.issuedAt it[refreshTokenMetadata] = refreshToken?.metadata?.let { it1 -> mapToJson(it1) }
it[oidcIdTokenExpiresAt] = oidcIdToken?.token?.expiresAt it[oidcIdTokenValue] = oidcIdToken?.token?.tokenValue
it[oidcIdTokenMetadata] = oidcIdToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } it[oidcIdTokenIssuedAt] = oidcIdToken?.token?.issuedAt
it[userCodeValue] = userCode?.token?.tokenValue it[oidcIdTokenExpiresAt] = oidcIdToken?.token?.expiresAt
it[userCodeIssuedAt] = userCode?.token?.issuedAt it[oidcIdTokenMetadata] = oidcIdToken?.metadata?.let { it1 -> mapToJson(it1) }
it[userCodeExpiresAt] = userCode?.token?.expiresAt it[userCodeValue] = userCode?.token?.tokenValue
it[userCodeMetadata] = userCode?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } it[userCodeIssuedAt] = userCode?.token?.issuedAt
it[deviceCodeValue] = deviceCode?.token?.tokenValue it[userCodeExpiresAt] = userCode?.token?.expiresAt
it[deviceCodeIssuedAt] = deviceCode?.token?.issuedAt it[userCodeMetadata] = userCode?.metadata?.let { it1 -> mapToJson(it1) }
it[deviceCodeExpiresAt] = deviceCode?.token?.expiresAt it[deviceCodeValue] = deviceCode?.token?.tokenValue
it[deviceCodeMetadata] = deviceCode?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } it[deviceCodeIssuedAt] = deviceCode?.token?.issuedAt
} it[deviceCodeExpiresAt] = deviceCode?.token?.expiresAt
} else { it[deviceCodeMetadata] = deviceCode?.metadata?.let { it1 -> mapToJson(it1) }
val authorizationCodeToken = authorization.getToken(OAuth2AuthorizationCode::class.java) }
val accessToken = authorization.getToken(OAuth2AccessToken::class.java) } else {
val refreshToken = authorization.getToken(OAuth2RefreshToken::class.java) val authorizationCodeToken = authorization.getToken(OAuth2AuthorizationCode::class.java)
val oidcIdToken = authorization.getToken(OidcIdToken::class.java) val accessToken = authorization.getToken(OAuth2AccessToken::class.java)
val userCode = authorization.getToken(OAuth2UserCode::class.java) val refreshToken = authorization.getToken(OAuth2RefreshToken::class.java)
val deviceCode = authorization.getToken(OAuth2DeviceCode::class.java) val oidcIdToken = authorization.getToken(OidcIdToken::class.java)
Authorization.update({ Authorization.id eq authorization.id }) { val userCode = authorization.getToken(OAuth2UserCode::class.java)
it[registeredClientId] = authorization.registeredClientId val deviceCode = authorization.getToken(OAuth2DeviceCode::class.java)
it[principalName] = authorization.principalName Authorization.update({ Authorization.id eq authorization.id }) {
it[authorizationGrantType] = authorization.authorizationGrantType.value it[registeredClientId] = authorization.registeredClientId
it[authorizedScopes] = authorization.authorizedScopes.joinToString(",").takeIf { it.isEmpty() } it[principalName] = authorization.principalName
it[attributes] = JsonUtil.mapToJson(authorization.attributes) it[authorizationGrantType] = authorization.authorizationGrantType.value
it[state] = authorization.getAttribute(OAuth2ParameterNames.STATE) it[authorizedScopes] = authorization.authorizedScopes.joinToString(",").takeIf { it.isEmpty() }
it[authorizationCodeValue] = authorizationCodeToken?.token?.tokenValue it[attributes] = mapToJson(authorization.attributes)
it[authorizationCodeIssuedAt] = authorizationCodeToken?.token?.issuedAt it[state] = authorization.getAttribute(OAuth2ParameterNames.STATE)
it[authorizationCodeExpiresAt] = authorizationCodeToken?.token?.expiresAt it[authorizationCodeValue] = authorizationCodeToken?.token?.tokenValue
it[authorizationCodeMetadata] = authorizationCodeToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } it[authorizationCodeIssuedAt] = authorizationCodeToken?.token?.issuedAt
it[accessTokenValue] = accessToken?.token?.tokenValue it[authorizationCodeExpiresAt] = authorizationCodeToken?.token?.expiresAt
it[accessTokenIssuedAt] = accessToken?.token?.issuedAt it[authorizationCodeMetadata] =
it[accessTokenExpiresAt] = accessToken?.token?.expiresAt authorizationCodeToken?.metadata?.let { it1 -> mapToJson(it1) }
it[accessTokenMetadata] = accessToken?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) } it[accessTokenValue] = accessToken?.token?.tokenValue
it[accessTokenType] = accessToken?.token?.tokenType?.value it[accessTokenIssuedAt] = accessToken?.token?.issuedAt
it[accessTokenScopes] = accessToken?.token?.scopes?.joinToString(",")?.takeIf { it.isEmpty() } it[accessTokenExpiresAt] = accessToken?.token?.expiresAt
it[refreshTokenValue] = refreshToken?.token?.tokenValue it[accessTokenMetadata] = accessToken?.metadata?.let { it1 -> mapToJson(it1) }
it[refreshTokenIssuedAt] = refreshToken?.token?.issuedAt it[accessTokenType] = accessToken?.token?.tokenType?.value
it[refreshTokenExpiresAt] = refreshToken?.token?.expiresAt it[accessTokenScopes] = accessToken?.token?.scopes?.joinToString(",")?.takeIf { it.isEmpty() }
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
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() 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 +211,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 +222,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 +243,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 +269,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 +286,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 +298,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 +308,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 +316,26 @@ 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())
}
}
} }
object Authorization : Table("authorization") { object Authorization : Table("application_authorization") {
val id = varchar("id", 255) val id = varchar("id", 255)
val registeredClientId = varchar("registered_client_id", 255) val registeredClientId = varchar("registered_client_id", 255)
val principalName = varchar("principal_name", 255) val principalName = varchar("principal_name", 255)

View File

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

View File

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

View File

@ -1,5 +1,6 @@
package dev.usbharu.hideout.service.auth package dev.usbharu.hideout.service.auth
import dev.usbharu.hideout.config.ApplicationConfig
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
@ -8,16 +9,21 @@ 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( User(
findById.name, findById.name,
findById.password, findById.password,

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder> </encoder>
</appender> </appender>
<root level="DEBUG"> <root level="TRACE">
<appender-ref ref="STDOUT"/> <appender-ref ref="STDOUT"/>
</root> </root>
<logger name="org.eclipse.jetty" level="INFO"/> <logger name="org.eclipse.jetty" level="INFO"/>

View File

@ -5,9 +5,22 @@ info:
version: 1.0.0 version: 1.0.0
servers: servers:
- url: 'https://test-hideout.usbharu.dev' - url: 'https://test-hideout.usbharu.dev'
tags:
- name: status
description: status
- name: account
description: account
- name: app
description: app
- name: instance
description: instance
paths: paths:
/api/v2/instance: /api/v2/instance:
get: get:
tags:
- instance
security: security:
- { } - { }
responses: responses:
@ -20,6 +33,8 @@ paths:
/api/v1/instance/peers: /api/v1/instance/peers:
get: get:
tags:
- instance
security: security:
- { } - { }
responses: responses:
@ -34,6 +49,8 @@ paths:
/api/v1/instance/activity: /api/v1/instance/activity:
get: get:
tags:
- instance
security: security:
- { } - { }
responses: responses:
@ -48,6 +65,8 @@ paths:
/api/v1/instance/rules: /api/v1/instance/rules:
get: get:
tags:
- instance
security: security:
- { } - { }
responses: responses:
@ -62,6 +81,8 @@ paths:
/api/v1/instance/domain_blocks: /api/v1/instance/domain_blocks:
get: get:
tags:
- instance
security: security:
- { } - { }
responses: responses:
@ -76,6 +97,8 @@ paths:
/api/v1/instance/extended_description: /api/v1/instance/extended_description:
get: get:
tags:
- instance
security: security:
- { } - { }
responses: responses:
@ -88,6 +111,8 @@ paths:
/api/v1/instance: /api/v1/instance:
get: get:
tags:
- instance
security: security:
- { } - { }
responses: responses:
@ -100,6 +125,8 @@ paths:
/api/v1/statuses: /api/v1/statuses:
post: post:
tags:
- status
security: security:
- OAuth2: - OAuth2:
- "write:statuses" - "write:statuses"
@ -118,6 +145,28 @@ paths:
schema: schema:
$ref: "#/components/schemas/Status" $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: components:
schemas: schemas:
Account: Account:
@ -931,6 +980,39 @@ components:
hide_totals: hide_totals:
type: boolean 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: securitySchemes:
OAuth2: OAuth2:
type: oauth2 type: oauth2