Skip to content

Commit 0a21210

Browse files
Expose realms authentication metrics (#104200)
This PR adds metrics for recording successful and failed authentications for individual realms. Exposed metrics are: - `es.security.authc.realms.success.total` - `es.security.authc.realms.failures.total` - `es.security.authc.realms.time` Each of the metric is exposed at node level and includes additional information with these attributes: - `es.security.realm_type` - can be one of: `jwt`, `saml`, `oidc`, `active_directory`, `ldap`, `pki`, `kerberos`... - `es.security.realm_name` - `es.security.realm_authc_failure_reason`
1 parent be9034b commit 0a21210

File tree

7 files changed

+238
-62
lines changed

7 files changed

+238
-62
lines changed

docs/changelog/104200.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 104200
2+
summary: Expose realms authentication metrics
3+
area: Authentication
4+
type: enhancement
5+
issues: []

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ public AuthenticationService(
114114
new ServiceAccountAuthenticator(serviceAccountService, nodeName, meterRegistry),
115115
new OAuth2TokenAuthenticator(tokenService, meterRegistry),
116116
new ApiKeyAuthenticator(apiKeyService, nodeName, meterRegistry),
117-
new RealmsAuthenticator(numInvalidation, lastSuccessfulAuthCache)
117+
new RealmsAuthenticator(numInvalidation, lastSuccessfulAuthCache, meterRegistry)
118118
);
119119
}
120120

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/RealmsAuthenticator.java

Lines changed: 94 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.elasticsearch.common.util.concurrent.ThreadContext;
1818
import org.elasticsearch.core.Tuple;
1919
import org.elasticsearch.rest.RestStatus;
20+
import org.elasticsearch.telemetry.metric.MeterRegistry;
2021
import org.elasticsearch.xpack.core.common.IteratingActionListener;
2122
import org.elasticsearch.xpack.core.security.authc.Authentication;
2223
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
@@ -25,29 +26,54 @@
2526
import org.elasticsearch.xpack.core.security.authc.Realm;
2627
import org.elasticsearch.xpack.core.security.user.User;
2728
import org.elasticsearch.xpack.security.authc.support.RealmUserLookup;
29+
import org.elasticsearch.xpack.security.metric.InstrumentedSecurityActionListener;
30+
import org.elasticsearch.xpack.security.metric.SecurityMetricType;
31+
import org.elasticsearch.xpack.security.metric.SecurityMetrics;
2832

2933
import java.util.ArrayList;
3034
import java.util.Collections;
35+
import java.util.HashMap;
3136
import java.util.LinkedHashMap;
3237
import java.util.List;
3338
import java.util.Map;
3439
import java.util.Objects;
3540
import java.util.concurrent.atomic.AtomicLong;
3641
import java.util.concurrent.atomic.AtomicReference;
3742
import java.util.function.BiConsumer;
43+
import java.util.function.LongSupplier;
3844

3945
import static org.elasticsearch.core.Strings.format;
4046

