About decorating custom data on individual and every web flow

138 views
Skip to first unread message

Y G

unread,
Jul 24, 2024, 7:30:18 AM7/24/24
to CAS Community
Hi everyone,

My questions are:

1. For an individual flow based java decoration solution, how can i add flowVariable definition on the password reset flow correctly?
2. Is there a global way of defining custom datas for thmeleaf html using java other than custom groovy script or rest call result?

Any help would be much appreciated.

Details about my problem:

For a custom CAS theming, i wanted to decorate every page with this custom data class:

package tr.com.mycompany.cas.webflow;

import java.io.Serializable;
import java.util.List;
import java.util.Random;
import tr.com.
mycompany.cas.utils.AppInfo;

public class WebFlowCustomData implements Serializable {

  public String getRegistrationUrl() {
    String env = AppInfo.getEnvName();
    return String.format(
        "https://kayit%s.mycompany.com.tr/reg-form",
        "prod".equalsIgnoreCase(env) ? "" : "-" + env
    );
  }

  public String getRandomVideoSrc() {
    List<String> vids = List.of(
        "/cas/themes/theme2024/img/introDemo.webm",
        "/cas/themes/theme2024/img/loop.webm",
        "/cas/themes/theme2024/img/intro-light1.webm"
    );
    return vids.get(new Random().nextInt(3));
  }

}


