-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathVulnAuthenticationService.java
545 lines (480 loc) · 26.8 KB
/
VulnAuthenticationService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.security.authc;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.logging.log4j.util.Supplier;
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Setting.Property;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.node.Node;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportMessage;
import org.elasticsearch.xpack.common.IteratingActionListener;
import org.elasticsearch.xpack.security.audit.AuditTrail;
import org.elasticsearch.xpack.security.audit.AuditTrailService;
import org.elasticsearch.xpack.security.authc.Authentication.RealmRef;
import org.elasticsearch.xpack.security.crypto.CryptoService;
import org.elasticsearch.xpack.security.user.AnonymousUser;
import org.elasticsearch.xpack.security.user.User;
import java.io.IOException;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import static org.elasticsearch.xpack.security.Security.setting;
/**
* An authentication service that delegates the authentication process to its configured {@link Realm realms}.
* This service also supports request level caching of authenticated users (i.e. once a user authenticated
* successfully, it is set on the request context to avoid subsequent redundant authentication process)
*/
public class AuthenticationService extends AbstractComponent {
public static final Setting<Boolean> SIGN_USER_HEADER =
Setting.boolSetting(setting("authc.sign_user_header"), true, Property.NodeScope);
public static final Setting<Boolean> RUN_AS_ENABLED =
Setting.boolSetting(setting("authc.run_as.enabled"), true, Property.NodeScope);
public static final String RUN_AS_USER_HEADER = "es-security-runas-user";
private final Realms realms;
private final AuditTrail auditTrail;
private final CryptoService cryptoService;
private final AuthenticationFailureHandler failureHandler;
private final ThreadContext threadContext;
private final String nodeName;
private final AnonymousUser anonymousUser;
private final boolean signUserHeader;
private final boolean runAsEnabled;
private final boolean isAnonymousUserEnabled;
public AuthenticationService(Settings settings, Realms realms, AuditTrailService auditTrail, CryptoService cryptoService,
AuthenticationFailureHandler failureHandler, ThreadPool threadPool, AnonymousUser anonymousUser) {
super(settings);
this.nodeName = Node.NODE_NAME_SETTING.get(settings);
this.realms = realms;
this.auditTrail = auditTrail;
this.cryptoService = cryptoService;
this.failureHandler = failureHandler;
this.threadContext = threadPool.getThreadContext();
this.anonymousUser = anonymousUser;
this.signUserHeader = SIGN_USER_HEADER.get(settings);
this.runAsEnabled = RUN_AS_ENABLED.get(settings);
this.isAnonymousUserEnabled = AnonymousUser.isAnonymousEnabled(settings);
}
/**
* Authenticates the user that is associated with the given request. If the user was authenticated successfully (i.e.
* a user was indeed associated with the request and the credentials were verified to be valid), the method returns
* the user and that user is then "attached" to the request's context.
*
* @param request The request to be authenticated
*/
public void authenticate(RestRequest request, ActionListener<Authentication> authenticationListener) {
createAuthenticator(request, authenticationListener).authenticateAsync();
}
/**
* Authenticates the user that is associated with the given message. If the user was authenticated successfully (i.e.
* a user was indeed associated with the request and the credentials were verified to be valid), the method returns
* the user and that user is then "attached" to the message's context. If no user was found to be attached to the given
* message, the the given fallback user will be returned instead.
*
* @param action The action of the message
* @param message The message to be authenticated
* @param fallbackUser The default user that will be assumed if no other user is attached to the message. Can be
* {@code null}, in which case there will be no fallback user and the success/failure of the
* authentication will be based on the whether there's an attached user to in the message and
* if there is, whether its credentials are valid.
*/
public void authenticate(String action, TransportMessage message, User fallbackUser, ActionListener<Authentication> listener) {
createAuthenticator(action, message, fallbackUser, listener).authenticateAsync();
}
/**
* Checks if there's already a user header attached to the given message. If missing, a new header is
* set on the message with the given user (encoded).
*
* @param user The user to be attached if the header is missing
*/
void attachUserIfMissing(User user) throws IOException {
Authentication authentication = new Authentication(user, new RealmRef("__attach", "__attach", nodeName), null);
authentication.writeToContextIfMissing(threadContext, cryptoService, signUserHeader);
}
// pkg private method for testing
Authenticator createAuthenticator(RestRequest request, ActionListener<Authentication> listener) {
return new Authenticator(request, listener);
}
// pkg private method for testing
Authenticator createAuthenticator(String action, TransportMessage message, User fallbackUser, ActionListener<Authentication> listener) {
return new Authenticator(action, message, fallbackUser, listener);
}
/**
* This class is responsible for taking a request and executing the authentication. The authentication is executed in an asynchronous
* fashion in order to avoid blocking calls on a network thread. This class also performs the auditing necessary around authentication
*/
class Authenticator {
private final AuditableRequest request;
private final User fallbackUser;
private final ActionListener<Authentication> listener;
private RealmRef authenticatedBy = null;
private RealmRef lookedupBy = null;
private AuthenticationToken authenticationToken = null;
Authenticator(RestRequest request, ActionListener<Authentication> listener) {
this(new AuditableRestRequest(auditTrail, failureHandler, threadContext, request), null, listener);
}
Authenticator(String action, TransportMessage message, User fallbackUser, ActionListener<Authentication> listener) {
this(new AuditableTransportRequest(auditTrail, failureHandler, threadContext, action, message), fallbackUser, listener);
}
private Authenticator(AuditableRequest auditableRequest, User fallbackUser, ActionListener<Authentication> listener) {
this.request = auditableRequest;
this.fallbackUser = fallbackUser;
this.listener = listener;
}
/**
* This method starts the authentication process. The authentication process can be broken down into distinct operations. In order,
* these operations are:
*
* <ol>
* <li>look for existing authentication {@link #lookForExistingAuthentication(Consumer)}</li>
* <li>token extraction {@link #extractToken(Consumer)}</li>
* <li>token authentication {@link #consumeToken(AuthenticationToken)}</li>
* <li>user lookup for run as if necessary {@link #consumeUser(User)} and
* {@link #lookupRunAsUser(User, String, Consumer)}</li>
* <li>write authentication into the context {@link #finishAuthentication(User)}</li>
* </ol>
*/
private void authenticateAsync() {
lookForExistingAuthentication((authentication) -> {
if (authentication != null) {
listener.onResponse(authentication);
} else {
extractToken(this::consumeToken);
}
});
}
/**
* Looks to see if the request contains an existing {@link Authentication} and if so, that authentication will be used. The
* consumer is called if no exception was thrown while trying to read the authentication and may be called with a {@code null}
* value
*/
private void lookForExistingAuthentication(Consumer<Authentication> authenticationConsumer) {
Runnable action;
try {
final Authentication authentication = Authentication.readFromContext(threadContext, cryptoService, signUserHeader);
if (authentication != null && request instanceof AuditableRestRequest) {
action = () -> listener.onFailure(request.tamperedRequest());
} else {
action = () -> authenticationConsumer.accept(authentication);
}
} catch (Exception e) {
logger.error((Supplier<?>)
() -> new ParameterizedMessage("caught exception while trying to read authentication from request [{}]", request),
e);
action = () -> listener.onFailure(request.tamperedRequest());
}
// we use the success boolean as we need to know if the executed code block threw an exception and we already called on
// failure; if we did call the listener we do not need to continue. While we could place this call in the try block, the
// issue is that we catch all exceptions and could catch exceptions that have nothing to do with a tampered request.
action.run();
}
/**
* Attempts to extract an {@link AuthenticationToken} from the request by iterating over the {@link Realms} and calling
* {@link Realm#token(ThreadContext)}. The first non-null token that is returned will be used. The consumer is only called if
* no exception was caught during the extraction process and may be called with a {@code null} token.
*/
// pkg-private accessor testing token extraction with a consumer
void extractToken(Consumer<AuthenticationToken> consumer) {
Runnable action = () -> consumer.accept(null);
try {
for (Realm realm : realms) {
final AuthenticationToken token = realm.token(threadContext);
if (token != null) {
action = () -> consumer.accept(token);
break;
}
}
} catch (Exception e) {
action = () -> listener.onFailure(request.exceptionProcessingRequest(e, null));
}
action.run();
}
/**
* Consumes the {@link AuthenticationToken} provided by the caller. In the case of a {@code null} token, {@link #handleNullToken()}
* is called. In the case of a {@code non-null} token, the realms are iterated over and the first realm that returns a non-null
* {@link User} is the authenticating realm and iteration is stopped. This user is then passed to {@link #consumeUser(User)} if no
* exception was caught while trying to authenticate the token
*/
private void consumeToken(AuthenticationToken token) {
if (token == null) {
handleNullToken();
} else {
authenticationToken = token;
final List<Realm> realmsList = realms.asList();
final BiConsumer<Realm, ActionListener<User>> realmAuthenticatingConsumer = (realm, userListener) -> {
if (realm.supports(authenticationToken)) {
realm.authenticate(authenticationToken, ActionListener.wrap((user) -> {
if (user == null) {
// the user was not authenticated, call this so we can audit the correct event
request.realmAuthenticationFailed(authenticationToken, realm.name());
} else {
// user was authenticated, populate the authenticated by information
authenticatedBy = new RealmRef(realm.name(), realm.type(), nodeName);
}
userListener.onResponse(user);
}, userListener::onFailure));
} else {
userListener.onResponse(null);
}
};
final IteratingActionListener<User, Realm> authenticatingListener =
new IteratingActionListener<>(ActionListener.wrap(this::consumeUser,
(e) -> listener.onFailure(request.exceptionProcessingRequest(e, token))),
realmAuthenticatingConsumer, realmsList);
try {
authenticatingListener.run();
} catch (Exception e) {
listener.onFailure(request.exceptionProcessingRequest(e, token));
}
}
}
/**
* Handles failed extraction of an authentication token. This can happen in a few different scenarios:
*
* <ul>
* <li>this is an initial request from a client without preemptive authentication, so we must return an authentication
* challenge</li>
* <li>this is a request made internally within a node and there is a fallback user, which is typically the
* {@link org.elasticsearch.xpack.security.user.SystemUser}</li>
* <li>anonymous access is enabled and this will be considered an anonymous request</li>
* </ul>
*
* Regardless of the scenario, this method will call the listener with either failure or success.
*/
// pkg-private for tests
void handleNullToken() {
final Authentication authentication;
if (fallbackUser != null) {
RealmRef authenticatedBy = new RealmRef("__fallback", "__fallback", nodeName);
authentication = new Authentication(fallbackUser, authenticatedBy, null);
} else if (isAnonymousUserEnabled) {
RealmRef authenticatedBy = new RealmRef("__anonymous", "__anonymous", nodeName);
authentication = new Authentication(anonymousUser, authenticatedBy, null);
} else {
authentication = null;
}
Runnable action;
if (authentication != null) {
try {
authentication.writeToContext(threadContext, cryptoService, signUserHeader);
request.authenticationSuccess(authentication.getAuthenticatedBy().getName(), authentication.getUser());
action = () -> listener.onResponse(authentication);
} catch (Exception e) {
action = () -> listener.onFailure(request.exceptionProcessingRequest(e, authenticationToken));
}
} else {
action = () -> listener.onFailure(request.anonymousAccessDenied());
}
// we assign the listener call to an action to avoid calling the listener within a try block and auditing the wrong thing when
// an exception bubbles up even after successful authentication
action.run();
}
/**
* Consumes the {@link User} that resulted from attempting to authenticate a token against the {@link Realms}. When the user is
* {@code null}, authentication fails and does not proceed. When there is a user, the request is inspected to see if the run as
* functionality is in use. When run as is not in use, {@link #finishAuthentication(User)} is called, otherwise we try to lookup
* the run as user in {@link #lookupRunAsUser(User, String, Consumer)}
*/
private void consumeUser(User user) {
if (user == null) {
listener.onFailure(request.authenticationFailed(authenticationToken));
} else {
if (runAsEnabled) {
final String runAsUsername = threadContext.getHeader(RUN_AS_USER_HEADER);
if (runAsUsername != null && runAsUsername.isEmpty() == false) {
lookupRunAsUser(user, runAsUsername, this::finishAuthentication);
} else if (runAsUsername == null) {
finishAuthentication(user);
} else {
assert runAsUsername.isEmpty() : "the run as username may not be empty";
logger.debug("user [{}] attempted to runAs with an empty username", user.principal());
listener.onFailure(request.runAsDenied(new User(user.principal(), user.roles(),
new User(runAsUsername, Strings.EMPTY_ARRAY)), authenticationToken));
}
} else {
finishAuthentication(user);
}
}
}
/**
* Iterates over the realms and attempts to lookup the run as user by the given username. The consumer will be called regardless of
* if the user is found or not, with a non-null user. We do not fail requests if the run as user is not found as that can leak the
* names of users that exist using a timing attack
*/
private void lookupRunAsUser(final User user, String runAsUsername, Consumer<User> userConsumer) {
final List<Realm> realmsList = realms.asList();
final BiConsumer<Realm, ActionListener<User>> realmLookupConsumer = (realm, lookupUserListener) ->
realm.lookupUser(runAsUsername, ActionListener.wrap((lookedupUser) -> {
if (lookedupUser != null) {
lookedupBy = new RealmRef(realm.name(), realm.type(), nodeName);
lookupUserListener.onResponse(lookedupUser);
} else {
lookupUserListener.onResponse(null);
}
}, lookupUserListener::onFailure));
final IteratingActionListener<User, Realm> userLookupListener =
new IteratingActionListener<>(ActionListener.wrap((lookupUser) -> userConsumer.accept(new User(user, lookupUser)),
(e) -> listener.onFailure(request.exceptionProcessingRequest(e, authenticationToken))),
realmLookupConsumer, realmsList);
try {
userLookupListener.run();
} catch (Exception e) {
listener.onFailure(request.exceptionProcessingRequest(e, authenticationToken));
}
}
/**
* Finishes the authentication process by ensuring the returned user is enabled and that the run as user is enabled if there is
* one. If authentication is successful, this method also ensures that the authentication is written to the ThreadContext
*/
void finishAuthentication(User finalUser) {
if (finalUser.enabled() == false || (finalUser.runAs() != null && finalUser.runAs().enabled() == false)) {
logger.debug("user [{}] is disabled. failing authentication", finalUser);
listener.onFailure(request.authenticationFailed(authenticationToken));
} else {
request.authenticationSuccess(authenticatedBy.getName(), finalUser);
final Authentication finalAuth = new Authentication(finalUser, authenticatedBy, lookedupBy);
Runnable action = () -> listener.onResponse(finalAuth);
try {
finalAuth.writeToContext(threadContext, cryptoService, signUserHeader);
} catch (Exception e) {
action = () -> listener.onFailure(request.exceptionProcessingRequest(e, authenticationToken));
}
// we assign the listener call to an action to avoid calling the listener within a try block and auditing the wrong thing
// when an exception bubbles up even after successful authentication
action.run();
}
}
}
abstract static class AuditableRequest {
final AuditTrail auditTrail;
final AuthenticationFailureHandler failureHandler;
final ThreadContext threadContext;
AuditableRequest(AuditTrail auditTrail, AuthenticationFailureHandler failureHandler, ThreadContext threadContext) {
this.auditTrail = auditTrail;
this.failureHandler = failureHandler;
this.threadContext = threadContext;
}
abstract void realmAuthenticationFailed(AuthenticationToken token, String realm);
abstract ElasticsearchSecurityException tamperedRequest();
abstract ElasticsearchSecurityException exceptionProcessingRequest(Exception e, @Nullable AuthenticationToken token);
abstract ElasticsearchSecurityException authenticationFailed(AuthenticationToken token);
abstract ElasticsearchSecurityException anonymousAccessDenied();
abstract ElasticsearchSecurityException runAsDenied(User user, AuthenticationToken token);
abstract void authenticationSuccess(String realm, User user);
}
static class AuditableTransportRequest extends AuditableRequest {
private final String action;
private final TransportMessage message;
AuditableTransportRequest(AuditTrail auditTrail, AuthenticationFailureHandler failureHandler, ThreadContext threadContext,
String action, TransportMessage message) {
super(auditTrail, failureHandler, threadContext);
this.action = action;
this.message = message;
}
@Override
void authenticationSuccess(String realm, User user) {
auditTrail.authenticationSuccess(realm, user, action, message);
}
@Override
void realmAuthenticationFailed(AuthenticationToken token, String realm) {
auditTrail.authenticationFailed(realm, token, action, message);
}
@Override
ElasticsearchSecurityException tamperedRequest() {
auditTrail.tamperedRequest(action, message);
return new ElasticsearchSecurityException("failed to verify signed authentication information");
}
@Override
ElasticsearchSecurityException exceptionProcessingRequest(Exception e, @Nullable AuthenticationToken token) {
if (token != null) {
auditTrail.authenticationFailed(token, action, message);
} else {
auditTrail.authenticationFailed(action, message);
}
return failureHandler.exceptionProcessingRequest(message, action, e, threadContext);
}
@Override
ElasticsearchSecurityException authenticationFailed(AuthenticationToken token) {
auditTrail.authenticationFailed(token, action, message);
return failureHandler.failedAuthentication(message, token, action, threadContext);
}
@Override
ElasticsearchSecurityException anonymousAccessDenied() {
auditTrail.anonymousAccessDenied(action, message);
return failureHandler.missingToken(message, action, threadContext);
}
@Override
ElasticsearchSecurityException runAsDenied(User user, AuthenticationToken token) {
auditTrail.runAsDenied(user, action, message);
return failureHandler.failedAuthentication(message, token, action, threadContext);
}
@Override
public String toString() {
return "transport request action [" + action + "]";
}
}
static class AuditableRestRequest extends AuditableRequest {
private final RestRequest request;
AuditableRestRequest(AuditTrail auditTrail, AuthenticationFailureHandler failureHandler, ThreadContext threadContext,
RestRequest request) {
super(auditTrail, failureHandler, threadContext);
this.request = request;
}
@Override
void authenticationSuccess(String realm, User user) {
auditTrail.authenticationSuccess(realm, user, request);
}
@Override
void realmAuthenticationFailed(AuthenticationToken token, String realm) {
auditTrail.authenticationFailed(realm, token, request);
}
@Override
ElasticsearchSecurityException tamperedRequest() {
auditTrail.tamperedRequest(request);
return new ElasticsearchSecurityException("rest request attempted to inject a user");
}
@Override
ElasticsearchSecurityException exceptionProcessingRequest(Exception e, @Nullable AuthenticationToken token) {
if (token != null) {
auditTrail.authenticationFailed(token, request);
} else {
auditTrail.authenticationFailed(request);
}
return failureHandler.exceptionProcessingRequest(request, e, threadContext);
}
@Override
ElasticsearchSecurityException authenticationFailed(AuthenticationToken token) {
auditTrail.authenticationFailed(token, request);
return failureHandler.failedAuthentication(request, token, threadContext);
}
@Override
ElasticsearchSecurityException anonymousAccessDenied() {
auditTrail.anonymousAccessDenied(request);
return failureHandler.missingToken(request, threadContext);
}
@Override
ElasticsearchSecurityException runAsDenied(User user, AuthenticationToken token) {
auditTrail.runAsDenied(user, request);
return failureHandler.failedAuthentication(request, token, threadContext);
}
@Override
public String toString() {
return "rest request uri [" + request.uri() + "]";
}
}
public static void addSettings(List<Setting<?>> settings) {
settings.add(SIGN_USER_HEADER);
settings.add(RUN_AS_ENABLED);
}
}