[java-identity-provider] 01/01: POC demonstrating the use of Thymeleaf for view rendering.

Marvin S. Addison marvin.addison at gmail.com
Tue Dec 12 14:08:33 UTC 2023


This is an automated email from the git hooks/post-receive script.

serac pushed a commit to branch dev/thymeleaf
in repository java-identity-provider.

View the commit online:
http://git.shibboleth.net/view/?p=java-identity-provider.git;a=commit;h=630c77aeaf2fa25683d139333e8e53bbdcd9e93d

commit 630c77aeaf2fa25683d139333e8e53bbdcd9e93d
Author: Marvin S. Addison <serac at vt.edu>
AuthorDate: Tue Dec 12 09:07:22 2023 -0500

    POC demonstrating the use of Thymeleaf for view rendering.
---
 .../authn/context/AuthenticationErrorContext.java  |  27 +++-
 .../impl/ExtractAuthenticationErrorMessage.java    |  59 +++++++++
 idp-conf-impl/pom.xml                              |   7 ++
 .../net/shibboleth/idp/conf/mvc-beans.xml          |  35 +++++-
 .../net/shibboleth/idp/conf/webflow-config.xml     |   1 +
 .../idp/flows/authn/password-authn-beans.xml       |   3 +
 .../idp/flows/authn/password-authn-flow.xml        |   7 ++
 .../idp/module/authn/impl/module.properties        |  22 ++--
 .../net/shibboleth/idp/module/views/login-error.vm |  24 ----
 .../net/shibboleth/idp/module/views/login.html     | 138 +++++++++++++++++++++
 .../net/shibboleth/idp/module/views/login.vm       | 109 ----------------
 .../net/shibboleth/idp/views/csrf/csrf.html        |  10 ++
 .../net/shibboleth/idp/module/conf/idp.properties  |   2 +-
 idp-distribution/pom.xml                           |  52 ++++----
 pom.xml                                            |   6 +
 15 files changed, 326 insertions(+), 176 deletions(-)

