mirror of https://github.com/usbharu/Hideout.git
feat: TwidereでOAuth2ログインができるように
This commit is contained in:
parent
834e40894b
commit
43a914331f
|
@ -15,7 +15,7 @@ plugins {
|
||||||
id("org.graalvm.buildtools.native") version "0.9.21"
|
id("org.graalvm.buildtools.native") version "0.9.21"
|
||||||
id("io.gitlab.arturbosch.detekt") version "1.23.1"
|
id("io.gitlab.arturbosch.detekt") version "1.23.1"
|
||||||
id("com.google.devtools.ksp") version "1.8.21-1.0.11"
|
id("com.google.devtools.ksp") version "1.8.21-1.0.11"
|
||||||
id("org.springframework.boot") version "3.1.2"
|
id("org.springframework.boot") version "3.1.3"
|
||||||
kotlin("plugin.spring") version "1.8.21"
|
kotlin("plugin.spring") version "1.8.21"
|
||||||
id("org.openapi.generator") version "7.0.1"
|
id("org.openapi.generator") version "7.0.1"
|
||||||
// id("org.jetbrains.kotlin.plugin.serialization") version "1.8.10"
|
// id("org.jetbrains.kotlin.plugin.serialization") version "1.8.10"
|
||||||
|
@ -153,6 +153,7 @@ dependencies {
|
||||||
implementation("io.insert-koin:koin-logger-slf4j:$koin_version")
|
implementation("io.insert-koin:koin-logger-slf4j:$koin_version")
|
||||||
implementation("io.insert-koin:koin-annotations:1.2.0")
|
implementation("io.insert-koin:koin-annotations:1.2.0")
|
||||||
implementation("io.ktor:ktor-server-compression-jvm:2.3.0")
|
implementation("io.ktor:ktor-server-compression-jvm:2.3.0")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||||
ksp("io.insert-koin:koin-ksp-compiler:1.2.0")
|
ksp("io.insert-koin:koin-ksp-compiler:1.2.0")
|
||||||
|
|
||||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||||
|
@ -174,6 +175,7 @@ dependencies {
|
||||||
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.datatype:jackson-datatype-jsr310")
|
||||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||||
|
implementation("org.springframework.security:spring-security-oauth2-jose")
|
||||||
|
|
||||||
implementation("io.ktor:ktor-client-logging-jvm:$ktor_version")
|
implementation("io.ktor:ktor-client-logging-jvm:$ktor_version")
|
||||||
implementation("io.ktor:ktor-server-host-common-jvm:$ktor_version")
|
implementation("io.ktor:ktor-server-host-common-jvm:$ktor_version")
|
||||||
|
|
|
@ -5,47 +5,57 @@ import com.nimbusds.jose.jwk.RSAKey
|
||||||
import com.nimbusds.jose.jwk.source.ImmutableJWKSet
|
import com.nimbusds.jose.jwk.source.ImmutableJWKSet
|
||||||
import com.nimbusds.jose.jwk.source.JWKSource
|
import com.nimbusds.jose.jwk.source.JWKSource
|
||||||
import com.nimbusds.jose.proc.SecurityContext
|
import com.nimbusds.jose.proc.SecurityContext
|
||||||
|
import dev.usbharu.hideout.domain.model.UserDetailsImpl
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
||||||
import org.springframework.boot.autoconfigure.security.servlet.PathRequest
|
import org.springframework.boot.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
|
||||||
|
import org.springframework.security.core.Authentication
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
import org.springframework.security.oauth2.jwt.JwtDecoder
|
import org.springframework.security.oauth2.jwt.JwtDecoder
|
||||||
|
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType
|
||||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration
|
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration
|
||||||
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings
|
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext
|
||||||
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer
|
||||||
import org.springframework.security.web.SecurityFilterChain
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint
|
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint
|
||||||
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher
|
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher
|
||||||
|
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher
|
||||||
import org.springframework.web.servlet.handler.HandlerMappingIntrospector
|
import org.springframework.web.servlet.handler.HandlerMappingIntrospector
|
||||||
import java.security.KeyPairGenerator
|
import java.security.KeyPairGenerator
|
||||||
import java.security.interfaces.RSAPrivateKey
|
import java.security.interfaces.RSAPrivateKey
|
||||||
import java.security.interfaces.RSAPublicKey
|
import java.security.interfaces.RSAPublicKey
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@EnableWebSecurity
|
|
||||||
|
@EnableWebSecurity(debug = true)
|
||||||
@Configuration
|
@Configuration
|
||||||
class SecurityConfig {
|
class SecurityConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Order(1)
|
@Order(1)
|
||||||
fun oauth2SecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
fun oauth2SecurityFilterChain(http: HttpSecurity, introspector: HandlerMappingIntrospector): SecurityFilterChain {
|
||||||
|
val builder = MvcRequestMatcher.Builder(introspector)
|
||||||
|
|
||||||
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
|
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
|
||||||
http
|
http
|
||||||
.exceptionHandling {
|
.exceptionHandling {
|
||||||
it.authenticationEntryPoint(LoginUrlAuthenticationEntryPoint("/login"))
|
it.defaultAuthenticationEntryPointFor(
|
||||||
|
LoginUrlAuthenticationEntryPoint("/login"),
|
||||||
|
MediaTypeRequestMatcher(MediaType.TEXT_HTML)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.oauth2ResourceServer {
|
.oauth2ResourceServer {
|
||||||
it.jwt(Customizer.withDefaults())
|
it.jwt(Customizer.withDefaults())
|
||||||
}
|
}
|
||||||
.csrf {
|
|
||||||
it.disable()
|
|
||||||
}
|
|
||||||
return http.build()
|
return http.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +64,9 @@ class SecurityConfig {
|
||||||
fun defaultSecurityFilterChain(http: HttpSecurity, introspector: HandlerMappingIntrospector): SecurityFilterChain {
|
fun defaultSecurityFilterChain(http: HttpSecurity, introspector: HandlerMappingIntrospector): SecurityFilterChain {
|
||||||
val builder = MvcRequestMatcher.Builder(introspector)
|
val builder = MvcRequestMatcher.Builder(introspector)
|
||||||
|
|
||||||
|
http.authorizeHttpRequests {
|
||||||
|
it.requestMatchers(builder.pattern("/api/v1/**")).hasAnyAuthority("SCOPE_read", "SCOPE_read:accounts")
|
||||||
|
}
|
||||||
http
|
http
|
||||||
.authorizeHttpRequests {
|
.authorizeHttpRequests {
|
||||||
it.requestMatchers(
|
it.requestMatchers(
|
||||||
|
@ -63,12 +75,18 @@ class SecurityConfig {
|
||||||
builder.pattern("/api/v1/instance/**")
|
builder.pattern("/api/v1/instance/**")
|
||||||
).permitAll()
|
).permitAll()
|
||||||
}
|
}
|
||||||
|
http
|
||||||
.authorizeHttpRequests {
|
.authorizeHttpRequests {
|
||||||
it.requestMatchers(PathRequest.toH2Console()).permitAll()
|
it.requestMatchers(PathRequest.toH2Console()).permitAll()
|
||||||
}
|
}
|
||||||
|
http
|
||||||
.authorizeHttpRequests {
|
.authorizeHttpRequests {
|
||||||
it.anyRequest().authenticated()
|
it.anyRequest().authenticated()
|
||||||
}
|
}
|
||||||
|
http
|
||||||
|
.oauth2ResourceServer {
|
||||||
|
it.jwt(Customizer.withDefaults())
|
||||||
|
}
|
||||||
.formLogin(Customizer.withDefaults())
|
.formLogin(Customizer.withDefaults())
|
||||||
.csrf {
|
.csrf {
|
||||||
it.ignoringRequestMatchers(builder.pattern("/api/**"))
|
it.ignoringRequestMatchers(builder.pattern("/api/**"))
|
||||||
|
@ -127,6 +145,18 @@ class SecurityConfig {
|
||||||
.tokenRevocationEndpoint("/oauth/revoke")
|
.tokenRevocationEndpoint("/oauth/revoke")
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun jwtTokenCustomizer(): OAuth2TokenCustomizer<JwtEncodingContext> {
|
||||||
|
return OAuth2TokenCustomizer { context: JwtEncodingContext ->
|
||||||
|
if (OAuth2TokenType.ACCESS_TOKEN == context.tokenType) {
|
||||||
|
val userDetailsImpl = context.getPrincipal<Authentication>().principal as UserDetailsImpl
|
||||||
|
context.claims.claim("uid", userDetailsImpl.id.toString())
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ConfigurationProperties("hideout.security.jwt")
|
@ConfigurationProperties("hideout.security.jwt")
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
package dev.usbharu.hideout.controller.mastodon
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.controller.mastodon.generated.AccountApi
|
||||||
|
import dev.usbharu.hideout.domain.mastodon.model.generated.CredentialAccount
|
||||||
|
import dev.usbharu.hideout.service.api.mastodon.AccountApiService
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
|
import org.springframework.security.oauth2.jwt.Jwt
|
||||||
|
import org.springframework.stereotype.Controller
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
class MastodonAccountApiController(private val accountApiService: AccountApiService) : AccountApi {
|
||||||
|
override suspend fun apiV1AccountsVerifyCredentialsGet(): ResponseEntity<CredentialAccount> {
|
||||||
|
val principal = SecurityContextHolder.getContext().getAuthentication().principal as Jwt
|
||||||
|
|
||||||
|
return ResponseEntity(
|
||||||
|
accountApiService.verifyCredentials(principal.getClaim<String>("uid").toLong()),
|
||||||
|
HttpStatus.OK
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,18 @@
|
||||||
package dev.usbharu.hideout.domain.model
|
package dev.usbharu.hideout.domain.model
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonAutoDetect
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
import com.fasterxml.jackson.annotation.JsonSubTypes
|
||||||
|
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
||||||
|
import com.fasterxml.jackson.core.JsonParser
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationContext
|
||||||
|
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||||
import org.springframework.security.core.GrantedAuthority
|
import org.springframework.security.core.GrantedAuthority
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||||
import org.springframework.security.core.userdetails.User
|
import org.springframework.security.core.userdetails.User
|
||||||
import java.io.Serial
|
import java.io.Serial
|
||||||
|
|
||||||
|
@ -18,4 +30,54 @@ class UserDetailsImpl(
|
||||||
@Serial
|
@Serial
|
||||||
private const val serialVersionUID: Long = -899168205656607781L
|
private const val serialVersionUID: Long = -899168205656607781L
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
|
||||||
|
@JsonDeserialize(using = UserDetailsDeserializer::class)
|
||||||
|
@JsonAutoDetect(
|
||||||
|
fieldVisibility = JsonAutoDetect.Visibility.ANY,
|
||||||
|
getterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||||
|
isGetterVisibility = JsonAutoDetect.Visibility.NONE,
|
||||||
|
creatorVisibility = JsonAutoDetect.Visibility.NONE
|
||||||
|
)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
@JsonSubTypes
|
||||||
|
abstract class UserDetailsMixin
|
||||||
|
|
||||||
|
|
||||||
|
class UserDetailsDeserializer : JsonDeserializer<UserDetailsImpl>() {
|
||||||
|
val SIMPLE_GRANTED_AUTHORITY_SET = object : TypeReference<Set<SimpleGrantedAuthority>>() {}
|
||||||
|
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): UserDetailsImpl {
|
||||||
|
|
||||||
|
val mapper = p.codec as ObjectMapper
|
||||||
|
val jsonNode: JsonNode = mapper.readTree(p)
|
||||||
|
println(jsonNode)
|
||||||
|
val authorities: Set<GrantedAuthority> = mapper.convertValue(
|
||||||
|
jsonNode["authorities"],
|
||||||
|
SIMPLE_GRANTED_AUTHORITY_SET
|
||||||
|
)
|
||||||
|
|
||||||
|
val password = jsonNode.readText("password")
|
||||||
|
return UserDetailsImpl(
|
||||||
|
jsonNode["id"].longValue(),
|
||||||
|
jsonNode.readText("username"),
|
||||||
|
password,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
authorities.toMutableList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun JsonNode.readText(field: String, defaultValue: String = ""): String {
|
||||||
|
return when {
|
||||||
|
has(field) -> get(field).asText(defaultValue)
|
||||||
|
else -> defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
package dev.usbharu.hideout.service.api.mastodon
|
||||||
|
|
||||||
|
import dev.usbharu.hideout.domain.mastodon.model.generated.Account
|
||||||
|
import dev.usbharu.hideout.domain.mastodon.model.generated.CredentialAccount
|
||||||
|
import dev.usbharu.hideout.domain.mastodon.model.generated.CredentialAccountSource
|
||||||
|
import dev.usbharu.hideout.domain.mastodon.model.generated.Role
|
||||||
|
import dev.usbharu.hideout.service.core.Transaction
|
||||||
|
import dev.usbharu.hideout.service.mastodon.AccountService
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
interface AccountApiService {
|
||||||
|
suspend fun verifyCredentials(userid: Long): CredentialAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class AccountApiServiceImpl(private val accountService: AccountService, private val transaction: Transaction) :
|
||||||
|
AccountApiService {
|
||||||
|
override suspend fun verifyCredentials(userid: Long): CredentialAccount = transaction.transaction {
|
||||||
|
val account = accountService.findById(userid)
|
||||||
|
of(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun of(account: Account): CredentialAccount {
|
||||||
|
return CredentialAccount(
|
||||||
|
id = account.id,
|
||||||
|
username = account.username,
|
||||||
|
acct = account.acct,
|
||||||
|
url = account.url,
|
||||||
|
displayName = account.displayName,
|
||||||
|
note = account.note,
|
||||||
|
avatar = account.avatar,
|
||||||
|
avatarStatic = account.avatarStatic,
|
||||||
|
header = account.header,
|
||||||
|
headerStatic = account.headerStatic,
|
||||||
|
locked = account.locked,
|
||||||
|
fields = account.fields,
|
||||||
|
emojis = account.emojis,
|
||||||
|
bot = account.bot,
|
||||||
|
group = account.group,
|
||||||
|
discoverable = account.discoverable,
|
||||||
|
createdAt = account.createdAt,
|
||||||
|
lastStatusAt = account.lastStatusAt,
|
||||||
|
statusesCount = account.statusesCount,
|
||||||
|
followersCount = account.followersCount,
|
||||||
|
noindex = account.noindex,
|
||||||
|
moved = account.moved,
|
||||||
|
suspendex = account.suspendex,
|
||||||
|
limited = account.limited,
|
||||||
|
followingCount = account.followingCount,
|
||||||
|
source = CredentialAccountSource(
|
||||||
|
account.note,
|
||||||
|
account.fields,
|
||||||
|
CredentialAccountSource.Privacy.public,
|
||||||
|
false,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
role = Role(0, "Admin", "", 32)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -3,12 +3,15 @@ package dev.usbharu.hideout.service.auth
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
|
||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
|
import dev.usbharu.hideout.domain.model.UserDetailsImpl
|
||||||
|
import dev.usbharu.hideout.domain.model.UserDetailsMixin
|
||||||
import dev.usbharu.hideout.service.core.Transaction
|
import dev.usbharu.hideout.service.core.Transaction
|
||||||
import kotlinx.coroutines.runBlocking
|
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.jetbrains.exposed.sql.transactions.transaction
|
||||||
|
import org.springframework.security.jackson2.CoreJackson2Module
|
||||||
import org.springframework.security.jackson2.SecurityJackson2Modules
|
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
|
||||||
|
@ -53,7 +56,7 @@ class ExposedOAuth2AuthorizationService(
|
||||||
it[registeredClientId] = authorization.registeredClientId
|
it[registeredClientId] = authorization.registeredClientId
|
||||||
it[principalName] = authorization.principalName
|
it[principalName] = authorization.principalName
|
||||||
it[authorizationGrantType] = authorization.authorizationGrantType.value
|
it[authorizationGrantType] = authorization.authorizationGrantType.value
|
||||||
it[authorizedScopes] = authorization.authorizedScopes.joinToString(",").takeIf { it.isEmpty() }
|
it[authorizedScopes] = authorization.authorizedScopes.joinToString(",").takeIf { it.isNotEmpty() }
|
||||||
it[attributes] = mapToJson(authorization.attributes)
|
it[attributes] = mapToJson(authorization.attributes)
|
||||||
it[state] = authorization.getAttribute(OAuth2ParameterNames.STATE)
|
it[state] = authorization.getAttribute(OAuth2ParameterNames.STATE)
|
||||||
it[authorizationCodeValue] = authorizationCodeToken?.token?.tokenValue
|
it[authorizationCodeValue] = authorizationCodeToken?.token?.tokenValue
|
||||||
|
@ -66,7 +69,8 @@ class ExposedOAuth2AuthorizationService(
|
||||||
it[accessTokenExpiresAt] = accessToken?.token?.expiresAt
|
it[accessTokenExpiresAt] = accessToken?.token?.expiresAt
|
||||||
it[accessTokenMetadata] = accessToken?.metadata?.let { it1 -> mapToJson(it1) }
|
it[accessTokenMetadata] = accessToken?.metadata?.let { it1 -> mapToJson(it1) }
|
||||||
it[accessTokenType] = accessToken?.token?.tokenType?.value
|
it[accessTokenType] = accessToken?.token?.tokenType?.value
|
||||||
it[accessTokenScopes] = accessToken?.run { token.scopes.joinToString(",").takeIf { it.isEmpty() } }
|
it[accessTokenScopes] =
|
||||||
|
accessToken?.run { token.scopes.joinToString(",").takeIf { it.isNotEmpty() } }
|
||||||
it[refreshTokenValue] = refreshToken?.token?.tokenValue
|
it[refreshTokenValue] = refreshToken?.token?.tokenValue
|
||||||
it[refreshTokenIssuedAt] = refreshToken?.token?.issuedAt
|
it[refreshTokenIssuedAt] = refreshToken?.token?.issuedAt
|
||||||
it[refreshTokenExpiresAt] = refreshToken?.token?.expiresAt
|
it[refreshTokenExpiresAt] = refreshToken?.token?.expiresAt
|
||||||
|
@ -95,7 +99,7 @@ class ExposedOAuth2AuthorizationService(
|
||||||
it[registeredClientId] = authorization.registeredClientId
|
it[registeredClientId] = authorization.registeredClientId
|
||||||
it[principalName] = authorization.principalName
|
it[principalName] = authorization.principalName
|
||||||
it[authorizationGrantType] = authorization.authorizationGrantType.value
|
it[authorizationGrantType] = authorization.authorizationGrantType.value
|
||||||
it[authorizedScopes] = authorization.authorizedScopes.joinToString(",").takeIf { it.isEmpty() }
|
it[authorizedScopes] = authorization.authorizedScopes.joinToString(",").takeIf { it.isNotEmpty() }
|
||||||
it[attributes] = mapToJson(authorization.attributes)
|
it[attributes] = mapToJson(authorization.attributes)
|
||||||
it[state] = authorization.getAttribute(OAuth2ParameterNames.STATE)
|
it[state] = authorization.getAttribute(OAuth2ParameterNames.STATE)
|
||||||
it[authorizationCodeValue] = authorizationCodeToken?.token?.tokenValue
|
it[authorizationCodeValue] = authorizationCodeToken?.token?.tokenValue
|
||||||
|
@ -108,7 +112,7 @@ class ExposedOAuth2AuthorizationService(
|
||||||
it[accessTokenExpiresAt] = accessToken?.token?.expiresAt
|
it[accessTokenExpiresAt] = accessToken?.token?.expiresAt
|
||||||
it[accessTokenMetadata] = accessToken?.metadata?.let { it1 -> mapToJson(it1) }
|
it[accessTokenMetadata] = accessToken?.metadata?.let { it1 -> mapToJson(it1) }
|
||||||
it[accessTokenType] = accessToken?.token?.tokenType?.value
|
it[accessTokenType] = accessToken?.token?.tokenType?.value
|
||||||
it[accessTokenScopes] = accessToken?.token?.scopes?.joinToString(",")?.takeIf { it.isEmpty() }
|
it[accessTokenScopes] = accessToken?.token?.scopes?.joinToString(",")?.takeIf { it.isNotEmpty() }
|
||||||
it[refreshTokenValue] = refreshToken?.token?.tokenValue
|
it[refreshTokenValue] = refreshToken?.token?.tokenValue
|
||||||
it[refreshTokenIssuedAt] = refreshToken?.token?.issuedAt
|
it[refreshTokenIssuedAt] = refreshToken?.token?.issuedAt
|
||||||
it[refreshTokenExpiresAt] = refreshToken?.token?.expiresAt
|
it[refreshTokenExpiresAt] = refreshToken?.token?.expiresAt
|
||||||
|
@ -331,6 +335,8 @@ class ExposedOAuth2AuthorizationService(
|
||||||
this.objectMapper.registerModules(JavaTimeModule())
|
this.objectMapper.registerModules(JavaTimeModule())
|
||||||
this.objectMapper.registerModules(modules)
|
this.objectMapper.registerModules(modules)
|
||||||
this.objectMapper.registerModules(OAuth2AuthorizationServerJackson2Module())
|
this.objectMapper.registerModules(OAuth2AuthorizationServerJackson2Module())
|
||||||
|
this.objectMapper.registerModules(CoreJackson2Module())
|
||||||
|
this.objectMapper.addMixIn(UserDetailsImpl::class.java, UserDetailsMixin::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package dev.usbharu.hideout.service.auth
|
package dev.usbharu.hideout.service.auth
|
||||||
|
|
||||||
import dev.usbharu.hideout.config.ApplicationConfig
|
import dev.usbharu.hideout.config.ApplicationConfig
|
||||||
|
import dev.usbharu.hideout.domain.model.UserDetailsImpl
|
||||||
import dev.usbharu.hideout.query.UserQueryService
|
import dev.usbharu.hideout.query.UserQueryService
|
||||||
import dev.usbharu.hideout.service.core.Transaction
|
import dev.usbharu.hideout.service.core.Transaction
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.springframework.security.core.userdetails.User
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails
|
import org.springframework.security.core.userdetails.UserDetails
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService
|
import org.springframework.security.core.userdetails.UserDetailsService
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
||||||
|
@ -24,10 +24,15 @@ class UserDetailsServiceImpl(
|
||||||
}
|
}
|
||||||
transaction.transaction {
|
transaction.transaction {
|
||||||
val findById = userQueryService.findByNameAndDomain(username, URL(applicationConfig.url).host)
|
val findById = userQueryService.findByNameAndDomain(username, URL(applicationConfig.url).host)
|
||||||
User(
|
UserDetailsImpl(
|
||||||
|
findById.id,
|
||||||
findById.name,
|
findById.name,
|
||||||
findById.password,
|
findById.password,
|
||||||
emptyList()
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
mutableListOf()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,4 +12,5 @@
|
||||||
<logger name="kjob.core.internal.scheduler.JobServiceImpl" level="INFO"/>
|
<logger name="kjob.core.internal.scheduler.JobServiceImpl" level="INFO"/>
|
||||||
<logger name="Exposed" level="INFO"/>
|
<logger name="Exposed" level="INFO"/>
|
||||||
<logger name="io.ktor.server.plugins.contentnegotiation" level="INFO"/>
|
<logger name="io.ktor.server.plugins.contentnegotiation" level="INFO"/>
|
||||||
|
<logger name="org.springframework.security" level="trace"/>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|
|
@ -167,6 +167,21 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Application"
|
$ref: "#/components/schemas/Application"
|
||||||
|
|
||||||
|
/api/v1/accounts/verify_credentials:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- account
|
||||||
|
security:
|
||||||
|
- OAuth2:
|
||||||
|
- "read:accounts"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: 成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/CredentialAccount"
|
||||||
|
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
Account:
|
Account:
|
||||||
|
@ -251,6 +266,128 @@ components:
|
||||||
- followers_count
|
- followers_count
|
||||||
- followers_count
|
- followers_count
|
||||||
|
|
||||||
|
CredentialAccount:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
acct:
|
||||||
|
type: string
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
display_name:
|
||||||
|
type: string
|
||||||
|
note:
|
||||||
|
type: string
|
||||||
|
avatar:
|
||||||
|
type: string
|
||||||
|
avatar_static:
|
||||||
|
type: string
|
||||||
|
header:
|
||||||
|
type: string
|
||||||
|
header_static:
|
||||||
|
type: string
|
||||||
|
locked:
|
||||||
|
type: boolean
|
||||||
|
fields:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/Field"
|
||||||
|
emojis:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/CustomEmoji"
|
||||||
|
bot:
|
||||||
|
type: boolean
|
||||||
|
group:
|
||||||
|
type: boolean
|
||||||
|
discoverable:
|
||||||
|
type: boolean
|
||||||
|
nullable: true
|
||||||
|
noindex:
|
||||||
|
type: boolean
|
||||||
|
moved:
|
||||||
|
type: boolean
|
||||||
|
suspendex:
|
||||||
|
type: boolean
|
||||||
|
limited:
|
||||||
|
type: boolean
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
last_status_at:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
statuses_count:
|
||||||
|
type: integer
|
||||||
|
followers_count:
|
||||||
|
type: integer
|
||||||
|
following_count:
|
||||||
|
type: integer
|
||||||
|
source:
|
||||||
|
$ref: "#/components/schemas/CredentialAccountSource"
|
||||||
|
role:
|
||||||
|
$ref: "#/components/schemas/Role"
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- username
|
||||||
|
- acct
|
||||||
|
- url
|
||||||
|
- display_name
|
||||||
|
- note
|
||||||
|
- avatar
|
||||||
|
- avatar_static
|
||||||
|
- header
|
||||||
|
- header_static
|
||||||
|
- locked
|
||||||
|
- fields
|
||||||
|
- emojis
|
||||||
|
- bot
|
||||||
|
- group
|
||||||
|
- discoverable
|
||||||
|
- created_at
|
||||||
|
- last_status_at
|
||||||
|
- statuses_count
|
||||||
|
- followers_count
|
||||||
|
- followers_count
|
||||||
|
- source
|
||||||
|
|
||||||
|
CredentialAccountSource:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
note:
|
||||||
|
type: string
|
||||||
|
fields:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/Field"
|
||||||
|
privacy:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- public
|
||||||
|
- unlisted
|
||||||
|
- private
|
||||||
|
- direct
|
||||||
|
sensitive:
|
||||||
|
type: boolean
|
||||||
|
follow_requests_count:
|
||||||
|
type: integer
|
||||||
|
|
||||||
|
Role:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
color:
|
||||||
|
type: string
|
||||||
|
permissions:
|
||||||
|
type: integer
|
||||||
|
highlighted:
|
||||||
|
type: boolean
|
||||||
|
|
||||||
Field:
|
Field:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
Loading…
Reference in New Issue