AEM 65 sp10 - Simple Token Based Authentication

Goal

AEM Cloud Services provides OAuth 2 based authentication documented here. For OAuth 2 based authentication in AEM 65 check this post. The solution discussed here is a more simple non standard token based authentication....

Demo | Package Install | GitHub


User for Token Auth


Configure Token Key

                    http://localhost:4502/apps/eaem-simple-token-based-auth/extensions/simple-token-auth/token-config.html/conf/global/settings/dam/experience-aem


Access AEM using Token



Solution

1) Create an overlay for the tools nav item /apps/cq/core/content/nav/tools/experience-aem/experience-aem-config

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
    jcr:description="Token Configuration for Simple Readonly Auth"
    jcr:primaryType="nt:unstructured"
    jcr:title="Token Configuration"
    href="/apps/eaem-simple-token-based-auth/extensions/simple-token-auth/token-config.html/conf/global/settings/dam/experience-aem"
    icon="asset"
    id="experience-aem-config"
    size="XL"/>


2) Create the Token Auth Config page /apps/eaem-simple-token-based-auth/extensions/simple-token-auth/token-config accessed using http://localhost:4502/apps/eaem-simple-token-based-auth/extensions/simple-token-auth/token-config.html/conf/global/settings/dam/experience-aem

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
    jcr:primaryType="cq:Page">
    <jcr:content
        jcr:mixinTypes="[sling:VanityPath]"
        jcr:primaryType="nt:unstructured"
        jcr:title="Token Configuration"
        sling:resourceType="granite/ui/components/coral/foundation/page">
        <head jcr:primaryType="nt:unstructured">
            <favicon
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/page/favicon"/>
            <viewport
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/admin/page/viewport"/>
            <clientlibs
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/includeclientlibs"
                categories="[coralui3,granite.ui.coral.foundation,granite.ui.shell,dam.gui.admin.coral]"/>
        </head>
        <body
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/coral/foundation/page/body">
            <items jcr:primaryType="nt:unstructured">
                <content
                    jcr:primaryType="nt:unstructured"
                    sling:resourceType="granite/ui/components/coral/foundation/form"
                    action="/conf/global/settings/dam/experience-aem"
                    foundationForm="{Boolean}true"
                    maximized="{Boolean}true"
                    method="post"
                    novalidate="{Boolean}true"
                    style="vertical">
                    <successresponse
                        jcr:primaryType="nt:unstructured"
                        jcr:title="Success"
                        sling:resourceType="granite/ui/components/coral/foundation/form/responses/openprompt"
                        open="/assets.html"
                        redirect="/apps/eaem-simple-token-based-auth/extensions/simple-token-auth/token-config.html/conf/global/settings/dam/experience-aem"
                        text="Configuration saved"/>
                    <items jcr:primaryType="nt:unstructured">
                        <type
                            jcr:primaryType="nt:unstructured"
                            sling:resourceType="granite/ui/components/coral/foundation/form/hidden"
                            name="./jcr:primaryType"
                            value="nt:unstructured"/>
                        <wizard
                            jcr:primaryType="nt:unstructured"
                            jcr:title="Configuration"
                            sling:resourceType="granite/ui/components/coral/foundation/wizard">
                            <items jcr:primaryType="nt:unstructured">
                                <area
                                    jcr:primaryType="nt:unstructured"
                                    jcr:title="Configure Thumbnails"
                                    sling:resourceType="granite/ui/components/coral/foundation/container"
                                    maximized="{Boolean}true">
                                    <items jcr:primaryType="nt:unstructured">
                                        <columns
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns"
                                            margin="{Boolean}true">
                                            <items jcr:primaryType="nt:unstructured">
                                                <column
                                                    jcr:primaryType="nt:unstructured"
                                                    sling:resourceType="granite/ui/components/coral/foundation/container">
                                                    <items jcr:primaryType="nt:unstructured">
                                                        <token-key
                                                                jcr:primaryType="nt:unstructured"
                                                                sling:resourceType="/apps/eaem-simple-token-based-auth/extensions/simple-token-auth/generate-key"
                                                                fieldDescription="Enter the token key"
                                                                fieldLabel="Token Key"
                                                                name="./tokenKey"/>
                                                    </items>
                                                </column>
                                            </items>
                                        </columns>
                                    </items>
                                    <parentConfig jcr:primaryType="nt:unstructured">
                                        <prev
                                            granite:class="foundation-wizard-control"
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="granite/ui/components/coral/foundation/anchorbutton"
                                            href="/aem/start.html"
                                            text="Cancel">
                                            <granite:data
                                                jcr:primaryType="nt:unstructured"
                                                foundation-wizard-control-action="cancel"/>
                                        </prev>
                                        <next
                                            granite:class="foundation-wizard-control"
                                            jcr:primaryType="nt:unstructured"
                                            sling:resourceType="granite/ui/components/coral/foundation/button"
                                            text="Save"
                                            type="submit"
                                            variant="primary">
                                            <granite:data
                                                jcr:primaryType="nt:unstructured"
                                                foundation-wizard-control-action="next"/>
                                        </next>
                                    </parentConfig>
                                </area>
                            </items>
                        </wizard>
                    </items>
                </content>
            </items>
        </body>
    </jcr:content>
