From 95942875020c6c1dbe2a205b229e7454680fbdd2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9r=C3=B4me=20LELEU?= <jerome.leleu@teamdlab.com>
Date: Fri, 15 May 2020 16:01:57 +0200
Subject: [PATCH] =?UTF-8?q?RABB-519:=20corrige=20l'erreur=20en=20cas=20de?=
 =?UTF-8?q?=20ticket=20expir=C3=A9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../config/cas-server-application-dev.yml     |  4 +
 .../config/cas-server-application-recette.yml |  4 +
 .../vitamui/cas/config/WebflowConfig.java     |  6 ++
 ...tSessionTicketExpirationPolicyBuilder.java | 19 +++--
 ...ustomVerifyPasswordResetRequestAction.java | 80 +++++++++++++++++++
 ...ketRegistryTicketCatalogConfiguration.java | 32 ++++++++
 6 files changed, 135 insertions(+), 10 deletions(-)
 create mode 100644 cas/cas-server/src/main/java/fr/gouv/vitamui/cas/webflow/actions/CustomVerifyPasswordResetRequestAction.java
 create mode 100644 cas/cas-server/src/main/java/org/apereo/cas/config/HazelcastTicketRegistryTicketCatalogConfiguration.java

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 23773137..2b328858 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 19deecb3..fc372d2d 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 0d9265a7..9ae67f5c 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 e47c96ad..49e07c48 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 00000000..32a89cf8
--- /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 00000000..bd5f88e3
--- /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);
+    }
+}
-- 
GitLab