AEM 61 - Configuration property Maximum Password Age available in Apache Jackrabbit Oak UserConfiguration service (http://localhost:4502/system/console/configMgr/org.apache.jackrabbit.oak.security.user.UserConfigurationImpl) available in CRX Oak can be used to set the password expiry in days. Check this ticket for more information
Goal
For users existing in CQ, this extension helps providing a Password Expiry like feature in Classic UI. Check Demo, Source Code and Package Install
Assuming, only the administrators have access to /useradmin console and user nodes
Package logic adds a new textfield widget passwordExpiryInDays to the form in User Admin console (http://localhost:4502/useradmin)
Stored in CRX (http://localhost:4502/crx/de) as passwordExpiryInDays
When a value exists for the property passwordExpiryInDays on any user profile node (eg. /home/users/geometrixx/author/profile), logic checks if the value of passwordLastChanged date (this is set when user changes the password using Password Expired dialog, explained in next section) on user node (eg. /home/users/geometrixx/author ) + passwordExpiryInDays on user profile node (eg. /home/users/geometrixx/author/profile) is LESS than today's date on server; if it is, Password Expired dialog starts annoying the user on Classic UI admin and authoring. if passwordLastChanged doesn't exist, logic works with user's jcr:created date
On Admin UI
On Authoring UI
When user enters new password, the servlet /bin/experience-aem/pm/expiry sets property passwordLastChanged on user node
Please note, this works on Classic UI only and since most of the authoring activity happens on Classic UI in 561, the touch UI was not handled for password expiry. More enhancements are yet to be added like Password Strength, Forgot Password, Password Expiry Reminders etc.
Solution
We need a servlet GetPasswordOptions with doGet method returning json data for user password expired and doPost setting passwordLastChanged on user node. The package is created using multimodule-content-package-archetype, for detailed notes on how to create a package using multimodule-content-package-archetype in Intellij check this post
package apps.experienceaem.pm; import org.apache.jackrabbit.api.security.user.Authorizable; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Properties; import org.apache.felix.scr.annotations.Property; import org.apache.felix.scr.annotations.Service; import org.apache.jackrabbit.api.JackrabbitSession; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.SlingHttpServletResponse; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ValueMap; import org.apache.sling.api.servlets.SlingAllMethodsServlet; import org.apache.sling.commons.json.io.JSONWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.jcr.Node; import javax.jcr.Session; import javax.servlet.ServletException; import java.io.IOException; import java.util.Calendar; import java.util.Date; @Component(metatype = false) @Service @Properties({ @Property(name = "sling.servlet.paths", value = "/bin/experience-aem/pm/expiry"), @Property(name = "sling.servlet.methods", value = { "GET", "POST" } ) }) public class GetPasswordOptions extends SlingAllMethodsServlet { private final static Logger log = LoggerFactory.getLogger(GetPasswordOptions.class); @Override protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException { response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); try{ ResourceResolver resolver = request.getResourceResolver(); JackrabbitSession session = (JackrabbitSession)resolver.adaptTo(Session.class); Authorizable user = session.getUserManager().getAuthorizable(session.getUserID()); ValueMap profileMap = resolver.getResource(user.getPath() + "/profile").adaptTo(ValueMap.class); JSONWriter jw = new JSONWriter(response.getWriter()); jw.object(); if(!profileMap.containsKey("passwordExpiryInDays")){ jw.key("expired").value(false); }else{ int passwordExpiryInDays = profileMap.get("passwordExpiryInDays", Integer.class); ValueMap resMap = resolver.getResource(user.getPath()).adaptTo(ValueMap.class); Date lastChangedDate = resMap.containsKey("passwordLastChanged") ? resMap.get("passwordLastChanged", Date.class) : resMap.get("jcr:created", Date.class); jw.key("passwordLastChanged").value(lastChangedDate); //calculate the expiry date based on server time Calendar expiryDate = Calendar.getInstance(); expiryDate.setTime(lastChangedDate); expiryDate.add(Calendar.DAY_OF_YEAR, passwordExpiryInDays); Calendar today = Calendar.getInstance(); jw.key("expired").value(expiryDate.getTimeInMillis() < today.getTimeInMillis()); jw.key("userPath").value(user.getPath()); jw.key("passwordExpiryInDays").value(passwordExpiryInDays); } jw.endObject(); }catch(Exception e){ log.error("Error", e); throw new ServletException(e); } } @Override protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException { response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); try{ ResourceResolver resolver = request.getResourceResolver(); JackrabbitSession session = (JackrabbitSession)resolver.adaptTo(Session.class); Authorizable user = session.getUserManager().getAuthorizable(session.getUserID()); Node node = resolver.getResource(user.getPath()).adaptTo(Node.class); //set the last changed date to time on server node.setProperty("passwordLastChanged", Calendar.getInstance()); session.save(); JSONWriter jw = new JSONWriter(response.getWriter()); jw.object().key("success").value("success").endObject(); }catch(Exception e){ log.error("Error", e); throw new ServletException(e); } } }
On Classic UI, we need to add the Password Expiry (days) widget to user form and necessary logic to mask the UI when user password has expired, so create a clientlib
1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/password-expiration
2) Create node /apps/password-expiration/clientlib of type cq:ClientLibraryFolder and add a String property categories with value cq.security
3) Create file (nt:file) /apps/password-expiration/clientlib/js.txt and add
password-expiration.js
4) Create file (nt:file) /apps/password-expiration/clientlib/password-expiration.js and add the following code
CQ.Ext.ns("ExperienceAEM.PasswordMgmt"); ExperienceAEM.PasswordMgmt = { getChangePasswordDialog: function(userPath, passwordExpiryInDays){ var dialogCfg = CQ.Util.copyObject(CQ.UserInfo.PASSWORD_DIALOG_CFG); dialogCfg.title = "Password Expired"; dialogCfg.buttons = CQ.Dialog.OK; dialogCfg.closable = false; dialogCfg.ok = function(){ var dialog = this; var find = function(panel, name){ return panel.findBy(function(comp){ return comp["name"] == name; }, panel); }; var currentPassword = find(dialog, ":currentPassword"); var passwords = find(dialog, "rep:password"); if(currentPassword[0].getValue() && passwords[0].getValue() && passwords[1].getValue()){ if(passwords[0].getValue() == passwords[1].getValue()){ var options = { _charset_: "utf-8", ":status": "browser", ":currentPassword": currentPassword[0].getValue(), "rep:password": passwords[0].getValue() }; $.ajax({ url: userPath + ".rw.html", dataType: "html", data: options, success: function(){ $.ajax({ url: "/bin/experience-aem/pm/expiry", dataType: "json", success: function(){ CQ.Notification.notify("Password changed","New password expires in " + passwordExpiryInDays + " days"); dialog.close(); CQ.Ext.getBody().unmask(); if (( window.location.pathname == "/cf" ) || ( window.location.pathname.indexOf("/content") == 0)) { location.reload(); } }, error: function(j, t, e){ alert("Error changing password, couldn't set last changed date"); }, type: 'POST' }); }, error: function(j, t, e){ alert("Either the old password is incorrect or there is some error"); }, type: 'POST' }); } } }; return CQ.WCM.getDialog(dialogCfg); }, showChangePasswordDialog: function(){ $.ajax({ url: "/bin/experience-aem/pm/expiry", dataType: "json", async: false, success: function(data){ var expired = data["expired"]; if(expired){ var dialog = this.getChangePasswordDialog(data["userPath"], data["passwordExpiryInDays"]); CQ.Ext.getBody().mask(); if (( window.location.pathname == "/cf" ) || ( window.location.pathname.indexOf("/content") == 0)) { var cf = CQ.WCM.getContentFinder(); if(cf){ cf.getEl().mask(); } if (CQ.WCM.isEditMode() || CQ.WCM.isDesignMode()) { CQ.WCM.on("sidekickready", function(sk){ sk.getEl().mask() }); } } dialog.show(); } }.createDelegate(this), type: "GET" }); }, addExpiryOptions: function(propPanel){ var userForm = propPanel.userForm; var emailComp = userForm.find('name', 'email')[0]; var passwordExpiryInDays = { "xtype":"textfield", "fieldLabel": "Expire password (days)", "anchor":"15%", "name":"passwordExpiryInDays" }; userForm.insert(userForm.items.indexOf(emailComp) + 1, passwordExpiryInDays); userForm.setAutoScroll(true); userForm.doLayout(); } }; (function() { var PM = ExperienceAEM.PasswordMgmt; CQ.Ext.onReady(function(){ PM.showChangePasswordDialog(); }); if(window.location.pathname == "/useradmin"){ var fields = CQ.security.data.AuthRecord.FIELDS; fields.push({"name": "passwordExpiryInDays"}); fields.push({"name": "passwordExpiryReminderInDays"}); var UA_INTERVAL = setInterval(function(){ var userAdmin = CQ.Ext.getCmp("cq-useradmin"); if(userAdmin && userAdmin.userProperties){ clearInterval(UA_INTERVAL); PM.addExpiryOptions(userAdmin.userProperties); } }, 250); } })();
No comments:
Post a Comment