diff --git a/komga/build.gradle.kts b/komga/build.gradle.kts index c796343b5..9ce98c2bc 100644 --- a/komga/build.gradle.kts +++ b/komga/build.gradle.kts @@ -7,7 +7,7 @@ plugins { kotlin("plugin.spring") kotlin("kapt") } - id("org.springframework.boot") version "2.5.2" + id("org.springframework.boot") version "2.5.5" id("com.gorylenko.gradle-git-properties") version "2.3.1" id("nu.studer.jooq") version "5.2.2" id("org.flywaydb.flyway") version "7.10.0" @@ -22,7 +22,7 @@ dependencies { implementation(kotlin("stdlib-jdk8")) implementation(kotlin("reflect")) - implementation(platform("org.springframework.boot:spring-boot-dependencies:2.5.2")) + implementation(platform("org.springframework.boot:spring-boot-dependencies:2.5.5")) implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-validation") @@ -32,9 +32,10 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-thymeleaf") implementation("org.springframework.boot:spring-boot-starter-artemis") implementation("org.springframework.boot:spring-boot-starter-jooq") - implementation("org.springframework.session:spring-session-jdbc") + implementation("org.springframework.session:spring-session-core") + implementation("com.github.gotson:spring-session-caffeine:1.0.3") - kapt("org.springframework.boot:spring-boot-configuration-processor:2.5.2") + kapt("org.springframework.boot:spring-boot-configuration-processor:2.5.5") implementation("org.apache.activemq:artemis-jms-server") @@ -106,7 +107,7 @@ dependencies { testImplementation("com.tngtech.archunit:archunit-junit5:0.19.0") - developmentOnly("org.springframework.boot:spring-boot-devtools:2.5.2") + developmentOnly("org.springframework.boot:spring-boot-devtools:2.5.5") } val webui = "$rootDir/komga-webui" diff --git a/komga/src/flyway/resources/db/migration/sqlite/V20211006141333__remove_spring_session.sql b/komga/src/flyway/resources/db/migration/sqlite/V20211006141333__remove_spring_session.sql new file mode 100644 index 000000000..c376f04a2 --- /dev/null +++ b/komga/src/flyway/resources/db/migration/sqlite/V20211006141333__remove_spring_session.sql @@ -0,0 +1,2 @@ +drop table if exists SPRING_SESSION_ATTRIBUTES; +drop table if exists SPRING_SESSION; diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/KomgaUserLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/KomgaUserLifecycle.kt index 71552974c..138abf468 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/KomgaUserLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/KomgaUserLifecycle.kt @@ -60,10 +60,8 @@ class KomgaUserLifecycle( private fun expireSessions(user: KomgaUser) { logger.info { "Expiring all sessions for user: ${user.email}" } - sessionRegistry.allPrincipals - .filterIsInstance() - .filter { it.user.id == user.id } - .flatMap { sessionRegistry.getAllSessions(it, false) } + sessionRegistry + .getAllSessions(KomgaPrincipal(user), false) .forEach { logger.info { "Expiring session: ${it.sessionId}" } it.expireNow() diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/configuration/KomgaProperties.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/configuration/KomgaProperties.kt index ed3c64165..ea34fa3a8 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/configuration/KomgaProperties.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/configuration/KomgaProperties.kt @@ -1,8 +1,11 @@ package org.gotson.komga.infrastructure.configuration import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.boot.convert.DurationUnit import org.springframework.stereotype.Component import org.springframework.validation.annotation.Validated +import java.time.Duration +import java.time.temporal.ChronoUnit import javax.validation.constraints.NotBlank import javax.validation.constraints.Positive @@ -24,6 +27,9 @@ class KomgaProperties { var rememberMe = RememberMe() + @DurationUnit(ChronoUnit.SECONDS) + var sessionTimeout: Duration = Duration.ofMinutes(30) + var nativeWebp: Boolean = true var database = Database() diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/CorsConfiguration.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/CorsConfiguration.kt index e16f81d62..3c50fe8aa 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/CorsConfiguration.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/CorsConfiguration.kt @@ -18,13 +18,11 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource import java.util.Collections @Configuration -class CorsConfiguration( - private val komgaProperties: KomgaProperties -) { +class CorsConfiguration { @Bean @Conditional(CorsAllowedOriginsPresent::class) - fun corsConfigurationSource(): UrlBasedCorsConfigurationSource = + fun corsConfigurationSource(sessionHeaderName: String, komgaProperties: KomgaProperties): UrlBasedCorsConfigurationSource = UrlBasedCorsConfigurationSource().apply { registerCorsConfiguration( "/**", @@ -33,6 +31,7 @@ class CorsConfiguration( allowedMethods = HttpMethod.values().map { it.name } allowCredentials = true addExposedHeader(HttpHeaders.CONTENT_DISPOSITION) + addExposedHeader(sessionHeaderName) } ) } 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 6ed361a97..5224b3842 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 @@ -20,6 +20,7 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException import org.springframework.security.oauth2.core.oidc.user.OidcUser import org.springframework.security.oauth2.core.user.OAuth2User import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices private val logger = KotlinLogging.logger {} @@ -32,14 +33,14 @@ class SecurityConfiguration( private val oauth2UserService: OAuth2UserService, private val oidcUserService: OAuth2UserService, private val sessionRegistry: SessionRegistry, + private val sessionCookieName: String, + private val userAgentWebAuthenticationDetailsSource: WebAuthenticationDetailsSource, clientRegistrationRepository: InMemoryClientRegistrationRepository?, ) : WebSecurityConfigurerAdapter() { private val oauth2Enabled = clientRegistrationRepository != null override fun configure(http: HttpSecurity) { - val userAgentWebAuthenticationDetailsSource = UserAgentWebAuthenticationDetailsSource() - http .cors {} .csrf { it.disable() } @@ -52,6 +53,7 @@ class SecurityConfiguration( it.antMatchers( "/api/v1/claim", "/api/v1/oauth2/providers", + "/set-cookie", ).permitAll() // all other endpoints are restricted to authenticated users @@ -72,7 +74,7 @@ class SecurityConfiguration( .logout { it.logoutUrl("/api/v1/users/logout") - it.deleteCookies("JSESSIONID") + it.deleteCookies(sessionCookieName) it.invalidateHttpSession(true) } diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/SessionRegistryConfiguration.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/SessionRegistryConfiguration.kt deleted file mode 100644 index bf5aeb906..000000000 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/SessionRegistryConfiguration.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.gotson.komga.infrastructure.security - -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.security.core.session.SessionRegistry -import org.springframework.security.core.session.SessionRegistryImpl - -@Configuration -class SessionRegistryConfiguration { - @Bean - fun sessionRegistry(): SessionRegistry { - return SessionRegistryImpl() - } -} diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/UserAgentWebAuthenticationDetailsSource.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/UserAgentWebAuthenticationDetailsSource.kt index e5aa7048f..a8922952b 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/UserAgentWebAuthenticationDetailsSource.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/UserAgentWebAuthenticationDetailsSource.kt @@ -1,8 +1,10 @@ package org.gotson.komga.infrastructure.security import org.springframework.security.web.authentication.WebAuthenticationDetailsSource +import org.springframework.stereotype.Component import javax.servlet.http.HttpServletRequest +@Component class UserAgentWebAuthenticationDetailsSource : WebAuthenticationDetailsSource() { override fun buildDetails(context: HttpServletRequest): UserAgentWebAuthenticationDetails = UserAgentWebAuthenticationDetails(context) diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/session/SessionConfiguration.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/session/SessionConfiguration.kt new file mode 100644 index 000000000..0517fe160 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/session/SessionConfiguration.kt @@ -0,0 +1,45 @@ +package org.gotson.komga.infrastructure.security.session + +import com.github.gotson.spring.session.caffeine.CaffeineIndexedSessionRepository +import com.github.gotson.spring.session.caffeine.config.annotation.web.http.EnableCaffeineHttpSession +import org.gotson.komga.infrastructure.configuration.KomgaProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.core.session.SessionRegistry +import org.springframework.session.FindByIndexNameSessionRepository +import org.springframework.session.config.SessionRepositoryCustomizer +import org.springframework.session.security.SpringSessionBackedSessionRegistry +import org.springframework.session.web.http.CookieSerializer +import org.springframework.session.web.http.DefaultCookieSerializer +import org.springframework.session.web.http.HttpSessionIdResolver + +@EnableCaffeineHttpSession +@Configuration +class SessionConfiguration { + + @Bean + fun sessionCookieName() = "SESSION" + + @Bean + fun sessionHeaderName() = "X-Auth-Token" + + @Bean + fun cookieSerializer(sessionCookieName: String): CookieSerializer = + DefaultCookieSerializer().apply { + setCookieName(sessionCookieName) + } + + @Bean + fun httpSessionIdResolver(sessionHeaderName: String, cookieSerializer: CookieSerializer): HttpSessionIdResolver = + SmartHttpSessionIdResolver(sessionHeaderName, cookieSerializer) + + @Bean + fun customizeSessionRepository(komgaProperties: KomgaProperties) = + SessionRepositoryCustomizer() { + it.setDefaultMaxInactiveInterval(komgaProperties.sessionTimeout.seconds.toInt()) + } + + @Bean + fun sessionRegistry(sessionRepository: FindByIndexNameSessionRepository<*>): SessionRegistry = + SpringSessionBackedSessionRegistry(sessionRepository) +} diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/session/SmartHttpSessionIdResolver.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/session/SmartHttpSessionIdResolver.kt new file mode 100644 index 000000000..264b67e3f --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/session/SmartHttpSessionIdResolver.kt @@ -0,0 +1,29 @@ +package org.gotson.komga.infrastructure.security.session + +import org.springframework.session.web.http.CookieHttpSessionIdResolver +import org.springframework.session.web.http.CookieSerializer +import org.springframework.session.web.http.HeaderHttpSessionIdResolver +import org.springframework.session.web.http.HttpSessionIdResolver +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +class SmartHttpSessionIdResolver( + private val sessionHeaderName: String, + cookieSerializer: CookieSerializer, +) : HttpSessionIdResolver { + private val cookie = CookieHttpSessionIdResolver().apply { setCookieSerializer(cookieSerializer) } + private val header = HeaderHttpSessionIdResolver(sessionHeaderName) + + override fun resolveSessionIds(request: HttpServletRequest): List = + request.getResolver().resolveSessionIds(request) + + override fun setSessionId(request: HttpServletRequest, response: HttpServletResponse, sessionId: String) { + request.getResolver().setSessionId(request, response, sessionId) + } + + override fun expireSession(request: HttpServletRequest, response: HttpServletResponse) { + request.getResolver().expireSession(request, response) + } + + private fun HttpServletRequest.getResolver() = if (this.getHeader(sessionHeaderName) != null) header else cookie +} diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/LoginController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/LoginController.kt new file mode 100644 index 000000000..5ea8242da --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/rest/LoginController.kt @@ -0,0 +1,25 @@ +package org.gotson.komga.interfaces.rest + +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.session.web.http.CookieSerializer +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.bind.annotation.RestController +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse +import javax.servlet.http.HttpSession + +@RestController +@RequestMapping(produces = [MediaType.APPLICATION_JSON_VALUE]) +class LoginController( + private val cookieSerializer: CookieSerializer, +) { + + @GetMapping("api/v1/login/set-cookie") + @ResponseStatus(HttpStatus.NO_CONTENT) + fun headerToCookie(request: HttpServletRequest, response: HttpServletResponse, session: HttpSession) { + cookieSerializer.writeCookieValue(CookieSerializer.CookieValue(request, response, session.id)) + } +} diff --git a/komga/src/main/resources/application-dev.yml b/komga/src/main/resources/application-dev.yml index 1fb362db7..001129222 100644 --- a/komga/src/main/resources/application-dev.yml +++ b/komga/src/main/resources/application-dev.yml @@ -26,6 +26,7 @@ logging: # web: DEBUG # com.zaxxer.hikari: DEBUG # org.springframework.jms: DEBUG +# org.springframework.boot.autoconfigure: DEBUG # org.springframework.security.web.FilterChainProxy: DEBUG logback: rollingpolicy: diff --git a/komga/src/main/resources/application.yml b/komga/src/main/resources/application.yml index 94d3de0bd..5f117bb02 100644 --- a/komga/src/main/resources/application.yml +++ b/komga/src/main/resources/application.yml @@ -16,6 +16,7 @@ komga: file: \${user.home}/.komga/database.sqlite lucene: data-directory: \${user.home}/.komga/lucene + session-timeout: 7d spring: flyway: @@ -35,10 +36,6 @@ spring: web: resources: add-mappings: false - session: - store-type: none - jdbc: - initialize-schema: never server: servlet.session.timeout: 7d