From 6d9485e5cab87aa053a942d9ee94c5ffc7184bb3 Mon Sep 17 00:00:00 2001 From: mrflick72 Date: Sat, 21 Oct 2023 15:51:06 +0200 Subject: [PATCH] tests for the child handlers --- .../server/config/WebSecurityConfig.kt | 23 ++-- .../server/login/LoginWorkflowHandler.kt | 101 ------------------ .../login/workflow/LoginWorkflowEngine.kt | 44 ++++++++ .../workflow/LoginWorkflowEngineController.kt | 39 +++++++ .../login/workflow/LoginWorkflowHandler.kt | 21 ++++ .../server/mfa/MfaLoginWorkflowHandler.kt | 2 +- .../changepassword/ChangePasswordEndPoint.kt | 2 +- .../ChangePasswordLoginWorkflowHandler.kt | 26 +++-- .../server/mfa/MfaLoginWorkflowHandlerTest.kt | 80 ++++++++++++++ .../ChangePasswordLoginWorkflowHandlerTest.kt | 67 ++++++++++++ 10 files changed, 284 insertions(+), 121 deletions(-) delete mode 100644 src/main/kotlin/com/vauthenticator/server/login/LoginWorkflowHandler.kt create mode 100644 src/main/kotlin/com/vauthenticator/server/login/workflow/LoginWorkflowEngine.kt create mode 100644 src/main/kotlin/com/vauthenticator/server/login/workflow/LoginWorkflowEngineController.kt create mode 100644 src/main/kotlin/com/vauthenticator/server/login/workflow/LoginWorkflowHandler.kt create mode 100644 src/test/kotlin/com/vauthenticator/server/mfa/MfaLoginWorkflowHandlerTest.kt create mode 100644 src/test/kotlin/com/vauthenticator/server/password/changepassword/ChangePasswordLoginWorkflowHandlerTest.kt diff --git a/src/main/kotlin/com/vauthenticator/server/config/WebSecurityConfig.kt b/src/main/kotlin/com/vauthenticator/server/config/WebSecurityConfig.kt index fa699b0e..c434ec54 100644 --- a/src/main/kotlin/com/vauthenticator/server/config/WebSecurityConfig.kt +++ b/src/main/kotlin/com/vauthenticator/server/config/WebSecurityConfig.kt @@ -1,15 +1,16 @@ package com.vauthenticator.server.config import com.vauthenticator.server.account.repository.AccountRepository -import com.vauthenticator.server.login.CompositeLoginWorkflowEngine -import com.vauthenticator.server.login.LOGIN_ENGINE_BROKER_PAGE import com.vauthenticator.server.login.userdetails.AccountUserDetailsService +import com.vauthenticator.server.login.workflow.CompositeLoginWorkflowEngine +import com.vauthenticator.server.login.workflow.LOGIN_ENGINE_BROKER_PAGE import com.vauthenticator.server.mfa.MfaLoginWorkflowHandler import com.vauthenticator.server.oauth2.clientapp.ClientApplicationRepository import com.vauthenticator.server.oauth2.clientapp.Scope import com.vauthenticator.server.oidc.logout.ClearSessionStateLogoutHandler import com.vauthenticator.server.oidc.sessionmanagement.SessionManagementFactory import com.vauthenticator.server.password.BcryptVAuthenticatorPasswordEncoder +import com.vauthenticator.server.password.changepassword.CHANGE_PASSWORD_URL import com.vauthenticator.server.password.changepassword.ChangePasswordLoginWorkflowHandler import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -123,13 +124,19 @@ class WebSecurityConfig( } @Bean - fun loginWorkflowEngine(clientApplicationRepository: ClientApplicationRepository) = + fun loginWorkflowEngine( + accountRepository: AccountRepository, + clientApplicationRepository: ClientApplicationRepository + ) = CompositeLoginWorkflowEngine( - LOGIN_ENGINE_BROKER_PAGE, listOf( MfaLoginWorkflowHandler(clientApplicationRepository, "/mfa-challenge/send"), - ChangePasswordLoginWorkflowHandler("/change-password") - ) + ChangePasswordLoginWorkflowHandler( + accountRepository, + SimpleUrlAuthenticationSuccessHandler(CHANGE_PASSWORD_URL) + ) + ), + SimpleUrlAuthenticationSuccessHandler(LOGIN_ENGINE_BROKER_PAGE) ) @@ -172,6 +179,7 @@ class WebSecurityConfig( fun successHandler(): AuthenticationSuccessHandler { return SavedRequestAwareAuthenticationSuccessHandler() } + @Bean fun nextHopeLoginWorkflowSuccessHandler(): AuthenticationSuccessHandler { return SimpleUrlAuthenticationSuccessHandler(LOGIN_ENGINE_BROKER_PAGE) @@ -181,7 +189,8 @@ class WebSecurityConfig( fun mfaFailureHandler(): AuthenticationFailureHandler { return SimpleUrlAuthenticationFailureHandler("/mfa-challenge?error") } - @Bean + + @Bean fun changePasswordFailureHandler(): AuthenticationFailureHandler { return SimpleUrlAuthenticationFailureHandler("/change-password?error") } diff --git a/src/main/kotlin/com/vauthenticator/server/login/LoginWorkflowHandler.kt b/src/main/kotlin/com/vauthenticator/server/login/LoginWorkflowHandler.kt deleted file mode 100644 index baa0c8c9..00000000 --- a/src/main/kotlin/com/vauthenticator/server/login/LoginWorkflowHandler.kt +++ /dev/null @@ -1,101 +0,0 @@ -package com.vauthenticator.server.login - -import jakarta.servlet.http.HttpServletRequest -import jakarta.servlet.http.HttpServletResponse -import jakarta.servlet.http.HttpSession -import org.springframework.security.core.Authentication -import org.springframework.security.core.context.SecurityContextHolder -import org.springframework.security.web.DefaultRedirectStrategy -import org.springframework.security.web.RedirectStrategy -import org.springframework.security.web.authentication.AuthenticationSuccessHandler -import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler -import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler -import org.springframework.stereotype.Controller -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.servlet.ModelAndView -import java.util.Optional.* -import kotlin.jvm.optionals.getOrElse - -class CompositeLoginWorkflowEngine( - val url: String, - private val handlers: List -) : - LoginWorkflowEngine, AuthenticationSuccessHandler { - private val defaultSuccessHandler: AuthenticationSuccessHandler - - init { - this.defaultSuccessHandler = SimpleUrlAuthenticationSuccessHandler(url) - this.defaultSuccessHandler.setAlwaysUseDefaultTargetUrl(true) - } - - override fun onAuthenticationSuccess( - request: HttpServletRequest, - response: HttpServletResponse, - authentication: Authentication - ) { - println("authentication on engine: $authentication") - SecurityContextHolder.getContext().authentication = authentication - this.defaultSuccessHandler.onAuthenticationSuccess(request, response, authentication) - } - - override fun workflowsNextHop(session: HttpSession): LoginWorkflowHandler { - val index = ofNullable(session.getAttribute("CompositeLoginWorkflowEngine_index")).getOrElse { 0 } as Int - val nextHandlerIndex = index + 1 - - return if (nextHandlerIndex > handlers.size) { - DefaultLoginWorkflowHandler - } else { - session.setAttribute("CompositeLoginWorkflowEngine_index", nextHandlerIndex) - handlers[index] - } - - } - -} - -interface LoginWorkflowEngine { - fun workflowsNextHop(session: HttpSession): LoginWorkflowHandler -} - -object DefaultLoginWorkflowHandler : LoginWorkflowHandler { - override fun view() = "" - - override fun canHandle(request: HttpServletRequest, response: HttpServletResponse) = false - -} - -interface LoginWorkflowHandler { - fun view(): String - - fun canHandle(request: HttpServletRequest, response: HttpServletResponse): Boolean - -} - -const val LOGIN_ENGINE_BROKER_PAGE = "/login-workflow" - -@Controller -class LoginWorkflowEngineController(private val engine: LoginWorkflowEngine) { - - - val defaultNextHope = SavedRequestAwareAuthenticationSuccessHandler() - private val redirectStrategy: RedirectStrategy = DefaultRedirectStrategy() - - @GetMapping(LOGIN_ENGINE_BROKER_PAGE) - fun view( - modelAndView: ModelAndView, - session: HttpSession, - authentication: Authentication?, - request: HttpServletRequest, - response: HttpServletResponse - ) { - val workflowsNextHop = engine.workflowsNextHop(session) - println("workflowsNextHop: ${workflowsNextHop}") - - if (workflowsNextHop.canHandle(request, response)) { - println("go to redirect on ${workflowsNextHop.view()}") - redirectStrategy.sendRedirect(request, response, workflowsNextHop.view()) - } else { - defaultNextHope.onAuthenticationSuccess(request, response, authentication) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/vauthenticator/server/login/workflow/LoginWorkflowEngine.kt b/src/main/kotlin/com/vauthenticator/server/login/workflow/LoginWorkflowEngine.kt new file mode 100644 index 00000000..6c0c69ee --- /dev/null +++ b/src/main/kotlin/com/vauthenticator/server/login/workflow/LoginWorkflowEngine.kt @@ -0,0 +1,44 @@ +package com.vauthenticator.server.login.workflow + +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import jakarta.servlet.http.HttpSession +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.web.authentication.AuthenticationSuccessHandler +import java.util.* +import kotlin.jvm.optionals.getOrElse + +interface LoginWorkflowEngine { + fun workflowsNextHop(session: HttpSession): LoginWorkflowHandler +} + +class CompositeLoginWorkflowEngine( + private val handlers: List, + private val defaultSuccessHandler: AuthenticationSuccessHandler +) : LoginWorkflowEngine, AuthenticationSuccessHandler { + + override fun onAuthenticationSuccess( + request: HttpServletRequest, + response: HttpServletResponse, + authentication: Authentication + ) { + SecurityContextHolder.getContext().authentication = authentication + this.defaultSuccessHandler.onAuthenticationSuccess(request, response, authentication) + } + + override fun workflowsNextHop(session: HttpSession): LoginWorkflowHandler { + val index = Optional.ofNullable(session.getAttribute("CompositeLoginWorkflowEngine_index")).getOrElse { 0 } as Int + val nextHandlerIndex = index + 1 + + return if (nextHandlerIndex > handlers.size) { + DefaultLoginWorkflowHandler + } else { + session.setAttribute("CompositeLoginWorkflowEngine_index", nextHandlerIndex) + handlers[index] + } + + } + +} + diff --git a/src/main/kotlin/com/vauthenticator/server/login/workflow/LoginWorkflowEngineController.kt b/src/main/kotlin/com/vauthenticator/server/login/workflow/LoginWorkflowEngineController.kt new file mode 100644 index 00000000..05c160fc --- /dev/null +++ b/src/main/kotlin/com/vauthenticator/server/login/workflow/LoginWorkflowEngineController.kt @@ -0,0 +1,39 @@ +package com.vauthenticator.server.login.workflow + +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import jakarta.servlet.http.HttpSession +import org.slf4j.LoggerFactory +import org.springframework.security.core.Authentication +import org.springframework.security.web.DefaultRedirectStrategy +import org.springframework.security.web.RedirectStrategy +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.GetMapping + +@Controller +class LoginWorkflowEngineController(private val engine: LoginWorkflowEngine) { + + private val logger = LoggerFactory.getLogger(LoginWorkflowEngineController::class.java) + + val defaultNextHope = SavedRequestAwareAuthenticationSuccessHandler() + private val redirectStrategy: RedirectStrategy = DefaultRedirectStrategy() + + @GetMapping(LOGIN_ENGINE_BROKER_PAGE) + fun view( + session: HttpSession, + request: HttpServletRequest, + response: HttpServletResponse, + authentication: Authentication + ) { + val workflowsNextHop = engine.workflowsNextHop(session) + logger.debug("workflowsNextHop: $workflowsNextHop") + + if (workflowsNextHop.canHandle(request, response)) { + logger.debug("go to redirect on ${workflowsNextHop.view()}") + redirectStrategy.sendRedirect(request, response, workflowsNextHop.view()) + } else { + defaultNextHope.onAuthenticationSuccess(request, response, authentication) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/vauthenticator/server/login/workflow/LoginWorkflowHandler.kt b/src/main/kotlin/com/vauthenticator/server/login/workflow/LoginWorkflowHandler.kt new file mode 100644 index 00000000..9645092a --- /dev/null +++ b/src/main/kotlin/com/vauthenticator/server/login/workflow/LoginWorkflowHandler.kt @@ -0,0 +1,21 @@ +package com.vauthenticator.server.login.workflow + +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse + + +const val LOGIN_ENGINE_BROKER_PAGE = "/login-workflow" + +interface LoginWorkflowHandler { + + fun view(): String + + fun canHandle(request: HttpServletRequest, response: HttpServletResponse): Boolean +} + +object DefaultLoginWorkflowHandler : LoginWorkflowHandler { + override fun view() = "" + + override fun canHandle(request: HttpServletRequest, response: HttpServletResponse) = false + +} \ No newline at end of file diff --git a/src/main/kotlin/com/vauthenticator/server/mfa/MfaLoginWorkflowHandler.kt b/src/main/kotlin/com/vauthenticator/server/mfa/MfaLoginWorkflowHandler.kt index 9c54b928..66a4bb30 100644 --- a/src/main/kotlin/com/vauthenticator/server/mfa/MfaLoginWorkflowHandler.kt +++ b/src/main/kotlin/com/vauthenticator/server/mfa/MfaLoginWorkflowHandler.kt @@ -1,7 +1,7 @@ package com.vauthenticator.server.mfa import com.vauthenticator.server.extentions.hasEnoughScopes -import com.vauthenticator.server.login.LoginWorkflowHandler +import com.vauthenticator.server.login.workflow.LoginWorkflowHandler import com.vauthenticator.server.oauth2.clientapp.ClientAppId import com.vauthenticator.server.oauth2.clientapp.ClientApplicationRepository import com.vauthenticator.server.oauth2.clientapp.Scope diff --git a/src/main/kotlin/com/vauthenticator/server/password/changepassword/ChangePasswordEndPoint.kt b/src/main/kotlin/com/vauthenticator/server/password/changepassword/ChangePasswordEndPoint.kt index f33e0cb0..84ab7747 100644 --- a/src/main/kotlin/com/vauthenticator/server/password/changepassword/ChangePasswordEndPoint.kt +++ b/src/main/kotlin/com/vauthenticator/server/password/changepassword/ChangePasswordEndPoint.kt @@ -14,7 +14,7 @@ import org.springframework.web.bind.annotation.RestController @RestController class ChangePasswordEndPoint(val changePassword: ChangePassword) { - @PutMapping("/api/password") + @PutMapping("/api/account/password") fun sendVerifyMail( @RequestBody request: Map, principal: JwtAuthenticationToken diff --git a/src/main/kotlin/com/vauthenticator/server/password/changepassword/ChangePasswordLoginWorkflowHandler.kt b/src/main/kotlin/com/vauthenticator/server/password/changepassword/ChangePasswordLoginWorkflowHandler.kt index 574cfeb7..cd5e13c5 100644 --- a/src/main/kotlin/com/vauthenticator/server/password/changepassword/ChangePasswordLoginWorkflowHandler.kt +++ b/src/main/kotlin/com/vauthenticator/server/password/changepassword/ChangePasswordLoginWorkflowHandler.kt @@ -1,26 +1,30 @@ package com.vauthenticator.server.password.changepassword -import com.vauthenticator.server.login.LoginWorkflowHandler +import com.vauthenticator.server.account.AccountMandatoryAction +import com.vauthenticator.server.account.repository.AccountRepository +import com.vauthenticator.server.login.workflow.LoginWorkflowHandler import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse -import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.web.authentication.AuthenticationSuccessHandler -class ChangePasswordLoginWorkflowHandler(val url: String) : - LoginWorkflowHandler { +const val CHANGE_PASSWORD_URL = "/change-password" - private val handler = SimpleUrlAuthenticationSuccessHandler(url) +class ChangePasswordLoginWorkflowHandler( + val accountRepository: AccountRepository, + val handler: AuthenticationSuccessHandler +) : LoginWorkflowHandler { - init { - handler.setAlwaysUseDefaultTargetUrl(true) - } - - override fun view(): String = "/change-password" + override fun view(): String = CHANGE_PASSWORD_URL override fun canHandle( request: HttpServletRequest, response: HttpServletResponse, ): Boolean { - return true + val username = SecurityContextHolder.getContext().authentication.name + return accountRepository.accountFor(username) + .map { account -> account.mandatoryAction == AccountMandatoryAction.RESET_PASSWORD } + .orElseGet { false } } } diff --git a/src/test/kotlin/com/vauthenticator/server/mfa/MfaLoginWorkflowHandlerTest.kt b/src/test/kotlin/com/vauthenticator/server/mfa/MfaLoginWorkflowHandlerTest.kt new file mode 100644 index 00000000..85de6958 --- /dev/null +++ b/src/test/kotlin/com/vauthenticator/server/mfa/MfaLoginWorkflowHandlerTest.kt @@ -0,0 +1,80 @@ +package com.vauthenticator.server.mfa + +import com.vauthenticator.server.clientapp.ClientAppFixture +import com.vauthenticator.server.oauth2.clientapp.* +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.verify +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import jakarta.servlet.http.HttpSession +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import java.util.* + +@ExtendWith(MockKExtension::class) +class MfaLoginWorkflowHandlerTest { + + @MockK + lateinit var clientApplicationRepository: ClientApplicationRepository + + @MockK + lateinit var request: HttpServletRequest + + @MockK + lateinit var resposnse: HttpServletResponse + + @MockK + lateinit var session: HttpSession + + private val clientAppId = ClientAppFixture.aClientAppId() + private val clientApp = ClientAppFixture.aClientApp(clientAppId) + + @Test + fun `when the mfa is required`() { + val clientApp = clientApp.copy(scopes = Scopes.from(Scope.MFA_ALWAYS)) + + val uut = MfaLoginWorkflowHandler(clientApplicationRepository, "") + + given(clientAppId, clientApp) + + val actual = uut.canHandle(request, resposnse) + + assertTrue(actual) + + verifyExpectationFor(clientAppId) + } + + @Test + fun `when the mfa is not required`() { + val clientApp = ClientAppFixture.aClientApp(clientAppId) + + val uut = MfaLoginWorkflowHandler(clientApplicationRepository, "") + + given(clientAppId, clientApp) + + val actual = uut.canHandle(request, resposnse) + + assertFalse(actual) + + verifyExpectationFor(clientAppId) + } + + private fun given( + clientAppId: ClientAppId, + clientApp: ClientApplication + ) { + every { request.session } returns session + every { session.getAttribute("clientId") } returns clientAppId.content + every { clientApplicationRepository.findOne(clientAppId) } returns Optional.of(clientApp) + } + + private fun verifyExpectationFor(clientAppId: ClientAppId) { + verify { request.session } + verify { session.getAttribute("clientId") } + verify { clientApplicationRepository.findOne(clientAppId) } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/vauthenticator/server/password/changepassword/ChangePasswordLoginWorkflowHandlerTest.kt b/src/test/kotlin/com/vauthenticator/server/password/changepassword/ChangePasswordLoginWorkflowHandlerTest.kt new file mode 100644 index 00000000..5b5bc2ca --- /dev/null +++ b/src/test/kotlin/com/vauthenticator/server/password/changepassword/ChangePasswordLoginWorkflowHandlerTest.kt @@ -0,0 +1,67 @@ +package com.vauthenticator.server.password.changepassword + +import com.vauthenticator.server.account.AccountMandatoryAction.RESET_PASSWORD +import com.vauthenticator.server.account.AccountTestFixture +import com.vauthenticator.server.account.repository.AccountRepository +import com.vauthenticator.server.support.SecurityFixture.principalFor +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.verify +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.web.authentication.AuthenticationSuccessHandler +import java.util.* + +@ExtendWith(MockKExtension::class) +class ChangePasswordLoginWorkflowHandlerTest { + + @MockK + lateinit var handler: AuthenticationSuccessHandler + + @MockK + lateinit var accountRepository: AccountRepository + + @MockK + lateinit var request: HttpServletRequest + + @MockK + lateinit var response: HttpServletResponse + + + private val account = AccountTestFixture.anAccount() + + @Test + fun `when the change password is required`() { + val account = account.copy(mandatoryAction = RESET_PASSWORD) + val uut = ChangePasswordLoginWorkflowHandler(accountRepository, handler) + + SecurityContextHolder.getContext().authentication = principalFor(account.email) + every { accountRepository.accountFor(account.email) } returns Optional.of(account) + + val actual = uut.canHandle(request, response) + + verify { accountRepository.accountFor(account.email) } + + assertTrue(actual) + } + @Test + fun `when the change password is not required`() { + val uut = ChangePasswordLoginWorkflowHandler(accountRepository, handler) + + SecurityContextHolder.getContext().authentication = principalFor(account.email) + every { accountRepository.accountFor(account.email) } returns Optional.of(account) + + val actual = uut.canHandle(request, response) + + verify { accountRepository.accountFor(account.email) } + + assertFalse(actual) + } + +} \ No newline at end of file