diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index 45f1a42a49..2773989d18 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -173,7 +173,7 @@ public class AuthenticationManager { public AuthResult authenticateIdentityCookie(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers, boolean checkActive) { logger.info("authenticateIdentityCookie"); Cookie cookie = headers.getCookies().get(KEYCLOAK_IDENTITY_COOKIE); - if (cookie == null) { + if (cookie == null || "".equals(cookie.getValue())) { logger.infov("authenticateCookie could not find cookie: {0}", KEYCLOAK_IDENTITY_COOKIE); return null; } diff --git a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java index 5a14b68a91..0b10857f43 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/OAuthFlows.java @@ -96,12 +96,16 @@ public class OAuthFlows { Cookie sessionCookie = request.getHttpHeaders().getCookies().get(AuthenticationManager.KEYCLOAK_SESSION_COOKIE); if (sessionCookie != null) { - String oldSessionId = sessionCookie.getValue().split("/")[2]; - if (!oldSessionId.equals(userSession.getId())) { - UserSessionModel oldSession = session.sessions().getUserSession(realm, oldSessionId); - if (oldSession != null) { - log.debugv("Removing old user session: session: {0}", oldSessionId); - session.sessions().removeUserSession(realm, oldSession); + + String[] split = sessionCookie.getValue().split("/"); + if (split.length >= 3) { + String oldSessionId = split[2]; + if (!oldSessionId.equals(userSession.getId())) { + UserSessionModel oldSession = session.sessions().getUserSession(realm, oldSessionId); + if (oldSession != null) { + log.debugv("Removing old user session: session: {0}", oldSessionId); + session.sessions().removeUserSession(realm, oldSession); + } } } } diff --git a/services/src/main/java/org/keycloak/services/util/CookieHelper.java b/services/src/main/java/org/keycloak/services/util/CookieHelper.java index 1b3b0e0d5b..04413cea75 100755 --- a/services/src/main/java/org/keycloak/services/util/CookieHelper.java +++ b/services/src/main/java/org/keycloak/services/util/CookieHelper.java @@ -29,7 +29,7 @@ public class CookieHelper { * @param secure * @param httpOnly */ - public static void addCookie2(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly) { + public static void addCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly) { HttpResponse response = ResteasyProviderFactory.getContextData(HttpResponse.class); StringBuffer cookieBuf = new StringBuffer(); ServerCookie.appendCookieValue(cookieBuf, 1, name, value, path, domain, comment, maxAge, secure, httpOnly); @@ -37,19 +37,5 @@ public class CookieHelper { response.getOutputHeaders().add(HttpHeaders.SET_COOKIE, cookie); } - public static void addCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly) { - HttpServletResponse response = ResteasyProviderFactory.getContextData(HttpServletResponse.class); - Cookie cookie = new Cookie(name, value); - if (path != null) cookie.setPath(path); - if (domain != null) cookie.setDomain(domain); - if (comment != null) cookie.setComment(comment); - cookie.setMaxAge(maxAge); - cookie.setSecure(secure); - cookie.setHttpOnly(httpOnly); - - response.addCookie(cookie); - - } - } diff --git a/services/src/main/java/org/keycloak/services/util/ServerCookie.java b/services/src/main/java/org/keycloak/services/util/ServerCookie.java index 236da7eade..d41cc2e05b 100755 --- a/services/src/main/java/org/keycloak/services/util/ServerCookie.java +++ b/services/src/main/java/org/keycloak/services/util/ServerCookie.java @@ -11,337 +11,295 @@ import java.util.TimeZone; /** * Server-side cookie representation. borrowed from Tomcat. */ -public class ServerCookie implements Serializable -{ - private static final String tspecials = ",; "; - private static final String tspecials2 = "()<>@,;:\\\"/[]?={} \t"; +public class ServerCookie implements Serializable { + private static final String tspecials = ",; "; + private static final String tspecials2 = "()<>@,;:\\\"/[]?={} \t"; - /* - * Tests a string and returns true if the string counts as a - * reserved token in the Java language. - * - * @param value the String to be tested - * - * @return true if the String is a reserved - * token; false if it is not - */ - public static boolean isToken(String value) - { - if (value == null) return true; - int len = value.length(); - - for (int i = 0; i < len; i++) - { - char c = value.charAt(i); - - if (tspecials.indexOf(c) != -1) - return false; - } - return true; - } - - public static boolean containsCTL(String value, int version) - { - if (value == null) return false; - int len = value.length(); - for (int i = 0; i < len; i++) - { - char c = value.charAt(i); - if (c < 0x20 || c >= 0x7f) - { - if (c == 0x09) - continue; //allow horizontal tabs - return true; - } - } - return false; - } - - - public static boolean isToken2(String value) - { - if (value == null) return true; - int len = value.length(); - - for (int i = 0; i < len; i++) - { - char c = value.charAt(i); - if (tspecials2.indexOf(c) != -1) - return false; - } - return true; - } - - /** - * @deprecated - Not used - */ - public static boolean checkName(String name) - { - if (!isToken(name) - || name.equalsIgnoreCase("Comment") // rfc2019 - || name.equalsIgnoreCase("Discard") // rfc2965 - || name.equalsIgnoreCase("Domain") // rfc2019 - || name.equalsIgnoreCase("Expires") // Netscape - || name.equalsIgnoreCase("Max-Age") // rfc2019 - || name.equalsIgnoreCase("Path") // rfc2019 - || name.equalsIgnoreCase("Secure") // rfc2019 - || name.equalsIgnoreCase("Version") // rfc2019 - // TODO remaining RFC2965 attributes - ) - { - return false; - } - return true; - } - - // -------------------- Cookie parsing tools - - - /** - * Return the header name to set the cookie, based on cookie version. - */ - public static String getCookieHeaderName(int version) - { - // TODO Re-enable logging when RFC2965 is implemented - // log( (version==1) ? "Set-Cookie2" : "Set-Cookie"); - if (version == 1) - { - // XXX RFC2965 not referenced in Servlet Spec - // Set-Cookie2 is not supported by Netscape 4, 6, IE 3, 5 - // Set-Cookie2 is supported by Lynx and Opera - // Need to check on later IE and FF releases but for now... - // RFC2109 - return "Set-Cookie"; - // return "Set-Cookie2"; - } - else - { - // Old Netscape - return "Set-Cookie"; - } - } - - /** - * US locale - all HTTP dates are in english - */ - private final static Locale LOCALE_US = Locale.US; - - /** - * GMT timezone - all HTTP dates are on GMT - */ - public final static TimeZone GMT_ZONE = TimeZone.getTimeZone("GMT"); - /** - * Pattern used for old cookies - */ - private final static String OLD_COOKIE_PATTERN = "EEE, dd-MMM-yyyy HH:mm:ss z"; - - - private final static DateFormat oldCookieFormat = new SimpleDateFormat(OLD_COOKIE_PATTERN, LOCALE_US); - - public static String formatOldCookie(Date d) - { - String ocf = null; - synchronized (oldCookieFormat) - { - ocf = oldCookieFormat.format(d); - } - return ocf; - } - - public static void formatOldCookie(Date d, StringBuffer sb, - FieldPosition fp) - { - synchronized (oldCookieFormat) - { - oldCookieFormat.format(d, sb, fp); - } - } - - - private static final String ancientDate = formatOldCookie(new Date(10000)); - - - // TODO RFC2965 fields also need to be passed - public static void appendCookieValue(StringBuffer headerBuf, - int version, - String name, - String value, - String path, - String domain, - String comment, - int maxAge, - boolean isSecure, - boolean httpOnly) - { - StringBuffer buf = new StringBuffer(); - // Servlet implementation checks name - buf.append(name); - buf.append("="); - // Servlet implementation does not check anything else - - maybeQuote2(version, buf, value); - - // Add version 1 specific information - if (version == 1) - { - // Version=1 ... required - buf.append("; Version=1"); - - // Comment=comment - if (comment != null) - { - buf.append("; Comment="); - maybeQuote2(version, buf, comment); - } - } - - // Add domain information, if present - if (domain != null) - { - buf.append("; Domain="); - maybeQuote2(version, buf, domain); - } - - // Max-Age=secs ... or use old "Expires" format - // TODO RFC2965 Discard - if (maxAge >= 0) - { - // Wdy, DD-Mon-YY HH:MM:SS GMT ( Expires Netscape format ) - buf.append("; Expires="); - // To expire immediately we need to set the time in past - if (maxAge == 0) - buf.append(ancientDate); - else - formatOldCookie - (new Date(System.currentTimeMillis() + - maxAge * 1000L), buf, - new FieldPosition(0)); - - buf.append("; Max-Age="); - buf.append(maxAge); - } - - // Path=path - if (path != null) - { - buf.append("; Path="); - buf.append(path); - } - - // Secure - if (isSecure) - { - buf.append("; Secure"); - } - - // HttpOnly - if (httpOnly) - { - buf.append("; HttpOnly"); - } - - headerBuf.append(buf); - } - - /** - * @deprecated - Not used - */ - @Deprecated - public static void maybeQuote(int version, StringBuffer buf, String value) - { - // special case - a \n or \r shouldn't happen in any case - if (isToken(value)) - { - buf.append(value); - } - else - { - buf.append('"'); - buf.append(escapeDoubleQuotes(value, 0, value.length())); - buf.append('"'); - } - } - - public static boolean alreadyQuoted(String value) - { - if (value == null || value.length() == 0) return false; - return (value.charAt(0) == '\"' && value.charAt(value.length() - 1) == '\"'); - } - - /** - * Quotes values using rules that vary depending on Cookie version. + /* + * Tests a string and returns true if the string counts as a + * reserved token in the Java language. * - * @param version - * @param buf - * @param value - */ - public static void maybeQuote2(int version, StringBuffer buf, String value) - { - if (value == null || value.length() == 0) - { - buf.append("\"\""); - } - else if (containsCTL(value, version)) - throw new IllegalArgumentException("Control character in cookie value, consider BASE64 encoding your value"); - else if (alreadyQuoted(value)) - { - buf.append('"'); - buf.append(escapeDoubleQuotes(value, 1, value.length() - 1)); - buf.append('"'); - } - else if (version == 0 && !isToken(value)) - { - buf.append('"'); - buf.append(escapeDoubleQuotes(value, 0, value.length())); - buf.append('"'); - } - else if (version == 1 && !isToken2(value)) - { - buf.append('"'); - buf.append(escapeDoubleQuotes(value, 0, value.length())); - buf.append('"'); - } - else - { - buf.append(value); - } - } - - - /** - * Escapes any double quotes in the given string. + * @param value the String to be tested * - * @param s the input string - * @param beginIndex start index inclusive - * @param endIndex exclusive - * @return The (possibly) escaped string + * @return true if the String is a reserved + * token; false if it is not */ - private static String escapeDoubleQuotes(String s, int beginIndex, int endIndex) - { + public static boolean isToken(String value) { + if (value == null) return true; + int len = value.length(); - if (s == null || s.length() == 0 || s.indexOf('"') == -1) - { - return s; - } + for (int i = 0; i < len; i++) { + char c = value.charAt(i); - StringBuffer b = new StringBuffer(); - for (int i = beginIndex; i < endIndex; i++) - { - char c = s.charAt(i); - if (c == '\\') - { - b.append(c); - //ignore the character after an escape, just append it - if (++i >= endIndex) throw new IllegalArgumentException("Invalid escape character in cookie value."); - b.append(s.charAt(i)); - } - else if (c == '"') - b.append('\\').append('"'); - else - b.append(c); - } + if (tspecials.indexOf(c) != -1) + return false; + } + return true; + } - return b.toString(); - } + public static boolean containsCTL(String value, int version) { + if (value == null) return false; + int len = value.length(); + for (int i = 0; i < len; i++) { + char c = value.charAt(i); + if (c < 0x20 || c >= 0x7f) { + if (c == 0x09) + continue; //allow horizontal tabs + return true; + } + } + return false; + } + + + public static boolean isToken2(String value) { + if (value == null) return true; + int len = value.length(); + + for (int i = 0; i < len; i++) { + char c = value.charAt(i); + if (tspecials2.indexOf(c) != -1) + return false; + } + return true; + } + + /** + * @deprecated - Not used + */ + public static boolean checkName(String name) { + if (!isToken(name) + || name.equalsIgnoreCase("Comment") // rfc2019 + || name.equalsIgnoreCase("Discard") // rfc2965 + || name.equalsIgnoreCase("Domain") // rfc2019 + || name.equalsIgnoreCase("Expires") // Netscape + || name.equalsIgnoreCase("Max-Age") // rfc2019 + || name.equalsIgnoreCase("Path") // rfc2019 + || name.equalsIgnoreCase("Secure") // rfc2019 + || name.equalsIgnoreCase("Version") // rfc2019 + // TODO remaining RFC2965 attributes + ) { + return false; + } + return true; + } + + // -------------------- Cookie parsing tools + + + /** + * Return the header name to set the cookie, based on cookie version. + */ + public static String getCookieHeaderName(int version) { + // TODO Re-enable logging when RFC2965 is implemented + // log( (version==1) ? "Set-Cookie2" : "Set-Cookie"); + if (version == 1) { + // XXX RFC2965 not referenced in Servlet Spec + // Set-Cookie2 is not supported by Netscape 4, 6, IE 3, 5 + // Set-Cookie2 is supported by Lynx and Opera + // Need to check on later IE and FF releases but for now... + // RFC2109 + return "Set-Cookie"; + // return "Set-Cookie2"; + } else { + // Old Netscape + return "Set-Cookie"; + } + } + + /** + * US locale - all HTTP dates are in english + */ + private final static Locale LOCALE_US = Locale.US; + + /** + * GMT timezone - all HTTP dates are on GMT + */ + public final static TimeZone GMT_ZONE = TimeZone.getTimeZone("GMT"); + /** + * Pattern used for old cookies + */ + private final static String OLD_COOKIE_PATTERN = "EEE, dd-MMM-yyyy HH:mm:ss z"; + + + private final static DateFormat oldCookieFormat = new SimpleDateFormat(OLD_COOKIE_PATTERN, LOCALE_US); + + public static String formatOldCookie(Date d) { + String ocf = null; + synchronized (oldCookieFormat) { + ocf = oldCookieFormat.format(d); + } + return ocf; + } + + public static void formatOldCookie(Date d, StringBuffer sb, + FieldPosition fp) { + synchronized (oldCookieFormat) { + oldCookieFormat.format(d, sb, fp); + } + } + + + private static final String ancientDate = formatOldCookie(new Date(10000)); + + + // TODO RFC2965 fields also need to be passed + public static void appendCookieValue(StringBuffer headerBuf, + int version, + String name, + String value, + String path, + String domain, + String comment, + int maxAge, + boolean isSecure, + boolean httpOnly) { + StringBuffer buf = new StringBuffer(); + // Servlet implementation checks name + buf.append(name); + buf.append("="); + // Servlet implementation does not check anything else + + // NOTE!!! BROWSERS REALLY DON'T LIKE QUOTING + //maybeQuote2(version, buf, value); + buf.append(value); + + // Add version 1 specific information + if (version == 1) { + // Version=1 ... required + buf.append("; Version=1"); + + // Comment=comment + if (comment != null) { + buf.append("; Comment="); + //maybeQuote2(version, buf, comment); + buf.append(comment); + } + } + + // Add domain information, if present + if (domain != null) { + buf.append("; Domain="); + //maybeQuote2(version, buf, domain); + buf.append(domain); + } + + // Max-Age=secs ... or use old "Expires" format + // TODO RFC2965 Discard + if (maxAge >= 0) { + // Wdy, DD-Mon-YY HH:MM:SS GMT ( Expires Netscape format ) + buf.append("; Expires="); + // To expire immediately we need to set the time in past + if (maxAge == 0) + buf.append(ancientDate); + else + formatOldCookie + (new Date(System.currentTimeMillis() + + maxAge * 1000L), buf, + new FieldPosition(0)); + + buf.append("; Max-Age="); + buf.append(maxAge); + } + + // Path=path + if (path != null) { + buf.append("; Path="); + buf.append(path); + } + + // Secure + if (isSecure) { + buf.append("; Secure"); + } + + // HttpOnly + if (httpOnly) { + buf.append("; HttpOnly"); + } + + headerBuf.append(buf); + } + + /** + * @deprecated - Not used + */ + @Deprecated + public static void maybeQuote(int version, StringBuffer buf, String value) { + // special case - a \n or \r shouldn't happen in any case + if (isToken(value)) { + buf.append(value); + } else { + buf.append('"'); + buf.append(escapeDoubleQuotes(value, 0, value.length())); + buf.append('"'); + } + } + + public static boolean alreadyQuoted(String value) { + if (value == null || value.length() == 0) return false; + return (value.charAt(0) == '\"' && value.charAt(value.length() - 1) == '\"'); + } + + /** + * Quotes values using rules that vary depending on Cookie version. + * + * @param version + * @param buf + * @param value + */ + public static void maybeQuote2(int version, StringBuffer buf, String value) { + if (value == null || value.length() == 0) { + buf.append("\"\""); + } else if (containsCTL(value, version)) + throw new IllegalArgumentException("Control character in cookie value, consider BASE64 encoding your value"); + else if (alreadyQuoted(value)) { + buf.append('"'); + buf.append(escapeDoubleQuotes(value, 1, value.length() - 1)); + buf.append('"'); + } else if (version == 0 && !isToken(value)) { + buf.append('"'); + buf.append(escapeDoubleQuotes(value, 0, value.length())); + buf.append('"'); + } else if (version == 1 && !isToken2(value)) { + buf.append('"'); + buf.append(escapeDoubleQuotes(value, 0, value.length())); + buf.append('"'); + } else { + buf.append(value); + } + } + + + /** + * Escapes any double quotes in the given string. + * + * @param s the input string + * @param beginIndex start index inclusive + * @param endIndex exclusive + * @return The (possibly) escaped string + */ + private static String escapeDoubleQuotes(String s, int beginIndex, int endIndex) { + + if (s == null || s.length() == 0 || s.indexOf('"') == -1) { + return s; + } + + StringBuffer b = new StringBuffer(); + for (int i = beginIndex; i < endIndex; i++) { + char c = s.charAt(i); + if (c == '\\') { + b.append(c); + //ignore the character after an escape, just append it + if (++i >= endIndex) throw new IllegalArgumentException("Invalid escape character in cookie value."); + b.append(s.charAt(i)); + } else if (c == '"') + b.append('\\').append('"'); + else + b.append(c); + } + + return b.toString(); + } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/perf/AccessTokenPerfTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/perf/AccessTokenPerfTest.java index b2d80fc5c6..686b785e75 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/perf/AccessTokenPerfTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/perf/AccessTokenPerfTest.java @@ -180,6 +180,11 @@ public class AccessTokenPerfTest { } Assert.assertEquals(302, response.getStatus()); uri = response.getLocation(); + for (String header : response.getHeaders().keySet()) { + for (Object value : response.getHeaders().get(header)) { + System.out.println(header + ": " + value); + } + } response.close(); Assert.assertNotNull(uri);