This commit is contained in:
usbharu 2023-10-21 04:02:58 +09:00
parent 151cc0ed4e
commit 2536a2d79a
Signed by: usbharu
GPG Key ID: 6556747BF94EEBC8
4 changed files with 118 additions and 101 deletions

View File

@ -26,6 +26,7 @@ import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary import org.springframework.context.annotation.Primary
import org.springframework.core.annotation.Order import org.springframework.core.annotation.Order
import org.springframework.http.HttpMethod import org.springframework.http.HttpMethod
import org.springframework.http.HttpStatus
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
import org.springframework.security.authentication.AccountStatusUserDetailsChecker import org.springframework.security.authentication.AccountStatusUserDetailsChecker
@ -45,6 +46,8 @@ import org.springframework.security.oauth2.server.authorization.settings.Authori
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer 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.WebAttributes
import org.springframework.security.web.authentication.HttpStatusEntryPoint
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher
@ -54,7 +57,8 @@ 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
@Suppress("FunctionMaxLength", "TooManyFunctions") @Suppress("FunctionMaxLength", "TooManyFunctions")
class SecurityConfig { class SecurityConfig {
@ -69,7 +73,9 @@ class SecurityConfig {
@Bean @Bean
@Order(1) @Order(1)
fun httpSignatureFilterChain(http: HttpSecurity, httpSignatureFilter: HttpSignatureFilter): SecurityFilterChain { fun httpSignatureFilterChain(http: HttpSecurity, httpSignatureFilter: HttpSignatureFilter): SecurityFilterChain {
http.securityMatcher("/inbox", "/outbox", "/users/*/inbox", "/users/*/outbox") http
.securityMatcher("/inbox", "/outbox", "/users/*/inbox", "/users/*/outbox", "/users/*/posts/*")
.addFilter(httpSignatureFilter) .addFilter(httpSignatureFilter)
.authorizeHttpRequests { .authorizeHttpRequests {
it.anyRequest().permitAll() it.anyRequest().permitAll()
@ -77,9 +83,13 @@ class SecurityConfig {
.csrf { .csrf {
it.disable() it.disable()
} }
.exceptionHandling {
it.authenticationEntryPoint(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
}
.sessionManagement { .sessionManagement {
it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
} }
return http.build() return http.build()
} }
@ -87,6 +97,17 @@ class SecurityConfig {
fun getHttpSignatureFilter(authenticationManager: AuthenticationManager): HttpSignatureFilter { fun getHttpSignatureFilter(authenticationManager: AuthenticationManager): HttpSignatureFilter {
val httpSignatureFilter = HttpSignatureFilter(DefaultSignatureHeaderParser()) val httpSignatureFilter = HttpSignatureFilter(DefaultSignatureHeaderParser())
httpSignatureFilter.setAuthenticationManager(authenticationManager) httpSignatureFilter.setAuthenticationManager(authenticationManager)
httpSignatureFilter.setAuthenticationFailureHandler { request, response, exception ->
println(response::class.java)
if (response.isCommitted) {
return@setAuthenticationFailureHandler
}
response.setStatus(HttpStatus.UNAUTHORIZED.value())
request.getSession(false)?.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION)
response.outputStream.close()
}
httpSignatureFilter.setCheckForPrincipalChanges(true)
httpSignatureFilter.setInvalidateSessionOnPrincipalChange(true)
return httpSignatureFilter return httpSignatureFilter
} }
@ -95,17 +116,13 @@ class SecurityConfig {
val provider = PreAuthenticatedAuthenticationProvider() val provider = PreAuthenticatedAuthenticationProvider()
provider.setPreAuthenticatedUserDetailsService( provider.setPreAuthenticatedUserDetailsService(
HttpSignatureUserDetailsService( HttpSignatureUserDetailsService(
userQueryService, userQueryService, HttpSignatureVerifierComposite(
HttpSignatureVerifierComposite(
mapOf( mapOf(
"rsa-sha256" to RsaSha256HttpSignatureVerifier( "rsa-sha256" to RsaSha256HttpSignatureVerifier(
DefaultSignatureHeaderParser(), DefaultSignatureHeaderParser(), RsaSha256HttpSignatureSigner()
RsaSha256HttpSignatureSigner()
) )
), ), DefaultSignatureHeaderParser()
DefaultSignatureHeaderParser() ), transaction
),
transaction
) )
) )
provider.setUserDetailsChecker(AccountStatusUserDetailsChecker()) provider.setUserDetailsChecker(AccountStatusUserDetailsChecker())
@ -118,13 +135,11 @@ class SecurityConfig {
val builder = MvcRequestMatcher.Builder(introspector) val builder = MvcRequestMatcher.Builder(introspector)
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http) OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http)
http http.exceptionHandling {
.exceptionHandling {
it.authenticationEntryPoint( it.authenticationEntryPoint(
LoginUrlAuthenticationEntryPoint("/login") LoginUrlAuthenticationEntryPoint("/login")
) )
} }.oauth2ResourceServer {
.oauth2ResourceServer {
it.jwt(Customizer.withDefaults()) it.jwt(Customizer.withDefaults())
} }
return http.build() return http.build()
@ -135,8 +150,7 @@ 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 http.authorizeHttpRequests {
.authorizeHttpRequests {
it.requestMatchers(PathRequest.toH2Console()).permitAll() it.requestMatchers(PathRequest.toH2Console()).permitAll()
it.requestMatchers( it.requestMatchers(
builder.pattern("/inbox"), builder.pattern("/inbox"),
@ -155,19 +169,14 @@ class SecurityConfig {
.hasAnyAuthority("SCOPE_read", "SCOPE_read:accounts") .hasAnyAuthority("SCOPE_read", "SCOPE_read:accounts")
it.anyRequest().permitAll() it.anyRequest().permitAll()
} }
http http.oauth2ResourceServer {
.oauth2ResourceServer {
it.jwt(Customizer.withDefaults()) it.jwt(Customizer.withDefaults())
} }.passwordManagement { }.formLogin(Customizer.withDefaults()).csrf {
.passwordManagement { }
.formLogin(Customizer.withDefaults())
.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(HttpMethod.POST, "/api/v1/apps"))
it.ignoringRequestMatchers(builder.pattern("/inbox")) it.ignoringRequestMatchers(builder.pattern("/inbox"))
it.ignoringRequestMatchers(PathRequest.toH2Console()) it.ignoringRequestMatchers(PathRequest.toH2Console())
} }.headers {
.headers {
it.frameOptions { it.frameOptions {
it.sameOrigin() it.sameOrigin()
} }
@ -186,11 +195,7 @@ class SecurityConfig {
val generateKeyPair = keyPairGenerator.generateKeyPair() val generateKeyPair = keyPairGenerator.generateKeyPair()
val rsaPublicKey = generateKeyPair.public as RSAPublicKey val rsaPublicKey = generateKeyPair.public as RSAPublicKey
val rsaPrivateKey = generateKeyPair.private as RSAPrivateKey val rsaPrivateKey = generateKeyPair.private as RSAPrivateKey
val rsaKey = RSAKey val rsaKey = RSAKey.Builder(rsaPublicKey).privateKey(rsaPrivateKey).keyID(UUID.randomUUID().toString()).build()
.Builder(rsaPublicKey)
.privateKey(rsaPrivateKey)
.keyID(UUID.randomUUID().toString())
.build()
val jwkSet = JWKSet(rsaKey) val jwkSet = JWKSet(rsaKey)
return ImmutableJWKSet(jwkSet) return ImmutableJWKSet(jwkSet)
@ -200,9 +205,7 @@ class SecurityConfig {
@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(RsaUtil.decodeRsaPublicKey(jwkConfig.publicKey)) val rsaKey = RSAKey.Builder(RsaUtil.decodeRsaPublicKey(jwkConfig.publicKey))
.privateKey(RsaUtil.decodeRsaPrivateKey(jwkConfig.privateKey)) .privateKey(RsaUtil.decodeRsaPrivateKey(jwkConfig.privateKey)).keyID(jwkConfig.keyId).build()
.keyID(jwkConfig.keyId)
.build()
return ImmutableJWKSet(JWKSet(rsaKey)) return ImmutableJWKSet(JWKSet(rsaKey))
} }
@ -212,11 +215,8 @@ class SecurityConfig {
@Bean @Bean
fun authorizationServerSettings(): AuthorizationServerSettings { fun authorizationServerSettings(): AuthorizationServerSettings {
return AuthorizationServerSettings.builder() return AuthorizationServerSettings.builder().authorizationEndpoint("/oauth/authorize")
.authorizationEndpoint("/oauth/authorize") .tokenEndpoint("/oauth/token").tokenRevocationEndpoint("/oauth/revoke").build()
.tokenEndpoint("/oauth/token")
.tokenRevocationEndpoint("/oauth/revoke")
.build()
} }
@Bean @Bean
@ -239,8 +239,7 @@ class SecurityConfig {
@Bean @Bean
fun mappingJackson2HttpMessageConverter(): MappingJackson2HttpMessageConverter { fun mappingJackson2HttpMessageConverter(): MappingJackson2HttpMessageConverter {
val builder = Jackson2ObjectMapperBuilder() val builder = Jackson2ObjectMapperBuilder().serializationInclusion(JsonInclude.Include.NON_NULL)
.serializationInclusion(JsonInclude.Include.NON_NULL)
return MappingJackson2HttpMessageConverter(builder.build()) return MappingJackson2HttpMessageConverter(builder.build())
} }
} }
@ -248,7 +247,5 @@ class SecurityConfig {
@ConfigurationProperties("hideout.security.jwt") @ConfigurationProperties("hideout.security.jwt")
@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: String, val privateKey: String
val publicKey: String,
val privateKey: String
) )

View File

@ -1,12 +1,11 @@
package dev.usbharu.hideout.exception package dev.usbharu.hideout.exception
import java.io.Serial import java.io.Serial
import javax.naming.AuthenticationException
class HttpSignatureVerifyException : IllegalArgumentException { class HttpSignatureVerifyException : AuthenticationException {
constructor() : super() constructor() : super()
constructor(s: String?) : super(s) constructor(s: String?) : super(s)
constructor(message: String?, cause: Throwable?) : super(message, cause)
constructor(cause: Throwable?) : super(cause)
companion object { companion object {
@Serial @Serial

View File

@ -10,13 +10,20 @@ import java.net.URL
class HttpSignatureFilter(private val httpSignatureHeaderParser: SignatureHeaderParser) : class HttpSignatureFilter(private val httpSignatureHeaderParser: SignatureHeaderParser) :
AbstractPreAuthenticatedProcessingFilter() { AbstractPreAuthenticatedProcessingFilter() {
override fun getPreAuthenticatedPrincipal(request: HttpServletRequest?): Any { override fun getPreAuthenticatedPrincipal(request: HttpServletRequest?): Any? {
val headersList = request?.headerNames?.toList().orEmpty() val headersList = request?.headerNames?.toList().orEmpty()
val headers = val headers =
headersList.associateWith { header -> request?.getHeaders(header)?.toList().orEmpty() } headersList.associateWith { header -> request?.getHeaders(header)?.toList().orEmpty() }
val signature = httpSignatureHeaderParser.parse(HttpHeaders(headers)) val signature = try {
httpSignatureHeaderParser.parse(HttpHeaders(headers))
} catch (e: IllegalArgumentException) {
return null
} catch (e: RuntimeException) {
return ""
}
return signature.keyId return signature.keyId
} }

View File

@ -10,6 +10,8 @@ import dev.usbharu.httpsignature.common.PublicKey
import dev.usbharu.httpsignature.verify.FailedVerification import dev.usbharu.httpsignature.verify.FailedVerification
import dev.usbharu.httpsignature.verify.HttpSignatureVerifier import dev.usbharu.httpsignature.verify.HttpSignatureVerifier
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService import org.springframework.security.core.userdetails.AuthenticationUserDetailsService
import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.security.core.userdetails.UsernameNotFoundException
@ -22,7 +24,7 @@ class HttpSignatureUserDetailsService(
) : ) :
AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> { AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
override fun loadUserDetails(token: PreAuthenticatedAuthenticationToken): UserDetails = runBlocking { override fun loadUserDetails(token: PreAuthenticatedAuthenticationToken): UserDetails = runBlocking {
transaction.transaction {
if (token.principal !is String) { if (token.principal !is String) {
throw IllegalStateException("Token is not String") throw IllegalStateException("Token is not String")
} }
@ -31,18 +33,26 @@ class HttpSignatureUserDetailsService(
} }
val keyId = token.principal as String val keyId = token.principal as String
val findByKeyId = try { val findByKeyId = transaction.transaction {
try {
userQueryService.findByKeyId(keyId) userQueryService.findByKeyId(keyId)
} catch (e: FailedToGetResourcesException) { } catch (e: FailedToGetResourcesException) {
throw UsernameNotFoundException("User not found", e) throw UsernameNotFoundException("User not found", e)
} }
}
val verify = httpSignatureVerifier.verify(
val verify = try {
httpSignatureVerifier.verify(
token.credentials as HttpRequest, token.credentials as HttpRequest,
PublicKey(RsaUtil.decodeRsaPublicKeyPem(findByKeyId.publicKey), keyId) PublicKey(RsaUtil.decodeRsaPublicKeyPem(findByKeyId.publicKey), keyId)
) )
} catch (e: RuntimeException) {
throw BadCredentialsException("", e)
}
if (verify is FailedVerification) { if (verify is FailedVerification) {
logger.warn("FAILED Verify HTTP Signature reason: {}", verify.reason)
throw HttpSignatureVerifyException(verify.reason) throw HttpSignatureVerifyException(verify.reason)
} }
@ -54,6 +64,10 @@ class HttpSignatureUserDetailsService(
accountNonLocked = true, accountNonLocked = true,
authorities = mutableListOf() authorities = mutableListOf()
) )
} }
companion object {
private val logger = LoggerFactory.getLogger(HttpSignatureUserDetailsService::class.java)
} }
} }