</jcr:root>


3) Create a custom widget to generate random tokens and provide button to copy /apps/eaem-simple-token-based-auth/extensions/simple-token-auth/generate-key/generate-key.jsp

<%@include file="/libs/granite/ui/global.jsp" %>

<%@page session="false"
        import="org.apache.commons.lang3.StringUtils,
                  com.adobe.granite.ui.components.AttrBuilder,
                  com.adobe.granite.ui.components.Config,
                  com.adobe.granite.ui.components.Tag" %>
<%@ page import="apps.experienceaem.assets.core.services.SimpleTokenAuthService" %>
<%
    Config cfg = cmp.getConfig();

    SimpleTokenAuthService tokenService = sling.getService(SimpleTokenAuthService.class);
    String ssoKey = tokenService.getTokenKey();

    String name = cfg.get("name", String.class);

    Tag tag = cmp.consumeTag();

    AttrBuilder attrs = tag.getAttrs();
    cmp.populateCommonAttrs(attrs);

    attrs.add("name", name);

    String fieldLabel = cfg.get("fieldLabel", String.class);
    String fieldDesc = cfg.get("fieldDescription", String.class);
%>

<div class="coral-Form-fieldwrapper">
    <label class="coral-Form-fieldlabel"><%=fieldLabel%></label>
    <input is="coral-textfield" name="<%=name%>" value="<%=ssoKey%>" style="width: 100%;">
    <coral-icon class="coral-Form-fieldinfo" icon="infoCircle" size="S"></coral-icon>
    <coral-tooltip target="_prev" placement="left" class="coral3-Tooltip" variant="info" role="tooltip" style="display: none;">
        <coral-tooltip-content><%=fieldDesc%></coral-tooltip-content>
    </coral-tooltip>
</div>
<div style="text-align: right; width: 100%; margin: 15px 0 15px 0">
    <button is="coral-button" iconsize="S" id="<%=name%>Gen">Generate</button>
    <button is="coral-button" iconsize="S" id="<%=name%>Copy">copy</button>
</div>

<script>
    function addCopyListener(selector){
        var $widget = $("[name='" + selector + "']"),
            $widgetCopy = $("[id='" + selector + "Copy']"),
            $widgetGen = $("[id='" + selector + "Gen']");

        $widgetCopy.click(function(event){
            event.preventDefault();
            $widget[0].select();
            document.execCommand("copy");
        });

        $widgetGen.click(function(event){
            event.preventDefault();
            $widget.val([...Array(25)].map( i => (~~(Math.random()*36)).toString(36)).join(''));
        });
    }

    $(document).on("foundation-contentloaded", function(){
        addCopyListener("<%=name%>");
    });
</script>