diff --git a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/AuthenticationErrorContext.java b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/AuthenticationErrorContext.java
index 769fe3397..05ce61a53 100644
--- a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/AuthenticationErrorContext.java
+++ b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/context/AuthenticationErrorContext.java
@@ -45,7 +45,11 @@ public final class AuthenticationErrorContext extends BaseContext {
     
     /** Error conditions detected through classified error messages. */
     @Nonnull private Collection<String> classifiedErrors;
-    
+
+    /** Error message to be displayed to user. */
+    @Nullable private String displayErrorMessage;
+
+
     /** Constructor. */
     public AuthenticationErrorContext() {
         exceptions = new ArrayList<>();
@@ -107,5 +111,24 @@ public final class AuthenticationErrorContext extends BaseContext {
     @Nullable public String getLastClassifiedError() {
         return classifiedErrors.stream().reduce((first, second) -> second).orElse(null);
     }
-    
+
+    /**
+     * @return Error message to be displayed to the user.
+     *
+     * @since 5.1.0
+     */
+    @Nullable public String getDisplayErrorMessage() {
+        return displayErrorMessage;
+    }
+
+    /**
+     * Sets the error message to be displayed to the user.
+     *
+     * @param message Error message
+     *
+     * @since 5.1.0
+     */
+    public void setDisplayErrorMessage(@Nullable String message) {
+        this.displayErrorMessage = message;
+    }
 }
\ No newline at end of file
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/ExtractAuthenticationErrorMessage.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/ExtractAuthenticationErrorMessage.java
new file mode 100644
index 000000000..05012ad2b
--- /dev/null
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/ExtractAuthenticationErrorMessage.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.authn.impl;
+
+import java.util.Locale;
+import java.util.function.Function;
+import net.shibboleth.idp.authn.AbstractAuthenticationAction;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.context.AuthenticationErrorContext;
+import org.jetbrains.annotations.NotNull;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+/**
+ * Responsible for producing a single error message to be displayed on the login form on authentication failure.
+ * Supersedes the Velocity template login-error.vm from previous IdP versions.
+ *
+ * @since 5.1.0
+ */
+public class ExtractAuthenticationErrorMessage extends AbstractAuthenticationAction {
+
+    private static final Object[] NO_ARGS = new Object[0];
+
+    private Function<AuthenticationContext, AuthenticationErrorContext> errorContextLookup =
+            new ChildContextLookup<>(AuthenticationErrorContext.class);
+
+    @Override
+    protected boolean doPreExecute(@NotNull ProfileRequestContext profileRequestContext, @NotNull AuthenticationContext authenticationContext) {
+        return errorContextLookup.apply(authenticationContext) != null;
+    }
+
+    @Override
+    protected void doExecute(@NotNull ProfileRequestContext profileRequestContext, @NotNull AuthenticationContext authenticationContext) {
+        final AuthenticationErrorContext errorContext = errorContextLookup.apply(authenticationContext);
+        final String eventId = errorContext.getClassifiedErrors().iterator().next();
+        final Locale locale = Locale.getDefault();
+        String message = "Authentication failure";
+        if (!"ReselectFlow".equals(eventId)) {
+            final String eventKey = getMessage(eventId, NO_ARGS, "authn", locale);
+            message = getMessage(eventKey + ".message", NO_ARGS, "Login Failure: " + eventId, locale);
+        } else if (!errorContext.getExceptions().isEmpty()) {
+           final Exception ex = errorContext.getExceptions().get(0);
+           message = ex.getMessage() != null ? "Login Failure: " + ex.getMessage() : ex.toString();
+        }
+        errorContext.setDisplayErrorMessage(message);
+    }
+}
diff --git a/idp-conf-impl/pom.xml b/idp-conf-impl/pom.xml
index a32320996..f500cb88f 100644
--- a/idp-conf-impl/pom.xml
+++ b/idp-conf-impl/pom.xml
@@ -26,6 +26,7 @@
             <artifactId>idp-admin-api</artifactId>
             <version>${project.version}</version>
         </dependency>
+
         <dependency>
             <groupId>${project.groupId}</groupId>
             <artifactId>idp-admin-impl</artifactId>
@@ -162,6 +163,12 @@
             <scope>runtime</scope>
         </dependency>
 
+        <dependency>
+            <groupId>org.thymeleaf</groupId>
+            <artifactId>thymeleaf-spring6</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+
         <!-- Test Dependencies -->
         <dependency>
             <groupId>${project.groupId}</groupId>
diff --git a/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/mvc-beans.xml b/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/mvc-beans.xml
index 35e087f1c..30fb5a6f0 100644
--- a/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/mvc-beans.xml
+++ b/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/mvc-beans.xml
@@ -79,9 +79,16 @@
     <bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
         <property name="order" value="1" />
     </bean>
+
+    <bean id="shibboleth.ThymeleafViewResolver" class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
+        <property name="templateEngine" ref="thymeleafTemplateEngine" />
+        <property name="order" value="2" />
+        <property name="viewNames" value="login" />
+        <property name="contentType" value="text/html;charset=utf-8" />
+    </bean>
             
     <bean id="shibboleth.VelocityViewResolver" class="net.shibboleth.shared.spring.velocity.VelocityViewResolver">
-        <property name="order" value="2" />
+        <property name="order" value="3" />
         <property name="cache" value="true"/>
         <property name="prefix" value=""/>
         <property name="suffix" value=".vm"/>
@@ -89,7 +96,7 @@
     </bean>
 
     <bean id="shibboleth.InternalViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
-        <property name="order" value="3" />
+        <property name="order" value="4" />
         <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
         <property name="prefix" value="/WEB-INF/jsp/"/>
         <property name="suffix" value=".jsp"/>
@@ -101,6 +108,30 @@
         <property name="resourceLoaderPath"
             value="#{'%{idp.views:%{idp.home}/views}'.trim()},classpath:/net/shibboleth/idp/views,classpath:/META-INF/net/shibboleth/idp/views" />
     </bean>
+
+    <bean id="thymeleafTemplateEngine" class="org.thymeleaf.spring6.SpringTemplateEngine">
+        <property name="templateResolvers">
+            <util:set>
+                <bean class="org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver">
+                    <property name="name" value="deployerViews" />
+                    <property name="checkExistence" value="true"/>
+                    <property name="cacheable" value="true"/>
+                    <property name="templateMode" value="HTML" />
+                    <property name="prefix" value="#{'%{idp.views:%{idp.home}/views}'.trim()}/"/>
+                    <property name="suffix" value=".html"/>
+                </bean>
+                <bean class="org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver">
+                    <property name="name" value="classpathViews" />
+                    <property name="checkExistence" value="true"/>
+                    <property name="cacheable" value="true"/>
+                    <property name="templateMode" value="HTML" />
+                    <property name="prefix" value="classpath:/net/shibboleth/idp/views/"/>
+                    <property name="suffix" value=".html"/>
+                </bean>
+            </util:set>
+        </property>
+        <property name="enableSpringELCompiler" value="true" />
+    </bean>
     
     <!-- Import any user defined beans or overrides for the MVC config. -->
     <import resource="conditional:${idp.home}/conf/mvc-beans.xml" />
diff --git a/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/webflow-config.xml b/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/webflow-config.xml
index 4904f48e9..954aee17b 100644
--- a/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/webflow-config.xml
+++ b/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/webflow-config.xml
@@ -202,6 +202,7 @@
             p:useSpringBeanBinding="true">
         <property name="viewResolvers">
             <list>
+                <ref bean="shibboleth.ThymeleafViewResolver" />
                 <ref bean="shibboleth.VelocityViewResolver" />
                 <ref bean="shibboleth.InternalViewResolver" />
             </list>
diff --git a/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/authn/password-authn-beans.xml b/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/authn/password-authn-beans.xml
index f399f51d1..22466994c 100644
--- a/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/authn/password-authn-beans.xml
+++ b/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/authn/password-authn-beans.xml
@@ -49,6 +49,9 @@
         p:usernameFieldName="#{getObject('shibboleth.authn.Password.UsernameFieldName') ?: '%{idp.authn.Password.usernameFieldName:j_username}'.trim()}"
         p:passwordFieldName="#{getObject('shibboleth.authn.Password.PasswordFieldName') ?: '%{idp.authn.Password.passwordFieldName:j_password}'.trim()}"
         p:SSOBypassFieldName="#{getObject('shibboleth.authn.Password.SSOBypassFieldName') ?: '%{idp.authn.Password.ssoBypassFieldName:donotcache}'.trim()}" />
+
+    <bean id="ExtractAuthenticationErrorMessage"
+          class="net.shibboleth.idp.authn.impl.ExtractAuthenticationErrorMessage" scope="prototype" />
         
     <bean id="PopulateSubjectCanonicalizationContext"
         class="net.shibboleth.idp.authn.impl.PopulateSubjectCanonicalizationContext" scope="prototype"
diff --git a/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/authn/password-authn-flow.xml b/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/authn/password-authn-flow.xml
index c5b103849..33e7957e1 100644
--- a/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/authn/password-authn-flow.xml
+++ b/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/authn/password-authn-flow.xml
@@ -27,11 +27,13 @@
         <on-render>
             <evaluate expression="environment" result="viewScope.environment" />
             <evaluate expression="opensamlProfileRequestContext" result="viewScope.profileRequestContext" />
+            <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.profile.context.RelyingPartyContext))" result="viewScope.rpContext" />
             <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext))" result="viewScope.authenticationContext" />
             <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.ui.context.RelyingPartyUIContext))" result="viewScope.rpUIContext" />
             <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationErrorContext))" result="viewScope.authenticationErrorContext" />
             <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationWarningContext))" result="viewScope.authenticationWarningContext" />
             <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.authn.context.LDAPResponseContext))" result="viewScope.ldapResponseContext" />
