diff --git a/cas/cas-server/src/main/config/cas-server-application-dev.yml b/cas/cas-server/src/main/config/cas-server-application-dev.yml index 23773137a82f45744ff65be82aa1f20214f095ad..2b328858e9a976db1d40092fe99015a7a0bb1934 100644 --- a/cas/cas-server/src/main/config/cas-server-application-dev.yml +++ b/cas/cas-server/src/main/config/cas-server-application-dev.yml @@ -73,6 +73,10 @@ cas.authn.surrogate.separator: "," cas.authn.surrogate.sms.attributeName: fakeNameToBeSureToFindNoAttributeAndNeverSendAnSMS +# 5 minutes cache for login delegation +cas.ticket.tst.timeToKillInSeconds: 300 + + cas.authn.pm.enabled: true cas.authn.pm.policyPattern: '^(?=.*[$@!%*#?&=\-\/:;\(\)"\.,\?!''\[\]{}^\+\=_\\\|~<>`])(?=.*[\d])[A-Za-zÀ-ÿ0-9$@!%*#?&=\-\/:;\(\)"\.,\?!''\[\]{}^\+\=_\\\|~<>`]{8,}$' cas.authn.pm.reset.mail.subject: Requete de reinitialisation de mot de passe diff --git a/cas/cas-server/src/main/config/cas-server-application-recette.yml b/cas/cas-server/src/main/config/cas-server-application-recette.yml index 19deecb387894b2c618a6452e65c690a4ec85c69..fc372d2d604526e168135b475e6e247b2d49cae5 100644 --- a/cas/cas-server/src/main/config/cas-server-application-recette.yml +++ b/cas/cas-server/src/main/config/cas-server-application-recette.yml @@ -66,6 +66,10 @@ cas.authn.surrogate.separator: "," cas.authn.surrogate.sms.attributeName: fakeNameToBeSureToFindNoAttributeAndNeverSendAnSMS +# 5 minutes cache for login delegation +cas.ticket.tst.timeToKillInSeconds: 300 + + cas.authn.pm.enabled: true cas.authn.pm.policyPattern: '^(?=.*[$@!%*#?&=\-\/:;\(\)"\.,\?!''\[\]{}^\+\=_\\\|~<>`])(?=.*[\d])[A-Za-zÀ-ÿ0-9$@!%*#?&=\-\/:;\(\)"\.,\?!''\[\]{}^\+\=_\\\|~<>`]{8,}$' cas.authn.pm.reset.mail.subject: Requete de reinitialisation de mot de passe diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/WebflowConfig.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/WebflowConfig.java index 0d9265a77614240d2a858182d31a2c6c0b712ff4..9ae67f5cfeb98ee7c6b553a5c9b2ffb6b70448f1 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/WebflowConfig.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/config/WebflowConfig.java @@ -351,4 +351,10 @@ public class WebflowConfig { public Action delegatedAuthenticationClientLogoutAction() { return new NoOpAction(null); } + + @Bean + @RefreshScope + public Action verifyPasswordResetRequestAction() { + return new CustomVerifyPasswordResetRequestAction(casProperties, passwordManagementService, centralAuthenticationService.getObject()); + } } diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/PmTransientSessionTicketExpirationPolicyBuilder.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/PmTransientSessionTicketExpirationPolicyBuilder.java index e47c96ad3b4210e6ba1eb460227dc2d810abb535..49e07c48a7688ec7d5321a64a6f2366a3b09a9b4 100644 --- a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/PmTransientSessionTicketExpirationPolicyBuilder.java +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/pm/PmTransientSessionTicketExpirationPolicyBuilder.java @@ -43,10 +43,10 @@ import org.apereo.cas.configuration.CasConfigurationProperties; import org.apereo.cas.ticket.ExpirationPolicy; import org.apereo.cas.ticket.expiration.HardTimeoutExpirationPolicy; import org.apereo.cas.ticket.expiration.builder.TransientSessionTicketExpirationPolicyBuilder; -import org.apereo.cas.web.support.WebUtils; +import org.springframework.web.context.request.RequestContextHolder; /** - * Specific expiration policy builder for password management. + * Specific expiration policy builder for password management (retrieves the expiration in minutes pushed by the ResetPasswordController). */ public class PmTransientSessionTicketExpirationPolicyBuilder extends TransientSessionTicketExpirationPolicyBuilder { @@ -60,16 +60,15 @@ public class PmTransientSessionTicketExpirationPolicyBuilder extends TransientSe @Override public ExpirationPolicy toTransientSessionTicketExpirationPolicy() { - val request = WebUtils.getHttpServletRequestFromExternalWebflowContext(); - if (request != null) { - val expInMinutesAttribute = request.getAttribute(PM_EXPIRATION_IN_MINUTES_ATTRIBUTE); - if (expInMinutesAttribute != null) { - try { - val expInMinutes = Integer.parseInt((String) expInMinutesAttribute); + val attributes = RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + try { + val expInMinutes = (Integer) attributes.getAttribute(PM_EXPIRATION_IN_MINUTES_ATTRIBUTE, 0); + if (expInMinutes != null) { return new HardTimeoutExpirationPolicy(expInMinutes * 60); - } catch (final NumberFormatException e) { - LOGGER.error("Cannot get expiration in minutes", e); } + } catch (final ClassCastException e) { + LOGGER.error("Cannot get expiration in minutes", e); } } return new HardTimeoutExpirationPolicy(casProperties.getAuthn().getPm().getReset().getExpirationMinutes() * 60); diff --git a/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomVerifyPasswordResetRequestAction.java b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomVerifyPasswordResetRequestAction.java new file mode 100644 index 0000000000000000000000000000000000000000..32a89cf83d1553fc1eda823fb61089f720b16848 --- /dev/null +++ b/cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomVerifyPasswordResetRequestAction.java @@ -0,0 +1,80 @@ +package fr.gouv.vitamui.cas.webflow.actions; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.apache.commons.lang3.StringUtils; +import org.apereo.cas.CentralAuthenticationService; +import org.apereo.cas.configuration.CasConfigurationProperties; +import org.apereo.cas.pm.BasePasswordManagementService; +import org.apereo.cas.pm.PasswordManagementService; +import org.apereo.cas.pm.web.flow.PasswordManagementWebflowUtils; +import org.apereo.cas.ticket.InvalidTicketException; +import org.apereo.cas.ticket.TransientSessionTicket; +import org.apereo.cas.web.support.WebUtils; +import org.springframework.webflow.action.AbstractAction; +import org.springframework.webflow.action.EventFactorySupport; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; + +/** + * A custom verify passwod rest action which deals with expired (removed from registry) tickets. + */ +@Slf4j +@RequiredArgsConstructor +public class CustomVerifyPasswordResetRequestAction extends AbstractAction { + private final CasConfigurationProperties casProperties; + private final PasswordManagementService passwordManagementService; + private final CentralAuthenticationService centralAuthenticationService; + + @Override + protected Event doExecute(final RequestContext requestContext) { + + val request = WebUtils.getHttpServletRequestFromExternalWebflowContext(requestContext); + val transientTicket = request.getParameter(PasswordManagementWebflowUtils.REQUEST_PARAMETER_NAME_PASSWORD_RESET_TOKEN); + + if (StringUtils.isBlank(transientTicket)) { + LOGGER.error("Password reset token is missing"); + return error(); + } + + TransientSessionTicket tst = null; + try { + tst = this.centralAuthenticationService.getTicket(transientTicket, TransientSessionTicket.class); + } catch (final InvalidTicketException e) {} + if (tst == null) { + LOGGER.error("Unable to locate token [{}] in the ticket registry", transientTicket); + return error(); + } + + val token = tst.getProperties().get(PasswordManagementWebflowUtils.FLOWSCOPE_PARAMETER_NAME_TOKEN).toString(); + val username = passwordManagementService.parseToken(token); + if (StringUtils.isBlank(username)) { + LOGGER.error("Password reset token could not be verified"); + return error(); + } + this.centralAuthenticationService.deleteTicket(tst.getId()); + + PasswordManagementWebflowUtils.putPasswordResetToken(requestContext, token); + val pm = casProperties.getAuthn().getPm(); + if (pm.getReset().isSecurityQuestionsEnabled()) { + val questions = BasePasswordManagementService + .canonicalizeSecurityQuestions(passwordManagementService.getSecurityQuestions(username)); + if (questions.isEmpty()) { + LOGGER.warn("No security questions could be found for [{}]", username); + return error(); + } + PasswordManagementWebflowUtils.putPasswordResetSecurityQuestions(requestContext, questions); + } else { + LOGGER.debug("Security questions are not enabled"); + } + + PasswordManagementWebflowUtils.putPasswordResetUsername(requestContext, username); + PasswordManagementWebflowUtils.putPasswordResetSecurityQuestionsEnabled(requestContext, pm.getReset().isSecurityQuestionsEnabled()); + + if (pm.getReset().isSecurityQuestionsEnabled()) { + return success(); + } + return new EventFactorySupport().event(this, "questionsDisabled"); + } +} diff --git a/cas/cas-server/src/main/java/org/apereo/cas/config/HazelcastTicketRegistryTicketCatalogConfiguration.java b/cas/cas-server/src/main/java/org/apereo/cas/config/HazelcastTicketRegistryTicketCatalogConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..bd5f88e3b8b01905416aeeb346b5f9dd05603cf2 --- /dev/null +++ b/cas/cas-server/src/main/java/org/apereo/cas/config/HazelcastTicketRegistryTicketCatalogConfiguration.java @@ -0,0 +1,32 @@ +package org.apereo.cas.config; + +import org.apereo.cas.configuration.CasConfigurationProperties; + +import org.apereo.cas.ticket.TicketCatalog; +import org.apereo.cas.ticket.TicketDefinition; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * Override the transient session ticket map timeout. + */ +@Configuration(value = "hazelcastTicketRegistryTicketMetadataCatalogConfiguration", proxyBeanMethods = false) +@EnableConfigurationProperties(CasConfigurationProperties.class) +public class HazelcastTicketRegistryTicketCatalogConfiguration extends BaseTicketDefinitionBuilderSupportConfiguration { + + private final CasConfigurationProperties casProperties; + + public HazelcastTicketRegistryTicketCatalogConfiguration(final CasConfigurationProperties casProperties) { + super(casProperties, new CasTicketCatalogConfigurationValuesProvider() {}); + this.casProperties = casProperties; + } + + @Override + protected void buildAndRegisterTransientSessionTicketDefinition(final TicketCatalog plan, final TicketDefinition metadata) { + final CasTicketCatalogConfigurationValuesProvider configurationValuesProvider = new CasTicketCatalogConfigurationValuesProvider() {}; + metadata.getProperties().setStorageName(configurationValuesProvider.getTransientSessionStorageName().apply(casProperties)); + // changed from CAS: set one day of cache + metadata.getProperties().setStorageTimeout(86400); + super.buildAndRegisterTransientSessionTicketDefinition(plan, metadata); + } +}