4147
public class RealmsAuthenticator implements Authenticator {
4248

49+
public static final String ATTRIBUTE_REALM_NAME = "es.security.realm_name";
50+
public static final String ATTRIBUTE_REALM_TYPE = "es.security.realm_type";
51+
public static final String ATTRIBUTE_REALM_AUTHC_FAILURE_REASON = "es.security.realm_authc_failure_reason";
52+
4353
private static final Logger logger = LogManager.getLogger(RealmsAuthenticator.class);
4454

4555
private final AtomicLong numInvalidation;
4656
private final Cache<String, Realm> lastSuccessfulAuthCache;
57+
private final SecurityMetrics<Realm> authenticationMetrics;
58+
59+
public RealmsAuthenticator(AtomicLong numInvalidation, Cache<String, Realm> lastSuccessfulAuthCache, MeterRegistry meterRegistry) {
60+
this(numInvalidation, lastSuccessfulAuthCache, meterRegistry, System::nanoTime);
61+
}
4762

48-
public RealmsAuthenticator(AtomicLong numInvalidation, Cache<String, Realm> lastSuccessfulAuthCache) {
63+
RealmsAuthenticator(
64+
AtomicLong numInvalidation,
65+
Cache<String, Realm> lastSuccessfulAuthCache,
66+
MeterRegistry meterRegistry,
67+
LongSupplier nanoTimeSupplier
68+
) {
4969
this.numInvalidation = numInvalidation;
5070
this.lastSuccessfulAuthCache = lastSuccessfulAuthCache;
71+
this.authenticationMetrics = new SecurityMetrics<>(
72+
SecurityMetricType.AUTHC_REALMS,
73+
meterRegistry,
74+
this::buildMetricAttributes,
75+
nanoTimeSupplier
76+
);
5177
}
5278

5379
@Override
@@ -141,66 +167,69 @@ private void consumeToken(Context context, ActionListener<AuthenticationResult<A
141167
realm,
142168
authenticationToken.getClass().getName()
143169
);
144-
realm.authenticate(authenticationToken, ActionListener.wrap(result -> {
145-
assert result != null : "Realm " + realm + " produced a null authentication result";
146-
logger.debug(
147-
"Authentication of [{}] using realm [{}] with token [{}] was [{}]",
148-
authenticationToken.principal(),
149-
realm,
150-
authenticationToken.getClass().getSimpleName(),
151-
result
152-
);
153-
if (result.getStatus() == AuthenticationResult.Status.SUCCESS) {
154-
// user was authenticated, populate the authenticated by information
155-
authenticatedByRef.set(realm);
156-
authenticationResultRef.set(result);
157-
if (lastSuccessfulAuthCache != null && startInvalidation == numInvalidation.get()) {
158-
lastSuccessfulAuthCache.put(authenticationToken.principal(), realm);
159-
}
160-
userListener.onResponse(result.getValue());
161-
} else {
162-
// the user was not authenticated, call this so we can audit the correct event
163-
context.getRequest().realmAuthenticationFailed(authenticationToken, realm.name());
164-
if (result.getStatus() == AuthenticationResult.Status.TERMINATE) {
165-
final var resultException = result.getException();
166-
if (resultException != null) {
167-
logger.info(
168-
() -> format(
169-
"Authentication of [%s] was terminated by realm [%s] - %s",
170+
realm.authenticate(
171+
authenticationToken,
172+
InstrumentedSecurityActionListener.wrapForAuthc(authenticationMetrics, realm, ActionListener.wrap(result -> {
173+
assert result != null : "Realm " + realm + " produced a null authentication result";
174+
logger.debug(
175+
"Authentication of [{}] using realm [{}] with token [{}] was [{}]",
176+
authenticationToken.principal(),
177+
realm,
178+
authenticationToken.getClass().getSimpleName(),
179+
result
180+
);
181+
if (result.getStatus() == AuthenticationResult.Status.SUCCESS) {
182+
// user was authenticated, populate the authenticated by information
183+
authenticatedByRef.set(realm);
184+
authenticationResultRef.set(result);
185+
if (lastSuccessfulAuthCache != null && startInvalidation == numInvalidation.get()) {
186+
lastSuccessfulAuthCache.put(authenticationToken.principal(), realm);
187+
}
188+
userListener.onResponse(result.getValue());
189+
} else {
190+
// the user was not authenticated, call this so we can audit the correct event
191+
context.getRequest().realmAuthenticationFailed(authenticationToken, realm.name());
192+
if (result.getStatus() == AuthenticationResult.Status.TERMINATE) {
193+
final var resultException = result.getException();
194+
if (resultException != null) {
195+
logger.info(
196+
() -> format(
197+
"Authentication of [%s] was terminated by realm [%s] - %s",
198+
authenticationToken.principal(),
199+
realm.name(),
200+
result.getMessage()
201+
),
202+
resultException
203+
);
204+
userListener.onFailure(resultException);
205+
} else {
206+
logger.info(
207+
"Authentication of [{}] was terminated by realm [{}] - {}",
170208
authenticationToken.principal(),
171209
realm.name(),
172210
result.getMessage()
173-
),
174-
resultException
175-
);
176-
userListener.onFailure(resultException);
211+
);
212+
userListener.onFailure(AuthenticationTerminatedSuccessfullyException.INSTANCE);
213+
}
177214
} else {
178-
logger.info(
179-
"Authentication of [{}] was terminated by realm [{}] - {}",
180-
authenticationToken.principal(),
181-
realm.name(),
182-
result.getMessage()
183-
);
184-
userListener.onFailure(AuthenticationTerminatedSuccessfullyException.INSTANCE);
185-
}
186-
} else {
187-
if (result.getMessage() != null) {
188-
messages.put(realm, new Tuple<>(result.getMessage(), result.getException()));
215+
if (result.getMessage() != null) {
216+
messages.put(realm, new Tuple<>(result.getMessage(), result.getException()));
217+
}
218+
userListener.onResponse(null);
189219
}
190-
userListener.onResponse(null);
191220
}
192-
}
193-
}, (ex) -> {
194-
logger.warn(
195-
() -> format(
196-
"An error occurred while attempting to authenticate [%s] against realm [%s]",
197-
authenticationToken.principal(),
198-
realm.name()
199-
),
200-
ex
201-
);
202-
userListener.onFailure(ex);
203-
}));
221+
}, (ex) -> {
222+
logger.warn(
223+
() -> format(
224+
"An error occurred while attempting to authenticate [%s] against realm [%s]",
225+
authenticationToken.principal(),
226+
realm.name()
227+
),
228+
ex
229+
);
230+
userListener.onFailure(ex);
231+
}))
232+
);
204233
} else {
205234
userListener.onResponse(null);
206235
}
@@ -362,4 +391,14 @@ public synchronized Throwable fillInStackTrace() {
362391
return this;
363392
}
364393
}
394+
395+
private Map<String, Object> buildMetricAttributes(Realm realm, String failureReason) {
396+
final Map<String, Object> attributes = new HashMap<>(failureReason != null ? 3 : 2);
397+
attributes.put(ATTRIBUTE_REALM_NAME, realm.name());
398+
attributes.put(ATTRIBUTE_REALM_TYPE, realm.type());
399+
if (failureReason != null) {
400+
attributes.put(ATTRIBUTE_REALM_AUTHC_FAILURE_REASON, failureReason);
401+
}
402+
return attributes;
403+
}
365404
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/metric/SecurityMetricType.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ public enum SecurityMetricType {
4949
)
5050
),
5151

52+
AUTHC_REALMS(
53+
SecurityMetricGroup.AUTHC,
54+
new SecurityMetricInfo("es.security.authc.realms.success.total", "Number of successful realm authentications.", "count"),
55+
new SecurityMetricInfo("es.security.authc.realms.failures.total", "Number of failed realm authentications.", "count"),
56+
new SecurityMetricInfo("es.security.authc.realms.time", "Time it took (in nanoseconds) to execute realm authentication.", "ns")
57+
),
58+
5259
;
5360

5461
private final SecurityMetricGroup group;

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -575,14 +575,17 @@ public void testAuthenticateSmartRealmOrdering() {
575575
}, this::logAndFail));
576576

