Skip to content

Commit f27a5dc

Browse files
committed
[refactor] PersistentLogin for readability and maintainability
1 parent 024276e commit f27a5dc

File tree

2 files changed

+102
-81
lines changed

2 files changed

+102
-81
lines changed

extensions/modules/persistentlogin/src/main/java/org/exist/xquery/modules/persistentlogin/PersistentLogin.java

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
import java.security.SecureRandom;
3333
import java.util.*;
34+
import java.util.concurrent.ConcurrentHashMap;
3435

3536
/**
3637
* A persistent login feature ("remember me") similar to the implementation in <a href="https://github.com/SpringSource/spring-security">Spring Security</a>,
@@ -60,9 +61,9 @@ public static PersistentLogin getInstance() {
6061

6162
public final static int DEFAULT_TOKEN_LENGTH = 16;
6263

63-
public final static int INVALIDATION_TIMEOUT = 20000;
64+
public final static int INVALIDATION_TIMEOUT = 1000;
6465

65-
private Map<String, LoginDetails> seriesMap = Collections.synchronizedMap(new HashMap<>());
66+
private Map<String, LoginDetails> seriesMap = new ConcurrentHashMap<>();
6667

6768
private SecureRandom random;
6869

@@ -76,7 +77,7 @@ public PersistentLogin() {
7677
*
7778
* The generated token will have the format base64(series-hash):base64(token-hash).
7879
*
79-
* @param user the user name
80+
* @param user the username
8081
* @param password the password
8182
* @param timeToLive timeout of the token
8283
* @return a first login token
@@ -154,17 +155,18 @@ private String generateToken() {
154155

155156
public class LoginDetails {
156157

157-
private String userName;
158-
private String password;
159158
private String token;
160-
private String series;
161-
private long expires;
162-
private DurationValue timeToLive;
159+
160+
private final String userName;
161+
private final String password;
162+
private final String series;
163+
private final long expires;
164+
private final DurationValue timeToLive;
163165

164166
// disable sequential token checking by default
165-
private boolean seqBehavior = false;
167+
private final boolean seqBehavior = false;
166168

167-
private Map<String, Long> invalidatedTokens = new HashMap<>();
169+
private final Map<String, Long> invalidatedTokens = new HashMap<>();
168170

169171
public LoginDetails(String user, String password, DurationValue timeToLive, long expires) {
170172
this.userName = user;
@@ -176,19 +178,19 @@ public LoginDetails(String user, String password, DurationValue timeToLive, long
176178
}
177179

178180
public String getToken() {
179-
return this.token;
181+
return token;
180182
}
181183

182184
public String getSeries() {
183-
return this.series;
185+
return series;
184186
}
185187

186188
public String getUser() {
187-
return this.userName;
189+
return userName;
188190
}
189191

190192
public String getPassword() {
191-
return this.password;
193+
return password;
192194
}
193195

194196
public DurationValue getTimeToLive() {
@@ -197,13 +199,15 @@ public DurationValue getTimeToLive() {
197199

198200
public boolean checkAndUpdateToken(String token) {
199201
if (this.token.equals(token)) {
202+
timeoutCheck();
200203
update();
201204
return true;
202205
}
203206
// check map of invalidating tokens
204-
Long timeout = invalidatedTokens.get(token);
205-
if (timeout == null)
207+
final Long timeout = invalidatedTokens.get(token);
208+
if (timeout == null) {
206209
return false;
210+
}
207211
// timed out: remove
208212
if (System.currentTimeMillis() > timeout) {
209213
invalidatedTokens.remove(token);
@@ -214,22 +218,21 @@ public boolean checkAndUpdateToken(String token) {
214218
}
215219

216220
public String update() {
217-
timeoutCheck();
218-
// leave a small time window until previous token is deleted
221+
// leave a small time-window until previous token is deleted
219222
// to allow for concurrent requests
220-
invalidatedTokens.put(this.token, System.currentTimeMillis() + INVALIDATION_TIMEOUT);
221-
this.token = generateToken();
222-
return this.token;
223+
invalidatedTokens.put(token, System.currentTimeMillis() + INVALIDATION_TIMEOUT);
224+
token = generateToken();
225+
return token;
223226
}
224227

225228
private void timeoutCheck() {
226-
long now = System.currentTimeMillis();
229+
final long now = System.currentTimeMillis();
227230
invalidatedTokens.entrySet().removeIf(entry -> entry.getValue() < now);
228231
}
229232

230233
@Override
231234
public String toString() {
232-
return this.series + ":" + this.token;
235+
return series + ":" + token;
233236
}
234237
}
235238
}

extensions/modules/persistentlogin/src/main/java/org/exist/xquery/modules/persistentlogin/PersistentLoginFunctions.java

Lines changed: 76 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
*/
2222
package org.exist.xquery.modules.persistentlogin;
2323

