AEM CQ 561 - Adding Password Expiry Feature to Classic UI


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