diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/hash/Hasher.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/hash/Hasher.kt index 4f785bec4..097714288 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/hash/Hasher.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/hash/Hasher.kt @@ -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() diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/SecurityConfiguration.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/SecurityConfiguration.kt index ed0ef6033..3608beabc 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/SecurityConfiguration.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/SecurityConfiguration.kt @@ -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 = diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/apikey/ApiKeyAuthenticationFilter.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/apikey/ApiKeyAuthenticationFilter.kt index c8237cfde..48cb328c9 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/apikey/ApiKeyAuthenticationFilter.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/apikey/ApiKeyAuthenticationFilter.kt @@ -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 { diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/apikey/HeaderApiKeyAuthenticationConverter.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/apikey/HeaderApiKeyAuthenticationConverter.kt index 7a7d9ec82..1fb226d17 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/apikey/HeaderApiKeyAuthenticationConverter.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/apikey/HeaderApiKeyAuthenticationConverter.kt @@ -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, ) : 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) } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/apikey/UriRegexApiKeyAuthenticationConverter.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/apikey/UriRegexApiKeyAuthenticationConverter.kt index 6b81742f5..dad992089 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/apikey/UriRegexApiKeyAuthenticationConverter.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/apikey/UriRegexApiKeyAuthenticationConverter.kt @@ -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, ) : 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) }