24+
import org.checkerframework.checker.nullness.qual.Nullable;
2425
import org.exist.EXistException;
2526
import org.exist.dom.QName;
2627
import org.exist.security.AuthenticationException;
@@ -30,14 +31,42 @@
3031
import org.exist.xquery.*;
3132
import org.exist.xquery.value.*;
3233

34+
import java.util.EnumSet;
35+
import java.util.HashMap;
36+
import java.util.Map;
37+
3338
/**
3439
* Functions to access the persistent login module.
3540
*/
3641
public class PersistentLoginFunctions extends UserSwitchingBasicFunction {
42+
private enum Fn {
43+
REGISTER("register"),
44+
LOGIN("login"),
45+
INVALIDATE("invalidate");
46+
47+
final static Map<QName, Fn> lookup = new HashMap<>();
48+
static {
49+
for (Fn fn: EnumSet.allOf(Fn.class)) {
50+
lookup.put(fn.getQName(), fn);
51+
}
52+
}
53+
54+
static Fn get(Function f) {
55+
return lookup.get(f.getName());
56+
}
57+
58+
private final QName qname;
59+
60+
Fn(String name) {
61+
qname = new QName(name, PersistentLoginModule.NAMESPACE, PersistentLoginModule.PREFIX);
62+
}
3763

38-
public final static FunctionSignature signatures[] = {
64+
public QName getQName() { return qname; }
65+
}
66+
67+
public final static FunctionSignature[] signatures = {
3968
new FunctionSignature(
40-
new QName("register", PersistentLoginModule.NAMESPACE, PersistentLoginModule.PREFIX),
69+
Fn.REGISTER.getQName(),
4170
"Try to log in the user and create a one-time login token. The token can be stored to a cookie and used to log in " +
4271
"(via the login function) as the same user without " +
4372
"providing credentials. However, for security reasons the token will be valid only for " +
@@ -55,7 +84,7 @@ public class PersistentLoginFunctions extends UserSwitchingBasicFunction {
5584
new FunctionReturnSequenceType(Type.ITEM, Cardinality.ZERO_OR_MORE, "result of the callback function or the empty sequence")
5685
),
5786
new FunctionSignature(
58-
new QName("login", PersistentLoginModule.NAMESPACE, PersistentLoginModule.PREFIX),
87+
Fn.LOGIN.getQName(),
5988
"Try to log in the user based on the supplied token. If the login succeeds, the provided callback function " +
6089
"is called with 4 arguments: $token as xs:string, $user as xs:string, $password as xs:string, $timeToLive as duration. " +
6190
"$token will be a new token which can be used for the next request. The old token is deleted.",
@@ -67,7 +96,7 @@ public class PersistentLoginFunctions extends UserSwitchingBasicFunction {
6796
new FunctionReturnSequenceType(Type.ITEM, Cardinality.ZERO_OR_MORE, "result of the callback function or the empty sequence")
6897
),
6998
new FunctionSignature(
70-
new QName("invalidate", PersistentLoginModule.NAMESPACE, PersistentLoginModule.PREFIX),
99+
Fn.INVALIDATE.getQName(),
71100
"Invalidate the supplied one-time token, so it can no longer be used to log in.",
72101
new SequenceType[]{
73102
new FunctionParameterSequenceType("token", Type.STRING, Cardinality.EXACTLY_ONE, "a valid one-time token")
@@ -85,77 +114,59 @@ public PersistentLoginFunctions(final XQueryContext context, final FunctionSigna
85114
@Override
86115
public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException {
87116
super.analyze(contextInfo);
88-
this.cachedContextInfo = new AnalyzeContextInfo(contextInfo);
117+
cachedContextInfo = new AnalyzeContextInfo(contextInfo);
89118
}
90119

91120
@Override
92121
public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {
93-
if (isCalledAs("register")) {
94-
final String user = args[0].getStringValue();
95-
final String pass;
96-
if (!args[1].isEmpty()) {
97-
pass = args[1].getStringValue();
98-
} else {
99-
pass = null;
100-
}
101-
final DurationValue timeToLive = (DurationValue) args[2].itemAt(0);
102-
final FunctionReference callback;
103-
if (!args[3].isEmpty()) {
104-
callback = (FunctionReference) args[3].itemAt(0);
105-
} else {
106-
callback = null;
107-
}
108-
try {
109-
return register(user, pass, timeToLive, callback);
110-
} finally {
111-
if (callback != null) {
112-
callback.close();
113-
}
114-
}
115-
} else if (isCalledAs("login")) {
116-
final String token = args[0].getStringValue();
117-
final FunctionReference callback;
118-
if (!args[1].isEmpty()) {
119-
callback = (FunctionReference) args[1].itemAt(0);
120-
} else {
121-
callback = null;
122-
}
123-
try {
124-
return authenticate(token, callback);
125-
} finally {
126-
if (callback != null) {
127-
callback.close();
128-
}
129-
}
130-
} else {
131-
PersistentLogin.getInstance().invalidate(args[0].getStringValue());
132-
return Sequence.EMPTY_SEQUENCE;
122+
switch (Fn.get(this)) {
123+
case REGISTER: return register(args);
124+
case LOGIN: return login(args);
125+
case INVALIDATE: return invalidate(args);
126+
default: throw new XPathException(this, ErrorCodes.ERROR, "Unknown function: " + getName());
133127
}
134128
}
135129

136-
private Sequence register(final String user, final String pass, final DurationValue timeToLive, final FunctionReference callback) throws XPathException {
137-
if (login(user, pass)) {
130+
private Sequence register(Sequence[] args) throws XPathException {
131+
final String user = args[0].getStringValue();
132+
133+
final String pass;
134+
if (args[1].isEmpty()) {
135+
pass = null;
136+
} else {
137+
pass = args[1].getStringValue();
138+
}
139+
140+
final DurationValue timeToLive = (DurationValue) args[2].itemAt(0);
141+
142+
try (FunctionReference callback = getCallBack(args[3])) {
143+
if (!authenticated(user, pass)) {
144+
return Sequence.EMPTY_SEQUENCE;
145+
}
138146
final PersistentLogin.LoginDetails details = PersistentLogin.getInstance().register(user, pass, timeToLive);
139-
return callback(callback, null, details);
147+
return call(callback, null, details);
140148
}
141-
return Sequence.EMPTY_SEQUENCE;
142149
}
143150

144-
private Sequence authenticate(final String token, final FunctionReference callback) throws XPathException {
145-
final PersistentLogin.LoginDetails data = PersistentLogin.getInstance().lookup(token);
151+
private Sequence login(Sequence[] args) throws XPathException {
152+
final String token = args[0].getStringValue();
153+
try (FunctionReference callback = getCallBack(args[1])) {
154+
final PersistentLogin.LoginDetails data = PersistentLogin.getInstance().lookup(token);
146155

147-
if (data == null) {
148-
return Sequence.EMPTY_SEQUENCE;
149-
}
156+
if (data == null || !authenticated(data.getUser(), data.getPassword())) {
157+
return Sequence.EMPTY_SEQUENCE;
158+
}
159+
return call(callback, token, data);
150160

151-
if (login(data.getUser(), data.getPassword())) {
152-
return callback(callback, token, data);
153161
}
162+
}
154163

164+
private static Sequence invalidate(Sequence[] args) throws XPathException {
165+
PersistentLogin.getInstance().invalidate(args[0].getStringValue());
155166
return Sequence.EMPTY_SEQUENCE;
156167
}
157168

158-
private boolean login(final String user, final String pass) throws XPathException {
169+
private boolean authenticated(final String user, final String pass) {
159170
try {
160171
final SecurityManager sm = BrokerPool.getInstance().getSecurityManager();
161172
final Subject subject = sm.authenticate(user, pass);
@@ -169,7 +180,7 @@ private boolean login(final String user, final String pass) throws XPathExceptio
169180
}
170181
}
171182

172-
private Sequence callback(final FunctionReference func, final String oldToken, final PersistentLogin.LoginDetails details) throws XPathException {
183+
private Sequence call(@Nullable final FunctionReference func, final String oldToken, final PersistentLogin.LoginDetails details) throws XPathException {
173184
final Sequence[] args = new Sequence[4];
174185
final String newToken = details.toString();
175186

@@ -185,4 +196,11 @@ private Sequence callback(final FunctionReference func, final String oldToken, f
185196
func.analyze(cachedContextInfo);
186197
return func.evalFunction(null, null, args);
187198
}
199+
200+
private @Nullable FunctionReference getCallBack(final Sequence arg) {
201+
if (arg.isEmpty()) {
202+
return null;
203+
}
204+
return (FunctionReference) arg.itemAt(0);
205+
}
188206
}

0 commit comments

Comments
 (0)