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
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:
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ı: