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