4) Add a simple service to apps.experienceaem.assets.core.services.impl.SimpleTokenAuthServiceImpl read the token saved by the config page in path /conf/global/settings/dam/experience-aem

package apps.experienceaem.assets.core.services.impl;

import apps.experienceaem.assets.core.services.SimpleTokenAuthService;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.Designate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.Map;

@Component(service = SimpleTokenAuthService.class)
public class SimpleTokenAuthServiceImpl implements SimpleTokenAuthService{
    protected final static Logger log = LoggerFactory.getLogger(SimpleTokenAuthServiceImpl.class);

    private static final String EAEM_SERVICE_USER = "eaem-service-user";

    public String CONFIG_PATH = "/conf/global/settings/dam/experience-aem";

    @Reference
    private ResourceResolverFactory resourceResolverFactory;

    @Override
    public String getTokenKey() {
        String tokenKey = "";
        final String configPath = CONFIG_PATH;
        final ResourceResolver resourceResolver = getServiceResourceResolver(resourceResolverFactory);

        final Resource configRes = resourceResolver.getResource(configPath);

        if (configRes == null) {
            return tokenKey;
        }

        tokenKey = configRes.getValueMap().get("tokenKey", String.class);

        if (tokenKey == null) {
            tokenKey = "";
        }

        return tokenKey;
    }

    public static ResourceResolver getServiceResourceResolver(ResourceResolverFactory resourceResolverFactory) {
        Map<String, Object> subServiceUser = new HashMap<>();
        subServiceUser.put(ResourceResolverFactory.SUBSERVICE, EAEM_SERVICE_USER);
        try {
            return resourceResolverFactory.getServiceResourceResolver(subServiceUser);
        } catch (LoginException ex) {
            log.error("Could not login as SubService user {}", EAEM_SERVICE_USER, ex);
            return null;
        }
    }
}

5) Add an authentication handler apps.experienceaem.assets.core.services.TokenAuthHandler to validate the token and provide access impersonating as user eaem-readonly-user

package apps.experienceaem.assets.core.services;

import static org.osgi.framework.Constants.SERVICE_RANKING;

import java.io.IOException;

import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.SimpleCredentials;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.apache.sling.auth.core.spi.AuthenticationHandler;
import org.apache.sling.auth.core.spi.AuthenticationInfo;
import org.apache.sling.jcr.api.SlingRepository;
import org.apache.sling.jcr.resource.api.JcrResourceConstants;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Component(
        service = { AuthenticationHandler.class },
        immediate = true,
        property = {
                SERVICE_RANKING + ":Integer=" + 9999,
                AuthenticationHandler.PATH_PROPERTY + "=/",
                "service.description=Experience AEM Token Auth Handler",
                "sling.filter.scope=REQUEST"
        })
@Designate(ocd = TokenAuthHandler.Configuration.class)
public class TokenAuthHandler implements AuthenticationHandler {
    private static final Logger log = LoggerFactory.getLogger(TokenAuthHandler.class);

    private static String AUTH_TYPE_TOKEN_AUTH = "EXPERIENCE_AEM_TOKEN_AUTH";

    private static String TOKEN_PARAM = "tokenKey";

    private static final String SESSION_REQ_ATTR = TokenAuthHandler.class.getName() + ".session";

    private static final String REQUEST_URL_SUFFIX = "/j_security_check";

    private static final String LOGIN_URL = "/libs/granite/core/content/login.html";

    private static final String REQUEST_METHOD_POST = "POST";

    private static final String USER_NAME = "j_username";

    private static final String LOGIN_TOKEN = "login-token";

    private String readOnlyUser = "";

    @Reference
    private SlingRepository repository;

    @Reference
    private SimpleTokenAuthService tokenAuthService;

    //for crx on localhost two different session ids are returned, one ends with -org.apache.sling and other -org.osgi.service.http
    @java.lang.SuppressWarnings("AEM Rules:AEM-3")
    private Session localhostSession = null;