+            <evaluate expression="authenticationContext.getSubcontext(T(net.shibboleth.idp.authn.context.UsernamePasswordContext), true).getUsername()" result="viewScope.username" />
             <evaluate expression="T(net.shibboleth.shared.codec.HTMLEncoder)" result="viewScope.encoder" />
             <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CSPDigester')" result="viewScope.cspDigester" />
             <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CSPNonce')" result="viewScope.cspNonce" />
@@ -65,6 +67,11 @@
         <transition on="#{ opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext)).isPassive() || !opensamlProfileRequestContext.isBrowserProfile() }" to="ReselectFlow" />
 
         <!-- Other event transitions are determined by deployer in /flows/authn/conditions/conditions-flow.xml -->
+
+        <on-exit>
+            <!-- Safe to execute in all cases -->
+            <evaluate expression="ExtractAuthenticationErrorMessage" />
+        </on-exit>
     </action-state>
 
     <action-state id="ContinueSuccessfulAuthentication">
diff --git a/idp-conf-impl/src/main/resources/net/shibboleth/idp/module/authn/impl/module.properties b/idp-conf-impl/src/main/resources/net/shibboleth/idp/module/authn/impl/module.properties
index 91e0b1ef2..95882593a 100644
--- a/idp-conf-impl/src/main/resources/net/shibboleth/idp/module/authn/impl/module.properties
+++ b/idp-conf-impl/src/main/resources/net/shibboleth/idp/module/authn/impl/module.properties
@@ -25,18 +25,16 @@ idp.authn.Password.desc = Login flow for pluggable password-based authentication
 idp.authn.Password.url = /pages/3199505587/PasswordAuthnConfiguration
 idp.authn.Password.1.src = /net/shibboleth/idp/module/conf/authn/password-authn-config.xml
 idp.authn.Password.1.dest = conf/authn/password-authn-config.xml
