From d6fe604253e8d2449d3e5b72aef8a2fb98d0e184 Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Fri, 20 Oct 2023 11:05:02 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20HttpSignature=E3=81=A7=E7=BD=B2?= =?UTF-8?q?=E5=90=8D=E3=82=92=E6=A4=9C=E8=A8=BC=E3=81=99=E3=82=8B=E5=AE=9F?= =?UTF-8?q?=E8=A3=85=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../usbharu/hideout/query/UserQueryService.kt | 1 + .../hideout/query/UserQueryServiceImpl.kt | 6 +++ .../service/signature/HttpSignatureFilter.kt | 51 +++++++++++++++--- .../service/signature/HttpSignatureUser.kt | 27 ++++++++++ .../HttpSignatureUserDetailsService.kt | 54 +++++++++++++++++++ .../HttpSignatureVerifierComposite.kt | 22 ++++++++ 6 files changed, 154 insertions(+), 7 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUser.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUserDetailsService.kt create mode 100644 src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureVerifierComposite.kt diff --git a/src/main/kotlin/dev/usbharu/hideout/query/UserQueryService.kt b/src/main/kotlin/dev/usbharu/hideout/query/UserQueryService.kt index 09e5972a..0bab8304 100644 --- a/src/main/kotlin/dev/usbharu/hideout/query/UserQueryService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/query/UserQueryService.kt @@ -12,4 +12,5 @@ interface UserQueryService { suspend fun findByUrl(url: String): User suspend fun findByIds(ids: List): List suspend fun existByNameAndDomain(name: String, domain: String): Boolean + suspend fun findByKeyId(keyId: String): User } diff --git a/src/main/kotlin/dev/usbharu/hideout/query/UserQueryServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/query/UserQueryServiceImpl.kt index ef2e8cd6..351a8303 100644 --- a/src/main/kotlin/dev/usbharu/hideout/query/UserQueryServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/query/UserQueryServiceImpl.kt @@ -44,4 +44,10 @@ class UserQueryServiceImpl : UserQueryService { override suspend fun existByNameAndDomain(name: String, domain: String): Boolean = Users.select { Users.name eq name and (Users.domain eq domain) }.empty().not() + + override suspend fun findByKeyId(keyId: String): User { + return Users.select { Users.keyId eq keyId } + .singleOr { FailedToGetResourcesException("keyId: $keyId is duplicate or does not exist.", it) } + .toUser() + } } 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 32dc7581..d5b633fd 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureFilter.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureFilter.kt @@ -1,12 +1,49 @@ package dev.usbharu.hideout.service.signature -import jakarta.servlet.Filter -import jakarta.servlet.FilterChain -import jakarta.servlet.ServletRequest -import jakarta.servlet.ServletResponse +import dev.usbharu.httpsignature.common.HttpHeaders +import dev.usbharu.httpsignature.common.HttpMethod +import dev.usbharu.httpsignature.common.HttpRequest +import dev.usbharu.httpsignature.verify.SignatureHeaderParser +import jakarta.servlet.http.HttpServletRequest +import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter +import java.net.URL -class HttpSignatureFilter : Filter { - override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { - chain.doFilter(request, response) +class HttpSignatureFilter(private val httpSignatureHeaderParser: SignatureHeaderParser) : + AbstractPreAuthenticatedProcessingFilter() { + 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)) + return signature.keyId } + + override fun getPreAuthenticatedCredentials(request: HttpServletRequest?): Any { + requireNotNull(request) + val url = request.requestURL.toString() + + val headersList = request.headerNames?.toList().orEmpty() + + val headers = + headersList.associateWith { header -> request.getHeaders(header)?.toList().orEmpty() } + + val method = when (val method = request.method.lowercase()) { + "get" -> HttpMethod.GET + "post" -> HttpMethod.POST + else -> { + throw IllegalArgumentException("Unsupported method: $method") + } + } + + return HttpRequest( + URL(url + request.queryString), + HttpHeaders(headers), + method + ) + } + } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUser.kt b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUser.kt new file mode 100644 index 00000000..c710854f --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUser.kt @@ -0,0 +1,27 @@ +package dev.usbharu.hideout.service.signature + +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.userdetails.User +import java.io.Serial + +class HttpSignatureUser( + username: String, + val domain: String, + credentialsNonExpired: Boolean, + accountNonLocked: Boolean, + authorities: MutableCollection? +) : User( + username, + "", + true, + true, + credentialsNonExpired, + accountNonLocked, + authorities +) { + 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 new file mode 100644 index 00000000..f35799cf --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureUserDetailsService.kt @@ -0,0 +1,54 @@ +package dev.usbharu.hideout.service.signature + +import dev.usbharu.hideout.exception.FailedToGetResourcesException +import dev.usbharu.hideout.exception.HttpSignatureVerifyException +import dev.usbharu.hideout.query.UserQueryService +import dev.usbharu.hideout.util.RsaUtil +import dev.usbharu.httpsignature.common.HttpRequest +import dev.usbharu.httpsignature.common.PublicKey +import dev.usbharu.httpsignature.verify.FailedVerification +import dev.usbharu.httpsignature.verify.HttpSignatureVerifier +import kotlinx.coroutines.runBlocking +import org.springframework.security.core.userdetails.AuthenticationUserDetailsService +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken + +class HttpSignatureUserDetailsService( + private val userQueryService: UserQueryService, + private val httpSignatureVerifier: HttpSignatureVerifier +) : + AuthenticationUserDetailsService { + override fun loadUserDetails(token: PreAuthenticatedAuthenticationToken): UserDetails = runBlocking { + 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 { + userQueryService.findByKeyId(keyId) + } catch (e: FailedToGetResourcesException) { + throw UsernameNotFoundException("User not found", e) + } + + val verify = 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, + credentialsNonExpired = true, + accountNonLocked = true, + authorities = mutableListOf() + ) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureVerifierComposite.kt b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureVerifierComposite.kt new file mode 100644 index 00000000..c6a0041f --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/service/signature/HttpSignatureVerifierComposite.kt @@ -0,0 +1,22 @@ +package dev.usbharu.hideout.service.signature + +import dev.usbharu.httpsignature.common.HttpRequest +import dev.usbharu.httpsignature.common.PublicKey +import dev.usbharu.httpsignature.verify.HttpSignatureVerifier +import dev.usbharu.httpsignature.verify.SignatureHeaderParser +import dev.usbharu.httpsignature.verify.VerificationResult + +class HttpSignatureVerifierComposite( + private val map: Map, + private val httpSignatureHeaderParser: SignatureHeaderParser +) : HttpSignatureVerifier { + override fun verify(httpRequest: HttpRequest, key: PublicKey): VerificationResult { + val signature = httpSignatureHeaderParser.parse(httpRequest.headers) + val verify = map[signature.algorithm]?.verify(httpRequest, key) + if (verify != null) { + return verify + } + + throw IllegalArgumentException("Unsupported algorithm. ${signature.algorithm}") + } +}