com.day.crx.security.token.TokenCookie Maven / Gradle / Ivy
Show all versions of aem-sdk-api Show documentation
/*************************************************************************
*
* ADOBE CONFIDENTIAL
* ___________________
*
* Copyright 1997 Adobe Systems Incorporated
* All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains
* the property of Adobe Systems Incorporated and its suppliers,
* if any. The intellectual and technical concepts contained
* herein are proprietary to Adobe Systems Incorporated and its
* suppliers and are protected by trade secret or copyright law.
* Dissemination of this information or reproduction of this material
* is strictly forbidden unless prior written permission is obtained
* from Adobe Systems Incorporated.
**************************************************************************/
package com.day.crx.security.token;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.BitSet;
import java.util.Map;
import java.util.TreeMap;
import java.util.Vector;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.day.crx.security.token.impl.TokenAuthenticationHandler;
/**
* TokenCookie
provides methods to read and manipulate the value of
* a token cookie.
*
* The TokenCookie value is extracted from a request as follows:
*
* - If a Cookie named {@link #NAME} is present, its value is used
* - If a request parameter named {@link #PARAM_NAME} is present, its first
* value is used
*
*
* The value has the following format:
*
*
* value := info ( ";" info )* .
* info := [ repoid ":" ] workspace ":" token .
* repoid := CRXClusterId | RepositorySystemId | RequestPort .
*
*/
public class TokenCookie {
/**
* default logger
*/
private static final Logger log = LoggerFactory.getLogger(TokenCookie.class);
/**
* Name of the cookie that provides the login token.
*/
public static final String NAME = "login-token";
/**
* The value indicating that the cookie will
* only be sent along with "same-site" requests.
*/
public static final String SAMESITE_ATTR_STRICT = "Strict";
/**
* The value indicating that the cookie will be sent with same-site requests,
* and with "cross-site" top-level navigations.
*/
public static final String SAMESITE_ATTR_LAX = "Lax";
/**
* The value indicating that the cookie will be sent with same-site and
* cross-site requests.
*/
public static final String SAMESITE_ATTR_NONE = "None";
/**
* The value indicating that the cookie will be sent with same-site and
* cross-site requests, partitioned.
*/
public static final String SAMESITE_ATTR_PARTITIONED = "None; Partitioned";
/**
* Name of the request header optionally providing the token cookie value
* instead of the HTTP Cookie.
*
* @since 1.0.2 (Bundle version 2.2.0.2)
*/
public static final String PARAM_NAME = "j_login_token";
/**
* name of the request attribute
*/
public static final String ATTR_NAME = TokenCookie.class.getName();
private final Map infos = new TreeMap<>();
public Map getInfos() {
return infos;
}
/**
* Returns the cookie from the request. First checks if decoded cookie is
* already present as request attribute and reads if from the request
* cookies if needed.
*
* @param request servlet request
* @return a token cookie.
*/
public static TokenCookie fromRequest(HttpServletRequest request) {
TokenCookie t = (TokenCookie) request.getAttribute(ATTR_NAME);
if (t == null) {
String tokenString = getCookie(request, NAME);
if (tokenString == null || tokenString.length() == 0) {
tokenString = request.getParameter(PARAM_NAME);
}
t = TokenCookie.fromString(tokenString);
request.setAttribute(ATTR_NAME, t);
}
return t;
}
/**
* Returns the token info for the given request, respecting the port
* specified in the host header.
*
* This implementation calls the
* {@link #getTokenInfo(HttpServletRequest, String)} method using the
* request port as returned from {@link #getPort(HttpServletRequest)} as the
* repository ID.
*
* @param request the request
* @return the info or {@link Info#INVALID}
* @deprecated use {@link #getTokenInfo(HttpServletRequest, String)} instead
*/
@Deprecated
public static Info getTokenInfo(HttpServletRequest request) {
return getTokenInfo(request, getPort(request));
}
/**
* Returns the {@link Info} from the request for the given repository ID.
*
* @param request The request to extract the {@link Info} from
* @param repoId The repository ID identifying the actual {@link Info}
* instance from the {@link TokenCookie}. This must not be
* null
.
* @return the info or {@link Info#INVALID} if no {@link Info} is available
* for the given repository ID
*/
public static Info getTokenInfo(HttpServletRequest request, String repoId) {
Info info = (Info) request.getAttribute(Info.ATTR_NAME);
if (info == null) {
TokenCookie t = fromRequest(request);
info = t.getInfos().get(repoId);
if (info == null) {
info = Info.INVALID;
}
request.setAttribute(Info.ATTR_NAME, info);
}
return info;
}
/**
* Returns the port form the host header.
*
* @param request request
* @return the port.
*/
public static String getPort(HttpServletRequest request) {
String host = request.getHeader("Host");
String port = "";
if (host == null || host.length() == 0) {
log.warn(
"Request to {} does not include a host header. Using default port.",
request.getRequestURI());
} else {
int idx = host.indexOf(':');
if (idx > 0) {
port = host.substring(idx + 1);
}
}
if (port.length() == 0) {
port = request.isSecure() ? "443" : "80";
}
return port;
}
/**
* Updates the token cookie and sets the response cookie accordingly. if
* token
is null
, the token information is
* removed.
*
* This implementation calls the
* {@link #update(HttpServletRequest, HttpServletResponse, String, String, String, boolean)}
* with the repository ID set to the request's port as returned from
* #getport and not setting the HttpOnly
cookie flag.
*
* @param request servlet request
* @param response servlet response
* @param token token
* @param wsp workspace
* @deprecated use
* {@link #update(HttpServletRequest, HttpServletResponse, String, String, String, boolean)}
* instead
*/
@Deprecated
public static void update(HttpServletRequest request,
HttpServletResponse response, String token, String wsp) {
update(request, response, getPort(request), token, wsp, false);
}
/**
* Updates the token cookie and sets the response cookie accordingly. if
* token
is null
, the token information is
* removed.
*
* This implementation calls the
* {@link #update(HttpServletRequest, HttpServletResponse, String, String, String, boolean, String)}
* with the sameSiteCookieAttribute
set as the configuration
* token.samesite.cookie.attr
*
*
* @param request The request object providing the original token Cookie to
* be updated by this method.
* @param response The response object used to set the cookie on
* @param repoId The repository ID identifying the {@link Info} whose token
* value should be updated or removed.
* @param token The actual token or null
to remove the
* {@link Info} for the repository ID from the cookie.
* @param wsp The workspace which the token is mainly used to access. Ignored
* if token
is null
.
* @param isHttpOnly Whether or not to set the HttpOnly
* attribute on the cookie. For security reasons it is
* recommended to always set this parameter to true
* . The parameter mainly exists for backwards compatibility
* reasons to allow old use cases to still make the cookie
* visible to client side JavaScript.
*/
public static void update(HttpServletRequest request,
HttpServletResponse response, String repoId, String token,
String wsp, boolean isHttpOnly) {
String userAgent = request.getHeader("user-agent");
update(request, response, repoId, token, wsp, isHttpOnly, TokenAuthenticationHandler.getSameSiteCookieAttribute(userAgent));
}
/**
* Updates the token cookie and sets the response cookie accordingly. if
* token
is null
, the token information is
* removed.
*
* @param request The request object providing the original token Cookie to
* be updated by this method.
* @param response The response object used to set the cookie on
* @param repoId The repository ID identifying the {@link Info} whose token
* value should be updated or removed.
* @param token The actual token or null
to remove the
* {@link Info} for the repository ID from the cookie.
* @param wsp The workspace which the token is mainly used to access. Ignored
* if token
is null
.
* @param isHttpOnly Whether or not to set the HttpOnly
* attribute on the cookie. For security reasons it is
* recommended to always set this parameter to true
* . The parameter mainly exists for backwards compatibility
* reasons to allow old use cases to still make the cookie
* visible to client side JavaScript.
* @param sameSiteCookieAttribute The value for the SameSite attribute defined
* in https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-05#section-5.3.7 .
* Valid values are {@link #SAMESITE_ATTR_STRICT}, {@link #SAMESITE_ATTR_LAX}
* and {@link #SAMESITE_ATTR_NONE}.
*
*/
public static void update(HttpServletRequest request,
HttpServletResponse response, String repoId, String token,
String wsp, boolean isHttpOnly, String sameSiteCookieAttribute) {
TokenCookie t = TokenCookie.fromRequest(request);
Info newInfo = Info.INVALID;
if (token == null) {
t.getInfos().remove(repoId);
} else {
newInfo = new Info(token, wsp);
t.getInfos().put(repoId, newInfo);
}
request.setAttribute(Info.ATTR_NAME, newInfo);
String path = request.getContextPath();
if (path == null || path.length() == 0) {
path = "/";
}
String v = t.toString();
if (v.length() == 0) {
setCookie(response, NAME, v, 0, path, null, isHttpOnly,
request.isSecure(), sameSiteCookieAttribute);
} else {
setCookie(response, NAME, v, -1, path, null, isHttpOnly,
request.isSecure(), sameSiteCookieAttribute);
}
}
/**
* Decodes a token cookie value.
*
* This is the reverse operation to the {@link TokenCookie#toString()}
* method.
*
* @param value cookie value
* @return a token cookie
*/
public static TokenCookie fromString(String value) {
TokenCookie t = new TokenCookie();
if (value == null) {
return t;
}
value = unescape(value);
String[] infos = explode(value.trim(), ';', false);
for (String info : infos) {
String[] parts = explode(info.trim(), ':', true);
if (parts.length != 3) {
log.warn("invalid value in cookie: {}", info);
continue;
}
t.infos.put(parts[0].trim(),
new Info(unescape(parts[1].trim()), // token
unescape(parts[2].trim()) // workspace
));
}
return t;
}
/**
* Removes the info with the specified repository ID
*
* @param repoId The repository ID whose {@link Info} has to be removed
* @return true
if an {@link Info} object for the repository ID
* existed and is now removed.
*/
public boolean remove(String repoId) {
return infos.remove(repoId) != null;
}
/**
* Returns the string representation of this token cookie. The value
* returned by this method can be decoded with the
* {@link #fromString(String)} method.
*
* @return the string
*/
@Override
public String toString() {
StringBuilder b = new StringBuilder();
String delim = "";
for (Map.Entry e : infos.entrySet()) {
b.append(delim);
if (e.getKey().length() > 0) {
b.append(e.getKey()).append(":");
}
b.append(e.getValue());
delim = ";";
}
return escape(b.toString());
}
/**
* Retrieves the cookie with the given name from the request
*
* @param request servlet request
* @param name the name
* @return the cookie value or null
if no cookie with the given
* name exists whose value is not empty.
*/
public static String getCookie(HttpServletRequest request, String name) {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(name)
&& !"".equals(cookie.getValue())) {
return cookie.getValue();
}
}
}
return null;
}
/**
* Sets a cookie to the response
*
* @param response response
* @param name cookie name
* @param value value
* @param maxAge maxAge
* @param path path
* @deprecated use
* {@link #setCookie(HttpServletResponse, String, String, int, String, String, boolean, boolean)}
* instead
*/
@Deprecated
public static void setCookie(HttpServletResponse response, String name,
String value, int maxAge, String path) {
setCookie(response, name, value, maxAge, path, null, false, false);
}
/**
* Sets a cookie to the response
*
* This implementation calls the
* {@link #setCookie(HttpServletResponse, String, String, int, String,String, boolean, boolean, String)}
* with the sameSiteCookieAttribute
set as the configuration
* token.samesite.cookie.attr
* @param response response
* @param name cookie name
* @param value value
* @param maxAge maxAge
* @param path path
* @param domain The cookie domain or null
to not set an
* explicit domain on the cookie.
* @param isHttpOnly Whether to set (true
) or not the
* HttpOnly
attribute on the cookie. It is not
* recommended to set this parameter to false
unless
* the cookie must support certain use cases where it is
* essential for the client side to have access to the cookie
* despite the inherent security risks.
* @param isSecure Whether to set (true
) or not the
* Secure
attribute on the cookie. The value for
* this parameter should be derived from the current request,
* namely the ServletRequest.isSecure()
method.
*/
public static void setCookie(HttpServletResponse response, String name,
String value, int maxAge, String path, String domain,
boolean isHttpOnly, boolean isSecure) {
setCookie(response, name, value, maxAge, path, domain, isHttpOnly, isSecure, TokenAuthenticationHandler.getSameSiteCookieAttribute());
}
/**
* Sets a cookie to the response
*
* @param response response
* @param name cookie name
* @param value value
* @param maxAge maxAge
* @param path path
* @param domain The cookie domain or null
to not set an
* explicit domain on the cookie.
* @param isHttpOnly Whether to set (true
) or not the
* HttpOnly
attribute on the cookie. It is not
* recommended to set this parameter to false
unless
* the cookie must support certain use cases where it is
* essential for the client side to have access to the cookie
* despite the inherent security risks.
* @param isSecure Whether to set (true
) or not the
* Secure
attribute on the cookie. The value for
* this parameter should be derived from the current request,
* namely the ServletRequest.isSecure()
method.
* @param sameSiteCookieAttribute The value for the SameSite attribute defined
* in https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-05#section-5.3.7 .
* Valid values are {@link #SAMESITE_ATTR_STRICT}, {@link #SAMESITE_ATTR_LAX}
* and {@link #SAMESITE_ATTR_NONE}ornull
to not set an explicit
* value for the SameSite attribute.
*/
public static void setCookie(HttpServletResponse response, String name,
String value, int maxAge, String path, String domain,
boolean isHttpOnly, boolean isSecure, String sameSiteCookieAttribute) {
/*
* The Servlet Spec 2.5 does not allow us to set the commonly used
* HttpOnly attribute on cookies (Servlet API 3.0 does) so we create the
* Set-Cookie header manually. See
* http://www.owasp.org/index.php/HttpOnly for information on what the
* HttpOnly attribute is used for.
*/
final StringBuilder header = new StringBuilder();
// default setup with name, value, cookie path and HttpOnly
header.append(name).append("=").append(value);
header.append("; Path=").append(path);
// don't allow JS access if requested so
if (isHttpOnly) {
header.append("; HttpOnly");
}
// set the cookie domain if so configured
if (domain != null) {
header.append("; Domain=").append(domain);
}
// Only set the Max-Age attribute to remove the cookie
if (maxAge >= 0) {
header.append("; Max-Age=").append(maxAge);
}
// ensure the cookie is secured if this is an https request
if (isSecure) {
header.append("; Secure");
}
// SameSite configuration, only use None on secure connections
if (sameSiteCookieAttribute != null) {
if (!(SAMESITE_ATTR_NONE.equals(sameSiteCookieAttribute) || SAMESITE_ATTR_PARTITIONED.equals(sameSiteCookieAttribute))|| isSecure) {
header.append("; SameSite=").append(sameSiteCookieAttribute);
} else {
log.warn("Skip 'SameSite=None' since the connection is not secure");
}
}
response.addHeader("Set-Cookie", header.toString());
}
/**
* holds a token / workspace pair
*/
public static class Info {
public static final Info INVALID = new Info(null, null);
/**
* name of the request attribute
*/
public static final String ATTR_NAME = Info.class.getName();
public final String token;
public final String workspace;
public Info(String token, String workspace) {
this.workspace = workspace;
this.token = token;
}
public boolean isValid() {
return token != null && token.length() > 0;
}
@Override
public String toString() {
if (token == null) {
return "";
}
final StringBuilder sb = new StringBuilder();
sb.append(escape(token)).append(":");
sb.append(escape(workspace));
return sb.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Info info = (Info) o;
if (token != null ? !token.equals(info.token) : info.token != null)
return false;
if (workspace != null
? !workspace.equals(info.workspace)
: info.workspace != null) return false;
return true;
}
@Override
public int hashCode() {
int result = token != null ? token.hashCode() : 0;
result = 31 * result
+ (workspace != null ? workspace.hashCode() : 0);
return result;
}
}
private static final BitSet URISaveEx = new BitSet(256);
static {
int i;
for (i = 'a'; i <= 'z'; i++) {
URISaveEx.set(i);
}
for (i = 'A'; i <= 'Z'; i++) {
URISaveEx.set(i);
}
for (i = '0'; i <= '9'; i++) {
URISaveEx.set(i);
}
URISaveEx.set('-');
URISaveEx.set('_');
URISaveEx.set('.');
URISaveEx.set('!');
URISaveEx.set('~');
URISaveEx.set('*');
URISaveEx.set('\'');
URISaveEx.set('(');
URISaveEx.set(')');
URISaveEx.set('/');
}
/**
* used for the md5
*/
private final static char[] hexTable="0123456789abcdef".toCharArray();
/**
* Does an URL encoding of the string
using the
* escape
character. The characters that don't need encoding
* are those defined 'unreserved' in section 2.3 of the 'URI genric syntax'
* RFC 2396, but without the escape character. If isPath
is
* true
, additionally the slash '/' is ignored, too.
*
* @param string the string to encode.
* @param escape the escape character.
* @param isPath if true
, the string is treated as path
*
* @return the escaped string
*
* @throws NullPointerException if string
is null
.
*/
private static String escape(String string) {
try {
BitSet validChars = URISaveEx;
byte[] bytes = string.getBytes("utf-8");
StringBuffer out = new StringBuffer(bytes.length);
for (int i = 0; i < bytes.length; i++) {
int c = bytes[i]&0xff;
if (validChars.get(c) && c!='%') {
out.append((char)c);
} else {
out.append('%');
out.append(hexTable[(c>>4) & 0x0f]);
out.append(hexTable[(c ) & 0x0f]);
}
}
return out.toString();
} catch (UnsupportedEncodingException e) {
throw new InternalError(e.toString());
}
}
/**
* Does a URL decoding of the string
using the
* escape
character. Please note that in opposite to the
* {@link java.net.URLDecoder} it does not transform the + into spaces.
* @param string the string to decode
* @param escape the escape character
* @return the decoded string
*
* @throws NullPointerException if string
is null
.
* @throws ArrayIndexOutOfBoundsException if not enough character follow an
* escape character
* @throws IllegalArgumentException if the 2 characters following the escape
* character do not represent a hex-number.
*/
private static String unescape(String string) {
ByteArrayOutputStream out = new ByteArrayOutputStream(string.length()*2);
int lastPos=0;
int pos;
while ((pos=string.indexOf('%', lastPos))>=0) {
try {
out.write(string.substring(lastPos, pos).getBytes("utf-8"));
} catch (IOException e) {
throw new InternalError(e.toString());
}
lastPos = pos+3;
if (lastPos<=string.length()) {
try {
out.write(Integer.parseInt(string.substring(pos+1, lastPos),16));
} catch (NumberFormatException e) {
throw new IllegalArgumentException();
}
} else {
// not enough characters, ignore rest
}
}
if (lastPos>=0 && lastPos<=string.length()) {
try {
out.write(string.substring(lastPos).getBytes("utf-8"));
} catch (IOException e) {
throw new InternalError(e.toString());
}
}
try {
return new String(out.toByteArray(), "utf-8");
} catch (UnsupportedEncodingException e) {
throw new InternalError(e.toString());
}
}
/**
* returns an array of strings decomposed of the original string, split at
* every occurance of 'ch'.
* @param str the string to decompose
* @param ch the character to use a split pattern
* @param respectEmpty if true
, empty elements are generated
* @return an array of strings
*/
private static String[] explode(final String str, final int ch, final boolean respectEmpty) {
Vector strings = new Vector<>();
int pos;
int lastpos = 0;
// add snipples
while ((pos = str.indexOf(ch, lastpos)) >= 0) {
if (pos-lastpos>0 || respectEmpty)
strings.add(str.substring(lastpos, pos));
lastpos = pos+1;
}
// add rest
if (lastpos < str.length()) {
strings.add(str.substring(lastpos));
} else if (respectEmpty && lastpos==str.length()) {
strings.add("");
}
// return stringarray
return strings.toArray(new String[strings.size()]);
}
}