Skip to content

Commit

Permalink
login workflow implemented
Browse files Browse the repository at this point in the history
change password use case still to be implemented now we have a stubbed step just to try the workflow engine
  • Loading branch information
mrFlick72 committed Oct 18, 2023
1 parent b7d85e1 commit 7f01971
Show file tree
Hide file tree
Showing 13 changed files with 225 additions and 120 deletions.
40 changes: 40 additions & 0 deletions src/main/frontend/app/change-password/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import Template from "../component/Template";
import {Divider, Grid, Paper, ThemeProvider, Typography} from "@mui/material";
import {VpnKey} from "@mui/icons-material";
import theme from "../component/styles";
import getDataFromDomUtils from "../utils/getDataFromDomUtils";
import ComponentInitializer from "../utils/ComponentInitializer";

interface ChangePasswordPageProps {
metadata: string
}

const ResetChangePasswordPage: React.FC<ChangePasswordPageProps> = ({metadata}) => {

return (
<ThemeProvider theme={theme}>
<Template maxWidth="sm">
<Typography variant="h3" component="h3">
<VpnKey fontSize="large"/> Reset your password
</Typography>

<Grid style={{marginTop: '10px'}}>
<Divider/>
</Grid>

<Paper>
<form action="/change-password" method="post">

<button type="submit">Submit</button>
</form>
</Paper>
</Template>
</ThemeProvider>
)
}

let metadata = getDataFromDomUtils('metadata')
let page = <ResetChangePasswordPage metadata={metadata}/>;