Upon reading about Extending CAS Webflow(https://apereo.github.io/cas/6.6.x/webflow/Webflow-Customization-Extensions.html)  page , i tried doing it like this:
First add the customData variables in login and logout flows in this class.

package tr.com.mycompany.cas.webflow;

import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.web.flow.configurer.AbstractCasWebflowConfigurer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.webflow.definition.registry.FlowDefinitionRegistry;
import org.springframework.webflow.engine.builder.support.FlowBuilderServices;

public class CustomWebFlowDecorator extends AbstractCasWebflowConfigurer {
  public CustomWebFlowDecorator(FlowBuilderServices flowBuilderServices,
                                FlowDefinitionRegistry flowDefinitionRegistry,
                                ConfigurableApplicationContext applicationContext,
                                CasConfigurationProperties casProperties) {
    super(flowBuilderServices, flowDefinitionRegistry, applicationContext, casProperties);
  }

  @Override
  protected void doInitialize() {
    super.createFlowVariable(super.getLoginFlow(), "customData", WebFlowCustomData.class);
    super.createFlowVariable(super.getLogoutFlow(), "customData", WebFlowCustomData.class);
  }
}



And then, to register this class to the configuration i added the class below to the overlay project.


package tr.com.mycompany.cas.webflow;

import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.web.flow.CasWebflowConfigurer;
import org.apereo.cas.web.flow.CasWebflowConstants;
import org.apereo.cas.web.flow.CasWebflowExecutionPlan;
import org.apereo.cas.web.flow.CasWebflowExecutionPlanConfigurer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.webflow.definition.registry.FlowDefinitionRegistry;
import org.springframework.webflow.engine.builder.support.FlowBuilderServices;


@AutoConfiguration
@EnableConfigurationProperties(CasConfigurationProperties.class)
public class WebFlowConfig implements CasWebflowExecutionPlanConfigurer {

  @Autowired
  private CasConfigurationProperties casProperties;

  /**
   * flow definition registry for login.
   */
  @Autowired
  @Qualifier(CasWebflowConstants.BEAN_NAME_LOGIN_FLOW_DEFINITION_REGISTRY)
  private FlowDefinitionRegistry loginFlowDefinitionRegistry;

  /**
   * flow definition registry for logout.
   */
  @Autowired
  @Qualifier(CasWebflowConstants.BEAN_NAME_LOGOUT_FLOW_DEFINITION_REGISTRY)
  private FlowDefinitionRegistry logoutFlowDefinitionRegistry;

  @Autowired
  private ConfigurableApplicationContext applicationContext;

  @Autowired
  private FlowBuilderServices flowBuilderServices;

  @ConditionalOnMissingBean(name = "loginFlowCustomDecoratorConfigurer")
  @Bean
  public CasWebflowConfigurer loginFlowCustomDecoratorConfigurer() {
    CustomWebFlowDecorator customWebFlowDecorator = new CustomWebFlowDecorator(flowBuilderServices,
        loginFlowDefinitionRegistry, applicationContext, casProperties);
    customWebFlowDecorator.setLogoutFlowDefinitionRegistry(logoutFlowDefinitionRegistry);
    return customWebFlowDecorator;
  }

  @Override
  public void configureWebflowExecutionPlan(final CasWebflowExecutionPlan plan) {
    plan.registerWebflowConfigurer(loginFlowCustomDecoratorConfigurer());
  }
}


and lastly i added this class to spring factories file.

Cas documentation describes only login flow, for adding the same data on logout flow, i checked, autowired the logoutFlowDefinitionRegistry first, and used it on the setLogoutFlowDefinitionRegistry setter method of my CustomWebFlowDecorator. After that CustomWebFlowDecorator's doInitialize flowVariables worked without any problem.

I wanted to add this flow variable inside password reset flow too, and trying to add it like this did not work(null value on thymeleaf htmls and a warning on startup about not setup correctly):

...
  protected void doInitialize() {
  ...
  super.createFlowVariable(super.getFlow("pswdreset"), "customData", WebFlowCustomData.class);
...


Have a wonderful day.
Yusuf Gunduz.

Ray Bon

unread,
Jul 24, 2024, 7:30:01 PM7/24/24
to cas-...@apereo.org
Yusuf,

Could you implement the logic in javascript and add it to the pages?

Ray

From: cas-...@apereo.org <cas-...@apereo.org> on behalf of Y G <yusuf....@gmail.com>
Sent: 24 July 2024 04:24
To: CAS Community <cas-...@apereo.org>
Subject: [cas-user] About decorating custom data on individual and every web flow
 
You don't often get email from yusuf....@gmail.com. Learn why this is important
--
- Website: https://apereo.github.io/cas
- List Guidelines: https://goo.gl/1VRrw7
- Contributions: https://goo.gl/mh7qDG
---
You received this message because you are subscribed to the Google Groups "CAS Community" group.
To unsubscribe from this group and stop receiving emails from it, send an email to cas-user+u...@apereo.org.
To view this discussion on the web visit https://groups.google.com/a/apereo.org/d/msgid/cas-user/afbb62e7-c4ce-4e86-a27b-3eedbfe2aa2en%40apereo.org.

Y G

unread,
Jul 25, 2024, 6:38:26 AM7/25/24
to CAS Community, Ray Bon
That could be a solution for most use-cases (and i could even move the WebFlowCustomData.getRandomVideoSrc() method to js) but, i would like to test and implement for a java backed/backend flow extending solution the way i've written. The difficulty that i think is, when a need of decoration(loading of external/additional data to CAS web views to be used) that effects the view(thymeleaf htmls) occurs, a need of groovy knowledge or coding or implementing seperate project/service/endpoint happens.

To express my example context further, i wrote the  WebFlowCustomData.getRegistrationUrl()method that returns a url that can be changed according to deployed cas environment. In addition to production profile, i have set up dev,qa,prp profiles in my CAS 6.6.x overlay app. I would like this method to give me the necessary url, according to active CAS profile.

I will write an example CAS Overlay project, send to a github repo, and announce it here soon for your convenience and testing.

Thank you and  a have nice day.
Yusuf Gündüz

25 Temmuz 2024 Perşembe tarihinde saat 02:30:01 UTC+3 itibarıyla Ray Bon şunları yazdı:

Y G

unread,
Jul 26, 2024, 3:11:49 PM7/26/24
to CAS Community, Y G, Ray Bon
Hello again,
I've wrote an example project for to anyone who wish to see the details of my question.




25 Temmuz 2024 Perşembe tarihinde saat 13:38:26 UTC+3 itibarıyla Y G şunları yazdı:

Ray Bon

unread,
Jul 26, 2024, 10:50:36 PM7/26/24
to yusuf....@gmail.com, cas-...@apereo.org
Yusuf,

You may be able to get some hints from https://fawnoos.com/2024/07/01/cas71x-message-of-the-day/ or other articles on the site.

Ray

On Fri, 2024-07-26 at 12:08 -0700, Y G wrote:
Hello again,
I've wrote an example project for to anyone who wish to see the details of my question.




25 Temmuz 2024 Perşembe tarihinde saat 13:38:26 UTC+3 itibarıyla Y G şunları yazdı:
That could be a solution for most use-cases (and i could even move the WebFlowCustomData.getRandomVideoSrc()method to js) but, i would like to test and implement for a java backed/backend flow extending solution the way i've written. The difficulty that i think is, when a need of decoration(loading of external/additional data to CAS web views to be used) that effects the view(thymeleaf htmls) occurs, a need of groovy knowledge or coding or implementing seperate project/service/endpoint happens.

Y G

unread,
Sep 13, 2025, 10:25:32 AMSep 13
to CAS Community, Ray Bon, yusuf....@gmail.com
Hello again,
It's been a while and i wanted share my findings with similar solution for another use case. I wanted to learn about customizing the webflow and add custom datas and some modifications of the default CAS Webflow, which is not recommended btw.  After countless hours of research with the replies, i've tried it on an overlay from Cas initializr (i used the latest 7.3-RC4)

Before all, i needed all of these dependencies in the build.gradle file for below to work.
...

/**
* CAS dependencies and modules may be listed here.
*
* There is no need to specify the version number for each dependency
* since versions are all resolved and controlled by the dependency management
* plugin via the CAS bom.
**/
...
// For custom webflow customizations
implementation "org.apereo.cas:cas-server-core-webflow"
implementation "org.apereo.cas:cas-server-core-webflow-api"
implementation "org.apereo.cas:cas-server-support-actions-core"
implementation "org.apereo.cas:cas-server-core-web-api"
implementation "org.apereo.cas:cas-server-core-api"
implementation "org.apereo.cas:cas-server-core-api-configuration-model"
implementation "org.apereo.cas:cas-server-core-api-logout"
...

For adding custom data to successful login CAS Web Page, you can add it like this: 
1. Add an custom action class like this:
package tr.com.example.webflow;


import lombok.val;
import org.apereo.cas.web.flow.actions.BaseCasWebflowAction;

import lombok.extern.slf4j.Slf4j;
import org.apereo.cas.web.support.WebUtils;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.RequestContext;

import java.util.List;


/**
* Kullanıcı Login olduğunda gösterilecek Giriş Başarılı Cas Ekranına Uygulama Listesi ekleyebilmek için,
* db'den gerekli uygulama listesini alıp önyüze ileten custom webflow action.
*/
@Slf4j
public class AvailableAppsForUserCustomAction extends BaseCasWebflowAction {

@Override
protected Event doExecuteInternal(final RequestContext requestContext) {
LOGGER.info("🟢 Running AvailableAppsForUserCustomAction");
val auth = WebUtils.getAuthentication(requestContext);
if (null != auth) {
// user is logged in
requestContext.getFlowScope().put("userType", "VIP");
// generated here for example purposes.
var appList = List.of("APP1", "APP2", " APP3", "APP4");
requestContext.getFlowScope().put("availableApps", appList);
LOGGER.info(" Finished AvailableAppsForUserCustomAction for user: {}, available apps for user are: {}",
auth.getPrincipal().getId(),
String.join(",", appList)
);
}
return success();
}
}

2. Now this is the important part: you need some understanding of the innerworkings of CAS's Webflow configurations(DefaultLoginWebflowConfigurer). After some digging, for this use case (which is to add and use some data-like user available app list) for the CAS Login success web page, i've found out that i can add my custom action to the necessary state(which is  viewGenericLoginSuccess endstate meaning Cas logged in screen view state)'s entryActionList, and make sure that my custom action is ran every time cas login successful page is displayed and my custom data is available for the ui.
package tr.com. example.webflow;


import lombok.extern.slf4j.Slf4j;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.web.flow.CasWebflowConstants;

import org.apereo.cas.web.flow.configurer.AbstractCasWebflowConfigurer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.webflow.definition.registry.FlowDefinitionRegistry;
import org.springframework.webflow.engine.EndState;
import org.springframework.webflow.engine.Flow;
import org.springframework.webflow.engine.builder.support.FlowBuilderServices;

/**
* DANGER! This is an advanced configuration and modification of CASs 'Spring Webflow Configuration.
*/
@Slf4j
public class CustomLoginWebflowConfigurer extends AbstractCasWebflowConfigurer {

public CustomLoginWebflowConfigurer(CasConfigurationProperties casProperties,
FlowBuilderServices flowBuilderServices,
FlowDefinitionRegistry loginFlowDefinitionRegistry,
ConfigurableApplicationContext applicationContext

) {
super(flowBuilderServices, loginFlowDefinitionRegistry, applicationContext, casProperties);
}

@Override
protected void doInitialize() {
Flow loginFlow = getLoginFlow();
if (null != loginFlow) {
customizeGenericSuccessEndState(loginFlow);
} else {
LOGGER.warn("⚠️ login flow is null, webflow customization cancelled!");
}
}

/**
* Add custom entry action to Cas's generic login success end state.
* availableAppsForUserCustomAction is defined in the context here:
* {@link CustomWebflowExecutionConfiguration#availableAppsForUserCustomAction}
* and used as the Cas writing fashion.
*
* @param flow login flow.
*/
private void customizeGenericSuccessEndState(final Flow flow) {
var successEndState = getState(flow,
CasWebflowConstants.STATE_ID_VIEW_GENERIC_LOGIN_SUCCESS,
EndState.class);
if (successEndState != null) {
successEndState.getEntryActionList().add(createEvaluateAction("availableAppsForUserCustomAction"));
LOGGER.info(" Added availableAppsForUserCustomAction to VIEW_GENERIC_LOGIN_SUCCESS end state!");
} else {
LOGGER.warn("⚠️ VIEW_GENERIC_LOGIN_SUCCESS state not found in login flow");
}
}
}

3. And lastly, add an autoconfiguration class like this and register it to the src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports file for your overlay project to acknowledge it:
package tr.com.example.webflow;



import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.web.flow.CasWebflowConfigurer;
import org.apereo.cas.web.flow.CasWebflowConstants;
import org.apereo.cas.web.flow.CasWebflowExecutionPlan;
import org.apereo.cas.web.flow.CasWebflowExecutionPlanConfigurer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.webflow.definition.registry.FlowDefinitionRegistry;
import org.springframework.webflow.engine.builder.support.FlowBuilderServices;

/**
* DANGER! This is an advanced configuration and modification of CASs 'Spring Webflow Configuration.
* Autoconfiguration class for Cas Spring Webflow customization.
* Do not forget to add this class fqdn to file:
* src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
*/
@AutoConfiguration
@EnableConfigurationProperties(CasConfigurationProperties.class)
public class CustomWebflowExecutionConfiguration implements CasWebflowExecutionPlanConfigurer {

@Autowired
private CasConfigurationProperties casProperties;

@Autowired
@Qualifier(CasWebflowConstants.BEAN_NAME_FLOW_DEFINITION_REGISTRY)
private FlowDefinitionRegistry flowDefinitionRegistry;


@Autowired
private ConfigurableApplicationContext applicationContext;

@Autowired
private FlowBuilderServices flowBuilderServices;

@Bean
public AvailableAppsForUserCustomAction availableAppsForUserCustomAction() {
return new AvailableAppsForUserCustomAction();
}

@Bean
@ConditionalOnMissingBean(name = "customLoginWebflowConfigurer")
public CasWebflowConfigurer customLoginWebflowConfigurer(
CasConfigurationProperties casProperties,
FlowBuilderServices flowBuilderServices,
@Qualifier(CasWebflowConstants.BEAN_NAME_FLOW_DEFINITION_REGISTRY)
FlowDefinitionRegistry flowDefinitionRegistry,
ConfigurableApplicationContext applicationContext
) {
return new CustomLoginWebflowConfigurer(casProperties, flowBuilderServices, flowDefinitionRegistry, applicationContext);
}

@Override
public void configureWebflowExecutionPlan(CasWebflowExecutionPlan plan) {
plan.registerWebflowConfigurer(customLoginWebflowConfigurer(casProperties, flowBuilderServices, flowDefinitionRegistry, applicationContext));
}
}

src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports file after adding:
org.apereo.cas.config.CasOverlayOverrideConfiguration
tr.com.example.webflow. CustomWebflowExecutionConfiguration
 

Adsız.png


Another use case was adding/removing custom cookie for login/logout operations of CAS. On the CAS overlay application configuration class(org.apereo.cas.config.CasOverlayOverrideConfiguration), i added these custom actions that modifies default behaviour. 

@Bean
@Primary // ensures this bean replaces the default SendTicketGrantingTicketAction
public SendTicketGrantingTicketAction sendTicketGrantingTicketAction(
TicketRegistry ticketRegistry,
@Qualifier("ticketGrantingTicketCookieGenerator") CasCookieBuilder ticketGrantingCookieBuilder,
SingleSignOnParticipationStrategy singleSignOnParticipationStrategy,
CasConfigurationProperties casProperties
) {
final int maxAge = Math.toIntExact(Duration.parse(casProperties.getTicket().getTgt().getPrimary().getMaxTimeToLiveInSeconds()).toSeconds());
return new SendTicketGrantingTicketAction(ticketRegistry, ticketGrantingCookieBuilder, singleSignOnParticipationStrategy) {

@Override
protected Event createSingleSignOnCookie(final RequestContext requestContext,
final String ticketGrantingTicketId) {
// Call CAS’ normal TGC creation logic first
Event event = super.createSingleSignOnCookie(requestContext, ticketGrantingTicketId);

HttpServletResponse response = WebUtils.getHttpServletResponseFromExternalWebflowContext(requestContext);

// Add your custom cookie with the TGT value
// Bunda problem oldu, .example.org verince bu hata dönüyor.
// IllegalArgumentException: An invalid domain [.example.org] was specified for this cookie]
// Cookie customCookie = new Cookie("TGTA", ticketGrantingTicketId);
// customCookie.setPath("/");
// customCookie.setHttpOnly(true);
// customCookie.setSecure(true);
// response.addCookie(customCookie);

addCrossSubdomainCookie(response, ".example.org", maxAge, "TGTA", ticketGrantingTicketId);

// Optional: log for debugging
System.out.println(" Added custom cookie with TGT: " + ticketGrantingTicketId);

return event;
}
};
}

@Bean
@Primary
TerminateSessionAction terminateSessionAction(
CentralAuthenticationService centralAuthenticationService,
CasCookieBuilder ticketGrantingTicketCookieGenerator,
CasCookieBuilder warnCookieGenerator,
CasConfigurationProperties casProperties,
LogoutManager logoutManager,
SingleLogoutRequestExecutor singleLogoutRequestExecutor,
LogoutConfirmationResolver logoutConfirmationResolver) {

return new TerminateSessionAction(
centralAuthenticationService,
ticketGrantingTicketCookieGenerator,
warnCookieGenerator,
casProperties.getLogout(),
logoutManager,
singleLogoutRequestExecutor,
logoutConfirmationResolver
) {
@Override
protected Event terminate(final RequestContext requestContext) {
Event event = super.terminate(requestContext);

// delete custom cookie
val response = WebUtils.getHttpServletResponseFromExternalWebflowContext(requestContext);
addCrossSubdomainCookie(response, ".example.org", -1, "TGTA", null);
System.out.println(" Removed custom cookie!");
return event;
}
};
}

/**
* Because of the restriction in the servlet container / Java cookie API
* (IllegalArgumentException: An invalid domain [.example.org] was specified for this cookie])
* this method bypasses and sends cookie as raw Http header.
*
* @param response HttpServletResponse
* @param domain domain (.example.com for reading the cookie on every subdomain)
* @param maxAge max age in seconds (-1 means session, deletes on browser/tab close),
* @param name cookie name
* @param value cookie value
*/
private static void addCrossSubdomainCookie(HttpServletResponse response, String domain, int maxAge, String name, String value) {
String cookie = String.format("%s=%s; Path=/; Domain=%s; Max-Age=%d; Secure; HttpOnly; SameSite=Lax", name, value,domain, maxAge );
response.addHeader("Set-Cookie", cookie);
}


And when logged in and out you can see the logs and the custom cookie like this:
Adsız2.png

In the end  i've found some answers to my questions. This is not a recommended way of doing things, and modifying default Cas Webflow is not a good idea. but for a time it to be needed, i hope this helps anyone shed some light and make it as a starting point of it. And i hope there's more explanation and some example in the documentation(https://apereo.github.io/cas/development/webflow/Webflow-Customization-Extensions.html) in the future.

Have a nice day,
YG



27 Temmuz 2024 Cumartesi tarihinde saat 05:50:36 UTC+3 itibarıyla Ray Bon şunları yazdı:
Reply all
Reply to author
Forward
0 new messages