    @Reference(target = "(service.pid=com.day.crx.security.token.impl.impl.TokenAuthenticationHandler)")
    private AuthenticationHandler wrappedAuthHandler;

    @Activate
    protected void activate(final Configuration config) {
        readOnlyUser = config.read_only_user();
        localhostSession = null;
    }

    @Override
    public AuthenticationInfo extractCredentials(HttpServletRequest request, HttpServletResponse response) {
        Session userSession = (Session)request.getSession().getAttribute(SESSION_REQ_ATTR);
        AuthenticationInfo authInfo = new AuthenticationInfo(AUTH_TYPE_TOKEN_AUTH);

        if(isLogout(request)){
            request.getSession().removeAttribute(SESSION_REQ_ATTR);
            localhostSession = null;

            try {
                removeLoginTokenCookie(request, response);
                response.sendRedirect(LOGIN_URL);
                return authInfo;
            }catch (Exception e){
                log.error("Error redirecting to login url", e);
            }
        }

        if(userSession != null){
            authInfo.put(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, userSession);
            return authInfo;
        }else if(isLocalHost(request)){
            if(localhostSession != null){
                authInfo.put(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, localhostSession);
                return authInfo;
            }
        }

        try{
            if ((REQUEST_METHOD_POST.equals(request.getMethod())) && (request.getRequestURI().endsWith(REQUEST_URL_SUFFIX))
                    && StringUtils.isNotEmpty(USER_NAME)) {
                //constant not created for "j_password" to pass sonar checks
                SimpleCredentials creds = new SimpleCredentials(request.getParameter(USER_NAME),
                        request.getParameter("j_password").toCharArray());
                userSession = this.repository.login(creds);
            }else{
                String tokenKeyInRequest = request.getParameter(TOKEN_PARAM);
                String tokenKeyConfigured = tokenAuthService.getTokenKey();

                if(StringUtils.isEmpty(tokenKeyInRequest) || !tokenKeyInRequest.equals(tokenKeyConfigured)){
                    authInfo = wrappedAuthHandler.extractCredentials(request, response);
                    return authInfo;
                }

                Session adminSession = repository.loginAdministrative(null);

                userSession = adminSession.impersonate(new SimpleCredentials(readOnlyUser, new char[0]));
            }

            request.getSession().setAttribute(SESSION_REQ_ATTR, userSession);

            if(isLocalHost(request)){
                localhostSession = userSession;
            }

            authInfo.put(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, userSession);
        }catch(RepositoryException e){
            log.error("Error could not create session for  - " + AUTH_TYPE_TOKEN_AUTH, e);
            return AuthenticationInfo.FAIL_AUTH;
        }

        return authInfo;
    }

    private void removeLoginTokenCookie(HttpServletRequest request, HttpServletResponse response){
        Cookie[] cookies = request.getCookies();

        if(cookies == null){
            return;
        }

        for(Cookie cookie : cookies){
            if(!LOGIN_TOKEN.equals(cookie.getName())){
                continue;
            }

            cookie.setPath("/");
            cookie.setMaxAge(0);

            response.addCookie(cookie);
        }
    }

    private boolean isLogout(HttpServletRequest request){
        return request.getRequestURI().endsWith("/system/sling/logout.html");
    }

    private boolean isLocalHost(HttpServletRequest request){
        return request.getRequestURL().toString().startsWith("http://localhost:4502/");
    }

    @Override
    public boolean requestCredentials(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException {
        return wrappedAuthHandler.requestCredentials(httpServletRequest, httpServletResponse);
    }

    @Override
    public void dropCredentials(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException {
        wrappedAuthHandler.dropCredentials(httpServletRequest, httpServletResponse);
    }

    @ObjectClassDefinition(name = "Experience AEM Token Authentication Handler Configuration")
    public @interface Configuration {

        @AttributeDefinition(
                name = "Read only user",
                description = "Read only user for browse",
                type = AttributeType.STRING)
        String read_only_user() default "eaem-readonly-user";
    }
}



No comments:

Post a Comment