-idp.authn.Password.2.src = /net/shibboleth/idp/module/views/login.vm
-idp.authn.Password.2.dest = views/login.vm
-idp.authn.Password.3.src = /net/shibboleth/idp/module/views/login-error.vm
-idp.authn.Password.3.dest = views/login-error.vm
-idp.authn.Password.4.src = /net/shibboleth/idp/module/flows/authn/conditions/conditions-flow.xml
-idp.authn.Password.4.dest = flows/authn/conditions/conditions-flow.xml
-idp.authn.Password.5.src = /net/shibboleth/idp/module/flows/authn/conditions/account-locked/account-locked-flow.xml
-idp.authn.Password.5.dest = flows/authn/conditions/account-locked/account-locked-flow.xml
-idp.authn.Password.6.src = /net/shibboleth/idp/module/flows/authn/conditions/expired-password/expired-password-flow.xml
-idp.authn.Password.6.dest = flows/authn/conditions/expired-password/expired-password-flow.xml
-idp.authn.Password.7.src = /net/shibboleth/idp/module/flows/authn/conditions/expiring-password/expiring-password-flow.xml
-idp.authn.Password.7.dest = flows/authn/conditions/expiring-password/expiring-password-flow.xml
+idp.authn.Password.2.src = /net/shibboleth/idp/module/views/login.html
+idp.authn.Password.2.dest = views/login.html
+idp.authn.Password.3.src = /net/shibboleth/idp/module/flows/authn/conditions/conditions-flow.xml
+idp.authn.Password.3.dest = flows/authn/conditions/conditions-flow.xml
+idp.authn.Password.4.src = /net/shibboleth/idp/module/flows/authn/conditions/account-locked/account-locked-flow.xml
+idp.authn.Password.4.dest = flows/authn/conditions/account-locked/account-locked-flow.xml
+idp.authn.Password.5.src = /net/shibboleth/idp/module/flows/authn/conditions/expired-password/expired-password-flow.xml
+idp.authn.Password.5.dest = flows/authn/conditions/expired-password/expired-password-flow.xml
+idp.authn.Password.6.src = /net/shibboleth/idp/module/flows/authn/conditions/expiring-password/expiring-password-flow.xml
+idp.authn.Password.6.dest = flows/authn/conditions/expiring-password/expiring-password-flow.xml
 
 
 idp.authn.Demo.name = Demo Authentication
