Skip to content

Commit ad38cb7

Browse files
committed
[refactor] PersistentLogin for readability and maintainability
- add Enum for all functions exposed by this module - use Enum in PersistentLoginFunctions.eval - add com.google.code.findbugs to maven dependencies for this extension - invert function authenticated => unauthenticated - make parameters and variables final where possible - add @nullable annotations
1 parent 024276e commit ad38cb7

File tree

2 files changed

+136
-95
lines changed

2 files changed

+136
-95
lines changed

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

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,16 @@
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>,
3738
* which is based on <a href="http://jaspan.com/improved_persistent_login_cookie_best_practice">Improved Persistent Login Cookie
3839
* Best Practice</a> .
39-
*
40+
* <p>
4041
* The one-time tokens generated by this class are purely random and do not contain a user name or other information. For security reasons,
4142
* tokens and user information are not stored anywhere, so if the database is shut down, registered tokens will be gone.
42-
*
43+
* <p>
4344
* The one-time token approach has the negative effect that requests need to be made in sequence, which is sometimes difficult if an app uses
4445
* concurrent AJAX requests. Unfortunately, this is the price we have to pay for a sufficiently secure protection against
4546
* cookie stealing attacks.
@@ -60,11 +61,11 @@ 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 = 10000;
6465

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

67-
private SecureRandom random;
68+
private final SecureRandom random;
6869

