fix(api): empty content when x-api-key is sent alongside session

Closes: #2099
This commit is contained in:
Gauthier Roebroeck 2025-10-08 14:47:10 +08:00
parent bdca990e82
commit 5a5f8d701e
5 changed files with 22 additions and 8 deletions

View file

@ -20,6 +20,8 @@ class Hasher {
return computeHash(path.inputStream())
}
fun computeHash(string: String): String = computeHash(string.byteInputStream())
fun computeHash(stream: InputStream): String {
val hash = Algorithm.XXH3_128.Seeded(SEED.toLong()).createDigest()

View file

@ -3,6 +3,7 @@ package org.gotson.komga.infrastructure.security
import jakarta.servlet.Filter
import org.gotson.komga.domain.model.UserRoles
import org.gotson.komga.infrastructure.configuration.KomgaSettingsProvider
import org.gotson.komga.infrastructure.hash.Hasher
import org.gotson.komga.infrastructure.security.apikey.ApiKeyAuthenticationFilter
import org.gotson.komga.infrastructure.security.apikey.ApiKeyAuthenticationProvider
import org.gotson.komga.infrastructure.security.apikey.HeaderApiKeyAuthenticationConverter
@ -34,6 +35,7 @@ import org.springframework.security.web.authentication.AnonymousAuthenticationFi
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher
@Configuration
@ -51,6 +53,7 @@ class SecurityConfiguration(
private val opdsAuthenticationEntryPoint: OpdsAuthenticationEntryPoint,
private val authenticationEventPublisher: AuthenticationEventPublisher,
private val tokenEncoder: TokenEncoder,
private val hasher: Hasher,
clientRegistrationRepository: InMemoryClientRegistrationRepository?,
) {
private val oauth2Enabled = clientRegistrationRepository != null
@ -158,7 +161,7 @@ class SecurityConfiguration(
)
}
http.addFilterBefore(restAuthenticationFilter(), AnonymousAuthenticationFilter::class.java)
http.addFilterAfter(restAuthenticationFilter(), BasicAuthenticationFilter::class.java)
return http.build()
}
@ -239,19 +242,19 @@ class SecurityConfiguration(
fun koboAuthenticationFilter(): Filter =
ApiKeyAuthenticationFilter(
apiKeyAuthenticationProvider(),
UriRegexApiKeyAuthenticationConverter(Regex("""/kobo/([\w-]+)"""), tokenEncoder, userAgentWebAuthenticationDetailsSource),
UriRegexApiKeyAuthenticationConverter(Regex("""/kobo/([\w-]+)"""), hasher, tokenEncoder, userAgentWebAuthenticationDetailsSource),
)
fun kosyncAuthenticationFilter(): Filter =
ApiKeyAuthenticationFilter(
apiKeyAuthenticationProvider(),
HeaderApiKeyAuthenticationConverter("X-Auth-User", tokenEncoder, userAgentWebAuthenticationDetailsSource),
HeaderApiKeyAuthenticationConverter("X-Auth-User", hasher, tokenEncoder, userAgentWebAuthenticationDetailsSource),
)
fun restAuthenticationFilter(): Filter =
ApiKeyAuthenticationFilter(
apiKeyAuthenticationProvider(),
HeaderApiKeyAuthenticationConverter("X-API-Key", tokenEncoder, userAgentWebAuthenticationDetailsSource),
HeaderApiKeyAuthenticationConverter("X-API-Key", hasher, tokenEncoder, userAgentWebAuthenticationDetailsSource),
)
fun apiKeyAuthenticationProvider(): AuthenticationManager =

View file

@ -55,6 +55,8 @@ class ApiKeyAuthenticationFilter(
} catch (ex: AuthenticationException) {
unsuccessfulAuthentication(request, response, ex)
}
filterChain.doFilter(request, response)
}
private fun unsuccessfulAuthentication(
@ -78,7 +80,6 @@ class ApiKeyAuthenticationFilter(
}
securityContextHolderStrategy.context = context
securityContextRepository.saveContext(context, request, response)
filterChain.doFilter(request, response)
}
private fun authenticationIsRequired(username: String): Boolean {

View file

@ -1,6 +1,7 @@
package org.gotson.komga.infrastructure.security.apikey
import jakarta.servlet.http.HttpServletRequest
import org.gotson.komga.infrastructure.hash.Hasher
import org.gotson.komga.infrastructure.security.TokenEncoder
import org.springframework.security.authentication.AuthenticationDetailsSource
import org.springframework.security.core.Authentication
@ -11,11 +12,13 @@ import org.springframework.security.web.authentication.AuthenticationConverter
* and convert it to an [ApiKeyAuthenticationToken]
*
* @property headerName the header name from which to retrieve the API key
* @property hasher the hasher to use to encode the API key as username in the [Authentication] object
* @property tokenEncoder the encoder to use to encode the API key in the [Authentication] object
* @property authenticationDetailsSource the [AuthenticationDetailsSource] to enrich the [Authentication] details
*/
class HeaderApiKeyAuthenticationConverter(
private val headerName: String,
private val hasher: Hasher,
private val tokenEncoder: TokenEncoder,
private val authenticationDetailsSource: AuthenticationDetailsSource<HttpServletRequest, *>,
) : AuthenticationConverter {
@ -23,7 +26,8 @@ class HeaderApiKeyAuthenticationConverter(
request
.getHeader(headerName)
?.let {
val (maskedToken, hashedToken) = it.take(6) + "*".repeat(6) to tokenEncoder.encode(it)
val maskedToken = hasher.computeHash(it)
val hashedToken = tokenEncoder.encode(it)
ApiKeyAuthenticationToken
.unauthenticated(maskedToken, hashedToken)
.apply { details = authenticationDetailsSource.buildDetails(request) }

View file

@ -1,6 +1,7 @@
package org.gotson.komga.infrastructure.security.apikey
import jakarta.servlet.http.HttpServletRequest
import org.gotson.komga.infrastructure.hash.Hasher
import org.gotson.komga.infrastructure.security.TokenEncoder
import org.springframework.security.authentication.AuthenticationDetailsSource
import org.springframework.security.core.Authentication
@ -11,11 +12,13 @@ import org.springframework.security.web.authentication.AuthenticationConverter
* request URI, and convert it to an [ApiKeyAuthenticationToken]
*
* @property tokenRegex the regex used to extract the API key
* @property tokenEncoder the encoder to use to encode the API key in the [Authentication] object
* @property hasher the hasher to use to encode the API key as username in the [Authentication] object
* @property tokenEncoder the encoder to use to encode the API key as credentials in the [Authentication] object
* @property authenticationDetailsSource the [AuthenticationDetailsSource] to enrich the [Authentication] details
*/
class UriRegexApiKeyAuthenticationConverter(
private val tokenRegex: Regex,
private val hasher: Hasher,
private val tokenEncoder: TokenEncoder,
private val authenticationDetailsSource: AuthenticationDetailsSource<HttpServletRequest, *>,
) : AuthenticationConverter {
@ -24,7 +27,8 @@ class UriRegexApiKeyAuthenticationConverter(
?.let {
tokenRegex.find(it)?.groupValues?.lastOrNull()
}?.let {
val (maskedToken, hashedToken) = it.take(6) + "*".repeat(6) to tokenEncoder.encode(it)
val maskedToken = hasher.computeHash(it)
val hashedToken = tokenEncoder.encode(it)
ApiKeyAuthenticationToken
.unauthenticated(maskedToken, hashedToken)
.apply { details = authenticationDetailsSource.buildDetails(request) }