Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make backend session stateless #786

Draft
wants to merge 2 commits into
base: staging
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ dependencies {
implementation("jakarta.xml.bind:jakarta.xml.bind-api:4.0.2")
api("org.springframework.boot:spring-boot-configuration-processor")
api("org.springframework.boot:spring-boot-starter-data-jpa")
api("org.springframework.session:spring-session-jdbc")
api("org.springframework.boot:spring-boot-starter-oauth2-client")
api("org.springframework.boot:spring-boot-starter-security")
api("org.springframework.boot:spring-boot-starter-web")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
package hu.bme.sch.cmsch.component.login

import hu.bme.sch.cmsch.component.app.ApplicationComponent
import hu.bme.sch.cmsch.component.token.SESSION_TOKEN_COLLECTOR_ATTRIBUTE
import hu.bme.sch.cmsch.config.StartupPropertyConfig
import hu.bme.sch.cmsch.service.JwtTokenProvider
import hu.bme.sch.cmsch.util.getUserOrNull
import jakarta.servlet.http.Cookie
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean
Expand Down Expand Up @@ -37,11 +35,7 @@ class AuthschLoginController(
}

@GetMapping("/control/post-login")
fun postLogin(request: HttpServletRequest, httpResponse: HttpServletResponse, auth: Authentication?) {
if (request.getSession(true).getAttribute(SESSION_TOKEN_COLLECTOR_ATTRIBUTE) != null) {
httpResponse.sendRedirect("/api/token-after-login")
return
}
fun postLogin(httpResponse: HttpServletResponse, auth: Authentication?) {
httpResponse.sendRedirect(
if (auth != null && auth.isAuthenticated)
"/control/open-site"
Expand All @@ -51,21 +45,17 @@ class AuthschLoginController(
}

@GetMapping("/control/login")
fun loginDefault(request: HttpServletRequest): String {
fun loginDefault(): String {
return "redirect:${applicationComponent.siteUrl.getValue()}login"
}

@GetMapping("/control/logout")
fun logout(request: HttpServletRequest, auth: Authentication?, httpResponse: HttpServletResponse): String {
fun logout(auth: Authentication?, response: HttpServletResponse): String {
log.info("Logging out from user {}", auth?.getUserOrNull()?.internalId ?: "n/a")

try {
httpResponse.addCookie(createJwtCookie(null).apply { maxAge = 0 })
createJwtCookies(null).forEach { response.addCookie(it) }
SecurityContextHolder.getContext().authentication = null
val session = request.getSession(false)
session?.invalidate()
request.changeSessionId()

} catch (e: Exception) {
// It should be logged out anyway
}
Expand All @@ -80,8 +70,7 @@ class AuthschLoginController(
return "redirect:${applicationComponent.siteUrl.getValue()}?error=cannot-generate-jwt"
}
val jwtToken = jwtTokenProvider.createToken(auth.principal as CmschUser)

response.addCookie(createJwtCookie(jwtToken))
createJwtCookies(jwtToken).forEach { response.addCookie(it) }

return "redirect:${applicationComponent.siteUrl.getValue()}"
}
Expand All @@ -94,7 +83,7 @@ class AuthschLoginController(
if (auth == null)
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()

response.addCookie(createJwtCookie(jwtTokenProvider.refreshToken(auth)))
createJwtCookies(jwtTokenProvider.refreshToken(auth)).forEach { response.addCookie(it) }

return ResponseEntity.ok().build()
}
Expand All @@ -103,13 +92,22 @@ class AuthschLoginController(
return URI(url).host
}

private fun createJwtCookie(value: String?): Cookie {
return Cookie("jwt", value).apply {
isHttpOnly = true
path = "/"
maxAge = startupPropertyConfig.sessionValidityInMilliseconds.toInt() / 1000
secure = true
domain = getDomainFromUrl(applicationComponent.siteUrl.getValue())
}
private fun createJwtCookies(value: String?): List<Cookie> {
return listOf(
Cookie("jwt", value).apply {
isHttpOnly = true
path = "/"
maxAge = startupPropertyConfig.sessionValidityInMilliseconds.toInt() / 1000
secure = true
domain = getDomainFromUrl(applicationComponent.siteUrl.getValue())
},
Cookie("jwt", value).apply {
isHttpOnly = true
path = "/"
maxAge = startupPropertyConfig.sessionValidityInMilliseconds.toInt() / 1000
secure = true
domain = getDomainFromUrl(applicationComponent.adminSiteUrl.getValue())
},
)
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ import org.springframework.web.bind.annotation.*
import java.net.URLEncoder
import java.nio.charset.StandardCharsets

const val SESSION_TOKEN_COLLECTOR_ATTRIBUTE = "TOKEN_COLLECTOR_ATTRIBUTE"

@Controller
@RequestMapping("/api")
@ConditionalOnBean(TokenComponent::class)
Expand Down Expand Up @@ -56,23 +54,11 @@ class TokenApiController(
}
}

@GetMapping("/token-after-login")
fun submitTokenAfterLogin(request: HttpServletRequest, auth: Authentication): String {
val token = request.getSession(true).getAttribute(SESSION_TOKEN_COLLECTOR_ATTRIBUTE)?.toString()
request.getSession(true).setAttribute(SESSION_TOKEN_COLLECTOR_ATTRIBUTE, null)
return if (token == null) {
"redirect:${applicationComponent.siteUrl.getValue()}?error=failed-to-redeem"
} else {
collectToken(auth, token)
}
}

@GetMapping("/qr/{token}")
fun readQrManually(@PathVariable token: String, request: HttpServletRequest, auth: Authentication?): String {
fun readQrManually(@PathVariable token: String, auth: Authentication?): String {
return try {
val user = auth?.getUserOrNull()
if (user == null) {
request.getSession(true).setAttribute(SESSION_TOKEN_COLLECTOR_ATTRIBUTE, token)
"redirect:${applicationComponent.siteUrl.getValue()}login"
} else {
collectToken(auth, token)
Expand Down
36 changes: 26 additions & 10 deletions backend/src/main/kotlin/hu/bme/sch/cmsch/config/SecurityConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import com.fasterxml.jackson.databind.ObjectMapper
import hu.bme.sch.cmsch.component.countdown.CountdownFilterConfigurer
import hu.bme.sch.cmsch.component.login.LoginComponent
import hu.bme.sch.cmsch.component.login.LoginService
import hu.bme.sch.cmsch.component.login.SessionFilterConfigurer
import hu.bme.sch.cmsch.component.login.authsch.CmschAuthschUser
import hu.bme.sch.cmsch.component.login.authsch.ProfileResponse
import hu.bme.sch.cmsch.component.login.google.CmschGoogleUser
Expand All @@ -22,23 +21,29 @@ import org.springframework.context.annotation.Configuration
import org.springframework.core.Ordered
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.retry.annotation.EnableRetry
import org.springframework.security.config.Customizer
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.oauth2.client.JdbcOAuth2AuthorizedClientService
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser
import org.springframework.security.oauth2.core.user.DefaultOAuth2User
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher
import org.springframework.session.jdbc.config.annotation.web.http.EnableJdbcHttpSession
import org.springframework.web.reactive.function.client.WebClient
import java.util.*

@EnableWebSecurity
@Configuration
@EnableJdbcHttpSession
@EnableWebSecurity
@EnableRetry(order = Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnBean(LoginComponent::class)
open class SecurityConfig(
Expand All @@ -48,7 +53,6 @@ open class SecurityConfig(
private val countdownConfigurer: Optional<CountdownFilterConfigurer>,
private val authschLoginService: LoginService,
private val loginComponent: LoginComponent,
private val startupPropertyConfig: StartupPropertyConfig,
@Value("\${custom.keycloak.base-url:http://localhost:8081/auth/realms/master}") private val keycloakBaseUrl: String,
private val auditLogService: AuditLogService
) {
Expand All @@ -67,8 +71,18 @@ open class SecurityConfig(
.defaultHeader(HttpHeaders.USER_AGENT, "AuthSchKotlinAPI")
.build()


@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
fun authorizedClientService(
jdbcTemplate: JdbcTemplate,
clientRegistrationRepository: ClientRegistrationRepository
): OAuth2AuthorizedClientService = JdbcOAuth2AuthorizedClientService(jdbcTemplate, clientRegistrationRepository)

@Bean
fun securityFilterChain(
http: HttpSecurity,
authorizedClientService: OAuth2AuthorizedClientService
): SecurityFilterChain {
http.authorizeHttpRequests {
it.requestMatchers(
antMatcher("/"),
Expand Down Expand Up @@ -131,7 +145,6 @@ open class SecurityConfig(
RoleType.ADMIN.name,
RoleType.SUPERUSER.name
)

it.requestMatchers(
antMatcher("/admin/**"),
antMatcher("/cdn/**")
Expand All @@ -144,16 +157,18 @@ open class SecurityConfig(
http.formLogin { it.disable() }
http.exceptionHandling { it.accessDeniedPage("/403") }
http.with(JwtConfigurer(jwtTokenProvider), Customizer.withDefaults())
http.with(SessionFilterConfigurer(startupPropertyConfig), Customizer.withDefaults())
http.oauth2Login { oauth2 ->
oauth2.loginPage("/oauth2/authorization")
oauth2
.loginPage("/oauth2/authorization")
.authorizationEndpoint {
it.authorizationRequestResolver(
CustomAuthorizationRequestResolver(
clientRegistrationRepository, "/oauth2/authorization", loginComponent
)
)
}.userInfoEndpoint { userInfo ->
}
.authorizedClientService(authorizedClientService)
.userInfoEndpoint { userInfo ->
userInfo
.oidcUserService {
if (it.clientRegistration.clientId.contains("google")) {
Expand All @@ -163,7 +178,8 @@ open class SecurityConfig(
}
}
.userService { resolveAuthschUser(it) }
}.defaultSuccessUrl("/control/post-login")
}
.defaultSuccessUrl("/control/post-login")
}
countdownConfigurer.ifPresent { http.with(it, Customizer.withDefaults()) }
http.csrf {
Expand Down Expand Up @@ -212,7 +228,7 @@ open class SecurityConfig(
private fun resolveKeycloakUser(request: OidcUserRequest): DefaultOidcUser {
val decodedPayload = String(Base64.getDecoder().decode(request.accessToken.tokenValue.split(".")[1]))
val profile: KeycloakUserInfoResponse = objectMapper.readerFor(KeycloakUserInfoResponse::class.java)
.readValue(decodedPayload)
.readValue(decodedPayload)
val userEntity = authschLoginService.fetchKeycloakUserEntity(profile)

auditLogService.login(userEntity, "keycloak user login g:${userEntity.group} r:${userEntity.role}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,6 @@ data class StartupPropertyConfig @ConstructorBinding constructor(
val challengeOwnershipMode: OwnershipType,
val raceOwnershipMode: OwnershipType,

// Increased session
val increasedSessionTime: Int,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think, we need something like this but maybe in a different place.


// Microservice
val masterRole: Boolean,
val riddleMicroserviceEnabled: Boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ class InstanceInfoDashboard(
listOf("Time zone id", startupPropertyConfig.zoneId),
listOf("Mailgun token length", startupPropertyConfig.mailgunToken.length.toString()),
listOf("Session validity (ms)", startupPropertyConfig.sessionValidityInMilliseconds.toString()),
listOf("Increased session time (ms)", startupPropertyConfig.increasedSessionTime.toString()),
listOf("Profile QR enabled", startupPropertyConfig.profileQrEnabled.toString()),
listOf("Profile QR prefix", startupPropertyConfig.profileQrPrefix),
listOf("Profile generation target", startupPropertyConfig.profileGenerationTarget),
Expand Down
27 changes: 13 additions & 14 deletions backend/src/main/kotlin/hu/bme/sch/cmsch/jwt/JwtTokenFilter.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package hu.bme.sch.cmsch.jwt

import hu.bme.sch.cmsch.service.JwtTokenProvider
import org.slf4j.LoggerFactory
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.filter.GenericFilterBean
import java.io.IOException
import jakarta.servlet.FilterChain
import jakarta.servlet.ServletException
import jakarta.servlet.ServletRequest
import jakarta.servlet.ServletResponse
import jakarta.servlet.http.HttpServletRequest
import org.slf4j.LoggerFactory
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.filter.GenericFilterBean
import java.io.IOException


class JwtTokenFilter(
Expand All @@ -22,17 +22,16 @@ class JwtTokenFilter(
@Throws(IOException::class, ServletException::class)
override fun doFilter(req: ServletRequest, res: ServletResponse, filterChain: FilterChain) {
val httpRequest = req as HttpServletRequest
if (httpRequest.servletPath.startsWith("/api/")) {
val token: String? = jwtTokenProvider.resolveToken(httpRequest)
try {
if (token != null && jwtTokenProvider.validateToken(token)) {
val auth: Authentication = jwtTokenProvider.getAuthentication(token)
SecurityContextHolder.getContext().authentication = auth
}
} catch (e: Exception) {
log.warn("Invalid token: {} user cannot be resolved because: {}", token, e.message)
val token: String? = jwtTokenProvider.resolveToken(httpRequest)
try {
if (!token.isNullOrBlank() && jwtTokenProvider.validateToken(token)) {
val auth: Authentication = jwtTokenProvider.getAuthentication(token)
SecurityContextHolder.getContext().authentication = auth
}
} catch (e: Exception) {
log.warn("Invalid token: {} user cannot be resolved because: {}", token, e.message)
}

filterChain.doFilter(req, res)
}

Expand Down
Loading
Loading