package org.apereo.cas.config; import com.google.common.base.Throwables; import org.apereo.cas.CipherExecutor; import org.apereo.cas.configuration.CasConfigurationProperties; import org.apereo.cas.web.flow.CasDefaultFlowUrlHandler; import org.apereo.cas.web.flow.CasWebflowConfigurer; import org.apereo.cas.web.flow.DefaultWebflowConfigurer; import org.apereo.cas.web.flow.LogoutConversionService; import org.apereo.spring.webflow.plugin.ClientFlowExecutionRepository; import org.apereo.spring.webflow.plugin.EncryptedTranscoder; import org.apereo.spring.webflow.plugin.Transcoder; import org.cryptacular.bean.CipherBean; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.binding.convert.ConversionService; import org.springframework.binding.expression.ExpressionParser; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.web.servlet.HandlerAdapter; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; import org.springframework.webflow.config.FlowBuilderServicesBuilder; import org.springframework.webflow.config.FlowDefinitionRegistryBuilder; import org.springframework.webflow.config.FlowExecutorBuilder; import org.springframework.webflow.context.servlet.FlowUrlHandler; import org.springframework.webflow.conversation.impl.SessionBindingConversationManager; import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; import org.springframework.webflow.engine.builder.ViewFactoryCreator; import org.springframework.webflow.engine.builder.support.FlowBuilderServices; import org.springframework.webflow.engine.impl.FlowExecutionImplFactory; import org.springframework.webflow.execution.repository.impl.DefaultFlowExecutionRepository; import org.springframework.webflow.execution.repository.snapshot.SerializedFlowExecutionSnapshotFactory; import org.springframework.webflow.executor.FlowExecutor; import org.springframework.webflow.executor.FlowExecutorImpl; import org.springframework.webflow.expression.spel.WebFlowSpringELExpressionParser; import org.springframework.webflow.mvc.builder.MvcViewFactoryCreator; import org.springframework.webflow.mvc.servlet.FlowHandler; import org.springframework.webflow.mvc.servlet.FlowHandlerAdapter; import org.springframework.webflow.mvc.servlet.FlowHandlerMapping; import javax.naming.OperationNotSupportedException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * This is {@link CasWebflowContextConfiguration} that attempts to create Spring-managed beans * backed by external configuration. * * @author Misagh Moayyed * @since 5.0.0 */ @Configuration("casWebflowContextConfiguration") @EnableConfigurationProperties(CasConfigurationProperties.class) public class CasWebflowContextConfiguration { private static final int LOGOUT_FLOW_HANDLER_ORDER = 3; private static final String BASE_CLASSPATH_WEBFLOW = "classpath*:/webflow"; @Autowired private CasConfigurationProperties casProperties; @Autowired @Qualifier("registeredServiceViewResolver") private ViewResolver registeredServiceViewResolver; @Autowired private ApplicationContext applicationContext; @Autowired @Qualifier("webflowCipherExecutor") private CipherExecutor webflowCipherExecutor; @Bean public ExpressionParser expressionParser() { return new WebFlowSpringELExpressionParser( new SpelExpressionParser(), logoutConversionService()); } @Bean public ConversionService logoutConversionService() { return new LogoutConversionService(); } @RefreshScope @Bean public ViewFactoryCreator viewFactoryCreator() { final MvcViewFactoryCreator resolver = new MvcViewFactoryCreator(); resolver.setViewResolvers(Collections.singletonList(this.registeredServiceViewResolver)); return resolver; } @Bean public FlowUrlHandler loginFlowUrlHandler() { return new CasDefaultFlowUrlHandler(); } @Bean public FlowUrlHandler logoutFlowUrlHandler() { final CasDefaultFlowUrlHandler handler = new CasDefaultFlowUrlHandler(); handler.setFlowExecutionKeyParameter("RelayState"); return handler; } @RefreshScope @Bean public HandlerAdapter logoutHandlerAdapter() { final FlowHandlerAdapter handler = new FlowHandlerAdapter() { @Override public boolean supports(final Object handler) { return super.supports(handler) && ((FlowHandler) handler) .getFlowId().equals(CasWebflowConfigurer.FLOW_ID_LOGOUT); } }; handler.setFlowExecutor(logoutFlowExecutor()); handler.setFlowUrlHandler(logoutFlowUrlHandler()); return handler; } @RefreshScope @Bean public CipherBean loginFlowCipherBean() { try { return new CipherBean() { @Override public byte[] encrypt(final byte[] bytes) { return (byte[]) CasWebflowContextConfiguration.this.webflowCipherExecutor.encode(bytes); } @Override public void encrypt(final InputStream inputStream, final OutputStream outputStream) { throw new RuntimeException( new OperationNotSupportedException("Encrypting input stream is not supported")); } @Override public byte[] decrypt(final byte[] bytes) { return (byte[]) CasWebflowContextConfiguration.this.webflowCipherExecutor.decode(bytes); } @Override public void decrypt(final InputStream inputStream, final OutputStream outputStream) { throw new RuntimeException( new OperationNotSupportedException("Decrypting input stream is not supported")); } }; } catch (final Exception e) { throw Throwables.propagate(e); } } @RefreshScope @Bean public FlowBuilderServices builder() { final FlowBuilderServicesBuilder builder = new FlowBuilderServicesBuilder(this.applicationContext); builder.setViewFactoryCreator(viewFactoryCreator()); builder.setExpressionParser(expressionParser()); builder.setDevelopmentMode(casProperties.getWebflow().isRefresh()); return builder.build(); } @Bean public Transcoder loginFlowStateTranscoder() { try { return new EncryptedTranscoder(loginFlowCipherBean()); } catch (final Exception e) { throw new BeanCreationException(e.getMessage(), e); } } @Bean public HandlerAdapter loginHandlerAdapter() { final FlowHandlerAdapter handler = new FlowHandlerAdapter() { @Override public boolean supports(final Object handler) { return super.supports(handler) && ((FlowHandler) handler) .getFlowId().equals(CasWebflowConfigurer.FLOW_ID_LOGIN); } }; handler.setFlowExecutor(loginFlowExecutor()); handler.setFlowUrlHandler(loginFlowUrlHandler()); return handler; } @RefreshScope @Bean public LocaleChangeInterceptor localeChangeInterceptor() { final LocaleChangeInterceptor bean = new LocaleChangeInterceptor(); bean.setParamName(this.casProperties.getLocale().getParamName()); return bean; } @Bean public HandlerMapping logoutFlowHandlerMapping() { final FlowHandlerMapping handler = new FlowHandlerMapping(); handler.setOrder(LOGOUT_FLOW_HANDLER_ORDER); handler.setFlowRegistry(logoutFlowRegistry()); final Object[] interceptors = new Object[]{localeChangeInterceptor()}; handler.setInterceptors(interceptors); return handler; } @Lazy @Bean public Object[] loginFlowHandlerMappingInterceptors() { final List interceptors = new ArrayList<>(); interceptors.add(localeChangeInterceptor()); if (this.applicationContext.containsBean("authenticationThrottle")) { interceptors.add(this.applicationContext.getBean("authenticationThrottle", HandlerInterceptor.class)); } return interceptors.toArray(); } @Bean public HandlerMapping loginFlowHandlerMapping() { final FlowHandlerMapping handler = new FlowHandlerMapping(); handler.setOrder(LOGOUT_FLOW_HANDLER_ORDER - 1); handler.setFlowRegistry(loginFlowRegistry()); handler.setInterceptors(loginFlowHandlerMappingInterceptors()); return handler; } @RefreshScope @Bean public FlowExecutor logoutFlowExecutor() { final FlowExecutorBuilder builder = new FlowExecutorBuilder(logoutFlowRegistry(), this.applicationContext); builder.setAlwaysRedirectOnPause(casProperties.getWebflow().isAlwaysPauseRedirect()); builder.setRedirectInSameState(casProperties.getWebflow().isRedirectSameState()); return builder.build(); } @Bean public FlowDefinitionRegistry logoutFlowRegistry() { final FlowDefinitionRegistryBuilder builder = new FlowDefinitionRegistryBuilder(this.applicationContext, builder()); builder.setBasePath(BASE_CLASSPATH_WEBFLOW); builder.addFlowLocationPattern("/logout/*-webflow.xml"); return builder.build(); } @Bean public FlowDefinitionRegistry loginFlowRegistry() { final FlowDefinitionRegistryBuilder builder = new FlowDefinitionRegistryBuilder(this.applicationContext, builder()); builder.setBasePath(BASE_CLASSPATH_WEBFLOW); builder.addFlowLocationPattern("/login/*-webflow.xml"); return builder.build(); } @RefreshScope @Bean public FlowExecutor loginFlowExecutor() { if (casProperties.getWebflow().getSession().isStorage()) { return flowExecutorViaServerSessionBindingExecution(); } return flowExecutorViaClientFlowExecution(); } @Bean public FlowExecutor flowExecutorViaServerSessionBindingExecution() { final FlowDefinitionRegistry loginFlowRegistry = loginFlowRegistry(); final SessionBindingConversationManager conversationManager = new SessionBindingConversationManager(); conversationManager.setLockTimeoutSeconds(Long.valueOf(casProperties.getWebflow().getSession().getLockTimeout()).intValue()); conversationManager.setMaxConversations(casProperties.getWebflow().getSession().getMaxConversations()); final FlowExecutionImplFactory executionFactory = new FlowExecutionImplFactory(); final SerializedFlowExecutionSnapshotFactory flowExecutionSnapshotFactory = new SerializedFlowExecutionSnapshotFactory(executionFactory, loginFlowRegistry); flowExecutionSnapshotFactory.setCompress(casProperties.getWebflow().getSession().isCompress()); final DefaultFlowExecutionRepository repository = new DefaultFlowExecutionRepository(conversationManager, flowExecutionSnapshotFactory); executionFactory.setExecutionKeyFactory(repository); return new FlowExecutorImpl(loginFlowRegistry, executionFactory, repository); } @Bean public FlowExecutor flowExecutorViaClientFlowExecution() { final FlowDefinitionRegistry loginFlowRegistry = loginFlowRegistry(); final ClientFlowExecutionRepository repository = new ClientFlowExecutionRepository(); repository.setFlowDefinitionLocator(loginFlowRegistry); repository.setTranscoder(loginFlowStateTranscoder()); final FlowExecutionImplFactory factory = new FlowExecutionImplFactory(); factory.setExecutionKeyFactory(repository); repository.setFlowExecutionFactory(factory); return new FlowExecutorImpl(loginFlowRegistry, factory, repository); } @ConditionalOnMissingBean(name = "defaultWebflowConfigurer") @Bean public CasWebflowConfigurer defaultWebflowConfigurer() { final DefaultWebflowConfigurer c = new DefaultWebflowConfigurer(builder(), loginFlowRegistry()); c.setLogoutFlowDefinitionRegistry(logoutFlowRegistry()); return c; } }