Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
*/
@ApplicationScoped
@Named(MetricService.METRIC_SERVICE_COMPONENT_NAME)
public class MetricService extends io.jans.service.metric.MetricService {

Check failure on line 42 in jans-fido2/server/src/main/java/io/jans/fido2/service/shared/MetricService.java

View check run for this annotation

SonarQubeCloud / [Fido2 API] SonarCloud Code Analysis

Rename this class.

See more on https://sonarcloud.io/project/issues?id=JanssenProject_jans-fido2&issues=AZ8jTtXqeNwf2O9ZwUn6&open=AZ8jTtXqeNwf2O9ZwUn6&pullRequest=14481

public static final String METRIC_SERVICE_COMPONENT_NAME = "metricService";

Expand Down Expand Up @@ -214,16 +214,21 @@
/**
* Common method to record registration metrics
*/
private void recordRegistrationMetrics(String username, HttpServletRequest request, long startTime,
private void recordRegistrationMetrics(String username, HttpServletRequest request, long startTime,
String authenticatorType, String status, String errorReason, Fido2MetricType metricType) {
if (!isFido2MetricsEnabled()) {
return;
}

// Capture request-derived data (IP, User-Agent, device info) on the request thread.
// The injected @Context HttpServletRequest is a thread-bound proxy and cannot be read
// from the async task below, so it must be resolved synchronously first.
RequestData requestData = extractRequestData(request);

CompletableFuture.runAsync(() -> {
try {
recordBasicMetrics(metricType, startTime, status, Fido2MetricType.FIDO2_REGISTRATION_DURATION);
recordDetailedMetrics(username, status, request, startTime, authenticatorType, errorReason,
recordDetailedMetrics(username, status, requestData, startTime, authenticatorType, errorReason,
this::createRegistrationMetricsData);
} catch (Exception e) {
log.warn("Failed to record passkey registration {} metrics: {}", status.toLowerCase(), e.getMessage());
Expand Down Expand Up @@ -272,16 +277,21 @@
/**
* Common method to record authentication metrics
*/
private void recordAuthenticationMetrics(String username, HttpServletRequest request, long startTime,
private void recordAuthenticationMetrics(String username, HttpServletRequest request, long startTime,
String authenticatorType, String status, String errorReason, Fido2MetricType metricType) {
if (!isFido2MetricsEnabled()) {
return;
}

// Capture request-derived data (IP, User-Agent, device info) on the request thread.
// The injected @Context HttpServletRequest is a thread-bound proxy and cannot be read
// from the async task below, so it must be resolved synchronously first.
RequestData requestData = extractRequestData(request);

CompletableFuture.runAsync(() -> {
try {
recordBasicMetrics(metricType, startTime, status, Fido2MetricType.FIDO2_AUTHENTICATION_DURATION);
recordDetailedMetrics(username, status, request, startTime, authenticatorType, errorReason,
recordDetailedMetrics(username, status, requestData, startTime, authenticatorType, errorReason,
this::createAuthenticationMetricsData);
} catch (Exception e) {
log.warn("Failed to record passkey authentication {} metrics: {}", status.toLowerCase(), e.getMessage());
Expand All @@ -304,11 +314,11 @@
/**
* Record detailed metrics with device info collection
*/
private void recordDetailedMetrics(String username, String status, HttpServletRequest request, long startTime,
String authenticatorType, String errorReason,
private void recordDetailedMetrics(String username, String status, RequestData requestData, long startTime,
String authenticatorType, String errorReason,
MetricsDataCreator dataCreator) {
if (appConfiguration.isFido2DeviceInfoCollection()) {
Fido2MetricsData metricsData = dataCreator.create(username, status, request, authenticatorType);
Fido2MetricsData metricsData = dataCreator.create(username, status, requestData, authenticatorType);

if (!ATTEMPT_STATUS.equals(status)) {
long duration = System.currentTimeMillis() - startTime;
Expand Down Expand Up @@ -336,7 +346,7 @@
*/
@FunctionalInterface
private interface MetricsDataCreator {
Fido2MetricsData create(String username, String status, HttpServletRequest request, String authenticatorType);
Fido2MetricsData create(String username, String status, RequestData requestData, String authenticatorType);
}

// ========== FIDO2 PASSKEY FALLBACK METRICS ==========
Expand Down Expand Up @@ -389,21 +399,21 @@
/**
* Create registration metrics data object
*/
private Fido2MetricsData createRegistrationMetricsData(String username, String status, HttpServletRequest request, String authenticatorType) {
return createMetricsData("REGISTRATION", username, status, request, authenticatorType);
private Fido2MetricsData createRegistrationMetricsData(String username, String status, RequestData requestData, String authenticatorType) {
return createMetricsData("REGISTRATION", username, status, requestData, authenticatorType);
}

/**
* Create authentication metrics data object
*/
private Fido2MetricsData createAuthenticationMetricsData(String username, String status, HttpServletRequest request, String authenticatorType) {
return createMetricsData("AUTHENTICATION", username, status, request, authenticatorType);
private Fido2MetricsData createAuthenticationMetricsData(String username, String status, RequestData requestData, String authenticatorType) {
return createMetricsData("AUTHENTICATION", username, status, requestData, authenticatorType);
}

/**
* Common method to create metrics data objects
*/
private Fido2MetricsData createMetricsData(String operationType, String username, String status, HttpServletRequest request, String authenticatorType) {
private Fido2MetricsData createMetricsData(String operationType, String username, String status, RequestData requestData, String authenticatorType) {
Fido2MetricsData metricsData = new Fido2MetricsData();
metricsData.setOperationType(operationType);
metricsData.setOperationStatus(status);
Expand All @@ -424,31 +434,16 @@
incrementFido2Counter(Fido2MetricType.FIDO2_DEVICE_TYPE_USAGE);
}

// Extract HTTP request details
if (request != null) {
try {
// Extract IP address - check proxy headers first, then fall back to remote address
String ipAddress = extractIpAddress(request);
metricsData.setIpAddress(ipAddress);

// Extract User-Agent header
String userAgent = request.getHeader("User-Agent");
metricsData.setUserAgent(userAgent);
} catch (Exception e) {
log.debug("Failed to extract request details: {}", e.getMessage());
}

// Extract device info if enabled
if (appConfiguration.isFido2DeviceInfoCollection()) {
try {
metricsData.setDeviceInfo(deviceInfoExtractor.extractDeviceInfo(request));
} catch (Exception e) {
log.debug("Failed to extract device info: {}", e.getMessage());
metricsData.setDeviceInfo(deviceInfoExtractor.createMinimalDeviceInfo());
}
// Populate HTTP request details captured on the request thread (see extractRequestData).
// These values must not be read from the async task via the HttpServletRequest proxy.
if (requestData != null) {
metricsData.setIpAddress(requestData.getIpAddress());
metricsData.setUserAgent(requestData.getUserAgent());
if (requestData.getDeviceInfo() != null) {
metricsData.setDeviceInfo(requestData.getDeviceInfo());
}
}

// Set node identifier (for cluster environments) - only if available
try {
String nodeId = networkService.getMacAdress();
Expand All @@ -460,10 +455,79 @@
}

// Note: applicationType is not set as it's always "FIDO2" and redundant

return metricsData;
}

/**
* Capture request-derived data (client IP, User-Agent and device info) from the HTTP request.
*
* <p>This MUST be invoked on the request-handling thread. The injected
* {@code @Context HttpServletRequest} is a thread-bound proxy that resolves the active request
* from the current thread, so it cannot be read from the asynchronous tasks used to store
* metrics. Reading it there previously left ipAddress/userAgent/deviceInfo null, which caused
* them to be dropped from the metrics API response (fields use {@code @JsonInclude(NON_NULL)}).
*
* @param request HTTP servlet request (may be null)
* @return immutable holder with the extracted values (never null; fields may be null)
*/
private RequestData extractRequestData(HttpServletRequest request) {
if (request == null) {
return new RequestData(null, null, null);
}

String ipAddress = null;
String userAgent = null;
try {
// Extract IP address - check proxy headers first, then fall back to remote address
ipAddress = extractIpAddress(request);
// Extract User-Agent header
userAgent = request.getHeader("User-Agent");
} catch (Exception e) {
log.debug("Failed to extract request details: {}", e.getMessage());
}

Fido2MetricsData.DeviceInfo deviceInfo = null;
if (appConfiguration.isFido2DeviceInfoCollection()) {
try {
deviceInfo = deviceInfoExtractor.extractDeviceInfo(request);
} catch (Exception e) {
log.debug("Failed to extract device info: {}", e.getMessage());
deviceInfo = deviceInfoExtractor.createMinimalDeviceInfo();
}
}

return new RequestData(ipAddress, userAgent, deviceInfo);
}

/**
* Immutable holder for request-derived metric data captured on the request thread,
* so it can be safely consumed by the asynchronous metric-storage tasks.
*/
private static final class RequestData {
private final String ipAddress;
private final String userAgent;
private final Fido2MetricsData.DeviceInfo deviceInfo;

RequestData(String ipAddress, String userAgent, Fido2MetricsData.DeviceInfo deviceInfo) {
this.ipAddress = ipAddress;
this.userAgent = userAgent;
this.deviceInfo = deviceInfo;
}

String getIpAddress() {
return ipAddress;
}

String getUserAgent() {
return userAgent;
}

Fido2MetricsData.DeviceInfo getDeviceInfo() {
return deviceInfo;
}
}

/**
* Categorize error reasons for analytics
*/
Expand Down Expand Up @@ -564,8 +628,8 @@
};

// Check proxy headers (trusted only if behind reverse proxy)
// TODO: Add configuration option to enable/disable proxy header trust

Check warning on line 631 in jans-fido2/server/src/main/java/io/jans/fido2/service/shared/MetricService.java

View check run for this annotation

SonarQubeCloud / [Fido2 API] SonarCloud Code Analysis

Complete the task associated to this TODO comment.

See more on https://sonarcloud.io/project/issues?id=JanssenProject_jans-fido2&issues=AZ8jTtXqeNwf2O9ZwUn7&open=AZ8jTtXqeNwf2O9ZwUn7&pullRequest=14481
// TODO: Add validation to ensure request came from trusted proxy IP range

Check warning on line 632 in jans-fido2/server/src/main/java/io/jans/fido2/service/shared/MetricService.java

View check run for this annotation

SonarQubeCloud / [Fido2 API] SonarCloud Code Analysis

Complete the task associated to this TODO comment.

See more on https://sonarcloud.io/project/issues?id=JanssenProject_jans-fido2&issues=AZ8jTtXqeNwf2O9ZwUn8&open=AZ8jTtXqeNwf2O9ZwUn8&pullRequest=14481
for (String header : proxyHeadersToTry) {
String ip = request.getHeader(header);
if (ip != null && !ip.trim().isEmpty() && !"unknown".equalsIgnoreCase(ip)) {
Expand Down Expand Up @@ -602,7 +666,7 @@

// Validate IPv4: each octet must be 0-255
// More precise regex: (25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?) for each octet
if (ip.matches("^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")) {

Check warning on line 669 in jans-fido2/server/src/main/java/io/jans/fido2/service/shared/MetricService.java

View check run for this annotation

SonarQubeCloud / [Fido2 API] SonarCloud Code Analysis

Simplify this regular expression to reduce its complexity from 48 to the 20 allowed.

See more on https://sonarcloud.io/project/issues?id=JanssenProject_jans-fido2&issues=AZ8jTtXqeNwf2O9ZwUn9&open=AZ8jTtXqeNwf2O9ZwUn9&pullRequest=14481
return true;
}

Expand Down