/*
* Copyright 2015 Evgeny Dolganov (evgenij.dolganov@gmail.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.
*/
package och.front.service;
import static och.api.model.BaseBean.*;
import static och.api.model.PropKey.*;
import static och.api.model.remtoken.ClientRemToken.*;
import static och.api.model.user.SecurityContext.*;
import static och.util.Util.*;
import static och.util.servlet.WebUtil.*;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.Future;
import javax.servlet.ServletContext;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import och.api.exception.ExpectedException;
import och.api.exception.user.BannedUserException;
import och.api.exception.user.InvalidLoginDataException;
import och.api.exception.user.NotActivatedUserException;
import och.api.exception.user.UserSessionAlreadyExistsException;
import och.api.model.remtoken.ClientRemToken;
import och.api.model.remtoken.RemToken;
import och.api.model.user.LoginUserReq;
import och.api.model.user.UpdateUserReq;
import och.api.model.user.User;
import och.comp.db.base.universal.UniversalQueries;
import och.comp.db.base.universal.UpdateStub;
import och.comp.db.main.table.remtoken.CreateRemToken;
import och.comp.db.main.table.remtoken.DeleteRemToken;
import och.comp.db.main.table.remtoken.SelectRemToken;
import och.comp.web.BaseServlet.WebSecurityProvider;
import och.front.service.event.user.UserBannedEvent;
import och.front.service.event.user.UserSessionDesroyedEvent;
import och.front.service.event.user.UserUnbannedEvent;
import och.front.service.model.UserSession;
import och.util.servlet.WebUtil;
public class SecurityService extends BaseFrontService implements HttpSessionListener, WebSecurityProvider {
public static final String REM_TOKEN = "rem-t";
public static final String SESSION_OBJ_KEY = "security.user";
public static final String BANNED_CACHE_PREFIX = "sc.user.banned.";
public static final int INVALID_LOGINS_CACHE_LIVETIME_SEC = 60*30; //30 min
public static final int INVALID_LOGINS_CACHE_LIVETIME_MS = 1000*INVALID_LOGINS_CACHE_LIVETIME_SEC;
private UniversalQueries universal;
public SecurityService(FrontAppContext c) {
super(c);
}
@Override
public void init() throws Exception {
universal = c.db.universal;
c.events.addListener(UserBannedEvent.class, (event)-> onUserBanned(event.userId));
c.events.addListener(UserUnbannedEvent.class, (event)-> onUserUnbanned(event.userId));
}
public void init(ServletContext servletContext){
servletContext.addListener(this);
}
@Override
public void sessionCreated(HttpSessionEvent se) {
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
HttpSession session = se.getSession();
removeUserSession(session);
}
public int maxRemCount(){
return c.props.getIntVal(remToken_maxCount);
}
public int deleteRemCount(){
return c.props.getIntVal(remToken_deleteCount);
}
public boolean isUserInSession(HttpServletRequest req){
return getUserSession(req) != null;
}
@Override
public User getUserFromSession(HttpServletRequest req){
UserSession session = getUserSession(req);
return session == null? null : session.user;
}
public int getInvalidLoginsCount(HttpServletRequest req, LoginUserReq data, int onErrorVal){
String key = getInvalidLoginsKey(req, data);
try {
String val = c.cache.getVal(key);
return isEmpty(val)? 0 : Integer.parseInt(val);
}catch (Throwable t) {
ExpectedException.logError(log, t, "can't get val");
return onErrorVal;
}
}
public Future<?> setInvalidLoginsCountAsync(HttpServletRequest req, LoginUserReq data, int count){
String key = getInvalidLoginsKey(req, data);
if(count < 1) return c.cache.removeCacheAsync(key);
return c.cache.putCacheAsync(key, String.valueOf(count), INVALID_LOGINS_CACHE_LIVETIME_MS);
}
public User createUserSession(HttpServletRequest req, HttpServletResponse resp, LoginUserReq data)
throws NotActivatedUserException, BannedUserException, Exception {
validateState(data);
if(isUserInSession(req)) throw new UserSessionAlreadyExistsException();
User user = c.root.users.checkEmailOrLoginAndPsw(data.login, data.psw);
if(user == null) {
removeRemCookie(resp);
throw new InvalidLoginDataException();
}
HttpSession session = setUserToSession(req, user, null);
if(data.rememberMe){
createAndReplaceRemToken(resp, user.id, findRemCookie(req));
} else {
removeRemToken(req, resp, user.id);
}
log.info("session created: userId="+user.id
+", login="+user.login
+", ip="+getClientIp(req)
+", userAgent="+getUserAgent(req)
+", sessionId="+session.getId());
return user;
}
public User restoreUserSession(HttpServletRequest req, HttpServletResponse resp) throws Exception {
if(isUserInSession(req)) return null;
ClientRemToken curToken = findRemCookie(req);
if(curToken == null) return null;
RemToken stored = universal.selectOne(new SelectRemToken(curToken.uid));
if(stored == null) return null;
if( ! Arrays.equals(stored.tokenHash, curToken.getHash(stored.tokenSalt))){
//wrong cookies random
return null;
}
User user = c.root.users.checkClientUser(stored.userId, false);
if(user == null) {
removeRemCookie(resp);
return null;
}
HttpSession session = setUserToSession(req, user, null);
//remove cur token and create new for prevent stealing of cookie
createAndReplaceRemToken(resp, stored.userId, curToken);
log.info("session restored: userId="+user.id
+", login="+user.login
+", ip="+getClientIp(req)
+", userAgent="+getUserAgent(req)
+", sessionId="+session.getId());
return user;
}
public User updateUserSession(HttpServletRequest req, String curPsw, UpdateUserReq data) throws Exception{
UserSession oldSession = getUserSession(req);
if( oldSession == null) return null;
User user = oldSession.user;
User updated = null;
//update data
pushToSecurityContext_SYSTEM_USER();
try {
updated = c.root.users.updateUser(user.id, curPsw, data);
}finally {
popUserFromSecurityContext();
}
//update session
HttpSession session = setUserToSession(req, updated, oldSession.attrs);
log.info("session updated: userId="+updated.id
+", login="+updated.login
+", ip="+getClientIp(req)
+", userAgent="+getUserAgent(req)
+", sessionId="+session.getId());
return updated;
}
public void logout(HttpServletRequest req, HttpServletResponse resp){
HttpSession session = req.getSession(false);
if(session == null) return;
UserSession userSession = removeUserSession(session);
if(userSession == null) return;
User user = userSession.user;
removeRemToken(req, resp, user.id);
log.info("logout: userId="+user.id
+", login="+user.login
+", ip="+getClientIp(req)
+", userAgent="+getUserAgent(req)
+", sessionId="+session.getId());
}
private UserSession removeUserSession(HttpSession session){
UserSession userSession = (UserSession)session.getAttribute(SESSION_OBJ_KEY);
if(userSession == null) return null;
session.removeAttribute(SESSION_OBJ_KEY);
//fire event
c.events.tryFireEvent(new UserSessionDesroyedEvent(userSession));
return userSession;
}
public void setUserSessionAttr(HttpServletRequest req, String key, Object val){
setUserSessionAttr(req, key, val, true);
}
public Object setUserSessionAttr(HttpServletRequest req, String key, Object val, boolean replaceIfExists){
UserSession session = getUserSession(req);
if(session == null) return null;
synchronized (session.attrs) {
Object out = val;
if(replaceIfExists) session.attrs.put(key, val);
else {
if(session.attrs.containsKey(key)){
out = session.attrs.get(key);
} else {
session.attrs.put(key, val);
}
}
return out;
}
}
@SuppressWarnings("unchecked")
public <T> T getUserSessionAttr(HttpServletRequest req, String key){
UserSession session = getUserSession(req);
if(session == null) return null;
synchronized (session.attrs) {
return (T)session.attrs.get(key);
}
}
public boolean hasUserSessionAttr(HttpServletRequest req, String key){
UserSession session = getUserSession(req);
if(session == null) return false;
synchronized (session.attrs) {
return session.attrs.containsKey(key);
}
}
public void removeUserSessionAttr(HttpServletRequest req, String key){
UserSession session = getUserSession(req);
if(session == null) return;
synchronized (session.attrs) {
session.attrs.remove(key);
}
}
private UserSession getUserSession(HttpServletRequest req){
HttpSession session = req.getSession(false);
if(session == null) return null;
UserSession userSession = (UserSession)session.getAttribute(SESSION_OBJ_KEY);
if(userSession == null) return null;
//user banned runtime
String bannedFlag = c.cache.tryGetVal(getBannedCacheKey(userSession.user.id), null);
if(bannedFlag != null) return null;
return userSession;
}
/**
* (Based by
* http://jaspan.com/improved_persistent_login_cookie_best_practice
* http://docs.spring.io/spring-security/site/docs/3.0.x/reference/remember-me.html )
*/
private void createAndReplaceRemToken(HttpServletResponse resp, final long userId, final ClientRemToken oldToken){
ClientRemToken newToken = new ClientRemToken();
//add cookie
int maxAge = 365 * 24 * 60 * 60; // one year
resp.addCookie(cookie(REM_TOKEN, newToken.encodeToCookie(), true, maxAge));
//replace in db
c.async.invoke(()-> {
String salt = randomSimpleId();
byte[] hash = newToken.getHash(salt);
universal.update(
oldToken != null? new DeleteRemToken(oldToken.uid) : new UpdateStub(),
new CreateRemToken(newToken.uid, hash, salt, userId));
//delete if maxCount - protection from New Tokens Rush Attack
c.db.remTokens.deleteOldIfMaxCount(userId, maxRemCount(), deleteRemCount());
return null;
});
}
private void removeRemToken(HttpServletRequest req, HttpServletResponse resp, final long userId){
//remove resp cookie
removeRemCookie(resp);
//clean db
ClientRemToken reqToken = findRemCookie(req);
if(reqToken == null) return;
c.async.invoke(()->
universal.update(new DeleteRemToken(reqToken.uid))
);
}
private static HttpSession setUserToSession(HttpServletRequest req, User user, Map<String, Object> initAttrs) {
HttpSession session = req.getSession(true);
String ip = getClientIp(req);
String userAgent = getUserAgent(req);
session.setAttribute(SESSION_OBJ_KEY, new UserSession(ip, userAgent, user, initAttrs));
return session;
}
public static ClientRemToken findRemCookie(HttpServletRequest req){
Cookie remCookie = null;
Cookie[] cookies = req.getCookies();
if(isEmpty(cookies)) return null;
for (Cookie cookie : cookies) {
if(REM_TOKEN.equals(cookie.getName())
&& hasText(cookie.getValue())){
remCookie = cookie;
break;
}
}
if(remCookie == null) return null;
return decodeFromCookieVal(remCookie.getValue());
}
public static void removeRemCookie(HttpServletResponse resp) {
resp.addCookie(deletedCookie(REM_TOKEN));
}
private void onUserBanned(long userId) {
int banUserFlagLiveTime = c.props.getIntVal(users_banUserFlagLiveTime);
c.cache.tryPutCache(getBannedCacheKey(userId), "1", banUserFlagLiveTime);
}
private void onUserUnbanned(long userId) {
c.cache.tryRemoveCache(getBannedCacheKey(userId));
}
private static String getBannedCacheKey(long userId) {
return BANNED_CACHE_PREFIX + userId;
}
public static String getInvalidLoginsKey(HttpServletRequest req, LoginUserReq data) {
String clientIp = WebUtil.getClientIp(req);
return "invalidLogins."+data.login+"."+clientIp;
}
@Override
public boolean hasClientSession(HttpServletRequest req) {
return false;
}
}