mirror of https://github.com/usbharu/Hideout.git
260 lines
12 KiB
Kotlin
260 lines
12 KiB
Kotlin
package dev.usbharu.hideout.application.config
|
|
|
|
import com.fasterxml.jackson.annotation.JsonInclude
|
|
import com.nimbusds.jose.jwk.JWKSet
|
|
import com.nimbusds.jose.jwk.RSAKey
|
|
import com.nimbusds.jose.jwk.source.ImmutableJWKSet
|
|
import com.nimbusds.jose.jwk.source.JWKSource
|
|
import com.nimbusds.jose.proc.SecurityContext
|
|
import dev.usbharu.hideout.application.external.Transaction
|
|
import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureFilter
|
|
import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureUserDetailsService
|
|
import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureVerifierComposite
|
|
import dev.usbharu.hideout.core.infrastructure.springframework.oauth2.UserDetailsImpl
|
|
import dev.usbharu.hideout.core.query.UserQueryService
|
|
import dev.usbharu.hideout.util.RsaUtil
|
|
import dev.usbharu.httpsignature.sign.RsaSha256HttpSignatureSigner
|
|
import dev.usbharu.httpsignature.verify.DefaultSignatureHeaderParser
|
|
import dev.usbharu.httpsignature.verify.RsaSha256HttpSignatureVerifier
|
|
import org.springframework.beans.factory.annotation.Autowired
|
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
|
|
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer
|
|
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.context.annotation.Primary
|
|
import org.springframework.core.annotation.Order
|
|
import org.springframework.http.HttpMethod
|
|
import org.springframework.http.HttpStatus
|
|
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder
|
|
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
|
|
import org.springframework.security.authentication.AccountStatusUserDetailsChecker
|
|
import org.springframework.security.authentication.AuthenticationManager
|
|
import org.springframework.security.config.Customizer
|
|
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
|
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
|
import org.springframework.security.config.http.SessionCreationPolicy
|
|
import org.springframework.security.core.Authentication
|
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
|
import org.springframework.security.crypto.password.PasswordEncoder
|
|
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.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.access.ExceptionTranslationFilter
|
|
import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler
|
|
import org.springframework.security.web.authentication.HttpStatusEntryPoint
|
|
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint
|
|
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider
|
|
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher
|
|
import org.springframework.security.web.util.matcher.AnyRequestMatcher
|
|
import org.springframework.web.servlet.handler.HandlerMappingIntrospector
|
|
import java.security.KeyPairGenerator
|
|
import java.security.interfaces.RSAPrivateKey
|
|
import java.security.interfaces.RSAPublicKey
|
|
import java.util.*
|
|
|
|
@EnableWebSecurity(debug = true)
|
|
@Configuration
|
|
@Suppress("FunctionMaxLength", "TooManyFunctions")
|
|
class SecurityConfig {
|
|
|
|
@Autowired
|
|
private lateinit var userQueryService: UserQueryService
|
|
|
|
@Bean
|
|
fun authenticationManager(authenticationConfiguration: AuthenticationConfiguration): AuthenticationManager? =
|
|
authenticationConfiguration.authenticationManager
|
|
|
|
@Bean
|
|
@Order(1)
|
|
fun httpSignatureFilterChain(http: HttpSecurity, httpSignatureFilter: HttpSignatureFilter): SecurityFilterChain {
|
|
http
|
|
.securityMatcher("/inbox", "/outbox", "/users/*/inbox", "/users/*/outbox", "/users/*/posts/*")
|
|
.addFilter(httpSignatureFilter)
|
|
.addFilterBefore(
|
|
ExceptionTranslationFilter(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)),
|
|
HttpSignatureFilter::class.java
|
|
)
|
|
.authorizeHttpRequests {
|
|
it.requestMatchers("/inbox", "/outbox", "/users/*/inbox", "/users/*/outbox").authenticated()
|
|
it.anyRequest().permitAll()
|
|
}
|
|
.csrf {
|
|
it.disable()
|
|
}
|
|
.exceptionHandling {
|
|
it.authenticationEntryPoint(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
|
|
it.defaultAuthenticationEntryPointFor(
|
|
HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
|
|
AnyRequestMatcher.INSTANCE
|
|
)
|
|
}
|
|
.sessionManagement {
|
|
it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
|
}
|
|
|
|
return http.build()
|
|
}
|
|
|
|
@Bean
|
|
fun getHttpSignatureFilter(authenticationManager: AuthenticationManager): HttpSignatureFilter {
|
|
val httpSignatureFilter = HttpSignatureFilter(DefaultSignatureHeaderParser())
|
|
httpSignatureFilter.setAuthenticationManager(authenticationManager)
|
|
httpSignatureFilter.setContinueFilterChainOnUnsuccessfulAuthentication(false)
|
|
val authenticationEntryPointFailureHandler =
|
|
AuthenticationEntryPointFailureHandler(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
|
|
authenticationEntryPointFailureHandler.setRethrowAuthenticationServiceException(false)
|
|
httpSignatureFilter.setAuthenticationFailureHandler(authenticationEntryPointFailureHandler)
|
|
return httpSignatureFilter
|
|
}
|
|
|
|
@Bean
|
|
fun httpSignatureAuthenticationProvider(transaction: Transaction): PreAuthenticatedAuthenticationProvider {
|
|
val provider = PreAuthenticatedAuthenticationProvider()
|
|
provider.setPreAuthenticatedUserDetailsService(
|
|
HttpSignatureUserDetailsService(
|
|
userQueryService,
|
|
HttpSignatureVerifierComposite(
|
|
mapOf(
|
|
"rsa-sha256" to RsaSha256HttpSignatureVerifier(
|
|
DefaultSignatureHeaderParser(), RsaSha256HttpSignatureSigner()
|
|
)
|
|
),
|
|
DefaultSignatureHeaderParser()
|
|
),
|
|
transaction
|
|
)
|
|
)
|
|
provider.setUserDetailsChecker(AccountStatusUserDetailsChecker())
|
|
return provider
|
|
}
|
|
|
|
@Bean
|
|
@Order(2)
|
|
fun oauth2SecurityFilterChain(http: HttpSecurity, introspector: HandlerMappingIntrospector): SecurityFilterChain {
|
|
val builder = MvcRequestMatcher.Builder(introspector)
|
|
|
|
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
|
|
http.exceptionHandling {
|
|
it.authenticationEntryPoint(
|
|
LoginUrlAuthenticationEntryPoint("/login")
|
|
)
|
|
}.oauth2ResourceServer {
|
|
it.jwt(Customizer.withDefaults())
|
|
}
|
|
return http.build()
|
|
}
|
|
|
|
@Bean
|
|
@Order(4)
|
|
fun defaultSecurityFilterChain(http: HttpSecurity, introspector: HandlerMappingIntrospector): SecurityFilterChain {
|
|
val builder = MvcRequestMatcher.Builder(introspector)
|
|
|
|
http.authorizeHttpRequests {
|
|
it.requestMatchers(PathRequest.toH2Console()).permitAll()
|
|
it.requestMatchers(
|
|
builder.pattern("/inbox"),
|
|
builder.pattern("/users/*/inbox"),
|
|
builder.pattern("/api/v1/apps"),
|
|
builder.pattern("/api/v1/instance/**"),
|
|
builder.pattern("/.well-known/**"),
|
|
builder.pattern("/error"),
|
|
builder.pattern("/nodeinfo/2.0")
|
|
).permitAll()
|
|
it.requestMatchers(
|
|
builder.pattern("/auth/**")
|
|
).anonymous()
|
|
it.requestMatchers(builder.pattern("/change-password")).authenticated()
|
|
it.requestMatchers(builder.pattern("/api/v1/accounts/verify_credentials"))
|
|
.hasAnyAuthority("SCOPE_read", "SCOPE_read:accounts")
|
|
it.anyRequest().permitAll()
|
|
}
|
|
http.oauth2ResourceServer {
|
|
it.jwt(Customizer.withDefaults())
|
|
}.passwordManagement { }.formLogin(Customizer.withDefaults()).csrf {
|
|
it.ignoringRequestMatchers(builder.pattern("/users/*/inbox"))
|
|
it.ignoringRequestMatchers(builder.pattern(HttpMethod.POST, "/api/v1/apps"))
|
|
it.ignoringRequestMatchers(builder.pattern("/inbox"))
|
|
it.ignoringRequestMatchers(PathRequest.toH2Console())
|
|
}.headers {
|
|
it.frameOptions {
|
|
it.sameOrigin()
|
|
}
|
|
}
|
|
return http.build()
|
|
}
|
|
|
|
@Bean
|
|
fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
|
|
|
|
@Bean
|
|
@ConditionalOnProperty(name = ["hideout.security.jwt.generate"], havingValue = "false", matchIfMissing = true)
|
|
fun genJwkSource(): JWKSource<SecurityContext> {
|
|
val keyPairGenerator = KeyPairGenerator.getInstance("RSA")
|
|
keyPairGenerator.initialize(2048)
|
|
val generateKeyPair = keyPairGenerator.generateKeyPair()
|
|
val rsaPublicKey = generateKeyPair.public as RSAPublicKey
|
|
val rsaPrivateKey = generateKeyPair.private as RSAPrivateKey
|
|
val rsaKey = RSAKey.Builder(rsaPublicKey).privateKey(rsaPrivateKey).keyID(UUID.randomUUID().toString()).build()
|
|
|
|
val jwkSet = JWKSet(rsaKey)
|
|
return ImmutableJWKSet(jwkSet)
|
|
}
|
|
|
|
@Bean
|
|
@ConditionalOnProperty(name = ["hideout.security.jwt.generate"], havingValue = "")
|
|
fun loadJwkSource(jwkConfig: JwkConfig): JWKSource<SecurityContext> {
|
|
val rsaKey = RSAKey.Builder(RsaUtil.decodeRsaPublicKey(jwkConfig.publicKey))
|
|
.privateKey(RsaUtil.decodeRsaPrivateKey(jwkConfig.privateKey)).keyID(jwkConfig.keyId).build()
|
|
return ImmutableJWKSet(JWKSet(rsaKey))
|
|
}
|
|
|
|
@Bean
|
|
fun jwtDecoder(jwkSource: JWKSource<SecurityContext>): JwtDecoder =
|
|
OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource)
|
|
|
|
@Bean
|
|
fun authorizationServerSettings(): AuthorizationServerSettings {
|
|
return AuthorizationServerSettings.builder().authorizationEndpoint("/oauth/authorize")
|
|
.tokenEndpoint("/oauth/token").tokenRevocationEndpoint("/oauth/revoke").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())
|
|
}
|
|
}
|
|
}
|
|
|
|
@Bean
|
|
@Primary
|
|
fun jackson2ObjectMapperBuilderCustomizer(): Jackson2ObjectMapperBuilderCustomizer {
|
|
return Jackson2ObjectMapperBuilderCustomizer {
|
|
it.serializationInclusion(JsonInclude.Include.ALWAYS).serializers()
|
|
}
|
|
}
|
|
|
|
@Bean
|
|
fun mappingJackson2HttpMessageConverter(): MappingJackson2HttpMessageConverter {
|
|
val builder = Jackson2ObjectMapperBuilder().serializationInclusion(JsonInclude.Include.NON_NULL)
|
|
return MappingJackson2HttpMessageConverter(builder.build())
|
|
}
|
|
}
|
|
|
|
@ConfigurationProperties("hideout.security.jwt")
|
|
@ConditionalOnProperty(name = ["hideout.security.jwt.generate"], havingValue = "")
|
|
data class JwkConfig(
|
|
val keyId: String,
|
|
val publicKey: String,
|
|
val privateKey: String
|
|
)
|