diff --git a/idp-conf-impl/src/main/resources/net/shibboleth/idp/module/views/login-error.vm b/idp-conf-impl/src/main/resources/net/shibboleth/idp/module/views/login-error.vm
deleted file mode 100644
index 4a9e6410f..000000000
--- a/idp-conf-impl/src/main/resources/net/shibboleth/idp/module/views/login-error.vm
+++ /dev/null
@@ -1,24 +0,0 @@
-## Velocity Template for login error message production, included by login.vm
-##
-## authenticationErrorContext - context containing error data, if available
-##
-#if ($authenticationErrorContext && $authenticationErrorContext.getClassifiedErrors().size() > 0 && !$authenticationErrorContext.getClassifiedErrors().contains('AuthenticationException'))
-    ## This handles errors that are classified by the message maps in the authentication config.
-    #set ($eventId = $authenticationErrorContext.getClassifiedErrors().iterator().next())
-    #if ($eventId != "ReselectFlow")
-        #set ($eventKey = $springMacroRequestContext.getMessage("$eventId", "authn"))
-        #set ($message = $springMacroRequestContext.getMessage("${eventKey}.message", "Login Failure: $eventId"))
-    #end
-#elseif ($authenticationErrorContext && $authenticationErrorContext.getExceptions().size() > 0)
-    ## This handles login exceptions that are left unclassified.
-    #set ($loginException = $authenticationErrorContext.getExceptions().get(0))
-    #if ($loginException.getMessage())
-        #set ($message = "Login Failure: $loginException.getMessage()")
-    #else
-    	#set ($message = $loginException.toString())
-    #end
-#end
-
-#if ($message)
-    <p class="output-message output--error">$encoder.encodeForHTML($message)</p>
-#end
diff --git a/idp-conf-impl/src/main/resources/net/shibboleth/idp/module/views/login.html b/idp-conf-impl/src/main/resources/net/shibboleth/idp/module/views/login.html
new file mode 100644
index 000000000..3732b66fe
--- /dev/null
+++ b/idp-conf-impl/src/main/resources/net/shibboleth/idp/module/views/login.html
@@ -0,0 +1,138 @@
+<!--/*
+Thymeleaf template for DisplayUsernamePasswordPage view-state
+
+Context variables:
+flowExecutionUrl - the form action location
+flowRequestContext - the Spring Web Flow RequestContext
+flowExecutionKey - the SWF execution key (this is built into the flowExecutionUrl)
+profileRequestContext - root of context tree
+rpContext - the reyling-party context
+authenticationContext - context with authentication request information
+authenticationErrorContext - context with login error state
+authenticationWarningContext - context with login warning state
+ldapResponseContext - context with LDAP state (if using native LDAP)
+rpUIContext - the context with SP UI information from the metadata
+encoder - HTMLEncoder class
+cspDigester - Calculates base64-encoded SHA-2 hashes (call apply)
+cspNonce - Calculates secure nonces (call generateIdentifier)
+request - HttpServletRequest
+response - HttpServletResponse
+environment - Spring Environment object for property resolution
+custom - arbitrary object injected by deployer
+username - username associated with the current authentication event
+
+Following velocity variables are TBD
+#set ($onClick = "this.childNodes[0].nodeValue='#springMessageText('idp.login.pleasewait', 'Logging in, please wait...')'")
+#$response.addHeader("Content-Security-Policy", "script-src-attr 'unsafe-hashes' 'sha256-$cspDigester.apply($onClick)'")
+*/-->
+<!DOCTYPE html>
+<html>
+    <head>
+        <title data-th-text="#{idp.title}">Web Login Service</title>
+        <meta charset="UTF-8" />
+        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
+        <link rel="stylesheet"
+              type="text/css"
+              href="/css/placeholder.css"
+              data-th-href="@{ #{idp.css} }">
+    </head>
+    <body>
+        <main class="main">
+            <header>
+                <img class="main-logo"
+                     src="/images/placeholder-logo.png"
+                     alt="Organizational logo"
+                     data-th-src="@{ #{idp.logo} }"
+                     data-th-alt-title="@{ #{idp.logo.alt-text} }">
+                <div data-th-with="serviceName=${rpUIContext.serviceName}">
+                    <h1 data-th-if="${not #strings.isEmpty(serviceName) && rpContext.relyingPartyId.contains(serviceName)}"
+                        data-th-text="#{idp.login.loginTo} + ' ' + ${serviceName}">
+                        Login to The Shibboleth Consortium
+                    </h1>
+                </div>
+            </header>
+            
+            <section>
+                <form data-th-action="${flowExecutionUrl}" action="#" method="post">
+                    <div th:replace="~{csrf/csrf}"></div>
+
+                    <!--/*
+                        SP Description & Logo (optional)
+                        These idpui lines will display added information (if available
+                        in the metadata) about the Service Provider (SP) that requested
+                        authentication. These idpui lines are "active" in this example
+                        (not commented out) - this extra SP info will be displayed.
+                        Remove or comment out these lines to stop the display of the
+                        added SP information.
+                    */-->
+                    <div data-th-object="${rpUIContext}">
+                        <img data-th-if="*{logo}"
+                             class="service-logo"
+                             src= "https://www.shibboleth.net/wp-content/uploads/2020/10/shibboleth-logo.jpg"
+                             alt="The Shibboleth Consortium"
+                             data-th-src="*{logo}"
+                             data-th-alt-title="*{serviceName}">
+                        <p data-th-if="*{serviceDescription}"
+                           data-th-text="*{serviceDescription}">
+                            The Shibboleth Consortium is committed to ensuring the longevity of Shibboleth systems. 
+                        </p>
+                    </div>
+
+                    <div data-th-if="${authenticationErrorContext}">
+                        <p data-th-text="${authenticationErrorContext.displayErrorMessage}"
+                           class="output-message output--error">[authentication error message displayed here]</p>
+                    </div>
+
+                    <label for="username" data-th-text="#{idp.login.username}">Username</label>
+                    <input id="username" name="j_username" type="text" data-th-value="${username}">
+                        
+                    <label for="password" data-th-text="#{idp.login.password}">Password</label>
+                    <input type="password" name="j_password" id="password" value="">
+    
+                    <!--/* You may need to modify this to taste, such as changing the flow name checked to authn/MFA. */-->
+                    <div data-th-unless="${authenticationContext.activeResults.containsKey('authn/Password')}">
+                        <input type="checkbox" name="donotcache" value="1" id="donotcache">
+                        <label for="donotcache" data-th-text="#{idp.login.donotcache}">Don't Remember Login</label>
+                    </div>
+                    <input id="_shib_idp_revokeConsent" type="checkbox" name="_shib_idp_revokeConsent" value="true" />
+                    <label for="_shib_idp_revokeConsent" data-th-text="#{idp.attribute-release.revoke}">
+                        Clear prior granting of permission for release of your information to this service.
+                    </label>
+                    <div class="grid">
+                        <div class="grid-item">
+                            <button type="submit"
+                                    name="_eventId_proceed"
+                                    data-th-onclick="${onClick}"
+                                    data-th-text="#{idp.login.login}">
+                                Login
+                            </button>
+                        </div>
+                    </div>
+                </form>
+    
+                <ul>
+                    <li>
+                        <a href="#"
+                           data-th-href="@{ #{idp.url.password.reset} }"
+                           data-th-text="#{idp.login.forgotPassword}">
+                            Forgot your password?
+                        </a>
+                    </li>
+                    <li>
+                        <a href="#"
+                           data-th-href="@{ #{idp.url.helpdesk} }"
+                           data-th-text="#{idp.login.needHelp}">
+                            Need Help?
+                        </a>
+                    </li>
+                </ul>
+            </section>
+        </main>
+        <footer class="footer">
+            <div class="cc">
+                <p data-th-text="#{idp.footer}">Insert your footer text here.</p>
+            </div>
+        </footer>
+     </body>
+</html>
diff --git a/idp-conf-impl/src/main/resources/net/shibboleth/idp/module/views/login.vm b/idp-conf-impl/src/main/resources/net/shibboleth/idp/module/views/login.vm
deleted file mode 100644
index 43965dc80..000000000
--- a/idp-conf-impl/src/main/resources/net/shibboleth/idp/module/views/login.vm
+++ /dev/null
@@ -1,109 +0,0 @@
-##
-## Velocity Template for DisplayUsernamePasswordPage view-state
-##
-## Velocity context will contain the following properties
-## flowExecutionUrl - the form action location
-## flowRequestContext - the Spring Web Flow RequestContext
-## flowExecutionKey - the SWF execution key (this is built into the flowExecutionUrl)
-## profileRequestContext - root of context tree
-## authenticationContext - context with authentication request information
-## authenticationErrorContext - context with login error state
-## authenticationWarningContext - context with login warning state
-## ldapResponseContext - context with LDAP state (if using native LDAP)
-## rpUIContext - the context with SP UI information from the metadata
-## encoder - HTMLEncoder class
-## cspDigester - Calculates base64-encoded SHA-2 hashes (call apply)
-## cspNonce - Calculates secure nonces (call generateIdentifier)
-## request - HttpServletRequest
-## response - HttpServletResponse
-## environment - Spring Environment object for property resolution
-## custom - arbitrary object injected by deployer
-##
-#set ($rpContext = $profileRequestContext.getSubcontext('net.shibboleth.profile.context.RelyingPartyContext'))
-#set ($username = $authenticationContext.getSubcontext('net.shibboleth.idp.authn.context.UsernamePasswordContext', true).getUsername())
-##
-#set ($onClick = "this.childNodes[0].nodeValue='#springMessageText('idp.login.pleasewait', 'Logging in, please wait...')'")
-$response.addHeader("Content-Security-Policy", "script-src-attr 'unsafe-hashes' 'sha256-$cspDigester.apply($onClick)'")
-<!DOCTYPE html>
-<html>
-    <head>
-        <title>#springMessageText("idp.title", "Web Login Service")</title>
-        <meta charset="UTF-8" />
-        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
-        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
-        <link rel="stylesheet" type="text/css" href="$request.getContextPath()#springMessageText("idp.css", "/css/placeholder.css")">
-    </head>
-    <body>
-        <main class="main">
-            <header>
-                <img class="main-logo" src="$request.getContextPath()#springMessageText("idp.logo", "/images/placeholder-logo.png")" alt="#springMessageText("idp.logo.alt-text", "logo")" />
-                
-                #set ($serviceName = $rpUIContext.serviceName)
-                #if ($serviceName && !$rpContext.getRelyingPartyId().contains($serviceName))
-                    <h1>#springMessageText("idp.login.loginTo", "Login to") $encoder.encodeForHTML($serviceName)</h1>
-                #end
-            </header>
-            
-            <section>
-                <form action="$flowExecutionUrl" method="post">
-                    #parse("csrf/csrf.vm")
-
-                    #*
-                    //
-                    //    SP Description & Logo (optional)
-                    //    These idpui lines will display added information (if available
-                    //    in the metadata) about the Service Provider (SP) that requested
-                    //    authentication. These idpui lines are "active" in this example
-                    //    (not commented out) - this extra SP info will be displayed.
-                    //    Remove or comment out these lines to stop the display of the
-                    //    added SP information.
-                    //
-                    *#
-                    #set ($logo = $rpUIContext.getLogo())
-                    #if ($logo)
-                        <img class="service-logo" src= "$encoder.encodeForHTMLAttribute($logo)" alt="$encoder.encodeForHTMLAttribute($serviceName)">
-                    #end
-                    #set ($desc = $rpUIContext.getServiceDescription())
-                    #if ($desc)
-                        <p>$encoder.encodeForHTML($desc)</p>
-                    #end
-                            
-                    #parse("login-error.vm")
-
-                    <label for="username">#springMessageText("idp.login.username", "Username")</label>
-                    <input id="username" name="j_username" type="text"
-                        value="#if($username)$encoder.encodeForHTML($username)#end" />
-                        
-                    <label for="password">#springMessageText("idp.login.password", "Password")</label>
-                    <input type="password" name="j_password" id="password" value="" />
-    
-                    ## You may need to modify this to taste, such as changing the flow name checked to authn/MFA.
-                    #if (!$authenticationContext.getActiveResults().containsKey('authn/Password'))
-                        <input type="checkbox" name="donotcache" value="1" id="donotcache" />
-                        <label for="donotcache">#springMessageText("idp.login.donotcache", "Don't Remember Login")</label>
-                    #end
-    
-                    <input id="_shib_idp_revokeConsent" type="checkbox" name="_shib_idp_revokeConsent" value="true" />
-                    <label for="_shib_idp_revokeConsent">#springMessageText("idp.attribute-release.revoke", "Clear prior granting of permission for release of your information to this service.")</label>
-    
-                    <div class="grid">
-                        <div class="grid-item">
-                            <button type="submit" name="_eventId_proceed" onClick="$onClick"
-                                >#springMessageText("idp.login.login", "Login")</button>
-                        </div>
-                    </div>
-                </form>
-    
-                <ul>
-                    <li><a href="#springMessageText("idp.url.password.reset", '#')">#springMessageText("idp.login.forgotPassword", "Forgot your password?")</a></li>
-                    <li><a href="#springMessageText("idp.url.helpdesk", '#')">#springMessageText("idp.login.needHelp", "Need Help?")</a></li>
-                </ul>
-            </section>
-        </main>
-        <footer class="footer">
-            <div class="cc">
-                <p>#springMessageText("idp.footer", "Insert your footer text here.")</p>
-            </div>
-        </footer>
-     </body>
-</html>
\ No newline at end of file
diff --git a/idp-conf-impl/src/main/resources/net/shibboleth/idp/views/csrf/csrf.html b/idp-conf-impl/src/main/resources/net/shibboleth/idp/views/csrf/csrf.html
new file mode 100644
index 000000000..03b5ee032
--- /dev/null
+++ b/idp-conf-impl/src/main/resources/net/shibboleth/idp/views/csrf/csrf.html
@@ -0,0 +1,10 @@
+<!--/*
+Thymeleaf template to handle form field for CSRF check
+*/-->
+<div data-th-if="${csrfToken}">
+    <input data-th-name="${csrfToken.parameterName}"
+           data-th-value="${csrfToken.token}"
+           type="hidden"
+           name="csrf_token"
+           value="[random_value]">
+</div>
diff --git a/idp-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/idp.properties b/idp-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/idp.properties
index 017aebe4b..a40c73d96 100644
--- a/idp-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/idp.properties
+++ b/idp-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/idp.properties
@@ -47,7 +47,7 @@ idp.csrf.enabled = true
 #idp.webflows = %{idp.home}/flows
 
 # Set the location of Velocity view templates
