From e030e4dbc96456860fedf619f1abec3324579f3e Mon Sep 17 00:00:00 2001 From: "Bobryk-Ozaki, Terrence" Date: Sat, 24 Oct 2026 20:26:46 -0700 Subject: [PATCH] feat: audit-log LinOTP login success/failure (REST and RADIUS) UserResource.loginLinOTP had no security-audit-trail instrumentation at all, unlike loginNative/loginLDAP. Adds the same UserAuditTrailUtils.recordLoginFailed calls loginNative/loginLDAP make, at every genuine failure outcome: the "LinOTP login is not allowed" gate, a non-200 response (invalid credentials/OTP, RADIUS/LinOTP server unreachable, misconfiguration), and an unexpected exception. The successful login's audit entry is not added explicitly - addAccessToken -> userTrackingLogin already calls UserAuditTrailUtils.recordLogin for every provider, LinOTP included, confirmed by reading that path rather than assumed, to avoid double logging. A plain operational log line is still added on success for visibility. The intermediate "please enter your OTP" challenge-response step (a 200 response with no token yet) is deliberately NOT written to the audit trail - it is neither a success nor a failure, only the eventual accept/reject outcome is. Neither the submitted pin/password/OTP, nor a RADIUS shared secret, is ever written to any log. Companion to the new RADIUS auth mode added to LinOTPUtil in SEMOSS/Semoss PR #3192 - this covers both auth modes identically since both return through the same LinOTPResponse. Co-Authored-By: Claude Sonnet 5 --- .../web/services/local/UserResource.java | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/prerna/semoss/web/services/local/UserResource.java b/src/prerna/semoss/web/services/local/UserResource.java index 68cbea5b..7833746e 100644 --- a/src/prerna/semoss/web/services/local/UserResource.java +++ b/src/prerna/semoss/web/services/local/UserResource.java @@ -1567,8 +1567,14 @@ public Response changeADPassword(@Context HttpServletRequest request, @Context H public Response loginLinOTP(@Context HttpServletRequest request, @Context HttpServletResponse response) throws ClientProtocolException, IOException { Map returnMap = new HashMap<>(); + String attemptedUsername = request.getParameter("username"); + String clientIp = WebUtility.getClientIp(request); + String authMode = socialData.getProperty("linotp_auth_mode", "rest"); + if (socialData.getLoginsAllowed().get("linotp") == null || !socialData.getLoginsAllowed().get("linotp")) { returnMap.put(Constants.ERROR_MESSAGE, "LinOTP login is not allowed"); + UserAuditTrailUtils.recordLoginFailed(attemptedUsername, AuthProvider.LINOTP.getLabel(), + AuditErrorCode.LOGIN_METHOD_DISABLED, "LinOTP login is not allowed", clientIp, 400); return WebUtility.getResponse(returnMap, 400); } @@ -1577,23 +1583,47 @@ public Response loginLinOTP(@Context HttpServletRequest request, @Context HttpSe Boolean disableRedirect = Boolean.parseBoolean(request.getParameter("disableRedirect") + ""); boolean autoAdd = Boolean.parseBoolean(socialData.getProperty("linotp_auto_add", "true")); - LinOTPResponse linotpResponse = LinOTPUtil.login(request); + LinOTPResponse linotpResponse; + try { + linotpResponse = LinOTPUtil.login(request); + } catch (IOException e) { + classLogger.error("Unexpected error in loginLinOTP (auth mode=" + authMode + ")", e); + UserAuditTrailUtils.recordLoginFailed(attemptedUsername, AuthProvider.LINOTP.getLabel(), + AuditErrorCode.INTERNAL_ERROR, e.getMessage(), clientIp, 500); + returnMap.put(Constants.ERROR_MESSAGE, "An unexpected error happened. Please try again."); + return WebUtility.getResponse(returnMap, 500); + } returnMap = linotpResponse.getReturnMap(); int responseCode = linotpResponse.getResponseCode(); if (responseCode != 200) { - // we just throw the error + // a genuine failure: invalid credentials/OTP, RADIUS/LinOTP server + // unreachable, misconfiguration, etc. - the specific reason is already in + // returnMap's error message, which must never contain the submitted + // pin/password/OTP (LinOTPUtil never puts them there). + Object errorMessage = returnMap.get(Constants.ERROR_MESSAGE); + UserAuditTrailUtils.recordLoginFailed(attemptedUsername, AuthProvider.LINOTP.getLabel(), + AuditErrorCode.INVALID_CREDENTIALS, errorMessage == null ? null : errorMessage.toString(), + clientIp, responseCode); return WebUtility.getResponse(returnMap, responseCode); } else { // are we logged in? AccessToken token = linotpResponse.getToken(); if (token == null) { - // it is a 200 but we are not logged in - // you likely need to enter otp in 2nd response + // it is a 200 but we are not logged in: a challenge-response step (the + // pin/first factor checked out and the server is asking for an OTP next). + // This is neither a success nor a failure, so it is deliberately not + // written to the audit trail - only the eventual accept/reject outcome is. + classLogger.debug("LinOTP login ({}) for user='{}' is awaiting a second-factor OTP", authMode, + attemptedUsername); return WebUtility.getResponse(returnMap, responseCode); } // we have a login + // addAccessToken() -> userTrackingLogin() already calls + // UserAuditTrailUtils.recordLogin for every provider, LinOTP included, so + // there is nothing further to add to the audit trail on this path. addAccessToken(token, request, autoAdd); + classLogger.info("LinOTP login ({}) succeeded for user='{}'", authMode, token.getUsername()); // log the log in if (!disableRedirect) { setMainPageRedirect(request, response, redirect);