From 151cc0ed4e6a2e81bd5eb2abcf38c4dadfbc58ac Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sat, 21 Oct 2023 00:31:54 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E6=8A=95=E7=A8=BF=E3=81=AEap=20id?= =?UTF-8?q?=E3=81=AB=E3=82=A2=E3=82=AF=E3=82=BB=E3=82=B9=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=81=A8=E6=8A=95=E7=A8=BF=E3=82=92=E5=8F=96=E5=BE=97=E3=81=A7?= =?UTF-8?q?=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hideout/controller/NoteApController.kt | 16 ++++++++ .../controller/NoteApControllerImpl.kt | 32 +++++++++++++++ .../hideout/query/FollowerQueryService.kt | 2 - .../query/activitypub/NoteQueryService.kt | 8 ++++ .../query/activitypub/NoteQueryServiceImpl.kt | 39 +++++++++++++++++++ .../hideout/service/api/NoteApApiService.kt | 7 ++++ .../service/api/NoteApApiServiceImpl.kt | 37 ++++++++++++++++++ .../service/signature/HttpSignatureUser.kt | 21 ++++++++++ .../HttpSignatureUserDetailsService.kt | 1 + 9 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/controller/NoteApController.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/controller/NoteApControllerImpl.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/query/activitypub/NoteQueryService.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/query/activitypub/NoteQueryServiceImpl.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/service/api/NoteApApiService.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/service/api/NoteApApiServiceImpl.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/controller/NoteApController.kt b/src/main/kotlin/dev/usbharu/hideout/controller/NoteApController.kt new file mode 100644 index 00000000..f7fe6197 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/controller/NoteApController.kt @@ -0,0 +1,16 @@ +package dev.usbharu.hideout.controller + +import dev.usbharu.hideout.domain.model.ap.Note +import org.springframework.http.ResponseEntity +import org.springframework.security.core.annotation.CurrentSecurityContext +import org.springframework.security.core.context.SecurityContext +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable + +interface NoteApController { + @GetMapping("/users/*/posts/{postId}") + suspend fun postsAp( + @PathVariable("postId") postId: Long, + @CurrentSecurityContext context: SecurityContext + ): ResponseEntity +} diff --git a/src/main/kotlin/dev/usbharu/hideout/controller/NoteApControllerImpl.kt b/src/main/kotlin/dev/usbharu/hideout/controller/NoteApControllerImpl.kt new file mode 100644 index 00000000..76bd6d63 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/controller/NoteApControllerImpl.kt @@ -0,0 +1,32 @@ +package dev.usbharu.hideout.controller + +import dev.usbharu.hideout.domain.model.ap.Note +import dev.usbharu.hideout.service.api.NoteApApiService +import dev.usbharu.hideout.service.signature.HttpSignatureUser +import org.springframework.http.ResponseEntity +import org.springframework.security.core.annotation.CurrentSecurityContext +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RestController + +@RestController +class NoteApControllerImpl(private val noteApApiService: NoteApApiService) : NoteApController { + override suspend fun postsAp( + @PathVariable(value = "postId") postId: Long, @CurrentSecurityContext context: SecurityContext + ): ResponseEntity { + + val userId = + if (context.authentication is PreAuthenticatedAuthenticationToken && context.authentication.details is HttpSignatureUser) { + (context.authentication.details as HttpSignatureUser).id + } else { + null + } + + val note = noteApApiService.getNote(postId, userId) + if (note != null) { + return ResponseEntity.ok(note) + } + return ResponseEntity.notFound().build() + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/query/FollowerQueryService.kt b/src/main/kotlin/dev/usbharu/hideout/query/FollowerQueryService.kt index 4cc73ef1..4922b268 100644 --- a/src/main/kotlin/dev/usbharu/hideout/query/FollowerQueryService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/query/FollowerQueryService.kt @@ -1,9 +1,7 @@ package dev.usbharu.hideout.query import dev.usbharu.hideout.domain.model.hideout.entity.User -import org.springframework.stereotype.Repository -@Repository interface FollowerQueryService { suspend fun findFollowersById(id: Long): List suspend fun findFollowersByNameAndDomain(name: String, domain: String): List diff --git a/src/main/kotlin/dev/usbharu/hideout/query/activitypub/NoteQueryService.kt b/src/main/kotlin/dev/usbharu/hideout/query/activitypub/NoteQueryService.kt new file mode 100644 index 00000000..d0ee3adb --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/query/activitypub/NoteQueryService.kt @@ -0,0 +1,8 @@ +package dev.usbharu.hideout.query.activitypub + +import dev.usbharu.hideout.domain.model.ap.Note +import dev.usbharu.hideout.domain.model.hideout.entity.Post + +interface NoteQueryService { + suspend fun findById(id: Long): Pair +} diff --git a/src/main/kotlin/dev/usbharu/hideout/query/activitypub/NoteQueryServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/query/activitypub/NoteQueryServiceImpl.kt new file mode 100644 index 00000000..1ee6d038 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/query/activitypub/NoteQueryServiceImpl.kt @@ -0,0 +1,39 @@ +package dev.usbharu.hideout.query.activitypub + +import dev.usbharu.hideout.domain.model.ap.Note +import dev.usbharu.hideout.domain.model.hideout.entity.Post +import dev.usbharu.hideout.exception.FailedToGetResourcesException +import dev.usbharu.hideout.repository.Posts +import dev.usbharu.hideout.repository.Users +import dev.usbharu.hideout.repository.toPost +import dev.usbharu.hideout.util.singleOr +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.select +import org.springframework.stereotype.Repository +import java.time.Instant + +@Repository +class NoteQueryServiceImpl : NoteQueryService { + override suspend fun findById(id: Long): Pair { + return Posts + .leftJoin(Users) + .select { Posts.id eq id } + .singleOr { FailedToGetResourcesException("id $id is duplicate or does not exist.") } + .let { it.toNote() to it.toPost() } + + } + + private fun ResultRow.toNote(): Note { + return Note( + name = "Post", + id = this[Posts.apId], + attributedTo = this[Users.url], + content = this[Posts.text], + published = Instant.ofEpochMilli(this[Posts.createdAt]).toString(), + to = listOf(), + cc = listOf(), + inReplyTo = null, + sensitive = false + ) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/api/NoteApApiService.kt b/src/main/kotlin/dev/usbharu/hideout/service/api/NoteApApiService.kt new file mode 100644 index 00000000..38006f4b --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/api/NoteApApiService.kt @@ -0,0 +1,7 @@ +package dev.usbharu.hideout.service.api + +import dev.usbharu.hideout.domain.model.ap.Note + +interface NoteApApiService { + suspend fun getNote(postId: Long, userId: Long?): Note? +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/api/NoteApApiServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/api/NoteApApiServiceImpl.kt new file mode 100644 index 00000000..4fb14cfd --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/api/NoteApApiServiceImpl.kt @@ -0,0 +1,37 @@ +package dev.usbharu.hideout.service.api + +import dev.usbharu.hideout.domain.model.ap.Note +import dev.usbharu.hideout.domain.model.hideout.entity.Visibility +import dev.usbharu.hideout.query.FollowerQueryService +import dev.usbharu.hideout.query.activitypub.NoteQueryService +import dev.usbharu.hideout.service.core.Transaction +import org.springframework.stereotype.Service + +@Service +class NoteApApiServiceImpl( + private val noteQueryService: NoteQueryService, + private val followerQueryService: FollowerQueryService, + private val transaction: Transaction +) : NoteApApiService { + override suspend fun getNote(postId: Long, userId: Long?): Note? = transaction.transaction { + val findById = noteQueryService.findById(postId) + when (findById.second.visibility) { + Visibility.PUBLIC, Visibility.UNLISTED -> { + return@transaction findById.first + } + + Visibility.FOLLOWERS -> { + if (userId == null) { + return@transaction null + } + + if (followerQueryService.alreadyFollow(findById.second.userId, userId).not()) { + return@transaction null + } + return@transaction findById.first + } + + Visibility.DIRECT -> return@transaction null + } + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUser.kt b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUser.kt index d9c55816..ad1b1859 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUser.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUser.kt @@ -7,6 +7,7 @@ import java.io.Serial class HttpSignatureUser( username: String, val domain: String, + val id: Long, credentialsNonExpired: Boolean, accountNonLocked: Boolean, authorities: MutableCollection? @@ -19,6 +20,26 @@ class HttpSignatureUser( accountNonLocked, authorities ) { + + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is HttpSignatureUser) return false + if (!super.equals(other)) return false + + if (domain != other.domain) return false + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + domain.hashCode() + result = 31 * result + id.hashCode() + return result + } + companion object { @Serial private const val serialVersionUID: Long = -3330552099960982997L 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 f86d7bfb..d1d619ca 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUserDetailsService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUserDetailsService.kt @@ -49,6 +49,7 @@ class HttpSignatureUserDetailsService( HttpSignatureUser( username = findByKeyId.name, domain = findByKeyId.domain, + id = findByKeyId.id, credentialsNonExpired = true, accountNonLocked = true, authorities = mutableListOf() 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 2/4] 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) } } From ff5eb791790f4744ae9545f4fd9ee17ac518f5b1 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Sat, 21 Oct 2023 15:12:31 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20Signature=E3=83=98=E3=83=83?= =?UTF-8?q?=E3=83=80=E3=83=BC=E3=81=8C=E5=AD=98=E5=9C=A8=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=81=8C=E3=80=81=E8=AA=8D=E8=A8=BC=E3=81=AB=E5=A4=B1=E6=95=97?= =?UTF-8?q?=E3=81=97=E3=81=9F=E3=81=A8=E3=81=8D=E3=81=AF401=E3=82=92?= =?UTF-8?q?=E8=BF=94=E3=81=99=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../usbharu/hideout/config/SecurityConfig.kt | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt b/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt index 6e88e8e1..8d069735 100644 --- a/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt @@ -46,11 +46,13 @@ 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.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 @@ -58,7 +60,7 @@ import java.security.interfaces.RSAPublicKey import java.util.* -@EnableWebSecurity(debug = true) +@EnableWebSecurity(debug = false) @Configuration @Suppress("FunctionMaxLength", "TooManyFunctions") class SecurityConfig { @@ -77,6 +79,10 @@ class SecurityConfig { .securityMatcher("/inbox", "/outbox", "/users/*/inbox", "/users/*/outbox", "/users/*/posts/*") .addFilter(httpSignatureFilter) + .addFilterBefore( + ExceptionTranslationFilter(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)), + HttpSignatureFilter::class.java + ) .authorizeHttpRequests { it.anyRequest().permitAll() } @@ -85,6 +91,10 @@ class SecurityConfig { } .exceptionHandling { it.authenticationEntryPoint(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) + it.defaultAuthenticationEntryPointFor( + HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), + AnyRequestMatcher.INSTANCE + ) } .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) @@ -97,17 +107,12 @@ 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) + httpSignatureFilter.setContinueFilterChainOnUnsuccessfulAuthentication(false) + val authenticationEntryPointFailureHandler = + AuthenticationEntryPointFailureHandler(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) + authenticationEntryPointFailureHandler.setRethrowAuthenticationServiceException(false) + httpSignatureFilter.setAuthenticationFailureHandler(authenticationEntryPointFailureHandler) + return httpSignatureFilter } From 9466de644a7726f551856ffc03d578a169cfec61 Mon Sep 17 00:00:00 2001 From: usbharu Date: Sat, 21 Oct 2023 15:18:53 +0900 Subject: [PATCH 4/4] Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../dev/usbharu/hideout/config/SecurityConfig.kt | 15 +++++++++------ .../hideout/controller/NoteApControllerImpl.kt | 4 ++-- .../query/activitypub/NoteQueryServiceImpl.kt | 1 - .../service/signature/HttpSignatureFilter.kt | 1 - .../service/signature/HttpSignatureUser.kt | 1 - .../signature/HttpSignatureUserDetailsService.kt | 3 --- 6 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt b/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt index 8d069735..2f5b4f59 100644 --- a/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt @@ -59,7 +59,6 @@ import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey import java.util.* - @EnableWebSecurity(debug = false) @Configuration @Suppress("FunctionMaxLength", "TooManyFunctions") @@ -76,7 +75,6 @@ class SecurityConfig { @Order(1) fun httpSignatureFilterChain(http: HttpSecurity, httpSignatureFilter: HttpSignatureFilter): SecurityFilterChain { http - .securityMatcher("/inbox", "/outbox", "/users/*/inbox", "/users/*/outbox", "/users/*/posts/*") .addFilter(httpSignatureFilter) .addFilterBefore( @@ -121,13 +119,16 @@ class SecurityConfig { val provider = PreAuthenticatedAuthenticationProvider() provider.setPreAuthenticatedUserDetailsService( HttpSignatureUserDetailsService( - userQueryService, HttpSignatureVerifierComposite( + userQueryService, + HttpSignatureVerifierComposite( mapOf( "rsa-sha256" to RsaSha256HttpSignatureVerifier( DefaultSignatureHeaderParser(), RsaSha256HttpSignatureSigner() ) - ), DefaultSignatureHeaderParser() - ), transaction + ), + DefaultSignatureHeaderParser() + ), + transaction ) ) provider.setUserDetailsChecker(AccountStatusUserDetailsChecker()) @@ -252,5 +253,7 @@ 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/controller/NoteApControllerImpl.kt b/src/main/kotlin/dev/usbharu/hideout/controller/NoteApControllerImpl.kt index 76bd6d63..36e27859 100644 --- a/src/main/kotlin/dev/usbharu/hideout/controller/NoteApControllerImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/controller/NoteApControllerImpl.kt @@ -13,9 +13,9 @@ import org.springframework.web.bind.annotation.RestController @RestController class NoteApControllerImpl(private val noteApApiService: NoteApApiService) : NoteApController { override suspend fun postsAp( - @PathVariable(value = "postId") postId: Long, @CurrentSecurityContext context: SecurityContext + @PathVariable(value = "postId") postId: Long, + @CurrentSecurityContext context: SecurityContext ): ResponseEntity { - val userId = if (context.authentication is PreAuthenticatedAuthenticationToken && context.authentication.details is HttpSignatureUser) { (context.authentication.details as HttpSignatureUser).id diff --git a/src/main/kotlin/dev/usbharu/hideout/query/activitypub/NoteQueryServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/query/activitypub/NoteQueryServiceImpl.kt index 1ee6d038..4978b9f4 100644 --- a/src/main/kotlin/dev/usbharu/hideout/query/activitypub/NoteQueryServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/query/activitypub/NoteQueryServiceImpl.kt @@ -20,7 +20,6 @@ class NoteQueryServiceImpl : NoteQueryService { .select { Posts.id eq id } .singleOr { FailedToGetResourcesException("id $id is duplicate or does not exist.") } .let { it.toNote() to it.toPost() } - } private fun ResultRow.toNote(): Note { 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 8708efe8..07c2b7b1 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureFilter.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureFilter.kt @@ -21,7 +21,6 @@ class HttpSignatureFilter(private val httpSignatureHeaderParser: SignatureHeader } catch (e: IllegalArgumentException) { return null } catch (e: RuntimeException) { - return "" } return signature.keyId diff --git a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUser.kt b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUser.kt index ad1b1859..cb6160ee 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUser.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUser.kt @@ -21,7 +21,6 @@ class HttpSignatureUser( authorities ) { - override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is HttpSignatureUser) return false 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 a0ccdd36..0f58350c 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUserDetailsService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUserDetailsService.kt @@ -24,7 +24,6 @@ class HttpSignatureUserDetailsService( ) : AuthenticationUserDetailsService { override fun loadUserDetails(token: PreAuthenticatedAuthenticationToken): UserDetails = runBlocking { - if (token.principal !is String) { throw IllegalStateException("Token is not String") } @@ -41,7 +40,6 @@ class HttpSignatureUserDetailsService( } } - val verify = try { httpSignatureVerifier.verify( token.credentials as HttpRequest, @@ -64,7 +62,6 @@ class HttpSignatureUserDetailsService( accountNonLocked = true, authorities = mutableListOf() ) - } companion object {