fix: JWTトークンの有効期限を設定

This commit is contained in:
usbharu 2023-09-29 13:52:10 +09:00
parent d3a84b3157
commit c880e9c5d1
6 changed files with 46 additions and 17 deletions

View File

@ -6,12 +6,14 @@ 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 dev.usbharu.hideout.domain.model.UserDetailsImpl
import dev.usbharu.hideout.util.RsaUtil
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.HttpMethod
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
@ -33,7 +35,7 @@ import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey import java.security.interfaces.RSAPublicKey
import java.util.* import java.util.*
@EnableWebSecurity(debug = false) @EnableWebSecurity(debug = true)
@Configuration @Configuration
class SecurityConfig { class SecurityConfig {
@ -88,6 +90,7 @@ class SecurityConfig {
.formLogin(Customizer.withDefaults()) .formLogin(Customizer.withDefaults())
.csrf { .csrf {
it.ignoringRequestMatchers(builder.pattern("/users/*/inbox")) it.ignoringRequestMatchers(builder.pattern("/users/*/inbox"))
it.ignoringRequestMatchers(builder.pattern(HttpMethod.POST, "/api/v1/apps"))
it.ignoringRequestMatchers(builder.pattern("/inbox")) it.ignoringRequestMatchers(builder.pattern("/inbox"))
it.ignoringRequestMatchers(PathRequest.toH2Console()) it.ignoringRequestMatchers(PathRequest.toH2Console())
} }
@ -103,6 +106,7 @@ class SecurityConfig {
fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder() fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
@Bean @Bean
@ConditionalOnProperty(name = ["hideout.security.jwt.generate"], havingValue = "false", matchIfMissing = true)
fun genJwkSource(): JWKSource<SecurityContext> { fun genJwkSource(): JWKSource<SecurityContext> {
val keyPairGenerator = KeyPairGenerator.getInstance("RSA") val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
keyPairGenerator.initialize(2048) keyPairGenerator.initialize(2048)
@ -122,8 +126,8 @@ class SecurityConfig {
@Bean @Bean
@ConditionalOnProperty(name = ["hideout.security.jwt.generate"], havingValue = "") @ConditionalOnProperty(name = ["hideout.security.jwt.generate"], havingValue = "")
fun loadJwkSource(jwkConfig: JwkConfig): JWKSource<SecurityContext> { fun loadJwkSource(jwkConfig: JwkConfig): JWKSource<SecurityContext> {
val rsaKey = RSAKey.Builder(jwkConfig.publicKey) val rsaKey = RSAKey.Builder(RsaUtil.decodeRsaPublicKey(jwkConfig.publicKey))
.privateKey(jwkConfig.privateKey) .privateKey(RsaUtil.decodeRsaPrivateKey(jwkConfig.privateKey))
.keyID(jwkConfig.keyId) .keyID(jwkConfig.keyId)
.build() .build()
return ImmutableJWKSet(JWKSet(rsaKey)) return ImmutableJWKSet(JWKSet(rsaKey))
@ -157,6 +161,6 @@ class SecurityConfig {
@ConditionalOnProperty(name = ["hideout.security.jwt.generate"], havingValue = "") @ConditionalOnProperty(name = ["hideout.security.jwt.generate"], havingValue = "")
data class JwkConfig( data class JwkConfig(
val keyId: String, val keyId: String,
val publicKey: RSAPublicKey, val publicKey: String,
val privateKey: RSAPrivateKey val privateKey: String
) )

View File

@ -3,19 +3,24 @@ package dev.usbharu.hideout.controller.mastodon
import dev.usbharu.hideout.controller.mastodon.generated.StatusApi 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.model.UserDetailsImpl
import dev.usbharu.hideout.service.api.mastodon.StatusesApiService import dev.usbharu.hideout.service.api.mastodon.StatusesApiService
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
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.security.oauth2.jwt.Jwt
import org.springframework.stereotype.Controller import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.ModelAttribute
@Controller @Controller
class MastodonStatusesApiContoller(private val statusesApiService: StatusesApiService) : StatusApi { class MastodonStatusesApiContoller(private val statusesApiService: StatusesApiService) : StatusApi {
override fun apiV1StatusesPost(statusesRequest: StatusesRequest): ResponseEntity<Status> = runBlocking { override fun apiV1StatusesPost(@ModelAttribute statusesRequest: StatusesRequest): ResponseEntity<Status> =
val principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal() runBlocking {
require(principal is UserDetailsImpl) val jwt = SecurityContextHolder.getContext().authentication.principal as Jwt
ResponseEntity(statusesApiService.postStatus(statusesRequest, principal), HttpStatus.OK)
ResponseEntity(
statusesApiService.postStatus(statusesRequest, jwt.getClaim<String>("uid").toLong()),
HttpStatus.OK
)
} }
} }

View File

@ -10,7 +10,10 @@ import org.springframework.security.oauth2.core.ClientAuthenticationMethod
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient 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.client.RegisteredClientRepository
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.TokenSettings
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.time.Duration
import java.time.Instant
import java.util.* import java.util.*
@Service @Service
@ -40,6 +43,13 @@ class AppApiServiceImpl(
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri(appsRequest.redirectUris) .redirectUri(appsRequest.redirectUris)
.tokenSettings(
TokenSettings.builder()
.accessTokenTimeToLive(
Duration.ofSeconds((Instant.MAX.epochSecond - Instant.now().epochSecond - 10000) / 1000)
)
.build()
)
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()) .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.scopes { it.addAll(parseScope(appsRequest.scopes.orEmpty())) } .scopes { it.addAll(parseScope(appsRequest.scopes.orEmpty())) }
.build() .build()

View File

@ -2,12 +2,12 @@ package dev.usbharu.hideout.service.api.mastodon
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.model.UserDetailsImpl
import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto
import dev.usbharu.hideout.domain.model.hideout.entity.Visibility import dev.usbharu.hideout.domain.model.hideout.entity.Visibility
import dev.usbharu.hideout.exception.FailedToGetResourcesException import dev.usbharu.hideout.exception.FailedToGetResourcesException
import dev.usbharu.hideout.query.PostQueryService import dev.usbharu.hideout.query.PostQueryService
import dev.usbharu.hideout.query.UserQueryService import dev.usbharu.hideout.query.UserQueryService
import dev.usbharu.hideout.service.core.Transaction
import dev.usbharu.hideout.service.mastodon.AccountService import dev.usbharu.hideout.service.mastodon.AccountService
import dev.usbharu.hideout.service.post.PostService import dev.usbharu.hideout.service.post.PostService
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@ -15,7 +15,7 @@ import java.time.Instant
@Service @Service
interface StatusesApiService { interface StatusesApiService {
suspend fun postStatus(statusesRequest: StatusesRequest, user: UserDetailsImpl): Status suspend fun postStatus(statusesRequest: StatusesRequest, userId: Long): Status
} }
@Service @Service
@ -23,11 +23,12 @@ class StatsesApiServiceImpl(
private val postService: PostService, private val postService: PostService,
private val accountService: AccountService, private val accountService: AccountService,
private val postQueryService: PostQueryService, private val postQueryService: PostQueryService,
private val userQueryService: UserQueryService private val userQueryService: UserQueryService,
private val transaction: Transaction
) : ) :
StatusesApiService { StatusesApiService {
@Suppress("LongMethod") @Suppress("LongMethod")
override suspend fun postStatus(statusesRequest: StatusesRequest, user: UserDetailsImpl): Status { override suspend fun postStatus(statusesRequest: StatusesRequest, userId: Long): Status = transaction.transaction {
val visibility = when (statusesRequest.visibility) { val visibility = when (statusesRequest.visibility) {
StatusesRequest.Visibility.public -> Visibility.PUBLIC StatusesRequest.Visibility.public -> Visibility.PUBLIC
StatusesRequest.Visibility.unlisted -> Visibility.UNLISTED StatusesRequest.Visibility.unlisted -> Visibility.UNLISTED
@ -43,10 +44,10 @@ class StatsesApiServiceImpl(
visibility = visibility, visibility = visibility,
repostId = null, repostId = null,
repolyId = statusesRequest.inReplyToId?.toLongOrNull(), repolyId = statusesRequest.inReplyToId?.toLongOrNull(),
userId = user.id userId = userId
) )
) )
val account = accountService.findById(user.id) val account = accountService.findById(userId)
val postVisibility = when (statusesRequest.visibility) { val postVisibility = when (statusesRequest.visibility) {
StatusesRequest.Visibility.public -> Status.Visibility.public StatusesRequest.Visibility.public -> Status.Visibility.public
@ -66,7 +67,7 @@ class StatsesApiServiceImpl(
null null
} }
return Status( Status(
id = post.id.toString(), id = post.id.toString(),
uri = post.apId, uri = post.apId,
createdAt = Instant.ofEpochMilli(post.createdAt).toString(), createdAt = Instant.ofEpochMilli(post.createdAt).toString(),

View File

@ -7,6 +7,12 @@ hideout:
password: "" password: ""
job-queue: job-queue:
type: "nosql" type: "nosql"
security:
jwt:
generate: true
key-id: a
private-key: "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKjMzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvuNMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZqgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulgp2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlRZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwiVuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskVlaAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83HmQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwYdgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cwta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQDM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2TN0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPvt8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDUAhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISLDY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnKxt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEAmNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfzet6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhrVBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicDTQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cncdn/RsYEONbwQSjIfMPkvxF+8HQ=="
public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyehkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdgcKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbcmwIDAQAB"
spring: spring:
jackson: jackson:
serialization: serialization:

View File

@ -137,6 +137,9 @@ paths:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/StatusesRequest" $ref: "#/components/schemas/StatusesRequest"
application/x-www-form-urlencoded:
schema:
$ref: "#/components/schemas/StatusesRequest"
responses: responses:
200: 200:
description: 成功 description: 成功