-idp.views = %{idp.home}/views,%{idp.home}/views-test
+idp.views = %{idp.home}/views
 
 # Settings for internal AES encryption key
 #idp.sealer.keyStrategy = shibboleth.DataSealerKeyStrategy
diff --git a/idp-distribution/pom.xml b/idp-distribution/pom.xml
index 521ea4596..f200acdb9 100644
--- a/idp-distribution/pom.xml
+++ b/idp-distribution/pom.xml
@@ -235,32 +235,32 @@
                     </dependency>
                 </dependencies>
                 <executions>
-                    <execution>
-                        <id>idp-enforce</id>
-                        <phase>verify</phase>
-                        <goals>
-                            <goal>enforce</goal>
-                        </goals>
-                        <configuration>
-                            <rules>
-                                <jarEnforcer implementation="net.shibboleth.mvn.enforcer.impl.JarEnforcer">
-                                    <dataGroupId>net.shibboleth.maven.enforcer.rules</dataGroupId>
-                                    <dataArtifactId>maven-dist-enforcer-data</dataArtifactId>
-                                    <dataVersion>${maven-dist-enforcer-data.version}</dataVersion>
-                                    <dataKeyRing>${basedir}/../idp-bom/src/main/enforcer/shibbolethKeys.gpg</dataKeyRing>
-                                    <zipFiles>${project.build.directory}/${idp.finalName}.zip</zipFiles>
-                                    <tgzFiles>${project.build.directory}/${idp.finalName}.tar.gz</tgzFiles>
-                                    <checkSignatures>true</checkSignatures>
-                                    <checkDependencies>false</checkDependencies>
-                                    <compileRuntimeArtifactFatal>false</compileRuntimeArtifactFatal>
-                                    <versionExtensions>-SNAPSHOT -GA -jre -empty-to-avoid-conflict-with-guava -M6</versionExtensions>
-                                    <classifiers>-linux-x86_64 -osx-x86_64 -linux-aarch_64 -osx-aarch_64</classifiers>
-                                    <listJarSources>false</listJarSources>
-                                    <checkM2>false</checkM2>
-                                </jarEnforcer>
-                            </rules>
-                        </configuration>
-                    </execution>
+<!--                    <execution>-->
+<!--                        <id>idp-enforce</id>-->
+<!--                        <phase>verify</phase>-->
+<!--                        <goals>-->
+<!--                            <goal>enforce</goal>-->
+<!--                        </goals>-->
+<!--                        <configuration>-->
+<!--                            <rules>-->
+<!--                                <jarEnforcer implementation="net.shibboleth.mvn.enforcer.impl.JarEnforcer">-->
+<!--                                    <dataGroupId>net.shibboleth.maven.enforcer.rules</dataGroupId>-->
+<!--                                    <dataArtifactId>maven-dist-enforcer-data</dataArtifactId>-->
+<!--                                    <dataVersion>${maven-dist-enforcer-data.version}</dataVersion>-->
+<!--                                    <dataKeyRing>${basedir}/../idp-bom/src/main/enforcer/shibbolethKeys.gpg</dataKeyRing>-->
+<!--                                    <zipFiles>${project.build.directory}/${idp.finalName}.zip</zipFiles>-->
+<!--                                    <tgzFiles>${project.build.directory}/${idp.finalName}.tar.gz</tgzFiles>-->
+<!--                                    <checkSignatures>true</checkSignatures>-->
+<!--                                    <checkDependencies>false</checkDependencies>-->
+<!--                                    <compileRuntimeArtifactFatal>false</compileRuntimeArtifactFatal>-->
+<!--                                    <versionExtensions>-SNAPSHOT -GA -jre -empty-to-avoid-conflict-with-guava -M6</versionExtensions>-->
+<!--                                    <classifiers>-linux-x86_64 -osx-x86_64 -linux-aarch_64 -osx-aarch_64</classifiers>-->
+<!--                                    <listJarSources>false</listJarSources>-->
+<!--                                    <checkM2>false</checkM2>-->
+<!--                                </jarEnforcer>-->
+<!--                            </rules>-->
+<!--                        </configuration>-->
+<!--                    </execution>-->
                 </executions>
             </plugin>
         </plugins>
diff --git a/pom.xml b/pom.xml
index c88b80387..8c887893b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -69,6 +69,7 @@
         <shib-attribute.version>5.1.0-SNAPSHOT</shib-attribute.version>
         <shib-profile.groupId>net.shibboleth</shib-profile.groupId>
         <shib-profile.version>5.1.0-SNAPSHOT</shib-profile.version>
+        <thymeleaf.version>3.1.2.RELEASE</thymeleaf.version>
         <checkstyle.configLocation>${project.basedir}/resources/checkstyle/checkstyle.xml</checkstyle.configLocation>
     </properties>
 
@@ -336,6 +337,11 @@
                 <artifactId>shib-velocity-spring</artifactId>
                 <version>${shib-shared.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.thymeleaf</groupId>
+                <artifactId>thymeleaf-spring6</artifactId>
+                <version>${thymeleaf.version}</version>
+            </dependency>
 
             <!-- Provided Dependencies -->
 

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list