Skip to content

Commit

Permalink
avoid hard-coded shared state
Browse files Browse the repository at this point in the history
  • Loading branch information
ericvergnaud committed Apr 2, 2022
1 parent 0ca0b25 commit e1ef05b
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 65 deletions.
141 changes: 78 additions & 63 deletions Server/src/main/java/prompto/security/auth/StoredUserInfoCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,41 @@
public class StoredUserInfoCache {

static Logger logger = new Logger();

static StoredUserInfoCache instance = null;
static Timer timer = new Timer();
static long KEEP_ALIVE_DELAY = 30_000;

public static StoredUserInfoCache initialize(IStoredAuthenticationSourceConfiguration config) {
if(StoredUserInfoCache.instance==null) {
synchronized(StoredUserInfoCache.class) {
if(StoredUserInfoCache.instance==null) {
StoredUserInfoCache.instance = new StoredUserInfoCache(config, true);// ensure the cache doesn't grow out of control
TimerTask task = new TimerTask() { @Override public void run() { instance.evictOldEntriesFromCache(); }};
if (StoredUserInfoCache.instance == null) {
synchronized (StoredUserInfoCache.class) {
if (StoredUserInfoCache.instance == null) {
StoredUserInfoCache.instance = StoredUserInfoCache.fromConfig(config);
// ensure the cache doesn't grow out of control
TimerTask task = new TimerTask() {
@Override
public void run() {
instance.evictOldEntriesFromCache();
}
};
timer.scheduleAtFixedRate(task, 30_000L, 30_000L);
}
}
}
return StoredUserInfoCache.instance;
}


public static StoredUserInfoCache fromConfig(IStoredAuthenticationSourceConfiguration config) {
try {
IStoreConfiguration storeConfig = config.getStoreConfiguration();
IStore store = IStoreFactory.newStoreFromConfig(storeConfig);
store.createOrUpdateAttributes(Arrays.asList(LOGIN, SALT, METHOD, DIGEST, QUESTIONS, QUESTION, ANSWER));
return new StoredUserInfoCache(store);
} catch (Throwable t) {
throw new RuntimeException(t);
}
}

static final AttributeInfo LOGIN = new AttributeInfo("login", Family.TEXT, false, Arrays.asList("key"));
static final AttributeInfo SALT = new AttributeInfo("salt", Family.TEXT, false, null);
static final AttributeInfo METHOD = new AttributeInfo("method", Family.TEXT, false, null);
Expand All @@ -60,45 +77,35 @@ public static StoredUserInfoCache initialize(IStoredAuthenticationSourceConfigur
static final AttributeInfo QUESTION = new AttributeInfo("question", Family.TEXT, false, null);
static final AttributeInfo ANSWER = new AttributeInfo("answer", Family.TEXT, false, null);


Map<String, StoredPasswordDigestCredential> cache = new ConcurrentHashMap<>(new HashMap<>());
IStore store;
boolean shared;

public StoredUserInfoCache(IStoredAuthenticationSourceConfiguration config, boolean shared) {
this.shared = shared;
IStoreConfiguration storeConfig = config.getStoreConfiguration();
try {
store = IStoreFactory.newStoreFromConfig(storeConfig);
store.createOrUpdateAttributes(Arrays.asList(LOGIN, SALT, METHOD, DIGEST, QUESTIONS, QUESTION, ANSWER));
} catch (Throwable e) {
throw new RuntimeException(e);
}
}

public StoredUserInfoCache(IStore store, boolean shared) {

public StoredUserInfoCache(IStore store) {
this.store = store;
this.shared = shared;
store.createOrUpdateAttributes(Arrays.asList(LOGIN, SALT, METHOD, DIGEST, QUESTIONS, QUESTION, ANSWER));
}



public boolean isShared() {
return shared;
return this == instance;
}

void evictOldEntriesFromCache() {
final long now = System.currentTimeMillis();
cache.values().stream()
.filter(c->((now - c.lastChecked) > KEEP_ALIVE_DELAY))
.collect(Collectors.toList()) // need an intermediate list to avoid modifying while traversing
.forEach(c->cache.remove(c.userName));

}
cache.values().stream().filter(c -> ((now - c.lastChecked) > KEEP_ALIVE_DELAY)).collect(Collectors.toList()) // need
// an
// intermediate
// list
// to
// avoid
// modifying
// while
// traversing
.forEach(c -> cache.remove(c.userName));

}

public UserInfo getUserInfo(String username) {
StoredPasswordDigestCredential credential = cache.computeIfAbsent(username, u->new StoredPasswordDigestCredential(u));
StoredPasswordDigestCredential credential = cache.computeIfAbsent(username, u -> new StoredPasswordDigestCredential(u));
return new UserInfo(username, credential, Collections.singletonList("*"));
}

Expand All @@ -108,7 +115,7 @@ class StoredPasswordDigestCredential extends Credential {
String userName;
long lastChecked;
int hashed;

public StoredPasswordDigestCredential(String userName) {
this.userName = userName;
lastChecked = 0;
Expand All @@ -117,49 +124,49 @@ public StoredPasswordDigestCredential(String userName) {

@Override
public boolean check(Object credentials) {
if(wasCheckedRecently(credentials))
if (wasCheckedRecently(credentials))
return true;
if(!checkNow(credentials))
if (!checkNow(credentials))
return false;
lastChecked = System.currentTimeMillis();
hashed = String.valueOf(credentials).hashCode();
return true;
}

private boolean checkNow(Object credentials) {
logger.info(()->"Authenticating user: " + userName);
logger.info(() -> "Authenticating user: " + userName);
if (isNullCredentials(credentials)) {
logger.info(()->"Null credentials for user: " + this.userName);
logger.info(() -> "Null credentials for user: " + this.userName);
return false;
}
IStored authRecord = fetchStoredAuthRecord();
if (authRecord == null) {
logger.info(()->"Unknown user: " + this.userName);
logger.info(() -> "Unknown user: " + this.userName);
return false;
}
String methodName = (String)authRecord.getData(METHOD.getName());
String methodName = (String) authRecord.getData(METHOD.getName());
DigestMethod method = DigestMethod.forName(methodName);
if (method == null) {
logger.info(()->"Unknown digest method: " + methodName + " for user: " + this.userName);
return false;
logger.info(() -> "Unknown digest method: " + methodName + " for user: " + this.userName);
return false;
}
Object storedDigest = authRecord.getData(DIGEST.getName());
if (storedDigest == null) {
logger.info(()->"No digest stored for user: " + this.userName);
logger.info(() -> "No digest stored for user: " + this.userName);
return false;
}
Object storedSalt = authRecord.getData(SALT.getName());
if (storedSalt == null) {
logger.info(()->"No salt stored for user: " + this.userName);
logger.info(() -> "No salt stored for user: " + this.userName);
return false;
}
// compute value from credentials
String computedDigest = method.apply(credentials.toString(), storedSalt.toString());
boolean equal = Objects.equals(storedDigest, computedDigest);
if(equal)
logger.info(()->"Successfully authenticated user: " + this.userName);
if (equal)
logger.info(() -> "Successfully authenticated user: " + this.userName);
else
logger.info(()->"Invalid password for user: " + this.userName);
logger.info(() -> "Invalid password for user: " + this.userName);
return equal;
}

Expand All @@ -168,15 +175,15 @@ private IStored fetchStoredAuthRecord() {
IQueryBuilder query = store.newQueryBuilder();
query.verify(LOGIN, MatchOp.EQUALS, this.userName);
return store.fetchOne(query.build());
} catch(Throwable t) {
logger.error(()->"While authenticating user: " + this.userName, t);
} catch (Throwable t) {
logger.error(() -> "While authenticating user: " + this.userName, t);
return null;
}
}

private boolean wasCheckedRecently(Object credentials) {
long delay = System.currentTimeMillis() - lastChecked;
if(delay > KEEP_ALIVE_DELAY)
long delay = System.currentTimeMillis() - lastChecked;
if (delay > KEEP_ALIVE_DELAY)
return false;
else
return hashed == String.valueOf(credentials).hashCode();
Expand All @@ -187,8 +194,7 @@ private boolean isNullCredentials(Object credentials) {
}

}



public boolean hasLogin(String login) {
IQueryBuilder query = store.newQueryBuilder();
query.verify(LOGIN, MatchOp.EQUALS, login);
Expand All @@ -204,18 +210,28 @@ public boolean checkLogin(String login, String password) throws NoSuchAlgorithmE
public void createLogin(String login, String password) throws NoSuchAlgorithmException {
createLogin(store, login, password);
}

public void updateLogin(String login, String password) throws NoSuchAlgorithmException {
IQueryBuilder query = store.newQueryBuilder();
query.verify(LOGIN, MatchOp.EQUALS, login);
IStored stored = store.fetchOne(query.build());
if(stored==null)
if (stored == null)
return;
String salt = DigestMethod.newSalt();
IStorable storable = store.newStorable(Arrays.asList("User"), new IDbIdFactory() {
@Override public void accept(PromptoDbId t) { }
@Override public PromptoDbId get() { return stored.getDbId(); }
@Override public boolean isUpdate() { return true; }
@Override
public void accept(PromptoDbId t) {
}

@Override
public PromptoDbId get() {
return stored.getDbId();
}

@Override
public boolean isUpdate() {
return true;
}
});
storable.setData("login", login);
storable.setData("salt", salt);
Expand All @@ -225,11 +241,11 @@ public void updateLogin(String login, String password) throws NoSuchAlgorithmExc
store.store(storable);
cache.remove(login);
}

public void close() throws IOException {
store.close();
}

public static void createLogin(IStore store, String login, String password) throws NoSuchAlgorithmException {
String salt = DigestMethod.newSalt();
IStorable storable = store.newStorable(Arrays.asList("User"), null);
Expand All @@ -242,5 +258,4 @@ public static void createLogin(IStore store, String login, String password) thro
store.store(storable);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public void initialize(Subject subject, CallbackHandler callbackHandler, Map<Str

// handy constructor for offline operations, not using shared cache
public StoredPasswordDigestAuthenticationSource(IStoredAuthenticationSourceConfiguration config) {
cache = new StoredUserInfoCache(config, false);
cache = StoredUserInfoCache.fromConfig(config);
}

// handy constructor for testing, not using shared cache
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class TestStoreLoginSource_Mongo extends BaseMongoTest {
@Before
public void before() {
createStore("LOGIN_" + System.currentTimeMillis());
StoredUserInfoCache cache = new StoredUserInfoCache(store, false);
StoredUserInfoCache cache = new StoredUserInfoCache(store);
source = new StoredPasswordDigestAuthenticationSource(cache);
}

Expand Down

0 comments on commit e1ef05b

Please sign in to comment.