001/** 002 * Copyright 2014 Tampere University of Technology, Pori Department 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package service.tut.pori.users.facebook; 017 018import java.util.Date; 019import java.util.Map; 020import java.util.Map.Entry; 021 022import org.apache.commons.lang3.StringUtils; 023import org.apache.log4j.Logger; 024import org.springframework.beans.factory.annotation.Autowired; 025import org.springframework.jdbc.core.JdbcTemplate; 026import org.springframework.transaction.TransactionStatus; 027import org.springframework.transaction.support.TransactionCallback; 028 029import service.tut.pori.users.google.OAuth2Token; 030import service.tut.pori.users.UserDAO; 031import core.tut.pori.dao.SQLDAO; 032import core.tut.pori.dao.clause.SQLClause.SQLType; 033import core.tut.pori.users.ExternalAccountConnection; 034import core.tut.pori.users.UserIdentity; 035import core.tut.pori.users.ExternalAccountConnection.UserServiceType; 036import core.tut.pori.utils.NonceUtils; 037import core.tut.pori.utils.NonceUtils.Nonce; 038 039/** 040 * DAO for handling authorization tokens for Facebook User Service. 041 * 042 */ 043public class FacebookUserDAO extends SQLDAO { 044 private static final Logger LOGGER = Logger.getLogger(FacebookUserDAO.class); 045 /* tables */ 046 private static final String TABLE_USERS_FACEBOOK = DATABASE+".users_facebook"; 047 /* columns */ 048 private static final String COLUMN_ACCESS_TOKEN = "access_token"; 049 private static final String COLUMN_ACCESS_TOKEN_EXPIRES = "access_token_expires"; 050 private static final String COLUMN_NONCE = "nonce"; 051 private static final String COLUMN_NONCE_EXPIRES = "nonce_expires"; 052 /* sql strings */ 053 private static final String SQL_COUNT_USER_ID = "SELECT "+COLUMN_COUNT+" FROM "+TABLE_USERS_FACEBOOK+" WHERE "+COLUMN_USER_ID+"=?"; 054 private static final int[] SQL_COUNT_USER_ID_SQL_TYPES = new int[]{SQLType.LONG.toInt()}; 055 056 private static final String SQL_INSERT_NONCE = "INSERT INTO "+TABLE_USERS_FACEBOOK+" ("+COLUMN_USER_ID+","+COLUMN_NONCE+","+COLUMN_NONCE_EXPIRES+","+COLUMN_ROW_CREATED+") VALUES (?,?,?,NOW())"; 057 private static final int[] SQL_INSERT_NONCE_SQL_TYPES = new int[]{SQLType.LONG.toInt(),SQLType.STRING.toInt(),SQLType.LONG.toInt()}; 058 059 private static final String SQL_REMOVE_NONCE = "UPDATE "+TABLE_USERS_FACEBOOK+" SET "+COLUMN_NONCE+"=NULL WHERE "+COLUMN_NONCE+"=? LIMIT 1"; 060 private static final int[] SQL_REMOVE_NONCE_SQL_TYPES = new int[]{SQLType.STRING.toInt()}; 061 062 private static final String SQL_REMOVE_TOKEN = "DELETE FROM "+TABLE_USERS_FACEBOOK+" WHERE "+COLUMN_USER_ID+"=?"; 063 private static final int[] SQL_REMOVE_TOKEN_SQL_TYPES = new int[]{SQLType.LONG.toInt()}; 064 065 private static final String SQL_SELECT_TOKEN = "SELECT "+COLUMN_COUNT+", "+COLUMN_ACCESS_TOKEN+", "+COLUMN_ACCESS_TOKEN_EXPIRES+" FROM "+TABLE_USERS_FACEBOOK+" WHERE "+COLUMN_USER_ID+"=? AND "+COLUMN_ACCESS_TOKEN_EXPIRES+">? LIMIT 1"; 066 private static final int[] SQL_SELECT_TOKEN_SQL_TYPES = new int[]{SQLType.LONG.toInt(),SQLType.LONG.toInt()}; 067 068 private static final String SQL_SELECT_USER_ID = "SELECT "+COLUMN_COUNT+", "+COLUMN_USER_ID+" FROM "+TABLE_USERS_FACEBOOK+" WHERE "+COLUMN_NONCE+"=? AND "+COLUMN_NONCE_EXPIRES+">? LIMIT 1"; 069 private static final int[] SQL_SELECT_USER_ID_SQL_TYPES = new int[]{SQLType.STRING.toInt(),SQLType.LONG.toInt()}; 070 071 /** tokens generated by facebook should be unique, so the unique column constraint on access token should never match on ON DUPLICATE KEY UPDATE for two different users, the match will be solely done on user id */ 072 private static final String SQL_SET_TOKEN = "INSERT INTO "+TABLE_USERS_FACEBOOK+" ("+COLUMN_USER_ID+","+COLUMN_ACCESS_TOKEN+","+COLUMN_ACCESS_TOKEN_EXPIRES+","+COLUMN_ROW_CREATED+") VALUES (?,?,?,NOW()) ON DUPLICATE KEY UPDATE "+COLUMN_ACCESS_TOKEN+"=VALUES("+COLUMN_ACCESS_TOKEN+"), "+COLUMN_ACCESS_TOKEN_EXPIRES+"=VALUES("+COLUMN_ACCESS_TOKEN_EXPIRES+")"; 073 private static final int[] SQL_SET_TOKEN_SQL_TYPES = new int[]{SQLType.LONG.toInt(),SQLType.STRING.toInt(),SQLType.LONG.toInt()}; 074 075 private static final String SQL_UPDATE_NONCE = "UPDATE "+TABLE_USERS_FACEBOOK+" SET "+COLUMN_NONCE+"=?,"+COLUMN_NONCE_EXPIRES+"=? WHERE "+COLUMN_USER_ID+"=?"; 076 private static final int[] SQL_UPDATE_NONCE_SQL_TYPES = new int[]{SQLType.STRING.toInt(),SQLType.LONG.toInt(),SQLType.LONG.toInt()}; 077 078 @Autowired 079 private UserDAO _userDAO = null; 080 081 /** 082 * 083 * @param facebookUserId 084 * @param token 085 * @param userId 086 * @return true on success 087 */ 088 public boolean setToken(final String facebookUserId, final OAuth2Token token, final UserIdentity userId){ 089 if(!UserIdentity.isValid(userId) || !token.isValid() || StringUtils.isBlank(facebookUserId)){ 090 LOGGER.debug("Invalid userId or token."); 091 return false; 092 } 093 094 return getTransactionTemplate().execute(new TransactionCallback<Boolean>() { 095 096 @Override 097 public Boolean doInTransaction(TransactionStatus status) { 098 JdbcTemplate t = getJdbcTemplate(); 099 100 String oldFacebookUserId = getFacebookUserId(userId); 101 if(oldFacebookUserId == null){ 102 LOGGER.debug("Creating a new external account connection for user, id: "+userId.getUserId()+", google user id: "+facebookUserId); 103 _userDAO.insertExternalAccountConnection(new ExternalAccountConnection(facebookUserId, UserServiceType.FACEBOOK), userId); 104 }else if(!oldFacebookUserId.equals(facebookUserId)){ // do not allow changing the facebook user, this may cause undefined behavior on other parts of the system 105 LOGGER.warn("The given facebook user Id "+facebookUserId+" did not match the old facebook user id: "+oldFacebookUserId); 106 return false; 107 } 108 109 t.update(SQL_SET_TOKEN, new Object[]{userId.getUserId(),token.getAccessToken(),token.getExpires().getTime()}, SQL_SET_TOKEN_SQL_TYPES); 110 111 return true; 112 } 113 }); 114 } 115 116 /** 117 * 118 * @param userId non-null, valid userid 119 * @return facebook user id for the given userId, if known by the system, otherwise returns null 120 */ 121 public String getFacebookUserId(UserIdentity userId){ 122 ExternalAccountConnection connection = _userDAO.getExternalAccountConnection(userId, UserServiceType.FACEBOOK); 123 return (connection == null ? null : connection.getExternalId()); 124 } 125 126 /** 127 * 128 * @param nonce 129 */ 130 public void removeNonce(String nonce){ 131 if(getJdbcTemplate().update(SQL_REMOVE_NONCE, new Object[]{nonce}, SQL_REMOVE_NONCE_SQL_TYPES) < 1){ 132 LOGGER.debug("No nonce was cleared."); 133 }else{ 134 LOGGER.debug("Nonce cleared."); 135 } 136 } 137 138 /** 139 * 140 * @param userId 141 * @return token for the given user or null if none available 142 */ 143 public OAuth2Token getToken(UserIdentity userId){ 144 if(!UserIdentity.isValid(userId)){ 145 LOGGER.debug("Invalid userId."); 146 return null; 147 } 148 149 OAuth2Token token = new OAuth2Token(); 150 Map<String, Object> row = getJdbcTemplate().queryForMap(SQL_SELECT_TOKEN, new Object[]{userId.getUserId(), System.currentTimeMillis()}, SQL_SELECT_TOKEN_SQL_TYPES); 151 for(Entry<String,Object> entry : row.entrySet()){ 152 switch(entry.getKey()){ 153 case COLUMN_ACCESS_TOKEN: 154 token.setAccessToken((String) entry.getValue()); 155 break; 156 case COLUMN_ACCESS_TOKEN_EXPIRES: 157 token.setExpires(new Date((Long) entry.getValue())); 158 break; 159 default: 160 if(checkCountColumn(entry.getKey(), entry.getValue()) < 1){ // if we have only one column, check what it is (should be count) 161 LOGGER.debug("Unknown column name, or no results."); 162 return null; 163 } 164 break; 165 } 166 } 167 return token; 168 } 169 170 /** 171 * This generates a new nonce from the given UserId, replacing the old one if one was present 172 * 173 * @param userId 174 * @return randomly generated nonce for the given user 175 */ 176 public String generateNonce(UserIdentity userId){ 177 if(!UserIdentity.isValid(userId)){ 178 LOGGER.debug("Invalid userId."); 179 return null; 180 } 181 Nonce nonce = NonceUtils.generateNonce(); // in theory, it is extremely unlikely that the generated nonce will collide with previously generated one, but... 182 if(recordExists(userId.getUserId())){ // ...just in case do not use INSERT ... ON DUPLICATE KEY UPDATE for updating the nonce (as nonce itself is an unique column) 183 getJdbcTemplate().update(SQL_UPDATE_NONCE, new Object[]{nonce.getNonce(),nonce.getExpires(),userId.getUserId()}, SQL_UPDATE_NONCE_SQL_TYPES); // this throw an exception on duplicate nonce if one ever occurs 184 }else{ 185 getJdbcTemplate().update(SQL_INSERT_NONCE, new Object[]{userId.getUserId(),nonce.getNonce(),nonce.getExpires()}, SQL_INSERT_NONCE_SQL_TYPES); // this throw an exception on duplicate nonce if one ever occurs 186 } 187 return nonce.getNonce(); 188 } 189 190 /** 191 * remove token from the given userIdentity if one exists 192 * 193 * @param userId 194 */ 195 public void removeToken(UserIdentity userId){ 196 if(!UserIdentity.isValid(userId)){ 197 LOGGER.debug("Invalid userId."); 198 return; 199 } 200 Long id = userId.getUserId(); 201 if(getJdbcTemplate().update(SQL_REMOVE_TOKEN,new Object[]{id},SQL_REMOVE_TOKEN_SQL_TYPES) < 1){ 202 LOGGER.debug("No tokens removed for userId: "+id); 203 }else{ 204 LOGGER.debug("Token removed for userId: "+id); 205 } 206 } 207 208 /** 209 * Note: this will not populate the UserIdentity, only userId will be set 210 * 211 * @param nonce 212 * @return owner of the given nonce, or null if no such nonce exists or the nonce has expired 213 */ 214 public UserIdentity getUser(String nonce){ 215 Long userId = (Long) getJdbcTemplate().queryForMap(SQL_SELECT_USER_ID, new Object[]{nonce,System.currentTimeMillis()}, SQL_SELECT_USER_ID_SQL_TYPES).get(COLUMN_USER_ID); 216 return (userId == null ? null : new UserIdentity(userId)); 217 } 218 219 /** 220 * 221 * @param userId 222 * @return true if record exists for the given user 223 */ 224 private boolean recordExists(Long userId){ 225 return (getJdbcTemplate().queryForObject(SQL_COUNT_USER_ID, new Object[]{userId}, SQL_COUNT_USER_ID_SQL_TYPES, Long.class) > 0); 226 } 227}