577577
verify(auditTrail).authenticationFailed(reqId.get(), firstRealm.name(), token, "_action", transportRequest);
578-
verify(firstRealm, times(2)).name(); // used above one time
578+
verify(firstRealm, times(4)).name(); // used above one time plus two times for authc result and time metrics
579+
verify(firstRealm, times(2)).type(); // used two times to collect authc result and time metrics
579580
verify(secondRealm, times(2)).realmRef(); // also used in license tracking
580581
verify(firstRealm, times(2)).token(threadContext);
581582
verify(secondRealm, times(2)).token(threadContext);
582583
verify(firstRealm).supports(token);
583584
verify(secondRealm, times(2)).supports(token);
584585
verify(firstRealm).authenticate(eq(token), anyActionListener());
585586
verify(secondRealm, times(2)).authenticate(eq(token), anyActionListener());
587+
verify(secondRealm, times(4)).name(); // called two times for every authenticate call to collect authc result and time metrics
588+
verify(secondRealm, times(4)).type(); // called two times for every authenticate call to collect authc result and time metrics
586589
verifyNoMoreInteractions(auditTrail, firstRealm, secondRealm);
587590

588591
// Now assume some change in the backend system so that 2nd realm no longer has the user, but the 1st realm does.
@@ -711,14 +714,17 @@ public void testAuthenticateSmartRealmOrderingDisabled() {
711714
verify(operatorPrivilegesService).maybeMarkOperatorUser(eq(result), eq(threadContext));
712715
}, this::logAndFail));
713716
verify(auditTrail, times(2)).authenticationFailed(reqId.get(), firstRealm.name(), token, "_action", transportRequest);
714-
verify(firstRealm, times(3)).name(); // used above one time
717+
verify(firstRealm, times(7)).name(); // used above one time plus two times for every call to collect success and time metrics
718+
verify(firstRealm, times(4)).type(); // used two times for every call to collect authc result and time metrics
715719
verify(secondRealm, times(2)).realmRef();
716720
verify(firstRealm, times(2)).token(threadContext);
717721
verify(secondRealm, times(2)).token(threadContext);
718722
verify(firstRealm, times(2)).supports(token);
719723
verify(secondRealm, times(2)).supports(token);
720724
verify(firstRealm, times(2)).authenticate(eq(token), anyActionListener());
721725
verify(secondRealm, times(2)).authenticate(eq(token), anyActionListener());
726+
verify(secondRealm, times(4)).name(); // called two times for every authenticate call to collect authc result and time metrics
727+
verify(secondRealm, times(4)).type(); // called two times for every authenticate call to collect authc result and time metrics
722728
verifyNoMoreInteractions(auditTrail, firstRealm, secondRealm);
723729
}
724730

0 commit comments

Comments
 (0)