From 2536a2d79a227df2a1746444e142c8849da25b76 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sat, 21 Oct 2023 04:02:58 +0900 Subject: [PATCH] wip --- .../usbharu/hideout/config/SecurityConfig.kt | 143 +++++++++--------- .../exception/HttpSignatureVerifyException.kt | 5 +- .../service/signature/HttpSignatureFilter.kt | 11 +- .../HttpSignatureUserDetailsService.kt | 60 +++++--- 4 files changed, 118 insertions(+), 101 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt b/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt index 841abf53..6e88e8e1 100644 --- a/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt @@ -26,6 +26,7 @@ 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 @@ -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.OAuth2TokenCustomizer 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.preauth.PreAuthenticatedAuthenticationProvider 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.util.* -@EnableWebSecurity(debug = false) + +@EnableWebSecurity(debug = true) @Configuration @Suppress("FunctionMaxLength", "TooManyFunctions") class SecurityConfig { @@ -69,7 +73,9 @@ class SecurityConfig { @Bean @Order(1) 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) .authorizeHttpRequests { it.anyRequest().permitAll() @@ -77,9 +83,13 @@ class SecurityConfig { .csrf { it.disable() } + .exceptionHandling { + it.authenticationEntryPoint(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) + } .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) } + return http.build() } @@ -87,6 +97,17 @@ class SecurityConfig { fun getHttpSignatureFilter(authenticationManager: AuthenticationManager): HttpSignatureFilter { val httpSignatureFilter = HttpSignatureFilter(DefaultSignatureHeaderParser()) 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 } @@ -95,17 +116,13 @@ class SecurityConfig { val provider = PreAuthenticatedAuthenticationProvider() provider.setPreAuthenticatedUserDetailsService( HttpSignatureUserDetailsService( - userQueryService, - HttpSignatureVerifierComposite( + userQueryService, HttpSignatureVerifierComposite( mapOf( "rsa-sha256" to RsaSha256HttpSignatureVerifier( - DefaultSignatureHeaderParser(), - RsaSha256HttpSignatureSigner() + DefaultSignatureHeaderParser(), RsaSha256HttpSignatureSigner() ) - ), - DefaultSignatureHeaderParser() - ), - transaction + ), DefaultSignatureHeaderParser() + ), transaction ) ) provider.setUserDetailsChecker(AccountStatusUserDetailsChecker()) @@ -118,15 +135,13 @@ class SecurityConfig { val builder = MvcRequestMatcher.Builder(introspector) OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http) - http - .exceptionHandling { - it.authenticationEntryPoint( - LoginUrlAuthenticationEntryPoint("/login") - ) - } - .oauth2ResourceServer { - it.jwt(Customizer.withDefaults()) - } + http.exceptionHandling { + it.authenticationEntryPoint( + LoginUrlAuthenticationEntryPoint("/login") + ) + }.oauth2ResourceServer { + it.jwt(Customizer.withDefaults()) + } return http.build() } @@ -135,43 +150,37 @@ class SecurityConfig { 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() - } + 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() } @@ -186,11 +195,7 @@ class SecurityConfig { 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 rsaKey = RSAKey.Builder(rsaPublicKey).privateKey(rsaPrivateKey).keyID(UUID.randomUUID().toString()).build() val jwkSet = JWKSet(rsaKey) return ImmutableJWKSet(jwkSet) @@ -200,9 +205,7 @@ class SecurityConfig { @ConditionalOnProperty(name = ["hideout.security.jwt.generate"], havingValue = "") fun loadJwkSource(jwkConfig: JwkConfig): JWKSource { val rsaKey = RSAKey.Builder(RsaUtil.decodeRsaPublicKey(jwkConfig.publicKey)) - .privateKey(RsaUtil.decodeRsaPrivateKey(jwkConfig.privateKey)) - .keyID(jwkConfig.keyId) - .build() + .privateKey(RsaUtil.decodeRsaPrivateKey(jwkConfig.privateKey)).keyID(jwkConfig.keyId).build() return ImmutableJWKSet(JWKSet(rsaKey)) } @@ -212,11 +215,8 @@ class SecurityConfig { @Bean fun authorizationServerSettings(): AuthorizationServerSettings { - return AuthorizationServerSettings.builder() - .authorizationEndpoint("/oauth/authorize") - .tokenEndpoint("/oauth/token") - .tokenRevocationEndpoint("/oauth/revoke") - .build() + return AuthorizationServerSettings.builder().authorizationEndpoint("/oauth/authorize") + .tokenEndpoint("/oauth/token").tokenRevocationEndpoint("/oauth/revoke").build() } @Bean @@ -239,8 +239,7 @@ class SecurityConfig { @Bean fun mappingJackson2HttpMessageConverter(): MappingJackson2HttpMessageConverter { - val builder = Jackson2ObjectMapperBuilder() - .serializationInclusion(JsonInclude.Include.NON_NULL) + val builder = Jackson2ObjectMapperBuilder().serializationInclusion(JsonInclude.Include.NON_NULL) return MappingJackson2HttpMessageConverter(builder.build()) } } @@ -248,7 +247,5 @@ class SecurityConfig { @ConfigurationProperties("hideout.security.jwt") @ConditionalOnProperty(name = ["hideout.security.jwt.generate"], havingValue = "") data class JwkConfig( - val keyId: String, - val publicKey: String, - val privateKey: String + val keyId: String, val publicKey: String, val privateKey: String ) diff --git a/src/main/kotlin/dev/usbharu/hideout/exception/HttpSignatureVerifyException.kt b/src/main/kotlin/dev/usbharu/hideout/exception/HttpSignatureVerifyException.kt index 504ce898..edf64ed5 100644 --- a/src/main/kotlin/dev/usbharu/hideout/exception/HttpSignatureVerifyException.kt +++ b/src/main/kotlin/dev/usbharu/hideout/exception/HttpSignatureVerifyException.kt @@ -1,12 +1,11 @@ package dev.usbharu.hideout.exception import java.io.Serial +import javax.naming.AuthenticationException -class HttpSignatureVerifyException : IllegalArgumentException { +class HttpSignatureVerifyException : AuthenticationException { constructor() : super() constructor(s: String?) : super(s) - constructor(message: String?, cause: Throwable?) : super(message, cause) - constructor(cause: Throwable?) : super(cause) companion object { @Serial diff --git a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureFilter.kt b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureFilter.kt index 2a7e15b8..8708efe8 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureFilter.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureFilter.kt @@ -10,13 +10,20 @@ import java.net.URL class HttpSignatureFilter(private val httpSignatureHeaderParser: SignatureHeaderParser) : AbstractPreAuthenticatedProcessingFilter() { - override fun getPreAuthenticatedPrincipal(request: HttpServletRequest?): Any { + override fun getPreAuthenticatedPrincipal(request: HttpServletRequest?): Any? { val headersList = request?.headerNames?.toList().orEmpty() val headers = 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 } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUserDetailsService.kt b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUserDetailsService.kt index d1d619ca..a0ccdd36 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUserDetailsService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUserDetailsService.kt @@ -10,6 +10,8 @@ import dev.usbharu.httpsignature.common.PublicKey import dev.usbharu.httpsignature.verify.FailedVerification import dev.usbharu.httpsignature.verify.HttpSignatureVerifier 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.UserDetails import org.springframework.security.core.userdetails.UsernameNotFoundException @@ -22,38 +24,50 @@ class HttpSignatureUserDetailsService( ) : AuthenticationUserDetailsService { override fun loadUserDetails(token: PreAuthenticatedAuthenticationToken): UserDetails = runBlocking { - transaction.transaction { - if (token.principal !is String) { - throw IllegalStateException("Token is not String") - } - if (token.credentials !is HttpRequest) { - throw IllegalStateException("Credentials is not HttpRequest") - } - val keyId = token.principal as String - val findByKeyId = try { + if (token.principal !is String) { + throw IllegalStateException("Token is not String") + } + if (token.credentials !is HttpRequest) { + throw IllegalStateException("Credentials is not HttpRequest") + } + + val keyId = token.principal as String + val findByKeyId = transaction.transaction { + try { userQueryService.findByKeyId(keyId) } catch (e: FailedToGetResourcesException) { throw UsernameNotFoundException("User not found", e) } + } - val verify = httpSignatureVerifier.verify( + + val verify = try { + httpSignatureVerifier.verify( token.credentials as HttpRequest, PublicKey(RsaUtil.decodeRsaPublicKeyPem(findByKeyId.publicKey), keyId) ) - - if (verify is FailedVerification) { - throw HttpSignatureVerifyException(verify.reason) - } - - HttpSignatureUser( - username = findByKeyId.name, - domain = findByKeyId.domain, - id = findByKeyId.id, - credentialsNonExpired = true, - accountNonLocked = true, - authorities = mutableListOf() - ) + } catch (e: RuntimeException) { + throw BadCredentialsException("", e) } + + if (verify is FailedVerification) { + logger.warn("FAILED Verify HTTP Signature reason: {}", verify.reason) + throw HttpSignatureVerifyException(verify.reason) + } + + HttpSignatureUser( + username = findByKeyId.name, + domain = findByKeyId.domain, + id = findByKeyId.id, + credentialsNonExpired = true, + accountNonLocked = true, + authorities = mutableListOf() + ) + + } + + companion object { + private val logger = LoggerFactory.getLogger(HttpSignatureUserDetailsService::class.java) } }