001/* 002 * Copyright 2017 Anyware Services 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 org.ametys.core.authentication.token; 017 018import java.io.ByteArrayInputStream; 019import java.io.UnsupportedEncodingException; 020import java.sql.Blob; 021import java.sql.Connection; 022import java.sql.PreparedStatement; 023import java.sql.ResultSet; 024import java.sql.SQLException; 025import java.sql.Timestamp; 026import java.sql.Types; 027import java.util.ArrayList; 028import java.util.Base64; 029import java.util.Date; 030import java.util.List; 031import java.util.Map; 032 033import org.ametys.core.datasource.ConnectionHelper; 034import org.ametys.core.ui.Callable; 035import org.ametys.core.user.CurrentUserProvider; 036import org.ametys.core.user.UserIdentity; 037import org.ametys.runtime.config.Config; 038import org.ametys.runtime.plugin.component.AbstractLogEnabled; 039import org.apache.avalon.framework.activity.Initializable; 040import org.apache.avalon.framework.component.Component; 041import org.apache.avalon.framework.service.ServiceException; 042import org.apache.avalon.framework.service.ServiceManager; 043import org.apache.avalon.framework.service.Serviceable; 044import org.apache.commons.codec.digest.DigestUtils; 045import org.apache.commons.lang.RandomStringUtils; 046import org.apache.commons.lang3.StringUtils; 047 048/** 049 * The component to handle temporary authentication token.<br> 050 * Token can only be used once and are available for a short time only. 051 */ 052public class AuthenticationTokenManager extends AbstractLogEnabled implements Component, Serviceable, Initializable 053{ 054 /** The avalon role */ 055 public static final String ROLE = AuthenticationTokenManager.class.getName(); 056 057 /** The separator in token */ 058 public static final String TOKEN_SEPARATOR = "#"; 059 060 /** The user token type */ 061 public static final String USER_TOKEN_TYPE = "User"; 062 063 private ServiceManager _manager; 064 065 private CurrentUserProvider _currentUserProvider; 066 067 private String _datasourceId; 068 069 public void service(ServiceManager manager) throws ServiceException 070 { 071 _manager = manager; 072 } 073 074 public void initialize() throws Exception 075 { 076 _datasourceId = Config.getInstance() != null ? Config.getInstance().getValueAsString("runtime.assignments.authenticationtokens") : ""; 077 } 078 079 private CurrentUserProvider _getCurrentUserProvider() throws RuntimeException 080 { 081 if (_currentUserProvider == null) 082 { 083 try 084 { 085 _currentUserProvider = (CurrentUserProvider) _manager.lookup(CurrentUserProvider.ROLE); 086 } 087 catch (ServiceException e) 088 { 089 throw new RuntimeException(e); 090 } 091 } 092 return _currentUserProvider; 093 } 094 095 /** 096 * Get the existing tokens for the connected user 097 * 098 * @param type The type of tokens to return. null to return all. 099 * @return The tokens 100 * @throws RuntimeException If there is no user connected or if there is a 101 * database error 102 */ 103 public List<Token> getTokens(String type) throws RuntimeException 104 { 105 return getTokens(_getCurrentUserProvider().getUser(), type); 106 } 107 108 /** 109 * Get the existing tokens for this user 110 * 111 * @param type The type of tokens to return. null to return all. 112 * @param user The user. Cannot be null 113 * @return The tokens identifier and associated comment 114 * @throws RuntimeException If the user is null or if there is a database 115 * error 116 */ 117 public List<Token> getTokens(UserIdentity user, String type) throws RuntimeException 118 { 119 if (user == null) 120 { 121 throw new RuntimeException("Cannot generate a temporary authentication token for a null user"); 122 } 123 124 List<Token> tokens = new ArrayList<>(); 125 126 Connection connection = null; 127 try 128 { 129 connection = ConnectionHelper.getConnection(_datasourceId); 130 131 // Delete old entries 132 _deleteOldTokens(connection); 133 134 try (PreparedStatement selectStatement = _getSelectUserTokenStatement(connection, user.getLogin(), user.getPopulationId(), type); 135 ResultSet resultSet = selectStatement.executeQuery()) 136 { 137 // Find the database entry using this token 138 while (resultSet.next()) 139 { 140 Blob commentBlob = resultSet.getBlob("comment"); 141 String comment = new String(commentBlob.getBytes(1, (int) commentBlob.length())); 142 Token token = new Token(resultSet.getString("id"), resultSet.getString("type"), comment, resultSet.getTimestamp("creation_date"), 143 resultSet.getTimestamp("end_date"), resultSet.getTimestamp("last_update_date")); 144 tokens.add(token); 145 } 146 } 147 } 148 catch (Exception e) 149 { 150 getLogger().error("Communication error with the database", e); 151 } 152 finally 153 { 154 ConnectionHelper.cleanup(connection); 155 } 156 157 return tokens; 158 } 159 160 /** 161 * Generates a new token for the current user 162 * 163 * @param duration The time the token is valid in seconds. 0 means for ever 164 * and moreover the ticket will be reusable. 165 * @param type The type of token. Mandatory but can be anything you want 166 * between 1 to 32 characters. Such as "Cookie". 167 * @param comment An optional token comment to remember the reason of its 168 * creation 169 * @return The token 170 * @throws RuntimeException If the user is not authenticated, or if there is 171 * a database error 172 */ 173 public String generateToken(long duration, String type, String comment) throws RuntimeException 174 { 175 return generateToken(_getCurrentUserProvider().getUser(), duration, type, comment); 176 } 177 178 /** 179 * Generates a new token 180 * 181 * @param user The user that will be authenticated with the token 182 * @param duration The time the token is valid in seconds. 0 means for ever 183 * and moreover the ticket will be reusable 184 * @param type The type of token. Mandatory but can be anything you want 185 * between 1 to 32 characters. Such as "Cookie". 186 * @param comment An optional token comment to remember the reason of its 187 * creation 188 * @return The token 189 * @throws RuntimeException If the user is null or if there is a database 190 * error or if duration is negative 191 */ 192 @SuppressWarnings("resource") 193 public String generateToken(UserIdentity user, long duration, String type, String comment) throws RuntimeException 194 { 195 if (user == null) 196 { 197 throw new RuntimeException("Cannot generate a temporary authentication token for a null user"); 198 } 199 else if (duration < 0) 200 { 201 throw new RuntimeException("Cannot generate a token for a negative duration [" + duration + "]"); 202 } 203 204 String token = RandomStringUtils.randomAlphanumeric(duration == 0 ? 64 : 16); 205 String salt = RandomStringUtils.randomAlphanumeric(48); 206 Timestamp creationDateTime = new Timestamp(new Date().getTime()); 207 Timestamp endTime = duration > 0 ? new Timestamp(System.currentTimeMillis() + duration * 1000) : null; 208 String hashedTokenAndSalt = DigestUtils.sha512Hex(token + salt); 209 210 Connection connection = null; 211 PreparedStatement statement = null; 212 ResultSet rs = null; 213 try 214 { 215 connection = ConnectionHelper.getConnection(_datasourceId); 216 String dbType = ConnectionHelper.getDatabaseType(connection); 217 218 if (ConnectionHelper.DATABASE_ORACLE.equals(dbType)) 219 { 220 statement = connection.prepareStatement("SELECT seq_authenticationtoken.nextval FROM dual"); 221 rs = statement.executeQuery(); 222 223 String id = null; 224 if (rs.next()) 225 { 226 id = rs.getString(1); 227 } 228 ConnectionHelper.cleanup(rs); 229 ConnectionHelper.cleanup(statement); 230 231 statement = connection.prepareStatement( 232 "INSERT INTO Authentication_Token (id, login, population_id, token, salt, creation_date, end_date, type, comment) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"); 233 statement.setString(1, id); 234 statement.setString(2, user.getLogin()); 235 statement.setString(3, user.getPopulationId()); 236 statement.setString(4, hashedTokenAndSalt); 237 statement.setString(5, salt); 238 statement.setTimestamp(6, creationDateTime); 239 statement.setTimestamp(7, endTime); 240 statement.setString(8, type); 241 statement.setBlob(9, comment == null ? null : new ByteArrayInputStream(comment.getBytes("UTF-8"))); 242 } 243 else 244 { 245 statement = connection.prepareStatement( 246 "INSERT INTO Authentication_Token (login, population_id, token, salt, creation_date, end_date, type, comment) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); 247 248 statement.setString(1, user.getLogin()); 249 statement.setString(2, user.getPopulationId()); 250 statement.setString(3, hashedTokenAndSalt); 251 statement.setString(4, salt); 252 statement.setTimestamp(5, creationDateTime); 253 statement.setTimestamp(6, endTime); 254 statement.setString(7, type); 255 if (comment == null) 256 { 257 statement.setNull(8, Types.BLOB); 258 } 259 else 260 { 261 statement.setBlob(8, new ByteArrayInputStream(comment.getBytes("UTF-8"))); 262 } 263 } 264 265 statement.executeUpdate(); 266 } 267 catch (SQLException | UnsupportedEncodingException e) 268 { 269 throw new RuntimeException(e); 270 } 271 finally 272 { 273 ConnectionHelper.cleanup(rs); 274 ConnectionHelper.cleanup(connection); 275 } 276 277 String fullToken = user.getPopulationId() + TOKEN_SEPARATOR + user.getLogin() + TOKEN_SEPARATOR + token; 278 279 try 280 { 281 return Base64.getEncoder().withoutPadding().encodeToString(fullToken.getBytes("UTF-8")); 282 } 283 catch (UnsupportedEncodingException e) 284 { 285 // can't occur ... 286 throw new RuntimeException(e); 287 } 288 } 289 290 private UserIdentity _validateToken(String encodedToken, boolean forceRemove) 291 { 292 String token; 293 try 294 { 295 token = new String(Base64.getDecoder().decode(encodedToken), "UTF-8"); 296 } 297 catch (UnsupportedEncodingException e) 298 { 299 // can't occur ... 300 throw new RuntimeException(e); 301 } 302 catch (Exception e) 303 { 304 // Exception occured during token decoding 305 return null; 306 } 307 308 String[] split = StringUtils.split(token, TOKEN_SEPARATOR); 309 if (split == null || split.length != 3) 310 { 311 return null; 312 } 313 314 String populationId = split[0]; 315 String login = split[1]; 316 String tokenPart = split[2]; 317 318 Connection connection = null; 319 try 320 { 321 connection = ConnectionHelper.getConnection(_datasourceId); 322 323 // Delete old entries 324 _deleteOldTokens(connection); 325 326 try (PreparedStatement selectStatement = _getSelectUserTokenStatement(connection, login, populationId, null); ResultSet resultSet = selectStatement.executeQuery()) 327 { 328 // Find the database entry using this token 329 while (resultSet.next()) 330 { 331 if (resultSet.getString("token").equals(DigestUtils.sha512Hex(tokenPart + resultSet.getString("salt")))) 332 { 333 // Delete it 334 if (forceRemove || resultSet.getDate("end_date") != null) 335 { 336 _deleteUserToken(connection, resultSet.getString("id")); 337 } 338 else 339 { 340 _updateUserToken(connection, resultSet.getString("id")); 341 } 342 return new UserIdentity(login, populationId); 343 } 344 } 345 } 346 } 347 catch (Exception e) 348 { 349 getLogger().error("Communication error with the database", e); 350 } 351 finally 352 { 353 ConnectionHelper.cleanup(connection); 354 } 355 356 return null; 357 } 358 359 /** 360 * Check if a token is valid and return the s 361 * 362 * @param token The token to validate 363 * @return The user associated to the valid token, null otherwise 364 */ 365 public UserIdentity validateToken(String token) 366 { 367 return _validateToken(token, false); 368 } 369 370 /** 371 * Destroy the given token 372 * 373 * @param token The token to remove 374 */ 375 public void deleteTokenByValue(String token) 376 { 377 _validateToken(token, true); 378 } 379 380 /** 381 * Destroy the given token 382 * 383 * @param tokenId The token identifier to remove 384 */ 385 public void deleteTokenById(String tokenId) 386 { 387 Connection connection = null; 388 try 389 { 390 connection = ConnectionHelper.getConnection(_datasourceId); 391 392 _deleteUserToken(connection, tokenId); 393 } 394 catch (SQLException e) 395 { 396 throw new RuntimeException("Could not delete the authentication token with identifier " + tokenId, e); 397 } 398 finally 399 { 400 ConnectionHelper.cleanup(connection); 401 } 402 } 403 404 /** 405 * Generates the sql statement that deletes the entries of the users token 406 * database that are old 407 * 408 * @param connection the database's session 409 * @throws SQLException if a sql exception occurs 410 */ 411 private void _deleteOldTokens(Connection connection) throws SQLException 412 { 413 try (PreparedStatement statement = connection.prepareStatement("DELETE FROM Authentication_Token WHERE end_date < ?")) 414 { 415 statement.setTimestamp(1, new Timestamp(System.currentTimeMillis())); 416 statement.executeUpdate(); 417 } 418 } 419 420 /** 421 * Generates the statement that selects the users having the specified login 422 * in the Users_Token table 423 * 424 * @param connection the database's session 425 * @param login The login of the user 426 * @param populationId The populationId of the user 427 * @param type The type to filter or null to get all 428 * @return the retrieve statement 429 * @throws SQLException if a sql exception occurs 430 */ 431 private PreparedStatement _getSelectUserTokenStatement(Connection connection, String login, String populationId, String type) throws SQLException 432 { 433 String sqlRequest = "SELECT id, token, salt, creation_date, end_date, last_update_date, type, comment FROM Authentication_Token WHERE login=? AND population_id=?" 434 + (type != null ? " AND type=?" : ""); 435 436 PreparedStatement statement = connection.prepareStatement(sqlRequest); 437 statement.setString(1, login); 438 statement.setString(2, populationId); 439 if (type != null) 440 { 441 statement.setString(3, type); 442 } 443 444 return statement; 445 } 446 447 /** 448 * Deletes the database entry that has this token 449 * 450 * @param connection the database's session 451 * @param id the token id 452 * @throws SQLException if an error occurred 453 */ 454 private void _deleteUserToken(Connection connection, String id) throws SQLException 455 { 456 try (PreparedStatement statement = connection.prepareStatement("DELETE FROM Authentication_Token WHERE id = ?")) 457 { 458 statement.setString(1, id); 459 statement.executeUpdate(); 460 } 461 } 462 463 /** 464 * Update the last update date in the database 465 * 466 * @param connection the database's session 467 * @param id the token id 468 * @throws SQLException if an error occurred 469 */ 470 private void _updateUserToken(Connection connection, String id) throws SQLException 471 { 472 try (PreparedStatement statement = connection.prepareStatement("UPDATE Authentication_Token SET last_update_date = ? WHERE id = ?")) 473 { 474 Timestamp lastUpdateDate = new Timestamp(new Date().getTime()); 475 statement.setTimestamp(1, lastUpdateDate); 476 statement.setString(2, id); 477 statement.executeUpdate(); 478 } 479 } 480 481 /** 482 * Generate a new authentication token 483 * 484 * @param parameters a map of the following parameters for the 485 * authentication token : description 486 * @return The generated token 487 */ 488 @Callable 489 public String generateAuthenticationToken(Map<String, Object> parameters) 490 { 491 String description = (String) parameters.get("description"); 492 String generateToken = generateToken(0, USER_TOKEN_TYPE, description); 493 494 return generateToken; 495 } 496 497 /** 498 * Delete one or multiples authentication token 499 * 500 * @param ids a list of authentication token ids 501 */ 502 @Callable 503 public void deleteAuthenticationToken(List<String> ids) 504 { 505 for (String tokenId : ids) 506 { 507 deleteTokenById(tokenId); 508 } 509 } 510 511 /** 512 * An Ametys authentication token 513 */ 514 public static class Token 515 { 516 /** The token identifier */ 517 protected String _id; 518 519 /** The token type */ 520 protected String _type; 521 522 /** The token associated comment */ 523 protected String _comment; 524 525 /** The token creation date */ 526 protected Date _creationDate; 527 528 /** The token end date */ 529 protected Date _endDate; 530 531 /** The token last update date */ 532 protected Date _lastUpdateDate; 533 534 /** 535 * Creates a Token 536 * 537 * @param id The identifier 538 * @param type The type of token 539 * @param comment The comment. Can be null. 540 * @param creationDate The creation date. Can be null. 541 * @param endDate The end date. Can be null. 542 * @param lastUpdateDate The last update date. Can be null. 543 */ 544 protected Token(String id, String type, String comment, Date creationDate, Date endDate, Date lastUpdateDate) 545 { 546 _id = id; 547 _type = type; 548 _comment = comment; 549 _creationDate = creationDate; 550 _endDate = endDate; 551 _lastUpdateDate = lastUpdateDate; 552 } 553 554 /** 555 * Get the token identifier 556 * 557 * @return The identifier 558 */ 559 public String getId() 560 { 561 return _id; 562 } 563 564 /** 565 * Get the token type 566 * 567 * @return The type 568 */ 569 public String getType() 570 { 571 return _type; 572 } 573 574 /** 575 * Get the associated creation comment 576 * 577 * @return The comment or null 578 */ 579 public String getComment() 580 { 581 return _comment; 582 } 583 584 /** 585 * Get the creation date of the token 586 * 587 * @return The creation date or null 588 */ 589 public Date getCreationDate() 590 { 591 return _creationDate; 592 } 593 594 /** 595 * Get the end date of the token validity 596 * 597 * @return The end date or null 598 */ 599 public Date getEndDate() 600 { 601 return _endDate; 602 } 603 604 /** 605 * Get the last update date of the token 606 * 607 * @return The last update date or null 608 */ 609 public Date getLastUpdateDate() 610 { 611 return _endDate; 612 } 613 } 614}