package com.sixsq.slipstream.cookie;
/*
* +=================================================================+
* SlipStream Server (WAR)
* =====
* Copyright (C) 2013 SixSq Sarl (sixsq.com)
* =====
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* -=================================================================-
*/
import com.sixsq.slipstream.exceptions.ConfigurationException;
import com.sixsq.slipstream.exceptions.ValidationException;
import com.sixsq.slipstream.persistence.RuntimeParameter;
import com.sixsq.slipstream.persistence.User;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.data.Cookie;
import org.restlet.data.CookieSetting;
import org.restlet.data.Form;
import org.restlet.data.Status;
import org.restlet.resource.ResourceException;
import org.restlet.security.Verifier;
import org.restlet.util.Series;
import java.util.*;
import java.util.Map.Entry;
import java.util.logging.Logger;
/**
* Contains utilities for handling authentication cookies.
*
* @author loomis
*
*/
public class CookieUtils {
private static final Logger logger = Logger.getLogger(CookieUtils.class.getName());
public static final String TOKEN = "token";
// For testing, the age defaults to 30 minutes. The value is given in
// seconds!
private static final int COOKIE_DEFAULT_AGE = 60 * 60 * 24 * 7; // 7 days
// Name used to identify the authentication cookie.
public static String COOKIE_NAME = "com.sixsq.slipstream.cookie";
private static String COOKIE_PATH = "/";
// Names of fields containing cookie information.
public static final String COOKIE_RUN_ID = "com.sixsq.runId";
public static final String COOKIE_IS_MACHINE = "com.sixsq.isMachine";
public static final String COOKIE_EXPIRY_DATE = "com.sixsq.expirydate";
private static final String COOKIE_IDTYPE = "com.sixsq.idtype";
private static final String COOKIE_IDENTIFIER = "com.sixsq.identifier";
public static final String COOKIE_USERNAME = "username";
private static final String COOKIE_SIGNATURE = "com.sixsq.signature";
private static final String COOKIE_DEFAULT_IDTYPE = "local";
private static final Set<String> requiredCookieKeys = new TreeSet<String>();
static {
requiredCookieKeys.add(COOKIE_IDTYPE);
requiredCookieKeys.add(COOKIE_IDENTIFIER);
requiredCookieKeys.add(COOKIE_EXPIRY_DATE);
requiredCookieKeys.add(COOKIE_SIGNATURE);
}
public static void addAuthnCookieFromAuthnResponse(Response response, Response token) {
CookieSetting authnCookie = extractAuthnTokenCookie(token);
Series<CookieSetting> cookieSettings = response.getCookieSettings();
cookieSettings.removeAll(COOKIE_NAME);
cookieSettings.add(authnCookie);
}
/**
* Insert a new authentication cookie into a Request using the given values.
* None of the arguments can be null.
*
* @param request
* @param identifier
* @param cloudServiceName
*/
public static void addAuthnCookie(Request request, String identifier,
String cloudServiceName) {
request.getCookies().clear();
CookieSetting cookieSetting = createAuthnCookieSetting(
COOKIE_DEFAULT_IDTYPE, identifier,
generateCloudServiceNameProperties(cloudServiceName));
request.getCookies().add(cookieSetting);
}
/**
* Creates a new authentication cookie using the provided information. None
* of the arguments may be null.
*
* @param idType
* @param identifier
* @param properties
*
* @return new authentication cookie
*/
private static CookieSetting createAuthnCookieSetting(String idType,
String identifier,
Properties properties) {
String finalQuery = createCookieValue(idType, identifier, properties);
CookieSetting cookieSetting = new CookieSetting(COOKIE_NAME, finalQuery);
cookieSetting.setPath("/");
cookieSetting.setDomain("");
cookieSetting.setMaxAge(COOKIE_DEFAULT_AGE);
return cookieSetting;
}
public static String createCookie(String username, String cloudServiceName,
Properties extraProperties) {
Properties properties = generateCloudServiceNameProperties(cloudServiceName);
if (extraProperties != null) {
properties.putAll(extraProperties);
}
return getCookieName() + "="
+ CookieUtils.createCookieValue("local", username, properties)
+ "; Path:/";
}
private static Properties generateCloudServiceNameProperties(
String cloudServiceName) {
Properties properties = new Properties();
properties.put(RuntimeParameter.CLOUD_SERVICE_NAME, cloudServiceName);
return properties;
}
public static String createCookieValue(String idType, String identifier,
Properties properties) {
// Create the expiration date for the cookie. This is added to be sure
// that the server has control over this even if a malicious client
// extends the date of a cookie.
long expiryMillis = (new Date()).getTime() + COOKIE_DEFAULT_AGE * 1000L;
String expiryDate = Long.toString(expiryMillis);
// Create a form (and query) that contains the authentication
// information to be signed.
Form form = new Form();
form.add(COOKIE_IDTYPE, idType);
form.add(COOKIE_IDENTIFIER, identifier);
if (!properties.containsKey(COOKIE_EXPIRY_DATE)) {
form.add(COOKIE_EXPIRY_DATE, expiryDate);
}
// Add all parameters
for (Entry<Object, Object> entry : properties.entrySet()) {
form.add((String) entry.getKey(), (String) entry.getValue());
}
properties.put(COOKIE_IDENTIFIER, identifier);
String claimsToken = createToken(properties);
form.add(COOKIE_SIGNATURE, claimsToken);
return form.getQueryString();
}
private static String createToken(Properties claims) throws ResourceException {
String signedClaims = com.sixsq.slipstream.auth.TokenChecker.createMachineToken(claims);
logger.info(String.format("generated machine token: %s", signedClaims));
if (signedClaims == null) {
String message = "error creating machine token; invalid claims or authentication token";
logger.warning(message);
throw new ResourceException(Status.CLIENT_ERROR_BAD_REQUEST, message);
}
return signedClaims;
}
/**
* Force the authentication cookie to be deleted from the client's cache.
* This inserts an invalid cookie into the Response that expires immediately
* (max. age = 0).
*
* @param response
*/
public static void removeAuthnCookie(Response response) {
// Get the current cookie settings, remove any with the
// authorization cookie.
Series<CookieSetting> cookieSettings = response.getCookieSettings();
cookieSettings.removeAll(COOKIE_NAME);
// Create cookie to remove any cookies on client side. Add this to the
// response.
CookieSetting cookieSetting = createClearingCookie(response);
cookieSettings.add(cookieSetting);
}
/**
* Convenience method to create a CookieSetting object from a Response.
*
* @param response
*
* @return CookieSetting object that will remove authentication cookie from
* client's cache
*/
private static CookieSetting createClearingCookie(Response response) {
return createClearingCookie(response.getRequest());
}
/**
* Creates a CookieSetting object that will clear an authentication cookie
* from the client's cache.
*
* @param request
* @return CookieSetting object to remove authentication cookie
*/
private static CookieSetting createClearingCookie(Request request) {
CookieSetting cookieSetting = new CookieSetting(COOKIE_NAME, "INVALID");
cookieSetting.setPath(COOKIE_PATH);
// Note that the cookie setting/clearing doesn't work correctly if the
// domain is set.
cookieSetting.setDomain("");
// Setting the age to zero forces the cookie to be discarded
// immediately. The value -1 will discard the cookie at the end of
// the session.
cookieSetting.setMaxAge(0);
return cookieSetting;
}
/**
* Convenience method to extract an authentication cookie from a request.
*
* @param request
*
* @return authentication cookie if the request has one, null otherwise
*/
public static Cookie extractAuthnCookie(Request request) {
return request.getCookies().getFirst(COOKIE_NAME);
}
public static CookieSetting extractAuthnTokenCookie(Response response){
Series<CookieSetting> cookieSettings = response.getCookieSettings();
for (CookieSetting cookieSetting : cookieSettings) {
if(CookieUtils.COOKIE_NAME.equals(cookieSetting.getName())) {
return cookieSetting;
}
}
logger.warning("No authn cookie in response");
return null;
}
private static boolean checkValidClaimsInToken(Cookie cookie, String signature) {
Map<String, String> claimsInToken = com.sixsq.slipstream.auth.TokenChecker.claimsInToken(signature);
logger.info("checkValidClaimsInToken, signature = " + signature);
boolean invalidClaims = claimsInToken == null || claimsInToken.isEmpty();
if(invalidClaims) {
logger.severe("Invalid claims for " + cookie);
return false;
}
Form form = new Form(cookie.getValue());
String usernameInCookie = form.getFirstValue(COOKIE_IDENTIFIER);
String runIdInCookie = form.getFirstValue(COOKIE_RUN_ID);
boolean cookieAndClaimsMatch =
usernameInCookie!=null && usernameInCookie.equals(claimsInToken.get(COOKIE_IDENTIFIER)) &&
runIdInCookie!=null && runIdInCookie.equals(claimsInToken.get(COOKIE_RUN_ID));
return !invalidClaims && cookieAndClaimsMatch;
}
/**
* Verify that the given authentication cookie is valid. This checks that
* the name is correct, information is complete, the signature is correct,
* and that the cookie has not yet expired.
*
* @param cookie
*
* @return status code indicating whether the cookie is valid
*/
public static int verifyAuthnCookie(Cookie cookie) {
if (cookie == null || !COOKIE_NAME.equals(cookie.getName())) {
return Verifier.RESULT_MISSING;
}
if (!COOKIE_NAME.equals(cookie.getName())) {
return Verifier.RESULT_INVALID;
}
Form cookieInfo = extractCookieValueAsForm(cookie);
if (!cookieInfo.getNames().containsAll(requiredCookieKeys)) {
return Verifier.RESULT_INVALID;
}
// Pull out the values from the form.
String signature = cookieInfo.getFirstValue(COOKIE_SIGNATURE);
String expiryString = cookieInfo.getFirstValue(COOKIE_EXPIRY_DATE);
// Recreate a query string without the signature.
cookieInfo.removeAll(COOKIE_SIGNATURE);
if(!checkValidClaimsInToken(cookie, signature)) {
return Verifier.RESULT_INVALID;
}
// Create the expiration date.
Date expiryDate = dateFromExpiryString(expiryString);
if (!CookieUtils.isMachine(cookie)) {
// Check that the cookie has not yet expired.
if (expiryDate.before(new Date())) {
return Verifier.RESULT_STALE;
} else {
return Verifier.RESULT_VALID;
}
}
return Verifier.RESULT_VALID;
}
public static String getCookieUsername(Cookie cookie) {
if(isMachine(cookie)) {
String username = null;
if (cookie != null) {
Form form = new Form(cookie.getValue());
username = form.getFirstValue(COOKIE_IDENTIFIER);
}
return username;
} else {
return claimsInToken(cookie).get(COOKIE_USERNAME);
}
}
public static Map<String, String> claimsInToken(Cookie cookie) {
if (cookie == null || cookie.getValue() == null) {
logger.warning("No cookie provided");
return new HashMap<String, String>();
}
String token = tokenInCookie(cookie);
logger.fine("Token in cookie = " + token);
return com.sixsq.slipstream.auth.TokenChecker.claimsInToken(token);
}
public static String tokenInCookie(Cookie cookie) {
Form cookieInfo = CookieUtils.extractCookieValueAsForm(cookie);
String token = cookieInfo.getFirstValue(TOKEN);
if(token!=null && token.contains(",com.sixsq.slipstream.cookie=")) {
logger.info("Cookie value contains cookie key");
token = token.substring(0, token.indexOf(",com.sixsq.slipstream.cookie="));
}
return token;
}
public static String getCookieCloudServiceName(Cookie cookie) {
String cloudServiceName = null;
if (cookie != null) {
Form form = new Form(cookie.getValue());
cloudServiceName = form
.getFirstValue(RuntimeParameter.CLOUD_SERVICE_NAME);
}
return cloudServiceName;
}
public static User getCookieUser(Cookie cookie)
throws ConfigurationException, ValidationException {
String userName = getCookieUsername(cookie);
if (userName != null) {
return User.loadByName(userName);
}
return null;
}
private static Date dateFromExpiryString(String expiryString) {
try {
return new Date(Long.parseLong(expiryString));
} catch (NumberFormatException e) {
return new Date(0L);
}
}
private static Form extractCookieValueAsForm(Cookie cookie) {
String value = (cookie != null) ? cookie.getValue() : null;
return new Form((value != null) ? value : "");
}
public static String getCookieName() {
return COOKIE_NAME;
}
public static boolean isMachine(Cookie cookie) {
Form f = extractCookieValueAsForm(cookie);
return "true".equals(f.getFirstValue(COOKIE_IS_MACHINE, "false"));
}
public static String getRunId(Cookie cookie) {
Form f = extractCookieValueAsForm(cookie);
return f.getFirstValue(COOKIE_RUN_ID, null);
}
}