ComponentInitializer(page)
1 change: 1 addition & 0 deletions src/main/frontend/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ module.exports = {
"500_error": path.resolve(__dirname, './app/errors/DefaultGenericErrorPage.tsx'),
login: path.resolve(__dirname, './app/login/LoginPage.tsx'),
mfa: path.resolve(__dirname, './app/mfa/index.tsx'),
changePassword: path.resolve(__dirname, './app/change-password/index.tsx'),
signup: path.resolve(__dirname, './app/signup/SignUpPage.tsx'),
successfulSignUp: path.resolve(__dirname, './app/signup/SuccessfulSignUpPage.tsx'),
successfulMailVerify: path.resolve(__dirname, './app/mail-verify/SuccessfulMailVerifyPage.tsx'),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,34 @@
package com.vauthenticator.server.config

import com.vauthenticator.server.account.repository.AccountRepository
import com.vauthenticator.server.login.CompositeLoginWorkflowEngine
import com.vauthenticator.server.login.userdetails.AccountUserDetailsService
import com.vauthenticator.server.mfa.MfaAuthentication
import com.vauthenticator.server.mfa.MfaAuthenticationHandler
import com.vauthenticator.server.mfa.MfaTrustResolver
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.ChangePasswordAfterFirstLoginWorkflowHandler
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.http.HttpMethod
import org.springframework.security.authorization.AuthorizationDecision
import org.springframework.security.authorization.AuthorizationManager
import org.springframework.security.config.annotation.ObjectPostProcessor
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.core.Authentication
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.access.ExceptionTranslationFilter
import org.springframework.security.web.access.intercept.RequestAuthorizationContext
import org.springframework.security.web.authentication.AuthenticationFailureHandler
import org.springframework.security.web.authentication.AuthenticationSuccessHandler
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler
import java.util.function.Supplier


const val adminRole = "VAUTHENTICATOR_ADMIN"
Expand All @@ -57,29 +50,20 @@ class WebSecurityConfig(
@Bean
fun defaultSecurityFilterChain(
http: HttpSecurity,
loginWorkflowEngine: AuthenticationSuccessHandler,
clientApplicationRepository: ClientApplicationRepository,
mfaAuthorizationManager: AuthorizationManager<RequestAuthorizationContext>,
accountUserDetailsService: AccountUserDetailsService
): SecurityFilterChain {
http.csrf { it.disable() }
http.headers { it.frameOptions { it.disable() } }

http.formLogin {
it.successHandler(MfaAuthenticationHandler(clientApplicationRepository, "/mfa-challenge/send"))
it.successHandler(loginWorkflowEngine)
.loginProcessingUrl(LOG_IN_URL_PAGE)
.loginPage(LOG_IN_URL_PAGE)
.permitAll()
}

http.exceptionHandling {
it.withObjectPostProcessor(object : ObjectPostProcessor<ExceptionTranslationFilter> {
override fun <O : ExceptionTranslationFilter?> postProcess(filter: O): O {
filter!!.setAuthenticationTrustResolver(MfaTrustResolver())
return filter
}
})
}
.securityContext { it.requireExplicitSave(false) };

http.logout {
it.addLogoutHandler(
Expand All @@ -94,12 +78,14 @@ class WebSecurityConfig(

http.userDetailsService(accountUserDetailsService)
http.oauth2ResourceServer { it.jwt {} }
http.securityMatcher(*WHITE_LIST, "/api/**", "/mfa-challenge/**")
http.securityMatcher(*WHITE_LIST, "/api/**", "/mfa-challenge/**", "/change-password")
.authorizeHttpRequests { authz ->
authz
.requestMatchers("/mfa-challenge/send").permitAll()
.requestMatchers("/mfa-challenge")
.access(mfaAuthorizationManager)
.requestMatchers(HttpMethod.GET, "/mfa-challenge").permitAll()
.requestMatchers(HttpMethod.POST, "/mfa-challenge").authenticated()

.requestMatchers("/change-password").permitAll()

.requestMatchers(*WHITE_LIST).permitAll()
.requestMatchers("/api/accounts").permitAll()
Expand Down Expand Up @@ -139,13 +125,14 @@ class WebSecurityConfig(
}

@Bean
fun mfaAuthorizationManager(): AuthorizationManager<RequestAuthorizationContext> {
return AuthorizationManager { authentication: Supplier<Authentication>, _: RequestAuthorizationContext ->
AuthorizationDecision(
authentication.get() is MfaAuthentication
fun loginWorkflowEngine(clientApplicationRepository: ClientApplicationRepository) =
CompositeLoginWorkflowEngine(
"/login-workflow",
listOf(
ChangePasswordAfterFirstLoginWorkflowHandler("/change-password"),
MfaAuthenticationHandler(clientApplicationRepository, "/mfa-challenge/send")
)
}
}
)


@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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<LoginWorkflowHandler>
) :
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 nextHandler = index + 1
session.setAttribute("CompositeLoginWorkflowEngine_index", nextHandler)
return handlers[index]
}

}

interface LoginWorkflowEngine {
fun workflowsNextHop(session: HttpSession): LoginWorkflowHandler
}

interface LoginWorkflowHandler {
fun view(): String

fun canHandle(request: HttpServletRequest, response: HttpServletResponse): Boolean

}

@Controller
class LoginWorkflowEngineController(private val engine: LoginWorkflowEngine) {

val defaultNextHope = SavedRequestAwareAuthenticationSuccessHandler()
private val redirectStrategy: RedirectStrategy = DefaultRedirectStrategy()

@GetMapping("/login-workflow")
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)
}
}
}
27 changes: 0 additions & 27 deletions src/main/kotlin/com/vauthenticator/server/mfa/MfaAuthentication.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,45 +1,28 @@
package com.vauthenticator.server.mfa

import com.vauthenticator.server.extentions.hasEnoughScopes
import com.vauthenticator.server.login.LoginWorkflowHandler
import com.vauthenticator.server.oauth2.clientapp.ClientAppId
import com.vauthenticator.server.oauth2.clientapp.ClientApplicationRepository
import com.vauthenticator.server.oauth2.clientapp.Scope
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.web.authentication.AuthenticationSuccessHandler
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler

class MfaAuthenticationHandler(private val clientApplicationRepository: ClientApplicationRepository, url: String) :
AuthenticationSuccessHandler {
private val withMfaSuccessHandler: AuthenticationSuccessHandler
private val withoutMfaSuccessHandler: AuthenticationSuccessHandler
class MfaAuthenticationHandler(
private val clientApplicationRepository: ClientApplicationRepository,
private val url: String
) : LoginWorkflowHandler {

init {
val withMfaSuccessHandler = SimpleUrlAuthenticationSuccessHandler(url)
withMfaSuccessHandler.setAlwaysUseDefaultTargetUrl(true)
this.withMfaSuccessHandler = withMfaSuccessHandler
this.withoutMfaSuccessHandler = SavedRequestAwareAuthenticationSuccessHandler()
}

override fun onAuthenticationSuccess(
request: HttpServletRequest, response: HttpServletResponse,
authentication: Authentication
) {
override fun view(): String = this.url

override fun canHandle(
request: HttpServletRequest,
response: HttpServletResponse,
): Boolean {
val clientId = request.session.getAttribute("clientId") as String
val isMfaRequired = clientApplicationRepository.findOne(ClientAppId(clientId))
return clientApplicationRepository.findOne(ClientAppId(clientId))
.filter { it.hasEnoughScopes(Scope.MFA_ALWAYS) }
.isPresent

if (isMfaRequired) {
SecurityContextHolder.getContext().authentication = MfaAuthentication(authentication)
withMfaSuccessHandler.onAuthenticationSuccess(request, response, authentication)
} else {
withoutMfaSuccessHandler.onAuthenticationSuccess(request, response, authentication)
}
}

}
}
11 changes: 4 additions & 7 deletions src/main/kotlin/com/vauthenticator/server/mfa/MfaController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import jakarta.servlet.http.HttpServletResponse
import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationEventPublisher
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler
import org.springframework.security.web.authentication.AuthenticationFailureHandler
import org.springframework.security.web.authentication.AuthenticationSuccessHandler
import org.springframework.stereotype.Controller
Expand Down Expand Up @@ -58,20 +56,19 @@ class MfaController(
@PostMapping("/mfa-challenge")
fun processSecondFactor(
@RequestParam("mfa-code") mfaCode: String,
authentication: MfaAuthentication,
authentication: Authentication,
request: HttpServletRequest,
response: HttpServletResponse
) {
try {
otpMfaVerifier.verifyMfaChallengeFor(authentication.name, MfaChallenge(mfaCode))

SecurityContextHolder.getContext().authentication = authentication.delegate
publisher.publishEvent(MfaSuccessEvent( authentication.delegate))
successHandler.onAuthenticationSuccess(request, response, authentication.delegate)
publisher.publishEvent(MfaSuccessEvent( authentication))
successHandler.onAuthenticationSuccess(request, response, authentication)
} catch (e: Exception) {
logger.error(e.message, e)
val mfaException = MfaException("Invalid mfa code")
publisher.publishEvent(MfaFailureEvent(authentication.delegate, mfaException))
publisher.publishEvent(MfaFailureEvent(authentication, mfaException))
mfaFailureHandler.onAuthenticationFailure(request, response, mfaException)
}
}
Expand Down
16 changes: 0 additions & 16 deletions src/main/kotlin/com/vauthenticator/server/mfa/MfaTrustResolver.kt

This file was deleted.

Loading

0 comments on commit 7f01971

Please sign in to comment.