6970
public PersistentLogin() {
7071
random = new SecureRandom();
@@ -73,10 +74,10 @@ public PersistentLogin() {
7374
/**
7475
* Register the user and generate a first login token which will be valid for the next
7576
* call to {@link #lookup(String)}.
76-
*
77+
* <p>
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);
@@ -213,23 +217,21 @@ public boolean checkAndUpdateToken(String token) {
213217
return true;
214218
}
215219

216-
public String update() {
217-
timeoutCheck();
218-
// leave a small time window until previous token is deleted
220+
public void update() {
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();
223225
}
224226

225227
private void timeoutCheck() {
226-
long now = System.currentTimeMillis();
228+
final long now = System.currentTimeMillis();
227229
invalidatedTokens.entrySet().removeIf(entry -> entry.getValue() < now);
228230
}
229231

230232
@Override
231233
public String toString() {
232-
return this.series + ":" + this.token;
234+
return series + ":" + token;
233235
}
234236
}
235237
}

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

Lines changed: 106 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,36 @@
2727
import org.exist.security.SecurityManager;
2828
import org.exist.security.Subject;
2929
import org.exist.storage.BrokerPool;
30-
import org.exist.xquery.*;
31-
import org.exist.xquery.value.*;
30+
import org.exist.xquery.AnalyzeContextInfo;
31+
import org.exist.xquery.Cardinality;
32+
import org.exist.xquery.ErrorCodes;
33+
import org.exist.xquery.Function;
34+
import org.exist.xquery.FunctionSignature;
35+
import org.exist.xquery.UserSwitchingBasicFunction;
36+
import org.exist.xquery.XPathException;
37+
import org.exist.xquery.XQueryContext;
38+
import org.exist.xquery.value.DurationValue;
39+
import org.exist.xquery.value.FunctionParameterSequenceType;
40+
import org.exist.xquery.value.FunctionReference;
41+
import org.exist.xquery.value.FunctionReturnSequenceType;
42+
import org.exist.xquery.value.Sequence;
43+
import org.exist.xquery.value.SequenceType;
44+
import org.exist.xquery.value.StringValue;
45+
import org.exist.xquery.value.Type;
46+
47+
import javax.annotation.Nullable;
48+
49+
import java.util.EnumSet;
50+
import java.util.HashMap;
51+
import java.util.Map;
3252

3353
/**
3454
* Functions to access the persistent login module.
3555
*/
3656
public class PersistentLoginFunctions extends UserSwitchingBasicFunction {
37-
38-
public final static FunctionSignature signatures[] = {
57+
public final static FunctionSignature[] signatures = {
3958
new FunctionSignature(
40-
new QName("register", PersistentLoginModule.NAMESPACE, PersistentLoginModule.PREFIX),
59+
PersistentLoginFn.REGISTER.getQName(),
4160
"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 " +
4261
"(via the login function) as the same user without " +
4362
"providing credentials. However, for security reasons the token will be valid only for " +
@@ -55,7 +74,7 @@ public class PersistentLoginFunctions extends UserSwitchingBasicFunction {
5574
new FunctionReturnSequenceType(Type.ITEM, Cardinality.ZERO_OR_MORE, "result of the callback function or the empty sequence")
5675
),
5776
new FunctionSignature(
58-
new QName("login", PersistentLoginModule.NAMESPACE, PersistentLoginModule.PREFIX),
77+
PersistentLoginFn.LOGIN.getQName(),
5978
"Try to log in the user based on the supplied token. If the login succeeds, the provided callback function " +
6079
"is called with 4 arguments: $token as xs:string, $user as xs:string, $password as xs:string, $timeToLive as duration. " +
6180
"$token will be a new token which can be used for the next request. The old token is deleted.",
@@ -67,109 +86,94 @@ public class PersistentLoginFunctions extends UserSwitchingBasicFunction {
6786
new FunctionReturnSequenceType(Type.ITEM, Cardinality.ZERO_OR_MORE, "result of the callback function or the empty sequence")
6887
),
6988
new FunctionSignature(
70-
new QName("invalidate", PersistentLoginModule.NAMESPACE, PersistentLoginModule.PREFIX),
89+
PersistentLoginFn.INVALIDATE.getQName(),
7190
"Invalidate the supplied one-time token, so it can no longer be used to log in.",
7291
new SequenceType[]{
7392
new FunctionParameterSequenceType("token", Type.STRING, Cardinality.EXACTLY_ONE, "a valid one-time token")
7493
},
7594
new FunctionReturnSequenceType(Type.EMPTY, Cardinality.EXACTLY_ONE, "empty sequence")
7695
)
7796
};
78-
7997
private AnalyzeContextInfo cachedContextInfo;
8098

8199
public PersistentLoginFunctions(final XQueryContext context, final FunctionSignature signature) {
82100
super(context, signature);
83101
}
84102

103+
private static Sequence invalidate(Sequence[] args) throws XPathException {
104+
PersistentLogin.getInstance().invalidate(args[0].getStringValue());
105+
return Sequence.EMPTY_SEQUENCE;
106+
}
107+
85108
@Override
86109
public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException {
87110
super.analyze(contextInfo);
88-
this.cachedContextInfo = new AnalyzeContextInfo(contextInfo);
111+
cachedContextInfo = new AnalyzeContextInfo(contextInfo);
89112
}
90113

91114
@Override
92115
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;
116+
switch (PersistentLoginFn.get(this)) {
117+
case REGISTER:
118+
return register(args);
119+
case LOGIN:
120+
return login(args);
121+
case INVALIDATE:
122+
return invalidate(args);
123+
default:
124+
throw new XPathException(this, ErrorCodes.ERROR, "Unknown function: " + getName());
133125
}
134126
}
135127

136-
private Sequence register(final String user, final String pass, final DurationValue timeToLive, final FunctionReference callback) throws XPathException {
137-
if (login(user, pass)) {
138-
final PersistentLogin.LoginDetails details = PersistentLogin.getInstance().register(user, pass, timeToLive);
139-
return callback(callback, null, details);
128+
private Sequence register(Sequence[] args) throws XPathException {
129+
final String user = args[0].getStringValue();
130+
131+
final String pass;
132+
if (args[1].isEmpty()) {
133+
pass = null;
134+
} else {
135+
pass = args[1].getStringValue();
140136
}
141-
return Sequence.EMPTY_SEQUENCE;
142-
}
143137

144-
private Sequence authenticate(final String token, final FunctionReference callback) throws XPathException {
145-
final PersistentLogin.LoginDetails data = PersistentLogin.getInstance().lookup(token);
138+
final DurationValue timeToLive = (DurationValue) args[2].itemAt(0);
146139

147-
if (data == null) {
148-
return Sequence.EMPTY_SEQUENCE;
140+
try (FunctionReference callback = getCallBack(args[3])) {
141+
if (unauthenticated(user, pass)) {
142+
return Sequence.EMPTY_SEQUENCE;
143+
}
144+
final PersistentLogin.LoginDetails details = PersistentLogin.getInstance().register(user, pass, timeToLive);
145+
return call(callback, null, details);
149146
}
147+
}
150148

151-
if (login(data.getUser(), data.getPassword())) {
152-
return callback(callback, token, data);
153-
}
149+
private Sequence login(Sequence[] args) throws XPathException {
150+
final String token = args[0].getStringValue();
151+
try (FunctionReference callback = getCallBack(args[1])) {
152+
final PersistentLogin.LoginDetails data = PersistentLogin.getInstance().lookup(token);
154153

155-
return Sequence.EMPTY_SEQUENCE;
154+
if (data == null || unauthenticated(data.getUser(), data.getPassword())) {
155+
return Sequence.EMPTY_SEQUENCE;
156+
}
157+
return call(callback, token, data);
158+
}
156159
}
157160

158-
private boolean login(final String user, final String pass) throws XPathException {
161+
private boolean unauthenticated(final String user, final String pass) {
159162
try {
160163
final SecurityManager sm = BrokerPool.getInstance().getSecurityManager();
161164
final Subject subject = sm.authenticate(user, pass);
162165

163166
//switch the user of the current broker
164167
switchUser(subject);
165168

166-
return true;
167-
} catch (final AuthenticationException | EXistException e) {
168169
return false;
170+
} catch (final AuthenticationException | EXistException e) {
171+
return true;
169172
}
170173
}
171174

172-
private Sequence callback(final FunctionReference func, final String oldToken, final PersistentLogin.LoginDetails details) throws XPathException {
175+
private Sequence call(@Nullable final FunctionReference func, final String oldToken, final PersistentLogin.LoginDetails details) throws XPathException {
176+
if (func == null) return Sequence.EMPTY_SEQUENCE;
173177
final Sequence[] args = new Sequence[4];
174178
final String newToken = details.toString();
175179

@@ -185,4 +189,39 @@ private Sequence callback(final FunctionReference func, final String oldToken, f
185189
func.analyze(cachedContextInfo);
186190
return func.evalFunction(null, null, args);
187191
}
192+
193+
private @Nullable FunctionReference getCallBack(final Sequence arg) {
194+
if (arg.isEmpty()) {
195+
return null;
196+
}
197+
return (FunctionReference) arg.itemAt(0);
198+
}
199+
200+
private enum PersistentLoginFn {
201+
REGISTER("register"),
202+
LOGIN("login"),
203+
INVALIDATE("invalidate");
204+
205+
final static Map<QName, PersistentLoginFn> lookup = new HashMap<>();
206+
207+
static {
208+
for (PersistentLoginFn persistentLoginFn : EnumSet.allOf(PersistentLoginFn.class)) {
209+
lookup.put(persistentLoginFn.getQName(), persistentLoginFn);
210+
}
211+
}
212+
213+
private final QName qname;
214+
215+
PersistentLoginFn(String name) {
216+
qname = new QName(name, PersistentLoginModule.NAMESPACE, PersistentLoginModule.PREFIX);
217+
}
218+
219+
static PersistentLoginFn get(Function f) {
220+
return lookup.get(f.getName());
221+
}
222+
223+
public QName getQName() {
224+
return qname;
225+
}
226+
}
188227
}

0 commit comments

Comments
 (0)