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/
/src/main/web/generated/
/stats.html
/tomcat/

View File

@ -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")

View File

@ -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()
}

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
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<V1Instance> {
return ResponseEntity(instanceApiService.v1Instance(), HttpStatus.OK)
}
class MastodonStatusesApiContoller(private val statusesApiService: StatusesApiService) : StatusApi {
override suspend fun apiV1StatusesPost(statusesRequest: StatusesRequest): ResponseEntity<Status> {
val principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal()
require(principal is UserDetailsImpl)

View File

@ -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,32 +89,32 @@ 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
}
}
// org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql
object RegisteredClient : Table("registered_client") {
val id = varchar("id", 100)
val clientId = varchar("client_id", 100)
val clientIdIssuedAt = timestamp("client_id_issued_at").defaultExpression(CurrentTimestamp())
val clientSecret = varchar("client_secret", 200).nullable().default(null)
val clientSecretExpiresAt = timestamp("client_secret_expires_at").nullable().default(null)
val clientName = varchar("client_name", 200)
val clientAuthenticationMethods = varchar("client_authentication_methods", 1000)
val authorizationGrantTypes = varchar("authorization_grant_types", 1000)
val redirectUris = varchar("redirect_uris", 1000).nullable().default(null)
val postLogoutRedirectUris = varchar("post_logout_redirect_uris", 1000).nullable().default(null)
val scopes = varchar("scopes", 1000)
val clientSettings = varchar("client_settings", 2000)
val tokenSettings = varchar("token_settings", 2000)
private fun mapToJson(map: Map<*, *>): String = objectMapper.writeValueAsString(map)
override val primaryKey = PrimaryKey(id)
}
private fun <T, U> jsonToMap(json: String): Map<T, U> = objectMapper.readValue(json)
fun ResultRow.toRegisteredClient(): SpringRegisteredClient {
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
@ -156,9 +164,9 @@ fun ResultRow.toRegisteredClient(): SpringRegisteredClient {
.redirectUris { it.addAll(redirectUris) }
.postLogoutRedirectUris { it.addAll(postLogoutRedirectUris) }
.scopes { it.addAll(clientScopes) }
.clientSettings(ClientSettings.withSettings(JsonUtil.jsonToMap(this[clientSettings])).build())
.clientSettings(ClientSettings.withSettings(jsonToMap(this[clientSettings])).build())
val tokenSettingsMap = JsonUtil.jsonToMap<String, Any>(this[tokenSettings])
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)
@ -166,4 +174,25 @@ fun ResultRow.toRegisteredClient(): SpringRegisteredClient {
builder.tokenSettings(withSettings.build())
return builder.build()
}
}
// org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql
object RegisteredClient : Table("registered_client") {
val id = varchar("id", 100)
val clientId = varchar("client_id", 100)
val clientIdIssuedAt = timestamp("client_id_issued_at").defaultExpression(CurrentTimestamp())
val clientSecret = varchar("client_secret", 200).nullable().default(null)
val clientSecretExpiresAt = timestamp("client_secret_expires_at").nullable().default(null)
val clientName = varchar("client_name", 200)
val clientAuthenticationMethods = varchar("client_authentication_methods", 1000)
val authorizationGrantTypes = varchar("authorization_grant_types", 1000)
val redirectUris = varchar("redirect_uris", 1000).nullable().default(null)
val postLogoutRedirectUris = varchar("post_logout_redirect_uris", 1000).nullable().default(null)
val scopes = varchar("scopes", 1000)
val clientSettings = varchar("client_settings", 2000)
val tokenSettings = varchar("token_settings", 2000)
override val primaryKey = PrimaryKey(id)
}

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
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,10 +12,25 @@ 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)
transaction.transaction {
val singleOrNull =
OAuth2AuthorizationConsent.select {
OAuth2AuthorizationConsent.registeredClientId
@ -28,6 +46,7 @@ class ExposedOAuth2AuthorizationConsentService(private val registeredClientRepos
}
}
}
}
override fun remove(authorizationConsent: AuthorizationConsent?) {
if (authorizationConsent == null) {
@ -38,16 +57,18 @@ 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.select {
(OAuth2AuthorizationConsent.registeredClientId eq registeredClientId)
.and(OAuth2AuthorizationConsent.principalName eq principalName)
}
.singleOrNull()?.toAuthorizationConsent()
}
}
fun ResultRow.toAuthorizationConsent(): AuthorizationConsent {
val registeredClientId = this[OAuth2AuthorizationConsent.registeredClientId]

View File

@ -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,13 +19,27 @@ 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)
transaction.transaction {
val singleOrNull = Authorization.select { Authorization.id eq authorization.id }.singleOrNull()
if (singleOrNull == null) {
val authorizationCodeToken = authorization.getToken(OAuth2AuthorizationCode::class.java)
@ -34,34 +54,35 @@ class ExposedOAuth2AuthorizationService(private val registeredClientRepository:
it[principalName] = authorization.principalName
it[authorizationGrantType] = authorization.authorizationGrantType.value
it[authorizedScopes] = authorization.authorizedScopes.joinToString(",").takeIf { it.isEmpty() }
it[attributes] = JsonUtil.mapToJson(authorization.attributes)
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 -> JsonUtil.mapToJson(it1) }
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 -> JsonUtil.mapToJson(it1) }
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 -> JsonUtil.mapToJson(it1) }
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 -> JsonUtil.mapToJson(it1) }
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 -> JsonUtil.mapToJson(it1) }
it[userCodeMetadata] = userCode?.metadata?.let { it1 -> mapToJson(it1) }
it[deviceCodeValue] = deviceCode?.token?.tokenValue
it[deviceCodeIssuedAt] = deviceCode?.token?.issuedAt
it[deviceCodeExpiresAt] = deviceCode?.token?.expiresAt
it[deviceCodeMetadata] = deviceCode?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) }
it[deviceCodeMetadata] = deviceCode?.metadata?.let { it1 -> mapToJson(it1) }
}
} else {
val authorizationCodeToken = authorization.getToken(OAuth2AuthorizationCode::class.java)
@ -75,34 +96,36 @@ class ExposedOAuth2AuthorizationService(private val registeredClientRepository:
it[principalName] = authorization.principalName
it[authorizationGrantType] = authorization.authorizationGrantType.value
it[authorizedScopes] = authorization.authorizedScopes.joinToString(",").takeIf { it.isEmpty() }
it[attributes] = JsonUtil.mapToJson(authorization.attributes)
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 -> JsonUtil.mapToJson(it1) }
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 -> JsonUtil.mapToJson(it1) }
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 -> JsonUtil.mapToJson(it1) }
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 -> JsonUtil.mapToJson(it1) }
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 -> JsonUtil.mapToJson(it1) }
it[userCodeMetadata] = userCode?.metadata?.let { it1 -> mapToJson(it1) }
it[deviceCodeValue] = deviceCode?.token?.tokenValue
it[deviceCodeIssuedAt] = deviceCode?.token?.issuedAt
it[deviceCodeExpiresAt] = deviceCode?.token?.expiresAt
it[deviceCodeMetadata] = deviceCode?.metadata?.let { it1 -> JsonUtil.mapToJson(it1) }
it[deviceCodeMetadata] = deviceCode?.metadata?.let { it1 -> mapToJson(it1) }
}
}
}
}
@ -121,9 +144,12 @@ 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) {
transaction.transaction {
when (tokenType?.value) {
null -> {
Authorization.select {
Authorization.authorizationCodeValue eq token
@ -173,6 +199,7 @@ class ExposedOAuth2AuthorizationService(private val registeredClientRepository:
}
}?.singleOrNull()?.toAuthorization()
}
}
fun ResultRow.toAuthorization(): OAuth2Authorization {
val registeredClientId = this[Authorization.registeredClientId]
@ -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<String, Any>(it) }.orEmpty()
val attributes = this[Authorization.attributes]?.let { jsonToMap<String, Any>(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<String, Any>(
jsonToMap<String, Any>(
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<String, Any>(it) }.orEmpty()
this[Authorization.accessTokenMetadata]?.let { jsonToMap<String, Any>(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<String, Any>(it) }.orEmpty()
this[Authorization.oidcIdTokenMetadata]?.let { jsonToMap<String, Any>(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<String, Any>(it) }.orEmpty()
this[Authorization.refreshTokenMetadata]?.let { jsonToMap<String, Any>(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<String, Any>(it) }.orEmpty()
this[Authorization.userCodeMetadata]?.let { jsonToMap<String, Any>(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<String, Any>(it) }.orEmpty()
this[Authorization.deviceCodeMetadata]?.let { jsonToMap<String, Any>(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 <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 registeredClientId = varchar("registered_client_id", 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
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,

View File

@ -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 {

View File

@ -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)

View File

@ -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

View File

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

View File

@ -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