
org.sakaiproject.user.impl.AuthenticationCache Maven / Gradle / Ivy
/**
* $Id$
* $URL$
**************************************************************************
* Copyright (c) 2007, 2008 Sakai Foundation
*
* Licensed under the Educational Community 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.opensource.org/licenses/ECL-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 org.sakaiproject.user.impl;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.memory.api.Cache;
import org.sakaiproject.memory.api.MemoryService;
import org.sakaiproject.user.api.Authentication;
import org.sakaiproject.user.api.AuthenticationException;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
/**
* Because DAV clients do not understand the concept of secure sessions, a DAV
* user will end up asking Sakai to re-authenticate them for every action.
* To ease the overhead, this class checks a size-limited timing-out cache
* of one-way encrypted successful authentication IDs and passwords.
*
* There's nothing DAV-specific about this class, and it's also independent of
* any Sakai classes other than the "Authentication" user ID and EID holder.
*
* We salt password so precomputed dictionaries can't be used against a dump of the
* authentication cache.
*
* @author Aaron Zeckoski ([email protected])
*/
public class AuthenticationCache {
private static final Log log = LogFactory.getLog(AuthenticationCache.class);
private MemoryService memoryService;
private Cache authCache = null;
/**
* List of algorithms to attempt to use, best ones should come first.
*/
private List algorithms = Arrays.asList(new String[]{"SHA2","SHA1"});
private Random saltGenerator = new Random();
private int saltLength = 8;
public void setMemoryService(MemoryService memoryService) {
this.memoryService = memoryService;
}
public void init() {
log.info("INIT");
authCache = memoryService.getCache("org.sakaiproject.user.api.AuthenticationManager");
}
public void destroy() {
if (authCache != null) authCache.close();
}
/**
* The central cache object, should be injected
*/
public void setAuthCache(Cache authCache) {
this.authCache = authCache;
if (log.isDebugEnabled() && (authCache != null)) log.debug("authCache ");
}
public Authentication getAuthentication(String authenticationId, String password)
throws AuthenticationException {
Authentication auth = null;
AuthenticationRecord record = (AuthenticationRecord) authCache.get(authenticationId);
if (record != null) {
byte[] salt = new byte[saltLength];
System.arraycopy(record.encodedPassword, 0, salt, 0, salt.length);
byte[] encodedPassword = getEncrypted(password, salt);
if (MessageDigest.isEqual(record.encodedPassword, encodedPassword)) {
if (record.authentication == null) {
if (log.isDebugEnabled()) log.debug("getAuthentication: replaying authentication failure for authenticationId=" + authenticationId);
throw new AuthenticationException("repeated invalid login");
} else {
if (log.isDebugEnabled()) log.debug("getAuthentication: returning record for authenticationId=" + authenticationId);
auth = record.authentication;
}
} else {
// Since the passwords didn't match, we're no longer getting repeats,
// and so the record should be removed.
if (log.isDebugEnabled()) log.debug("getAuthentication: record for authenticationId=" + authenticationId + " failed password check");
authCache.remove(authenticationId);
}
}
return auth;
}
public void putAuthentication(String authenticationId, String password, Authentication authentication) {
putAuthenticationRecord(authenticationId, password, authentication);
}
public void putAuthenticationFailure(String authenticationId, String password) {
putAuthenticationRecord(authenticationId, password, null);
}
public void removeAuthentification(String authentificationId) {
authCache.remove(authentificationId);
}
protected void putAuthenticationRecord(String authenticationId, String password,
Authentication authentication) {
if (authCache.containsKey(authenticationId)) {
// Don't indefinitely renew the cached record -- we want to force
// real authentication after the timeout.
} else {
byte[] salt = new byte[saltLength];
saltGenerator.nextBytes(salt);
byte[] encrypted = getEncrypted(password, salt);
authCache.put(authenticationId,
new AuthenticationRecord(encrypted, authentication) );
}
}
private byte[] getEncrypted(String plaintext, byte[] salt) {
Exception lastException = null;
for (String algorithm : algorithms) {
try {
MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
messageDigest.update(salt);
messageDigest.update(plaintext.getBytes("UTF-8"));
byte[] encrypted = messageDigest.digest();
byte[] saltEncrypted = new byte[salt.length+ encrypted.length];
System.arraycopy(salt, 0, saltEncrypted, 0, salt.length);
System.arraycopy(encrypted, 0, saltEncrypted, salt.length, encrypted.length);
return saltEncrypted;
} catch (NoSuchAlgorithmException e) {
lastException = e;
} catch (UnsupportedEncodingException e) {
lastException = e;
}
}
throw new RuntimeException(lastException);
}
/**
* @deprecated No longer used. Use standard cache settings instead.
* @param maximumSize maximum capacity of the cache before replacing older records
*/
public void setMaximumSize(int maximumSize) {
if (log.isWarnEnabled()) log.warn("maximumSize property set but no longer used; should switch to maxElementsInMemory property instead");
}
/**
* @deprecated No longer used. Use standard cache settings instead.
* @param timeoutMs timeout of a cached authentication in milliseconds
*/
public void setTimeoutMs(int timeoutMs) {
if (log.isWarnEnabled()) log.warn("timeoutMs property set but no longer used; should switch to timeToLive seconds property instead");
}
/**
* @deprecated No longer used. Use standard cache settings instead.
* @param failureThrottleTimeoutMs timeout of a cached failed ID and
* password combination in milliseconds; used to prevent DAV clients
* with out-of-date passwords from swamping authentication services.
*/
public void setFailureThrottleTimeoutMs(int failureThrottleTimeoutMs) {
if (log.isWarnEnabled()) log.warn("failureThrottleTimeoutMs property set but no longer used; should switch to timeToLive seconds property instead");
}
static class AuthenticationRecord implements Serializable {
private static final long serialVersionUID = 1L;
byte[] encodedPassword;
Authentication authentication; // Null for failed authentication
public AuthenticationRecord(byte[] encodedPassword, Authentication authentication) {
this.encodedPassword = encodedPassword;
this.authentication = authentication;
}
}
}