/** * Copyright 2012 Comcast Corporation * * 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. */ package com.comcast.cmb.common.model; import com.comcast.cmb.common.persistence.IUserPersistence; import com.comcast.cmb.common.persistence.PersistenceFactory; import com.comcast.cmb.common.util.AuthUtil; import com.comcast.cmb.common.util.AuthenticationException; import com.comcast.cmb.common.util.CMBErrorCodes; import com.comcast.cmb.common.util.CMBException; import com.comcast.cmb.common.util.CMBProperties; import com.comcast.cmb.common.util.ExpiringCache; import com.comcast.cmb.common.util.ExpiringCache.CacheFullException; import org.apache.log4j.Logger; import javax.servlet.http.HttpServletRequest; import java.net.URL; import java.util.Arrays; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; /** * Provide user authentication capability * @author bwolf, aseem, michael, vvenkatraman * */ public class UserAuthModule implements IAuthModule { private IUserPersistence userPersistence; private static ExpiringCache<String, User> userCacheByAccessKey = new ExpiringCache<String, User>(CMBProperties.getInstance().getUserCacheSizeLimit()); private static ExpiringCache<String, User> userCacheByUserId = new ExpiringCache<String, User>(CMBProperties.getInstance().getUserCacheSizeLimit()); private static final Logger logger = Logger.getLogger(UserAuthModule.class); private static final List<String> ADMIN_ACTIONS = Arrays.asList(new String[] { "HealthCheck", "ManageService", "GetAPIStats", "GetWorkerStats", "ManageWorker" }); public class UserCallableByAccessKey implements Callable<User> { String accessKey = null; public UserCallableByAccessKey(String k) { this.accessKey = k; } @Override public User call() throws Exception { User u = userPersistence.getUserByAccessKey(accessKey); return u; } } public class UserCallableByUserId implements Callable<User> { String userId = null; public UserCallableByUserId(String k) { this.userId = k; } @Override public User call() throws Exception { User u = userPersistence.getUserById(userId); return u; } } @Override public void setUserPersistence(IUserPersistence up) { userPersistence = up; } private Map<String, String> getAllParameters(HttpServletRequest requestUrl) { Enumeration<String> enumeration = requestUrl.getParameterNames(); Map<String, String> parameters = new HashMap<String, String>(); while (enumeration.hasMoreElements()) { String name = enumeration.nextElement(); parameters.put(name, requestUrl.getParameter(name)); } return parameters; } private Map<String, String> getAllHeaders(HttpServletRequest requestUrl) { Enumeration<String> enumeration = requestUrl.getHeaderNames(); Map<String, String> parameters = new HashMap<String, String>(); while (enumeration.hasMoreElements()) { String name = enumeration.nextElement(); parameters.put(name, requestUrl.getHeader(name)); } return parameters; } @Override public User authenticateByRequest(HttpServletRequest request) throws CMBException { Map<String, String> parameters = getAllParameters(request); // sample header //content-type=application/x-www-form-urlencoded; //charset=utf-8 //x-amz-date=20130109T230435Z //connection=Keep-Alive //host=localhost:7070 //content-length=36 //user-agent=aws-sdk-java/1.3.27 Mac_OS_X/10.7 Java_HotSpot(TM)_64-Bit_Server_VM/20.12-b01-434 //authorization=AWS4-HMAC-SHA256 //Credential=50AL8M5BLRQB4LG5MU1C/20130109/us-east-1/us-east-1/aws4_request //SignedHeaders=host;user-agent;x-amz-content-sha256;x-amz-date //Signature=f4afd88c15fc41aacae2dc8b7d014673a0a51b4bfc7f2932993329be65c3f2fd //x-amz-content-sha256=48a38266faf90970d6c7fea9b15e6ba366e5f6397c2970fc893f8a7b5e207bd0 String accessKey = parameters.get("AWSAccessKeyId"); String authorizationHeader = request.getHeader("authorization"); Map<String, String> headers = getAllHeaders(request); if (accessKey == null && authorizationHeader != null) { if (authorizationHeader.contains("Credential=") && authorizationHeader.contains("/")) { accessKey = authorizationHeader.substring(authorizationHeader.indexOf("Credential=") + "Credential=".length(), authorizationHeader.indexOf("/")); } } if (accessKey == null) { logger.error("event=authenticate error_code=missing_access_key"); throw new AuthenticationException(CMBErrorCodes.InvalidAccessKeyId, "No access key provided"); } User user = null; try { try { user = userCacheByAccessKey.getAndSetIfNotPresent(accessKey, new UserCallableByAccessKey(accessKey), CMBProperties.getInstance().getUserCacheExpiring() * 1000); } catch (CacheFullException e) { user = new UserCallableByAccessKey(accessKey).call(); } if (user == null) { logger.error("event=authenticate access_key=" + accessKey + " error_code=invalid_accesskey"); throw new AuthenticationException(CMBErrorCodes.InvalidAccessKeyId, "AccessKey " + accessKey + " is not valid"); } } catch (Exception ex) { logger.error("event=authenticate", ex); throw new AuthenticationException(CMBErrorCodes.InvalidAccessKeyId, "AccessKey " + accessKey + " is not valid"); } // admin actions do not require signatures but can only be performed by admin user if (ADMIN_ACTIONS.contains(parameters.get("Action"))) { if (CMBProperties.getInstance().getCNSUserName().equals(user.getUserName())) { logger.debug("event=authenticate action=admin_action"); return user; } else { logger.error("event=authenticate error_code=regular_user_attempted_admin_op"); throw new AuthenticationException(CMBErrorCodes.InvalidAccessKeyId, "User not authorized to perform admin actions"); } } if ((!CMBProperties.getInstance().getEnableSignatureAuth())|| (request.getMethod().equals("GET")&&CMBProperties.getInstance().getAllowGetRequest())) { if (!user.getUserName().equals(CMBProperties.getInstance().getCNSUserName())) { logger.debug("event=authenticate verify_signature=not_required"); } return user; } //version 1 and 2 is from parameters String version = parameters.get("SignatureVersion"); //version 4 is recommended from header if((version == null)&&(authorizationHeader != null)){ if(authorizationHeader.trim().startsWith("AWS4")){ version="4"; } } if (!version.equals("1") && !version.equals("2")&&!version.equals("4")) { logger.error("event=authenticate signature_version="+version+" error_code=unsupported_signature_version"); throw new AuthenticationException(CMBErrorCodes.NoSuchVersion, "SignatureVersion="+version+" is not valid"); } //validate signature for version 1 and 2 if (version.equals("1")||version.equals("2")){ String signatureToCheck = parameters.get("Signature"); if (signatureToCheck == null) { logger.error("event=authenticate error_code=no_signature_provided"); throw new AuthenticationException(CMBErrorCodes.MissingParameter, "Signature not found"); } String timeStamp = parameters.get("Timestamp"); String expiration = parameters.get("Expires"); if (timeStamp != null) { AuthUtil.checkTimeStamp(timeStamp); } else if (expiration != null) { AuthUtil.checkExpiration(expiration); } else { logger.error("event=authenticate error_code=no_time_stamp_or_expiration"); throw new AuthenticationException(CMBErrorCodes.MissingParameter, "Request must provide either Timestamp or Expires parameter"); } String signatureMethod = parameters.get("SignatureMethod"); if (!signatureMethod.equals("HmacSHA256") && !signatureMethod.equals("HmacSHA1")) { logger.error("event=authenticate signature_method=" + signatureMethod + " error_code=unsupported_signature_method"); throw new AuthenticationException(CMBErrorCodes.InvalidParameterValue, "Signature method " + signatureMethod + " is not supported"); } URL url = null; String signature = null; try { url = new URL(request.getRequestURL().toString()); parameters.remove("Signature"); signature = AuthUtil.generateSignature(url, parameters, version, signatureMethod, user.getAccessSecret()); } catch (Exception ex) { logger.error("event=authenticate url="+url+" error_code=invalid_url"); throw new AuthenticationException(CMBErrorCodes.InternalError, "Invalid Url " + url); } if (signature == null || !signature.equals(signatureToCheck)) { logger.error("event=authenticate signature_calculated=" + signature + " signature_given=" + signatureToCheck + " error_code=signature_mismatch"); throw new AuthenticationException(CMBErrorCodes.InvalidSignature, "Invalid signature"); } } //validate signature for version 4 if (version.equals("4")){ //get the signature from head String signatureToCheck = authorizationHeader.substring(authorizationHeader.indexOf("Signature=") + "Signature=".length()); if (signatureToCheck == null) { logger.error("event=authenticate error_code=no_signature_provided"); throw new AuthenticationException(CMBErrorCodes.MissingParameter, "Signature not found"); } String timeStamp = request.getHeader("X-Amz-Date"); if (timeStamp != null) { AuthUtil.checkTimeStampV4(timeStamp); } else { logger.error("event=authenticate error_code=no_time_stamp_or_expiration"); throw new AuthenticationException(CMBErrorCodes.MissingParameter, "Request must provide either Timestamp or Expires parameter"); } String signatureMethod = authorizationHeader.substring("AWS4-".length(), authorizationHeader.indexOf("Credential=")).trim(); //currently support HMAC-SHA256 if (!signatureMethod.equals("HMAC-SHA256")) { logger.error("event=authenticate signature_method=" + signatureMethod + " error_code=unsupported_signature_method"); throw new AuthenticationException(CMBErrorCodes.InvalidParameterValue, "Signature method " + signatureMethod + " is not supported"); } URL url = null; String signature = null; try { String urlOriginal=request.getRequestURL().toString(); if(urlOriginal==null || urlOriginal.length()==0){ urlOriginal="/"; } url = new URL(urlOriginal); signature = AuthUtil.generateSignatureV4(request, url, parameters, headers, version, signatureMethod, user.getAccessSecret()); } catch (Exception ex) { logger.error("event=authenticate url="+url+" error_code=invalid_url"); throw new AuthenticationException(CMBErrorCodes.InternalError, "Invalid Url " + url); } if (signature == null || !signature.equals(signatureToCheck)) { logger.error("event=authenticate signature_calculated=" + signature + " signature_given=" + signatureToCheck + " error_code=signature_mismatch"); throw new AuthenticationException(CMBErrorCodes.InvalidSignature, "Invalid signature"); } } logger.debug("event=authenticated_by_signature username=" + user.getUserName()); return user; } @Override public User authenticateByPassword(String username, String password) throws CMBException { try { User user = userPersistence.getUserByName(username); if (user == null) { logger.error("event=authenticate_by_password user=" + username + " error_code=user_not_found"); throw new AuthenticationException(CMBErrorCodes.AuthFailure, "User " + username + " not found"); } if (!AuthUtil.verifyPassword(password, user.getHashPassword())) { logger.error("event=authenticate_by_password user=" + username + " error_code=invalid_password"); throw new AuthenticationException(CMBErrorCodes.AuthFailure, "Invalid password for user " + username); } logger.debug("event=authenticated_by_password username=" + username); return user; } catch (Exception ex) { logger.error("event=authenticate_by_password username=" + username, ex); throw new AuthenticationException(CMBErrorCodes.AuthFailure, "User " + username + " not found"); } } public User getUserByRequest(HttpServletRequest request) { Map<String, String> parameters = getAllParameters(request); String authorizationHeader = request.getHeader("authorization"); String accessKey = parameters.get("AWSAccessKeyId"); if (accessKey == null && authorizationHeader != null) { if (authorizationHeader.contains("Credential=") && authorizationHeader.contains("/")) { accessKey = authorizationHeader.substring(authorizationHeader.indexOf("Credential=") + "Credential=".length(), authorizationHeader.indexOf("/")); } } if (accessKey==null){ return null; } User user = null; try { try { user = userCacheByAccessKey.getAndSetIfNotPresent(accessKey, new UserCallableByAccessKey(accessKey), CMBProperties.getInstance().getUserCacheExpiring() * 1000); } catch (CacheFullException e) { user = new UserCallableByAccessKey(accessKey).call(); } if (user == null) { logger.error("event=get_user_by_request access_key=" + accessKey + " error_code=invalid_accesskey"); throw new AuthenticationException(CMBErrorCodes.InvalidAccessKeyId, "AccessKey " + accessKey + " is not valid"); } } catch (Exception ex) { logger.error("event=get_user_by_request", ex); return null; } return user; } public User getUserByUserId(String userId){ //set user Persistence setUserPersistence(PersistenceFactory.getUserPersistence()); User user = null; try { try { user = userCacheByUserId.getAndSetIfNotPresent(userId, new UserCallableByUserId(userId), CMBProperties.getInstance().getUserCacheExpiring() * 1000); } catch (CacheFullException e) { user = new UserCallableByAccessKey(userId).call(); } if (user == null) { logger.error("event=get_user_by_userid userId=" + userId + " error_code=invalid_userid"); } } catch (Exception ex) { logger.error("event=get_user_by_userid", ex); } return user; } }