Quantcast
Channel: Experiencing Adobe Experience Manager (AEM, CQ)
Viewing all articles
Browse latest Browse all 525

AEM 65 sp10 - Simple Token Based Authentication

$
0
0

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";
}
}




Viewing all articles
Browse latest Browse all 525

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>