Merge branch 'develop' into feature/mastodon-api-int-test

This commit is contained in:
usbharu 2023-11-29 17:03:42 +09:00 committed by GitHub
commit 0efd92a371
11 changed files with 74 additions and 44 deletions

View File

@ -64,4 +64,4 @@ jobs:
uses: mikepenz/action-junit-report@v2 uses: mikepenz/action-junit-report@v2
if: always() if: always()
with: with:
report_paths: '**/build/test-results/test/TEST-*.xml' report_paths: '**/build/test-results/integrationTest/TEST-*.xml'

View File

@ -45,6 +45,7 @@ class InboxTest {
content = "{}" content = "{}"
contentType = MediaType.APPLICATION_JSON contentType = MediaType.APPLICATION_JSON
} }
.asyncDispatch()
.andExpect { status { isUnauthorized() } } .andExpect { status { isUnauthorized() } }
} }
@ -55,6 +56,7 @@ class InboxTest {
.post("/inbox") { .post("/inbox") {
content = "{}" content = "{}"
contentType = MediaType.APPLICATION_JSON contentType = MediaType.APPLICATION_JSON
header("Signature", "")
} }
.asyncDispatch() .asyncDispatch()
.andExpect { status { isAccepted() } } .andExpect { status { isAccepted() } }
@ -68,6 +70,7 @@ class InboxTest {
content = "{}" content = "{}"
contentType = MediaType.APPLICATION_JSON contentType = MediaType.APPLICATION_JSON
} }
.asyncDispatch()
.andExpect { status { isUnauthorized() } } .andExpect { status { isUnauthorized() } }
} }
@ -78,6 +81,7 @@ class InboxTest {
.post("/users/hoge/inbox") { .post("/users/hoge/inbox") {
content = "{}" content = "{}"
contentType = MediaType.APPLICATION_JSON contentType = MediaType.APPLICATION_JSON
header("Signature", "")
} }
.asyncDispatch() .asyncDispatch()
.andExpect { status { isAccepted() } } .andExpect { status { isAccepted() } }

View File

@ -43,8 +43,8 @@ open class Delete : Object, HasId, HasActor {
override fun hashCode(): Int { override fun hashCode(): Int {
var result = super.hashCode() var result = super.hashCode()
result = 31 * result + (apObject?.hashCode() ?: 0) result = 31 * result + apObject.hashCode()
result = 31 * result + (published?.hashCode() ?: 0) result = 31 * result + published.hashCode()
result = 31 * result + actor.hashCode() result = 31 * result + actor.hashCode()
result = 31 * result + id.hashCode() result = 31 * result + id.hashCode()
return result return result

View File

@ -30,14 +30,13 @@ open class Undo(
override fun hashCode(): Int { override fun hashCode(): Int {
var result = super.hashCode() var result = super.hashCode()
result = 31 * result + (`object`?.hashCode() ?: 0) result = 31 * result + `object`.hashCode()
result = 31 * result + (published?.hashCode() ?: 0) result = 31 * result + published.hashCode()
result = 31 * result + actor.hashCode() result = 31 * result + actor.hashCode()
result = 31 * result + id.hashCode() result = 31 * result + id.hashCode()
return result return result
} }
override fun toString(): String { override fun toString(): String =
return "Undo(`object`=$`object`, published=$published, actor='$actor', id='$id') ${super.toString()}" "Undo(`object`=$`object`, published=$published, actor='$actor', id='$id') ${super.toString()}"
}
} }

View File

@ -5,6 +5,7 @@ import dev.usbharu.httpsignature.common.HttpHeaders
import dev.usbharu.httpsignature.common.HttpMethod import dev.usbharu.httpsignature.common.HttpMethod
import dev.usbharu.httpsignature.common.HttpRequest import dev.usbharu.httpsignature.common.HttpRequest
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.http.HttpHeaders.WWW_AUTHENTICATE
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestBody
@ -21,6 +22,16 @@ class InboxControllerImpl(private val apService: APService) : InboxController {
): ResponseEntity<Unit> { ): ResponseEntity<Unit> {
val request = (requireNotNull(RequestContextHolder.getRequestAttributes()) as ServletRequestAttributes).request val request = (requireNotNull(RequestContextHolder.getRequestAttributes()) as ServletRequestAttributes).request
val headersList = request.headerNames?.toList().orEmpty()
if (headersList.contains("Signature").not()) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.header(
WWW_AUTHENTICATE,
"Signature realm=\"Example\",headers=\"(request-target) date host digest\""
)
.build()
}
val parseActivity = try { val parseActivity = try {
apService.parseActivity(string) apService.parseActivity(string)
} catch (e: Exception) { } catch (e: Exception) {
@ -31,7 +42,6 @@ class InboxControllerImpl(private val apService: APService) : InboxController {
try { try {
val url = request.requestURL.toString() val url = request.requestURL.toString()
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() }
@ -43,8 +53,6 @@ class InboxControllerImpl(private val apService: APService) : InboxController {
} }
} }
println(headers)
apService.processActivity( apService.processActivity(
string, string,
parseActivity, parseActivity,

View File

@ -14,6 +14,7 @@ import dev.usbharu.hideout.core.query.UserQueryService
import dev.usbharu.hideout.core.service.job.JobProcessor import dev.usbharu.hideout.core.service.job.JobProcessor
import dev.usbharu.hideout.util.RsaUtil import dev.usbharu.hideout.util.RsaUtil
import dev.usbharu.httpsignature.common.HttpHeaders import dev.usbharu.httpsignature.common.HttpHeaders
import dev.usbharu.httpsignature.common.HttpMethod
import dev.usbharu.httpsignature.common.HttpRequest import dev.usbharu.httpsignature.common.HttpRequest
import dev.usbharu.httpsignature.common.PublicKey import dev.usbharu.httpsignature.common.PublicKey
import dev.usbharu.httpsignature.verify.HttpSignatureVerifier import dev.usbharu.httpsignature.verify.HttpSignatureVerifier
@ -34,6 +35,14 @@ class InboxJobProcessor(
) : JobProcessor<InboxJobParam, InboxJob> { ) : JobProcessor<InboxJobParam, InboxJob> {
private suspend fun verifyHttpSignature(httpRequest: HttpRequest, signature: Signature): Boolean { private suspend fun verifyHttpSignature(httpRequest: HttpRequest, signature: Signature): Boolean {
val requiredHeaders = when (httpRequest.method) {
HttpMethod.GET -> getRequiredHeaders
HttpMethod.POST -> postRequiredHeaders
}
if (signature.headers.containsAll(requiredHeaders).not()) {
return false
}
val user = try { val user = try {
userQueryService.findByKeyId(signature.keyId) userQueryService.findByKeyId(signature.keyId)
} catch (_: FailedToGetResourcesException) { } catch (_: FailedToGetResourcesException) {
@ -96,5 +105,7 @@ class InboxJobProcessor(
companion object { companion object {
private val logger = LoggerFactory.getLogger(InboxJobProcessor::class.java) private val logger = LoggerFactory.getLogger(InboxJobProcessor::class.java)
private val postRequiredHeaders = listOf("(request-target)", "date", "host", "digest")
private val getRequiredHeaders = listOf("(request-target)", "date", "host")
} }
} }

View File

@ -6,7 +6,6 @@ import com.nimbusds.jose.jwk.RSAKey
import com.nimbusds.jose.jwk.source.ImmutableJWKSet 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.activitypub.service.objects.user.APUserService
import dev.usbharu.hideout.application.external.Transaction import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.infrastructure.springframework.httpsignature.HttpSignatureFilter 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.HttpSignatureUserDetailsService
@ -81,7 +80,7 @@ class SecurityConfig {
): SecurityFilterChain { ): SecurityFilterChain {
val builder = MvcRequestMatcher.Builder(introspector) val builder = MvcRequestMatcher.Builder(introspector)
http http
.securityMatcher("/inbox", "/outbox", "/users/*/inbox", "/users/*/outbox", "/users/*/posts/*") .securityMatcher("/users/*/posts/*")
.addFilter(httpSignatureFilter) .addFilter(httpSignatureFilter)
.addFilterBefore( .addFilterBefore(
ExceptionTranslationFilter(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)), ExceptionTranslationFilter(HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)),
@ -116,12 +115,9 @@ class SecurityConfig {
@Bean @Bean
fun getHttpSignatureFilter( fun getHttpSignatureFilter(
authenticationManager: AuthenticationManager, authenticationManager: AuthenticationManager,
transaction: Transaction,
apUserService: APUserService,
userQueryService: UserQueryService
): HttpSignatureFilter { ): HttpSignatureFilter {
val httpSignatureFilter = val httpSignatureFilter =
HttpSignatureFilter(DefaultSignatureHeaderParser(), transaction, apUserService, userQueryService) HttpSignatureFilter(DefaultSignatureHeaderParser())
httpSignatureFilter.setAuthenticationManager(authenticationManager) httpSignatureFilter.setAuthenticationManager(authenticationManager)
httpSignatureFilter.setContinueFilterChainOnUnsuccessfulAuthentication(false) httpSignatureFilter.setContinueFilterChainOnUnsuccessfulAuthentication(false)
val authenticationEntryPointFailureHandler = val authenticationEntryPointFailureHandler =
@ -134,18 +130,20 @@ class SecurityConfig {
@Bean @Bean
fun httpSignatureAuthenticationProvider(transaction: Transaction): PreAuthenticatedAuthenticationProvider { fun httpSignatureAuthenticationProvider(transaction: Transaction): PreAuthenticatedAuthenticationProvider {
val provider = PreAuthenticatedAuthenticationProvider() val provider = PreAuthenticatedAuthenticationProvider()
val signatureHeaderParser = DefaultSignatureHeaderParser()
provider.setPreAuthenticatedUserDetailsService( provider.setPreAuthenticatedUserDetailsService(
HttpSignatureUserDetailsService( HttpSignatureUserDetailsService(
userQueryService, userQueryService,
HttpSignatureVerifierComposite( HttpSignatureVerifierComposite(
mapOf( mapOf(
"rsa-sha256" to RsaSha256HttpSignatureVerifier( "rsa-sha256" to RsaSha256HttpSignatureVerifier(
DefaultSignatureHeaderParser(), RsaSha256HttpSignatureSigner() signatureHeaderParser, RsaSha256HttpSignatureSigner()
) )
), ),
DefaultSignatureHeaderParser() signatureHeaderParser
), ),
transaction transaction,
signatureHeaderParser
) )
) )
provider.setUserDetailsChecker(AccountStatusUserDetailsChecker()) provider.setUserDetailsChecker(AccountStatusUserDetailsChecker())

View File

@ -3,17 +3,17 @@
package dev.usbharu.hideout.core.domain.model.instance package dev.usbharu.hideout.core.domain.model.instance
@Suppress("ClassNaming") @Suppress("ClassNaming")
class Nodeinfo2_0() { class Nodeinfo2_0 {
var metadata: Metadata? = null var metadata: Metadata? = null
var software: Software? = null var software: Software? = null
} }
class Metadata() { class Metadata {
var nodeName: String? = null var nodeName: String? = null
var nodeDescription: String? = null var nodeDescription: String? = null
} }
class Software() { class Software {
var name: String? = null var name: String? = null
var version: String? = null var version: String? = null
} }

View File

@ -1,23 +1,15 @@
package dev.usbharu.hideout.core.infrastructure.springframework.httpsignature package dev.usbharu.hideout.core.infrastructure.springframework.httpsignature
import dev.usbharu.hideout.activitypub.service.objects.user.APUserService
import dev.usbharu.hideout.application.external.Transaction
import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException
import dev.usbharu.hideout.core.query.UserQueryService
import dev.usbharu.httpsignature.common.HttpHeaders import dev.usbharu.httpsignature.common.HttpHeaders
import dev.usbharu.httpsignature.common.HttpMethod import dev.usbharu.httpsignature.common.HttpMethod
import dev.usbharu.httpsignature.common.HttpRequest import dev.usbharu.httpsignature.common.HttpRequest
import dev.usbharu.httpsignature.verify.SignatureHeaderParser import dev.usbharu.httpsignature.verify.SignatureHeaderParser
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import kotlinx.coroutines.runBlocking
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter
import java.net.URL import java.net.URL
class HttpSignatureFilter( class HttpSignatureFilter(
private val httpSignatureHeaderParser: SignatureHeaderParser, private val httpSignatureHeaderParser: SignatureHeaderParser
private val transaction: Transaction,
private val apUserService: APUserService,
private val userQueryService: UserQueryService
) : ) :
AbstractPreAuthenticatedProcessingFilter() { AbstractPreAuthenticatedProcessingFilter() {
override fun getPreAuthenticatedPrincipal(request: HttpServletRequest?): Any? { override fun getPreAuthenticatedPrincipal(request: HttpServletRequest?): Any? {
@ -33,15 +25,6 @@ class HttpSignatureFilter(
} catch (_: RuntimeException) { } catch (_: RuntimeException) {
return "" return ""
} }
runBlocking {
transaction.transaction {
try {
userQueryService.findByKeyId(signature.keyId)
} catch (_: FailedToGetResourcesException) {
apUserService.fetchPerson(signature.keyId)
}
}
}
return signature.keyId return signature.keyId
} }

View File

@ -5,10 +5,12 @@ import dev.usbharu.hideout.core.domain.exception.FailedToGetResourcesException
import dev.usbharu.hideout.core.domain.exception.HttpSignatureVerifyException import dev.usbharu.hideout.core.domain.exception.HttpSignatureVerifyException
import dev.usbharu.hideout.core.query.UserQueryService import dev.usbharu.hideout.core.query.UserQueryService
import dev.usbharu.hideout.util.RsaUtil import dev.usbharu.hideout.util.RsaUtil
import dev.usbharu.httpsignature.common.HttpMethod
import dev.usbharu.httpsignature.common.HttpRequest import dev.usbharu.httpsignature.common.HttpRequest
import dev.usbharu.httpsignature.common.PublicKey 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 dev.usbharu.httpsignature.verify.SignatureHeaderParser
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.security.authentication.BadCredentialsException import org.springframework.security.authentication.BadCredentialsException
@ -20,14 +22,16 @@ import org.springframework.security.web.authentication.preauth.PreAuthenticatedA
class HttpSignatureUserDetailsService( class HttpSignatureUserDetailsService(
private val userQueryService: UserQueryService, private val userQueryService: UserQueryService,
private val httpSignatureVerifier: HttpSignatureVerifier, private val httpSignatureVerifier: HttpSignatureVerifier,
private val transaction: Transaction private val transaction: Transaction,
private val httpSignatureHeaderParser: SignatureHeaderParser
) : ) :
AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> { AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
override fun loadUserDetails(token: PreAuthenticatedAuthenticationToken): UserDetails = runBlocking { override fun loadUserDetails(token: PreAuthenticatedAuthenticationToken): UserDetails = runBlocking {
if (token.principal !is String) { if (token.principal !is String) {
throw IllegalStateException("Token is not String") throw IllegalStateException("Token is not String")
} }
if (token.credentials !is HttpRequest) { val credentials = token.credentials
if (credentials !is HttpRequest) {
throw IllegalStateException("Credentials is not HttpRequest") throw IllegalStateException("Credentials is not HttpRequest")
} }
@ -40,10 +44,25 @@ class HttpSignatureUserDetailsService(
} }
} }
val signature = httpSignatureHeaderParser.parse(credentials.headers)
val requiredHeaders = when (credentials.method) {
HttpMethod.GET -> getRequiredHeaders
HttpMethod.POST -> postRequiredHeaders
}
if (signature.headers.containsAll(requiredHeaders).not()) {
logger.warn(
"FAILED Verify HTTP Signature. required headers: {} but actual: {}",
requiredHeaders,
signature.headers
)
throw BadCredentialsException("HTTP Signature. required headers: $requiredHeaders")
}
@Suppress("TooGenericExceptionCaught") @Suppress("TooGenericExceptionCaught")
val verify = try { val verify = try {
httpSignatureVerifier.verify( httpSignatureVerifier.verify(
token.credentials as HttpRequest, credentials,
PublicKey(RsaUtil.decodeRsaPublicKeyPem(findByKeyId.publicKey), keyId) PublicKey(RsaUtil.decodeRsaPublicKeyPem(findByKeyId.publicKey), keyId)
) )
} catch (e: RuntimeException) { } catch (e: RuntimeException) {
@ -67,5 +86,7 @@ class HttpSignatureUserDetailsService(
companion object { companion object {
private val logger = LoggerFactory.getLogger(HttpSignatureUserDetailsService::class.java) private val logger = LoggerFactory.getLogger(HttpSignatureUserDetailsService::class.java)
private val postRequiredHeaders = listOf("(request-target)", "date", "host", "digest")
private val getRequiredHeaders = listOf("(request-target)", "date", "host")
} }
} }

View File

@ -54,6 +54,7 @@ class InboxControllerImplTest {
.post("/inbox") { .post("/inbox") {
content = json content = json
contentType = MediaType.APPLICATION_JSON contentType = MediaType.APPLICATION_JSON
header("Signature", "")
} }
.asyncDispatch() .asyncDispatch()
.andExpect { .andExpect {
@ -71,6 +72,7 @@ class InboxControllerImplTest {
.post("/inbox") { .post("/inbox") {
content = json content = json
contentType = MediaType.APPLICATION_JSON contentType = MediaType.APPLICATION_JSON
header("Signature", "")
} }
.asyncDispatch() .asyncDispatch()
.andExpect { .andExpect {
@ -96,6 +98,7 @@ class InboxControllerImplTest {
.post("/inbox") { .post("/inbox") {
content = json content = json
contentType = MediaType.APPLICATION_JSON contentType = MediaType.APPLICATION_JSON
header("Signature", "")
} }
.asyncDispatch() .asyncDispatch()
.andExpect { .andExpect {
@ -123,6 +126,7 @@ class InboxControllerImplTest {
.post("/users/hoge/inbox") { .post("/users/hoge/inbox") {
content = json content = json
contentType = MediaType.APPLICATION_JSON contentType = MediaType.APPLICATION_JSON
header("Signature", "")
} }
.asyncDispatch() .asyncDispatch()
.andExpect { .andExpect {
@ -140,6 +144,7 @@ class InboxControllerImplTest {
.post("/users/hoge/inbox") { .post("/users/hoge/inbox") {
content = json content = json
contentType = MediaType.APPLICATION_JSON contentType = MediaType.APPLICATION_JSON
header("Signature", "")
} }
.asyncDispatch() .asyncDispatch()
.andExpect { .andExpect {
@ -165,6 +170,7 @@ class InboxControllerImplTest {
.post("/users/hoge/inbox") { .post("/users/hoge/inbox") {
content = json content = json
contentType = MediaType.APPLICATION_JSON contentType = MediaType.APPLICATION_JSON
header("Signature", "")
} }
.asyncDispatch() .asyncDispatch()
.andExpect { .andExpect {