From 2fc2fb3d5e7748d30e82a4f174bda5c7589c4097 Mon Sep 17 00:00:00 2001 From: Sangamesh Vijaykumar Date: Thu, 13 Aug 2026 22:45:53 +0530 Subject: [PATCH 1/9] feat: Add support for TOTP and multi-factor authentication (MFA) codes in the FoD session login command --- .../cli/cmd/FoDSessionLoginCommand.java | 2 +- .../cli/mixin/FoDSessionLoginOptions.java | 49 ++++++++++++++----- .../session/helper/oauth/FoDOAuthHelper.java | 42 ++++++++++++++++ .../helper/oauth/IFoDUserCredentials.java | 2 + .../cli/fod/i18n/FoDMessages.properties | 2 + 5 files changed, 85 insertions(+), 12 deletions(-) diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/cmd/FoDSessionLoginCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/cmd/FoDSessionLoginCommand.java index b6f9bbcd016..dacd37f5b09 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/cmd/FoDSessionLoginCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/cmd/FoDSessionLoginCommand.java @@ -54,7 +54,7 @@ protected FoDSessionDescriptor login(String sessionName) { FoDTokenCreateResponse createTokenResponse = FoDOAuthHelper.createToken(urlConfig, loginOptions.getClientCredentialOptions(), loginOptions.getAuthOptions().getScopes()); sessionDescriptor = new FoDSessionDescriptor(urlConfig, createTokenResponse); } else if ( loginOptions.hasUserCredentials() ) { - FoDTokenCreateResponse createTokenResponse = FoDOAuthHelper.createToken(urlConfig, loginOptions.getUserCredentials(), loginOptions.getAuthOptions().getScopes()); + FoDTokenCreateResponse createTokenResponse = FoDOAuthHelper.createUserToken(urlConfig, loginOptions); sessionDescriptor = new FoDSessionDescriptor(urlConfig, createTokenResponse); } else { throw new FcliSimpleException("Either FoD client or user credentials must be provided"); diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java index 3250f0e6596..6d366b865c6 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java @@ -33,28 +33,32 @@ public class FoDSessionLoginOptions { @Mixin @Getter private FoDUrlConfigOptions urlConfigOptions = new FoDUrlConfigOptions(); - + @ArgGroup(exclusive = false, multiplicity = "1", order = 2) @Getter private FoDAuthOptions authOptions = new FoDAuthOptions(); - + public static class FoDAuthOptions { @ArgGroup(exclusive = true, multiplicity = "1", order = 3) @Getter private FoDCredentialOptions credentialOptions = new FoDCredentialOptions(); @Option(names="--scopes", defaultValue="api-tenant", split=",") @Getter private String[] scopes; } - + public static class FoDCredentialOptions { - @ArgGroup(exclusive = false, multiplicity = "1", order = 1) + @ArgGroup(exclusive = false, multiplicity = "1", order = 1) @Getter private FoDUserCredentialOptions userCredentialOptions = new FoDUserCredentialOptions(); - @ArgGroup(exclusive = false, multiplicity = "1", order = 2) + @ArgGroup(exclusive = false, multiplicity = "1", order = 2) @Getter private FoDClientCredentialOptions clientCredentialOptions = new FoDClientCredentialOptions(); } - + public static class FoDUserCredentialOptions extends UserCredentialOptions { @Option(names = {"-t", "--tenant"}, required = true) @MaskValue(sensitivity = LogSensitivityLevel.low, description = "FOD TENANT") @Getter private String tenant; + @Option(names = {"--code", "-c" }, paramLabel = "", arity = "0..1", interactive = true, echo = false, description = "Security code (TOTP from authenticator or MFA from email/SMS)") + @Getter private char[] securityCode; + @Option(names = {"--totp" }, description = "Indicates the provided code is TOTP from authenticator app (sets do_totp=true)") + @Getter private boolean isTotp; } public static class FoDClientCredentialOptions implements IFoDClientCredentials { @@ -72,7 +76,7 @@ public FoDUserCredentialOptions getUserCredentialOptions() { .map(FoDCredentialOptions::getUserCredentialOptions) .orElse(null); } - + public FoDClientCredentialOptions getClientCredentialOptions() { return Optional.ofNullable(authOptions) .map(FoDAuthOptions::getCredentialOptions) @@ -93,21 +97,36 @@ public final BasicFoDUserCredentials getUserCredentials() { var u = getUserCredentialOptions(); return BasicFoDUserCredentials.builder().tenant(u.getTenant()).user(u.getUser()).password(u.getPassword()).build(); } - + public final boolean hasClientCredentials() { FoDClientCredentialOptions clientCredentialOptions = getClientCredentialOptions(); return clientCredentialOptions!=null && StringUtils.isNotBlank(clientCredentialOptions.getClientId()) && StringUtils.isNotBlank(clientCredentialOptions.getClientSecret()); } - + + public boolean hasSecurityCode() { + var userCred = getUserCredentialOptions(); + return userCred != null && userCred.securityCode != null && userCred.securityCode.length > 0; + } + + public char[] getSecurityCode() { + var userCred = getUserCredentialOptions(); + return userCred != null ? userCred.securityCode : null; + } + + public boolean isTotp() { + var userCred = getUserCredentialOptions(); + return userCred != null && userCred.isTotp; + } + @Command public static final class FoDUrlConfigOptions extends UrlConfigOptions { @Override @SneakyThrows public String getUrl() { return FoDProductHelper.INSTANCE.getApiUrl(super.getUrl()); } - + @Override protected int getDefaultSocketTimeoutInMillis() { return 600000; @@ -121,23 +140,31 @@ public static final class BasicFoDUserCredentials implements IFoDUserCredentials private final String tenant; private final String user; private final char[] password; + private final char[] securityCode; + private final boolean isTotp; private BasicFoDUserCredentials(Builder b) { this.tenant = b.tenant; this.user = b.user; this.password = b.password; + this.securityCode = b.securityCode; + this.isTotp = b.isTotp; } public static Builder builder() { return new Builder(); } @Override public String getTenant() { return tenant; } @Override public String getUser() { return user; } @Override public char[] getPassword() { return password; } + @Override public String getSecurityCode() { return securityCode != null ? String.valueOf(securityCode) : null;} + @Override public boolean isTotp() { return isTotp; } public static final class Builder { - private String tenant; private String user; private char[] password; + private String tenant; private String user; private char[] password; private char[] securityCode; private boolean isTotp; public Builder tenant(String tenant){ this.tenant=tenant; return this; } public Builder user(String user){ this.user=user; return this; } public Builder password(char[] password){ this.password=password; return this; } public BasicFoDUserCredentials build(){ return new BasicFoDUserCredentials(this); } + public Builder securityCode(char[] securityCode) { this.securityCode = securityCode; return this; } + public Builder isTotp(boolean isTotp) { this.isTotp = isTotp; return this; } } } } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java index 7dd2e21a28e..b3a46179cbd 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java @@ -15,13 +15,17 @@ import java.util.LinkedHashMap; import java.util.Map; +import com.fortify.cli.common.exception.FcliSimpleException; import com.fortify.cli.common.http.proxy.helper.ProxyHelper; import com.fortify.cli.common.rest.unirest.HttpHeader; +import com.fortify.cli.common.rest.unirest.UnexpectedHttpResponseException; import com.fortify.cli.common.rest.unirest.UnirestHelper; import com.fortify.cli.common.rest.unirest.config.IUrlConfig; import com.fortify.cli.common.rest.unirest.config.UnirestJsonHeaderConfigurer; import com.fortify.cli.common.rest.unirest.config.UnirestUnexpectedHttpResponseConfigurer; import com.fortify.cli.common.rest.unirest.config.UnirestUrlConfigConfigurer; +import com.fortify.cli.fod._common.session.cli.mixin.FoDSessionLoginOptions; +import com.fortify.cli.fod._common.session.cli.mixin.FoDSessionLoginOptions.BasicFoDUserCredentials; import kong.unirest.UnirestInstance; @@ -41,6 +45,40 @@ public static final FoDTokenCreateResponse createToken(IUrlConfig urlConfig, IFo return createToken(unirest, urlConfig, formData); } } + + public static final FoDTokenCreateResponse createUserToken(IUrlConfig urlConfig, FoDSessionLoginOptions loginOptions) { + var credBuilder = BasicFoDUserCredentials.builder() + .tenant(loginOptions.getUserCredentialOptions().getTenant()) + .user(loginOptions.getUserCredentialOptions().getUser()) + .password(loginOptions.getUserCredentialOptions().getPassword()); + if (loginOptions.hasSecurityCode()) { + credBuilder.securityCode(loginOptions.getSecurityCode()) + .isTotp(loginOptions.isTotp()); + } + try { + return createToken(urlConfig, credBuilder.build(), loginOptions.getAuthOptions().getScopes()); + } catch (UnexpectedHttpResponseException e) { + if (e.getStatus() == 400) { + if (loginOptions.hasSecurityCode()) { + // Security code was provided but rejected - likely expired or invalid + throw new FcliSimpleException( + "Authentication failed with security code. The code may have expired (TOTP codes expire after 30 seconds) " + + + "or be invalid. Please try again with a new code:\n" + + " --code (or -c ) to provide the new security code\n" + + " --totp if using a TOTP authenticator app"); + } else { + // No security code provided - MFA is required + throw new FcliSimpleException( + "FoD tenant requires TOTP or MFA authentication. Please provide the security code using:\n" + + + " --code (or -c ) to provide the security code\n" + + " --totp to indicate the code is from a TOTP authenticator app"); + } + } + throw new FcliSimpleException(e.getMessage(), e); + } + } private static final FoDTokenCreateResponse createToken(UnirestInstance unirest, IUrlConfig urlConfig, Map formData) { configureUnirest(unirest, urlConfig); @@ -66,6 +104,10 @@ private static final Map generateTokenRequest(IFoDUserCredential result.put("grant_type", "password"); result.put("username", String.format("%s\\%s", uc.getTenant(), uc.getUser())); result.put("password", String.valueOf(uc.getPassword())); + if (uc.getSecurityCode() != null) { + result.put("security_code", uc.getSecurityCode()); + result.put("do_totp", uc.isTotp()); + } return result; } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserCredentials.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserCredentials.java index 9b1859149c3..69c72dadcdb 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserCredentials.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserCredentials.java @@ -16,4 +16,6 @@ public interface IFoDUserCredentials { String getUser(); char[] getPassword(); String getTenant(); + default String getSecurityCode() { return null; } + default boolean isTotp() { return false; } } \ No newline at end of file diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties index 62dcf5b6c6d..dae8e46298f 100644 --- a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties @@ -123,6 +123,8 @@ fcli.fod.session.login.client-secret = FoD client secret. fcli.fod.session.login.scopes = FoD scopes to request. Default value: ${DEFAULT-VALUE} fcli.fod.session.login.fod-session = Name for this FoD session. Default value: ${DEFAULT-VALUE}. fcli.fod.session.login.header = Repeatable option to add custom HTTP headers in requests to FoD for this session, in format `NAME: VALUE`. +fcli.fod.session.login.options.code = Security code (TOTP from authenticator or MFA code from email/SMS). +fcli.fod.session.login.options.totp = Indicates the provided code is TOTP from authenticator app (sets do_totp=true). fcli.fod.session.logout.usage.header = Terminate FoD session. fcli.fod.session.logout.usage.description = This command terminates an FoD session previously created \ From 960a32f230bc3e3ae03d049de89693f87fb664c5 Mon Sep 17 00:00:00 2001 From: Sangamesh Vijaykumar Date: Fri, 14 Aug 2026 19:27:27 +0530 Subject: [PATCH 2/9] Class level constants for error messages and option description read from properties file --- .../cli/mixin/FoDSessionLoginOptions.java | 4 +- .../session/helper/oauth/FoDOAuthHelper.java | 37 +++++++++++-------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java index 6d366b865c6..a26127a9c03 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java @@ -55,9 +55,9 @@ public static class FoDUserCredentialOptions extends UserCredentialOptions { @Option(names = {"-t", "--tenant"}, required = true) @MaskValue(sensitivity = LogSensitivityLevel.low, description = "FOD TENANT") @Getter private String tenant; - @Option(names = {"--code", "-c" }, paramLabel = "", arity = "0..1", interactive = true, echo = false, description = "Security code (TOTP from authenticator or MFA from email/SMS)") + @Option(names = {"--code", "-c" }, paramLabel = "", arity = "0..1", interactive = true, echo = false, descriptionKey = "fcli.fod.session.login.options.code") @Getter private char[] securityCode; - @Option(names = {"--totp" }, description = "Indicates the provided code is TOTP from authenticator app (sets do_totp=true)") + @Option(names = {"--totp" }, descriptionKey = "fcli.fod.session.login.options.totp") @Getter private boolean isTotp; } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java index b3a46179cbd..2b318877e52 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java @@ -32,6 +32,25 @@ // TODO Consider moving all classes in this package to a more appropriate package, // for example as a sub-package of the 'rest' package. public class FoDOAuthHelper { + private static final String MFA_GUIDANCE = + "If MFA is required, provide the security code:\n" + + " --code (or -c ) to provide the security code\n" + + " --totp to indicate the code is from a TOTP authenticator app"; + + private static final String ERROR_WITH_CODE = + "Authentication failed. Possible causes:\n" + + " - MFA/TOTP code incorrect or expired\n" + + " - Incorrect username or password\n" + + " - Code format mismatch\n\n" + + "Please verify your credentials and try again with a new code if using TOTP:\n" + + MFA_GUIDANCE; + + private static final String ERROR_WITHOUT_CODE = + "Authentication failed. Possible causes:\n" + + " - Incorrect username or password\n" + + " - FoD tenant requires MFA/TOTP authentication\n\n" + + MFA_GUIDANCE; + public static final FoDTokenCreateResponse createToken(IUrlConfig urlConfig, IFoDUserCredentials uc, String... scopes) { Map formData = generateTokenRequest(uc, scopes); try ( var unirest = UnirestHelper.createUnirestInstance() ) { @@ -59,22 +78,8 @@ public static final FoDTokenCreateResponse createUserToken(IUrlConfig urlConfig, return createToken(urlConfig, credBuilder.build(), loginOptions.getAuthOptions().getScopes()); } catch (UnexpectedHttpResponseException e) { if (e.getStatus() == 400) { - if (loginOptions.hasSecurityCode()) { - // Security code was provided but rejected - likely expired or invalid - throw new FcliSimpleException( - "Authentication failed with security code. The code may have expired (TOTP codes expire after 30 seconds) " - + - "or be invalid. Please try again with a new code:\n" + - " --code (or -c ) to provide the new security code\n" + - " --totp if using a TOTP authenticator app"); - } else { - // No security code provided - MFA is required - throw new FcliSimpleException( - "FoD tenant requires TOTP or MFA authentication. Please provide the security code using:\n" - + - " --code (or -c ) to provide the security code\n" + - " --totp to indicate the code is from a TOTP authenticator app"); - } + String errorMessage = loginOptions.hasSecurityCode() ? ERROR_WITH_CODE : ERROR_WITHOUT_CODE; + throw new FcliSimpleException(errorMessage); } throw new FcliSimpleException(e.getMessage(), e); } From c23150076d64dc70266e4c5b26e2e38a9c0c0bf2 Mon Sep 17 00:00:00 2001 From: Sangamesh Vijaykumar Date: Fri, 14 Aug 2026 22:35:01 +0530 Subject: [PATCH 3/9] Update error message and the options description key(default) --- .../_common/session/cli/mixin/FoDSessionLoginOptions.java | 4 ++-- .../cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java | 5 ++--- .../com/fortify/cli/fod/i18n/FoDMessages.properties | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java index a26127a9c03..3a05b390c68 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java @@ -55,9 +55,9 @@ public static class FoDUserCredentialOptions extends UserCredentialOptions { @Option(names = {"-t", "--tenant"}, required = true) @MaskValue(sensitivity = LogSensitivityLevel.low, description = "FOD TENANT") @Getter private String tenant; - @Option(names = {"--code", "-c" }, paramLabel = "", arity = "0..1", interactive = true, echo = false, descriptionKey = "fcli.fod.session.login.options.code") + @Option(names = {"--code", "-c" }, paramLabel = "", arity = "0..1", interactive = true, echo = false) @Getter private char[] securityCode; - @Option(names = {"--totp" }, descriptionKey = "fcli.fod.session.login.options.totp") + @Option(names = {"--totp" }) @Getter private boolean isTotp; } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java index 2b318877e52..3b6ba9ff072 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java @@ -39,10 +39,9 @@ public class FoDOAuthHelper { private static final String ERROR_WITH_CODE = "Authentication failed. Possible causes:\n" + - " - MFA/TOTP code incorrect or expired\n" + " - Incorrect username or password\n" + - " - Code format mismatch\n\n" + - "Please verify your credentials and try again with a new code if using TOTP:\n" + + " - MFA/TOTP code incorrect, expired, or wrong type (TOTP vs MFA)\n" + + "Please verify your credentials and MFA/TOTP code if applicable:\n" + MFA_GUIDANCE; private static final String ERROR_WITHOUT_CODE = diff --git a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties index dae8e46298f..15976845f03 100644 --- a/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties +++ b/fcli-core/fcli-fod/src/main/resources/com/fortify/cli/fod/i18n/FoDMessages.properties @@ -123,8 +123,8 @@ fcli.fod.session.login.client-secret = FoD client secret. fcli.fod.session.login.scopes = FoD scopes to request. Default value: ${DEFAULT-VALUE} fcli.fod.session.login.fod-session = Name for this FoD session. Default value: ${DEFAULT-VALUE}. fcli.fod.session.login.header = Repeatable option to add custom HTTP headers in requests to FoD for this session, in format `NAME: VALUE`. -fcli.fod.session.login.options.code = Security code (TOTP from authenticator or MFA code from email/SMS). -fcli.fod.session.login.options.totp = Indicates the provided code is TOTP from authenticator app (sets do_totp=true). +fcli.fod.session.login.code = Security code (TOTP from authenticator or MFA code from email/SMS). +fcli.fod.session.login.totp = Indicates the provided code is TOTP from authenticator app (sets do_totp=true). fcli.fod.session.logout.usage.header = Terminate FoD session. fcli.fod.session.logout.usage.description = This command terminates an FoD session previously created \ From fafddda3a024b51dd6a3fe1373ea52a8910ad19f Mon Sep 17 00:00:00 2001 From: Sangamesh Vijaykumar Date: Wed, 19 Aug 2026 15:49:54 +0530 Subject: [PATCH 4/9] feat: Add MFA/TOTP authentication support for FoD session login --- .../cli/mixin/FoDSessionLoginOptions.java | 50 +++---------------- .../helper/oauth/BasicFoDUserAuthCode.java | 28 +++++++++++ .../helper/oauth/BasicFoDUserCredentials.java | 29 +++++++++++ .../session/helper/oauth/FoDOAuthHelper.java | 32 +++++++----- .../helper/oauth/IFoDUserAuthCode.java | 19 +++++++ .../helper/oauth/IFoDUserCredentials.java | 10 +++- 6 files changed, 112 insertions(+), 56 deletions(-) create mode 100644 fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/BasicFoDUserAuthCode.java create mode 100644 fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/BasicFoDUserCredentials.java create mode 100644 fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserAuthCode.java diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java index 3a05b390c68..fae06404329 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java @@ -56,7 +56,8 @@ public static class FoDUserCredentialOptions extends UserCredentialOptions { @MaskValue(sensitivity = LogSensitivityLevel.low, description = "FOD TENANT") @Getter private String tenant; @Option(names = {"--code", "-c" }, paramLabel = "", arity = "0..1", interactive = true, echo = false) - @Getter private char[] securityCode; + @MaskValue(sensitivity = LogSensitivityLevel.low, description = "FOD TOTP/MFA CODE") + @Getter private String securityCode; @Option(names = {"--totp" }) @Getter private boolean isTotp; } @@ -93,9 +94,9 @@ public final boolean hasUserCredentials() { && userCredentialOptions.getPassword().length > 0; } - public final BasicFoDUserCredentials getUserCredentials() { + public final IFoDUserCredentials getUserCredentials() { var u = getUserCredentialOptions(); - return BasicFoDUserCredentials.builder().tenant(u.getTenant()).user(u.getUser()).password(u.getPassword()).build(); + return IFoDUserCredentials.create(u.getTenant(), u.getUser(), u.getPassword()); } public final boolean hasClientCredentials() { @@ -107,17 +108,17 @@ public final boolean hasClientCredentials() { public boolean hasSecurityCode() { var userCred = getUserCredentialOptions(); - return userCred != null && userCred.securityCode != null && userCred.securityCode.length > 0; + return userCred != null && StringUtils.isNotBlank(userCred.getSecurityCode()); } - public char[] getSecurityCode() { + public String getSecurityCode() { var userCred = getUserCredentialOptions(); - return userCred != null ? userCred.securityCode : null; + return userCred != null ? userCred.getSecurityCode() : null; } public boolean isTotp() { var userCred = getUserCredentialOptions(); - return userCred != null && userCred.isTotp; + return userCred != null && userCred.isTotp(); } @Command @@ -132,39 +133,4 @@ protected int getDefaultSocketTimeoutInMillis() { return 600000; } } - - /** - * Basic immutable FoD user credentials with builder pattern. - */ - public static final class BasicFoDUserCredentials implements IFoDUserCredentials { - private final String tenant; - private final String user; - private final char[] password; - private final char[] securityCode; - private final boolean isTotp; - private BasicFoDUserCredentials(Builder b) { - this.tenant = b.tenant; - this.user = b.user; - this.password = b.password; - this.securityCode = b.securityCode; - this.isTotp = b.isTotp; - } - public static Builder builder() { return new Builder(); } - @Override public String getTenant() { return tenant; } - @Override public String getUser() { return user; } - @Override public char[] getPassword() { return password; } - @Override public String getSecurityCode() { return securityCode != null ? String.valueOf(securityCode) : null;} - @Override public boolean isTotp() { return isTotp; } - public static final class Builder { - private String tenant; private String user; private char[] password; private char[] securityCode; private boolean isTotp; - public Builder tenant(String tenant){ this.tenant=tenant; return this; } - public Builder user(String user){ this.user=user; return this; } - public Builder password(char[] password){ this.password=password; return this; } - public BasicFoDUserCredentials build(){ - return new BasicFoDUserCredentials(this); - } - public Builder securityCode(char[] securityCode) { this.securityCode = securityCode; return this; } - public Builder isTotp(boolean isTotp) { this.isTotp = isTotp; return this; } - } - } } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/BasicFoDUserAuthCode.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/BasicFoDUserAuthCode.java new file mode 100644 index 00000000000..960c2814277 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/BasicFoDUserAuthCode.java @@ -0,0 +1,28 @@ +/* + * Copyright 2021-2026 Open Text. + * + * The only warranties for products and services of Open Text + * and its affiliates and licensors ("Open Text") are as may + * be set forth in the express warranty statements accompanying + * such products and services. Nothing herein should be construed + * as constituting an additional warranty. Open Text shall not be + * liable for technical or editorial errors or omissions contained + * herein. The information contained herein is subject to change + * without notice. + */ +package com.fortify.cli.fod._common.session.helper.oauth; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Basic immutable FoD user auth code with builder pattern. + */ +@Builder +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public final class BasicFoDUserAuthCode implements IFoDUserAuthCode { + @Getter private final String securityCode; + @Getter private final boolean isTotp; +} diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/BasicFoDUserCredentials.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/BasicFoDUserCredentials.java new file mode 100644 index 00000000000..dd70c20f296 --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/BasicFoDUserCredentials.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021-2026 Open Text. + * + * The only warranties for products and services of Open Text + * and its affiliates and licensors ("Open Text") are as may + * be set forth in the express warranty statements accompanying + * such products and services. Nothing herein should be construed + * as constituting an additional warranty. Open Text shall not be + * liable for technical or editorial errors or omissions contained + * herein. The information contained herein is subject to change + * without notice. + */ +package com.fortify.cli.fod._common.session.helper.oauth; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Basic immutable FoD user credentials with builder pattern. + */ +@Builder +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public final class BasicFoDUserCredentials implements IFoDUserCredentials { + @Getter private final String tenant; + @Getter private final String user; + @Getter private final char[] password; +} diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java index 3b6ba9ff072..c9d91df888e 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java @@ -16,6 +16,7 @@ import java.util.Map; import com.fortify.cli.common.exception.FcliSimpleException; +import com.fortify.cli.common.exception.FcliTechnicalException; import com.fortify.cli.common.http.proxy.helper.ProxyHelper; import com.fortify.cli.common.rest.unirest.HttpHeader; import com.fortify.cli.common.rest.unirest.UnexpectedHttpResponseException; @@ -25,7 +26,6 @@ import com.fortify.cli.common.rest.unirest.config.UnirestUnexpectedHttpResponseConfigurer; import com.fortify.cli.common.rest.unirest.config.UnirestUrlConfigConfigurer; import com.fortify.cli.fod._common.session.cli.mixin.FoDSessionLoginOptions; -import com.fortify.cli.fod._common.session.cli.mixin.FoDSessionLoginOptions.BasicFoDUserCredentials; import kong.unirest.UnirestInstance; @@ -51,7 +51,14 @@ public class FoDOAuthHelper { MFA_GUIDANCE; public static final FoDTokenCreateResponse createToken(IUrlConfig urlConfig, IFoDUserCredentials uc, String... scopes) { - Map formData = generateTokenRequest(uc, scopes); + Map formData = generateTokenRequest(uc, IFoDUserAuthCode.NONE, scopes); + try ( var unirest = UnirestHelper.createUnirestInstance() ) { + return createToken(unirest, urlConfig, formData); + } + } + + public static final FoDTokenCreateResponse createToken(IUrlConfig urlConfig, IFoDUserCredentials uc, IFoDUserAuthCode authCode, String... scopes) { + Map formData = generateTokenRequest(uc, authCode, scopes); try ( var unirest = UnirestHelper.createUnirestInstance() ) { return createToken(unirest, urlConfig, formData); } @@ -69,18 +76,19 @@ public static final FoDTokenCreateResponse createUserToken(IUrlConfig urlConfig, .tenant(loginOptions.getUserCredentialOptions().getTenant()) .user(loginOptions.getUserCredentialOptions().getUser()) .password(loginOptions.getUserCredentialOptions().getPassword()); - if (loginOptions.hasSecurityCode()) { - credBuilder.securityCode(loginOptions.getSecurityCode()) - .isTotp(loginOptions.isTotp()); - } + + var authCodeBuilder = BasicFoDUserAuthCode.builder() + .securityCode(loginOptions.getUserCredentialOptions().getSecurityCode()) + .isTotp(loginOptions.getUserCredentialOptions().isTotp()); + try { - return createToken(urlConfig, credBuilder.build(), loginOptions.getAuthOptions().getScopes()); + return createToken(urlConfig, credBuilder.build(), authCodeBuilder.build(), loginOptions.getAuthOptions().getScopes()); } catch (UnexpectedHttpResponseException e) { if (e.getStatus() == 400) { String errorMessage = loginOptions.hasSecurityCode() ? ERROR_WITH_CODE : ERROR_WITHOUT_CODE; throw new FcliSimpleException(errorMessage); } - throw new FcliSimpleException(e.getMessage(), e); + throw new FcliTechnicalException(e.getMessage(), e); } } @@ -102,15 +110,15 @@ private static final void configureUnirest(UnirestInstance unirest, IUrlConfig u UnirestJsonHeaderConfigurer.configure(unirest); } - private static final Map generateTokenRequest(IFoDUserCredentials uc, String... scopes) { + private static final Map generateTokenRequest(IFoDUserCredentials uc, IFoDUserAuthCode authCode, String... scopes) { Map result = new LinkedHashMap<>(); result.put("scope", String.join(",", scopes)); result.put("grant_type", "password"); result.put("username", String.format("%s\\%s", uc.getTenant(), uc.getUser())); result.put("password", String.valueOf(uc.getPassword())); - if (uc.getSecurityCode() != null) { - result.put("security_code", uc.getSecurityCode()); - result.put("do_totp", uc.isTotp()); + if (authCode.getSecurityCode() != null) { + result.put("security_code", authCode.getSecurityCode()); + result.put("do_totp", authCode.isTotp()); } return result; } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserAuthCode.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserAuthCode.java new file mode 100644 index 00000000000..fbcd8ef42cb --- /dev/null +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserAuthCode.java @@ -0,0 +1,19 @@ +/* + * Copyright 2021-2026 Open Text. + * + * The only warranties for products and services of Open Text + * and its affiliates and licensors ("Open Text") are as may + * be set forth in the express warranty statements accompanying + * such products and services. Nothing herein should be construed + * as constituting an additional warranty. Open Text shall not be + * liable for technical or editorial errors or omissions contained + * herein. The information contained herein is subject to change + * without notice. + */ +package com.fortify.cli.fod._common.session.helper.oauth; + +public interface IFoDUserAuthCode { + IFoDUserAuthCode NONE = new IFoDUserAuthCode() {}; + default String getSecurityCode() { return null; } + default boolean isTotp() { return false; } +} diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserCredentials.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserCredentials.java index 69c72dadcdb..e53d0b6966a 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserCredentials.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserCredentials.java @@ -16,6 +16,12 @@ public interface IFoDUserCredentials { String getUser(); char[] getPassword(); String getTenant(); - default String getSecurityCode() { return null; } - default boolean isTotp() { return false; } + + static IFoDUserCredentials create(String tenant, String user, char[] password) { + return BasicFoDUserCredentials.builder() + .tenant(tenant) + .user(user) + .password(password) + .build(); + } } \ No newline at end of file From 334b3f0e6b685e2247a4a74edb1ccd478dbfd1f7 Mon Sep 17 00:00:00 2001 From: Sangamesh Vijaykumar Date: Wed, 19 Aug 2026 16:00:45 +0530 Subject: [PATCH 5/9] Moving impl classes to separate \impl package --- .../cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java | 2 ++ .../fod/_common/session/helper/oauth/IFoDUserCredentials.java | 2 ++ .../session/helper/oauth/{ => impl}/BasicFoDUserAuthCode.java | 4 +++- .../helper/oauth/{ => impl}/BasicFoDUserCredentials.java | 4 +++- 4 files changed, 10 insertions(+), 2 deletions(-) rename fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/{ => impl}/BasicFoDUserAuthCode.java (86%) rename fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/{ => impl}/BasicFoDUserCredentials.java (87%) diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java index c9d91df888e..26819aa9315 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java @@ -26,6 +26,8 @@ import com.fortify.cli.common.rest.unirest.config.UnirestUnexpectedHttpResponseConfigurer; import com.fortify.cli.common.rest.unirest.config.UnirestUrlConfigConfigurer; import com.fortify.cli.fod._common.session.cli.mixin.FoDSessionLoginOptions; +import com.fortify.cli.fod._common.session.helper.oauth.impl.BasicFoDUserAuthCode; +import com.fortify.cli.fod._common.session.helper.oauth.impl.BasicFoDUserCredentials; import kong.unirest.UnirestInstance; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserCredentials.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserCredentials.java index e53d0b6966a..1e042843726 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserCredentials.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserCredentials.java @@ -12,6 +12,8 @@ */ package com.fortify.cli.fod._common.session.helper.oauth; +import com.fortify.cli.fod._common.session.helper.oauth.impl.BasicFoDUserCredentials; + public interface IFoDUserCredentials { String getUser(); char[] getPassword(); diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/BasicFoDUserAuthCode.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/impl/BasicFoDUserAuthCode.java similarity index 86% rename from fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/BasicFoDUserAuthCode.java rename to fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/impl/BasicFoDUserAuthCode.java index 960c2814277..14396eb81fa 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/BasicFoDUserAuthCode.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/impl/BasicFoDUserAuthCode.java @@ -10,7 +10,9 @@ * herein. The information contained herein is subject to change * without notice. */ -package com.fortify.cli.fod._common.session.helper.oauth; +package com.fortify.cli.fod._common.session.helper.oauth.impl; + +import com.fortify.cli.fod._common.session.helper.oauth.IFoDUserAuthCode; import lombok.AccessLevel; import lombok.Builder; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/BasicFoDUserCredentials.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/impl/BasicFoDUserCredentials.java similarity index 87% rename from fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/BasicFoDUserCredentials.java rename to fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/impl/BasicFoDUserCredentials.java index dd70c20f296..f7a0834327a 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/BasicFoDUserCredentials.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/impl/BasicFoDUserCredentials.java @@ -10,7 +10,9 @@ * herein. The information contained herein is subject to change * without notice. */ -package com.fortify.cli.fod._common.session.helper.oauth; +package com.fortify.cli.fod._common.session.helper.oauth.impl; + +import com.fortify.cli.fod._common.session.helper.oauth.IFoDUserCredentials; import lombok.AccessLevel; import lombok.Builder; From 1621490084c44942be138c4d1949ac0c804769b6 Mon Sep 17 00:00:00 2001 From: Sangamesh Vijaykumar Date: Wed, 19 Aug 2026 17:47:51 +0530 Subject: [PATCH 6/9] Handle review comments --- .../cli/cmd/FoDSessionLoginCommand.java | 47 ++++++++++++++++--- .../cli/mixin/FoDSessionLoginOptions.java | 7 ++- .../session/helper/oauth/FoDOAuthHelper.java | 44 ----------------- .../helper/oauth/IFoDUserCredentials.java | 10 ---- 4 files changed, 47 insertions(+), 61 deletions(-) diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/cmd/FoDSessionLoginCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/cmd/FoDSessionLoginCommand.java index dacd37f5b09..71da8f0e4b2 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/cmd/FoDSessionLoginCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/cmd/FoDSessionLoginCommand.java @@ -13,7 +13,9 @@ package com.fortify.cli.fod._common.session.cli.cmd; import com.fortify.cli.common.exception.FcliSimpleException; +import com.fortify.cli.common.exception.FcliTechnicalException; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; +import com.fortify.cli.common.rest.unirest.UnexpectedHttpResponseException; import com.fortify.cli.common.rest.unirest.config.IUrlConfig; import com.fortify.cli.common.session.cli.cmd.AbstractSessionLoginCommand; import com.fortify.cli.common.session.cli.mixin.ISessionNameSupplier; @@ -23,6 +25,8 @@ import com.fortify.cli.fod._common.session.helper.FoDSessionHelper; import com.fortify.cli.fod._common.session.helper.oauth.FoDOAuthHelper; import com.fortify.cli.fod._common.session.helper.oauth.FoDTokenCreateResponse; +import com.fortify.cli.fod._common.session.helper.oauth.impl.BasicFoDUserAuthCode; +import com.fortify.cli.fod._common.session.helper.oauth.impl.BasicFoDUserCredentials; import lombok.Getter; import picocli.CommandLine.Command; @@ -34,12 +38,12 @@ public class FoDSessionLoginCommand extends AbstractSessionLoginCommand (or -c ) to provide the security code\n" + + " --totp to indicate the code is from a TOTP authenticator app"; + + String errorWithCode = "Authentication failed. Possible causes:\n" + + " - Incorrect username or password\n" + + " - MFA/TOTP code incorrect, expired, or wrong type (TOTP vs MFA)\n" + + "Please verify your credentials and MFA/TOTP code if applicable:\n" + + mfaGuidance; + + String errorWithoutCode = "Authentication failed. Possible causes:\n" + + " - Incorrect username or password\n" + + " - FoD tenant requires MFA/TOTP authentication\n\n" + + mfaGuidance; + String errorMessage = loginOptions.hasSecurityCode() ? errorWithCode : errorWithoutCode; + throw new FcliSimpleException(errorMessage); + } + throw new FcliTechnicalException(e.getMessage(), e); + } } else { throw new FcliSimpleException("Either FoD client or user credentials must be provided"); } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java index fae06404329..64b743c67c9 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java @@ -23,6 +23,7 @@ import com.fortify.cli.fod._common.rest.helper.FoDProductHelper; import com.fortify.cli.fod._common.session.helper.oauth.IFoDClientCredentials; import com.fortify.cli.fod._common.session.helper.oauth.IFoDUserCredentials; +import com.fortify.cli.fod._common.session.helper.oauth.impl.BasicFoDUserCredentials; import lombok.Getter; import lombok.SneakyThrows; @@ -96,7 +97,11 @@ public final boolean hasUserCredentials() { public final IFoDUserCredentials getUserCredentials() { var u = getUserCredentialOptions(); - return IFoDUserCredentials.create(u.getTenant(), u.getUser(), u.getPassword()); + return BasicFoDUserCredentials.builder() + .tenant(u.getTenant()) + .user(u.getUser()) + .password(u.getPassword()) + .build(); } public final boolean hasClientCredentials() { diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java index 26819aa9315..3cbcf5848cc 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java @@ -15,43 +15,20 @@ import java.util.LinkedHashMap; import java.util.Map; -import com.fortify.cli.common.exception.FcliSimpleException; -import com.fortify.cli.common.exception.FcliTechnicalException; import com.fortify.cli.common.http.proxy.helper.ProxyHelper; import com.fortify.cli.common.rest.unirest.HttpHeader; -import com.fortify.cli.common.rest.unirest.UnexpectedHttpResponseException; import com.fortify.cli.common.rest.unirest.UnirestHelper; import com.fortify.cli.common.rest.unirest.config.IUrlConfig; import com.fortify.cli.common.rest.unirest.config.UnirestJsonHeaderConfigurer; import com.fortify.cli.common.rest.unirest.config.UnirestUnexpectedHttpResponseConfigurer; import com.fortify.cli.common.rest.unirest.config.UnirestUrlConfigConfigurer; -import com.fortify.cli.fod._common.session.cli.mixin.FoDSessionLoginOptions; -import com.fortify.cli.fod._common.session.helper.oauth.impl.BasicFoDUserAuthCode; -import com.fortify.cli.fod._common.session.helper.oauth.impl.BasicFoDUserCredentials; import kong.unirest.UnirestInstance; // TODO Consider moving all classes in this package to a more appropriate package, // for example as a sub-package of the 'rest' package. public class FoDOAuthHelper { - private static final String MFA_GUIDANCE = - "If MFA is required, provide the security code:\n" + - " --code (or -c ) to provide the security code\n" + - " --totp to indicate the code is from a TOTP authenticator app"; - private static final String ERROR_WITH_CODE = - "Authentication failed. Possible causes:\n" + - " - Incorrect username or password\n" + - " - MFA/TOTP code incorrect, expired, or wrong type (TOTP vs MFA)\n" + - "Please verify your credentials and MFA/TOTP code if applicable:\n" + - MFA_GUIDANCE; - - private static final String ERROR_WITHOUT_CODE = - "Authentication failed. Possible causes:\n" + - " - Incorrect username or password\n" + - " - FoD tenant requires MFA/TOTP authentication\n\n" + - MFA_GUIDANCE; - public static final FoDTokenCreateResponse createToken(IUrlConfig urlConfig, IFoDUserCredentials uc, String... scopes) { Map formData = generateTokenRequest(uc, IFoDUserAuthCode.NONE, scopes); try ( var unirest = UnirestHelper.createUnirestInstance() ) { @@ -73,27 +50,6 @@ public static final FoDTokenCreateResponse createToken(IUrlConfig urlConfig, IFo } } - public static final FoDTokenCreateResponse createUserToken(IUrlConfig urlConfig, FoDSessionLoginOptions loginOptions) { - var credBuilder = BasicFoDUserCredentials.builder() - .tenant(loginOptions.getUserCredentialOptions().getTenant()) - .user(loginOptions.getUserCredentialOptions().getUser()) - .password(loginOptions.getUserCredentialOptions().getPassword()); - - var authCodeBuilder = BasicFoDUserAuthCode.builder() - .securityCode(loginOptions.getUserCredentialOptions().getSecurityCode()) - .isTotp(loginOptions.getUserCredentialOptions().isTotp()); - - try { - return createToken(urlConfig, credBuilder.build(), authCodeBuilder.build(), loginOptions.getAuthOptions().getScopes()); - } catch (UnexpectedHttpResponseException e) { - if (e.getStatus() == 400) { - String errorMessage = loginOptions.hasSecurityCode() ? ERROR_WITH_CODE : ERROR_WITHOUT_CODE; - throw new FcliSimpleException(errorMessage); - } - throw new FcliTechnicalException(e.getMessage(), e); - } - } - private static final FoDTokenCreateResponse createToken(UnirestInstance unirest, IUrlConfig urlConfig, Map formData) { configureUnirest(unirest, urlConfig); return unirest.post("/oauth/token") diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserCredentials.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserCredentials.java index 1e042843726..9b1859149c3 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserCredentials.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserCredentials.java @@ -12,18 +12,8 @@ */ package com.fortify.cli.fod._common.session.helper.oauth; -import com.fortify.cli.fod._common.session.helper.oauth.impl.BasicFoDUserCredentials; - public interface IFoDUserCredentials { String getUser(); char[] getPassword(); String getTenant(); - - static IFoDUserCredentials create(String tenant, String user, char[] password) { - return BasicFoDUserCredentials.builder() - .tenant(tenant) - .user(user) - .password(password) - .build(); - } } \ No newline at end of file From 8a14e1ce2debf5835357bb404856e7af4c048501 Mon Sep 17 00:00:00 2001 From: Sangamesh Vijaykumar Date: Wed, 19 Aug 2026 20:33:41 +0530 Subject: [PATCH 7/9] Handle review points --- ...CPServerHttpSessionDescriptorResolver.java | 39 ++---------- .../cli/cmd/FoDSessionLoginCommand.java | 62 ++++++++++--------- .../cli/mixin/FoDSessionLoginOptions.java | 11 ++++ .../session/helper/oauth/FoDOAuthHelper.java | 6 +- .../helper/oauth/IFoDUserAuthCode.java | 1 - 5 files changed, 53 insertions(+), 66 deletions(-) diff --git a/fcli-core/fcli-ai-assist/src/main/java/com/fortify/cli/ai_assist/mcp/helper/http/MCPServerHttpSessionDescriptorResolver.java b/fcli-core/fcli-ai-assist/src/main/java/com/fortify/cli/ai_assist/mcp/helper/http/MCPServerHttpSessionDescriptorResolver.java index 8a37a37baa0..c7bd5d5059f 100644 --- a/fcli-core/fcli-ai-assist/src/main/java/com/fortify/cli/ai_assist/mcp/helper/http/MCPServerHttpSessionDescriptorResolver.java +++ b/fcli-core/fcli-ai-assist/src/main/java/com/fortify/cli/ai_assist/mcp/helper/http/MCPServerHttpSessionDescriptorResolver.java @@ -41,7 +41,7 @@ import com.fortify.cli.fod._common.session.helper.oauth.FoDOAuthHelper; import com.fortify.cli.fod._common.session.helper.oauth.FoDTokenCreateResponse; import com.fortify.cli.fod._common.session.helper.oauth.IFoDClientCredentials; -import com.fortify.cli.fod._common.session.helper.oauth.IFoDUserCredentials; +import com.fortify.cli.fod._common.session.helper.oauth.impl.BasicFoDUserCredentials; import com.fortify.cli.ssc._common.session.cli.mixin.SSCAndScanCentralSessionLoginOptions.SSCAndScanCentralUrlConfigOptions.SSCComponentDisable; import com.fortify.cli.ssc._common.session.helper.ISSCAndScanCentralCredentialsConfig; import com.fortify.cli.ssc._common.session.helper.ISSCAndScanCentralUrlConfig; @@ -348,11 +348,11 @@ private FoDTokenCreateResponse createFoDTokenResponse(ParsedAuthorization auth, try { return FoDOAuthHelper.createToken( urlConfig, - new HttpMcpFoDUserCredentials( - auth.fodTenant(), - auth.fodUser(), - pwd - ), + BasicFoDUserCredentials.builder() + .tenant(auth.fodTenant()) + .user(auth.fodUser()) + .password(pwd) + .build(), DEFAULT_FOD_SCOPES ); } finally { @@ -380,33 +380,6 @@ public String getClientSecret() { } } - private static final class HttpMcpFoDUserCredentials implements IFoDUserCredentials { - private final String tenant; - private final String user; - private final char[] password; - - private HttpMcpFoDUserCredentials(String tenant, String user, char[] password) { - this.tenant = tenant; - this.user = user; - this.password = password; - } - - @Override - public String getUser() { - return user; - } - - @Override - public char[] getPassword() { - return password; - } - - @Override - public String getTenant() { - return tenant; - } - } - private static final class HttpMcpSscUrlConfig implements ISSCAndScanCentralUrlConfig { private final MCPServerHttpConfig.SscConfig config; diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/cmd/FoDSessionLoginCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/cmd/FoDSessionLoginCommand.java index 71da8f0e4b2..64a317cc813 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/cmd/FoDSessionLoginCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/cmd/FoDSessionLoginCommand.java @@ -25,8 +25,6 @@ import com.fortify.cli.fod._common.session.helper.FoDSessionHelper; import com.fortify.cli.fod._common.session.helper.oauth.FoDOAuthHelper; import com.fortify.cli.fod._common.session.helper.oauth.FoDTokenCreateResponse; -import com.fortify.cli.fod._common.session.helper.oauth.impl.BasicFoDUserAuthCode; -import com.fortify.cli.fod._common.session.helper.oauth.impl.BasicFoDUserCredentials; import lombok.Getter; import picocli.CommandLine.Command; @@ -39,6 +37,25 @@ public class FoDSessionLoginCommand extends AbstractSessionLoginCommand (or -c ) to provide the security code\n" + + " --totp to indicate the code is from a TOTP authenticator app"; + + private static final String ERROR_WITH_CODE = "Authentication failed. Possible causes:\n" + + " - Incorrect username or password\n" + + " - MFA/TOTP code incorrect, expired, or wrong type (TOTP vs MFA)\n" + + "Please verify your credentials and MFA/TOTP code if applicable:\n" + + MFA_GUIDANCE; + + private static final String ERROR_WITHOUT_CODE = "Authentication failed. Possible causes:\n" + + " - Incorrect username or password\n" + + " - FoD tenant requires MFA/TOTP authentication\n\n" + + MFA_GUIDANCE; + + private static final String ERROR_CLIENT_CREDENTIALS = "Authentication failed. Possible causes:\n" + + " - Incorrect client ID or client secret\n" + + " - Client credentials have expired"; + @Override public ISessionNameSupplier getSessionNameSupplier() { return unirestInstanceSupplierMixin; @@ -55,38 +72,25 @@ protected FoDSessionDescriptor login(String sessionName) { FoDSessionDescriptor sessionDescriptor; IUrlConfig urlConfig = loginOptions.getUrlConfigOptions(); if (loginOptions.hasClientCredentials()) { - FoDTokenCreateResponse createTokenResponse = FoDOAuthHelper.createToken(urlConfig, loginOptions.getClientCredentialOptions(), loginOptions.getAuthOptions().getScopes()); - sessionDescriptor = new FoDSessionDescriptor(urlConfig, createTokenResponse); + try { + FoDTokenCreateResponse createTokenResponse = FoDOAuthHelper.createToken(urlConfig, + loginOptions.getClientCredentialOptions(), loginOptions.getAuthOptions().getScopes()); + sessionDescriptor = new FoDSessionDescriptor(urlConfig, createTokenResponse); + } catch (UnexpectedHttpResponseException e) { + if (e.getStatus() == 400) { + throw new FcliSimpleException(ERROR_CLIENT_CREDENTIALS); + } + throw new FcliTechnicalException(e.getMessage(), e); + } } else if (loginOptions.hasUserCredentials()) { - var credBuilder = BasicFoDUserCredentials.builder() - .tenant(loginOptions.getUserCredentialOptions().getTenant()) - .user(loginOptions.getUserCredentialOptions().getUser()) - .password(loginOptions.getUserCredentialOptions().getPassword()); - - var authCodeBuilder = BasicFoDUserAuthCode.builder() - .securityCode(loginOptions.getUserCredentialOptions().getSecurityCode()) - .isTotp(loginOptions.getUserCredentialOptions().isTotp()); try { - FoDTokenCreateResponse createTokenResponse = FoDOAuthHelper.createToken(urlConfig, credBuilder.build(), - authCodeBuilder.build(), loginOptions.getAuthOptions().getScopes()); + FoDTokenCreateResponse createTokenResponse = FoDOAuthHelper.createToken(urlConfig, + loginOptions.getUserCredentials(), loginOptions.getAuthCode(), + loginOptions.getAuthOptions().getScopes()); sessionDescriptor = new FoDSessionDescriptor(urlConfig, createTokenResponse); } catch (UnexpectedHttpResponseException e) { if (e.getStatus() == 400) { - String mfaGuidance = "If MFA is required, provide the security code:\n" - + " --code (or -c ) to provide the security code\n" - + " --totp to indicate the code is from a TOTP authenticator app"; - - String errorWithCode = "Authentication failed. Possible causes:\n" - + " - Incorrect username or password\n" - + " - MFA/TOTP code incorrect, expired, or wrong type (TOTP vs MFA)\n" - + "Please verify your credentials and MFA/TOTP code if applicable:\n" - + mfaGuidance; - - String errorWithoutCode = "Authentication failed. Possible causes:\n" + - " - Incorrect username or password\n" + - " - FoD tenant requires MFA/TOTP authentication\n\n" + - mfaGuidance; - String errorMessage = loginOptions.hasSecurityCode() ? errorWithCode : errorWithoutCode; + String errorMessage = loginOptions.hasSecurityCode() ? ERROR_WITH_CODE : ERROR_WITHOUT_CODE; throw new FcliSimpleException(errorMessage); } throw new FcliTechnicalException(e.getMessage(), e); diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java index 64b743c67c9..484ef85b831 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/mixin/FoDSessionLoginOptions.java @@ -22,7 +22,9 @@ import com.fortify.cli.common.session.cli.mixin.UserCredentialOptions; import com.fortify.cli.fod._common.rest.helper.FoDProductHelper; import com.fortify.cli.fod._common.session.helper.oauth.IFoDClientCredentials; +import com.fortify.cli.fod._common.session.helper.oauth.IFoDUserAuthCode; import com.fortify.cli.fod._common.session.helper.oauth.IFoDUserCredentials; +import com.fortify.cli.fod._common.session.helper.oauth.impl.BasicFoDUserAuthCode; import com.fortify.cli.fod._common.session.helper.oauth.impl.BasicFoDUserCredentials; import lombok.Getter; @@ -126,6 +128,15 @@ public boolean isTotp() { return userCred != null && userCred.isTotp(); } + public IFoDUserAuthCode getAuthCode() { + var u = getUserCredentialOptions(); + if (u == null || StringUtils.isBlank(u.getSecurityCode())) { return null; } + return BasicFoDUserAuthCode.builder() + .securityCode(u.getSecurityCode()) + .isTotp(u.isTotp()) + .build(); + } + @Command public static final class FoDUrlConfigOptions extends UrlConfigOptions { @Override @SneakyThrows diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java index 3cbcf5848cc..6909e77554a 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java @@ -30,7 +30,7 @@ public class FoDOAuthHelper { public static final FoDTokenCreateResponse createToken(IUrlConfig urlConfig, IFoDUserCredentials uc, String... scopes) { - Map formData = generateTokenRequest(uc, IFoDUserAuthCode.NONE, scopes); + Map formData = generateTokenRequest(uc, null, scopes); try ( var unirest = UnirestHelper.createUnirestInstance() ) { return createToken(unirest, urlConfig, formData); } @@ -38,7 +38,7 @@ public static final FoDTokenCreateResponse createToken(IUrlConfig urlConfig, IFo public static final FoDTokenCreateResponse createToken(IUrlConfig urlConfig, IFoDUserCredentials uc, IFoDUserAuthCode authCode, String... scopes) { Map formData = generateTokenRequest(uc, authCode, scopes); - try ( var unirest = UnirestHelper.createUnirestInstance() ) { + try ( var unirest = UnirestHelper.createUnirestInstance() ) { return createToken(unirest, urlConfig, formData); } } @@ -74,7 +74,7 @@ private static final Map generateTokenRequest(IFoDUserCredential result.put("grant_type", "password"); result.put("username", String.format("%s\\%s", uc.getTenant(), uc.getUser())); result.put("password", String.valueOf(uc.getPassword())); - if (authCode.getSecurityCode() != null) { + if (null != authCode && null != authCode.getSecurityCode()) { result.put("security_code", authCode.getSecurityCode()); result.put("do_totp", authCode.isTotp()); } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserAuthCode.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserAuthCode.java index fbcd8ef42cb..2a2daa088f0 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserAuthCode.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/IFoDUserAuthCode.java @@ -13,7 +13,6 @@ package com.fortify.cli.fod._common.session.helper.oauth; public interface IFoDUserAuthCode { - IFoDUserAuthCode NONE = new IFoDUserAuthCode() {}; default String getSecurityCode() { return null; } default boolean isTotp() { return false; } } From 7aa3cdb1a379f476ead42eb38cb023eb9ba70a1c Mon Sep 17 00:00:00 2001 From: Sangamesh Vijaykumar Date: Thu, 20 Aug 2026 20:45:03 +0530 Subject: [PATCH 8/9] Handle review comments --- .../cli/cmd/FoDSessionLoginCommand.java | 21 ++++++++++--------- .../session/helper/oauth/FoDOAuthHelper.java | 4 ++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/cmd/FoDSessionLoginCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/cmd/FoDSessionLoginCommand.java index 64a317cc813..caaa55cbf94 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/cmd/FoDSessionLoginCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/cmd/FoDSessionLoginCommand.java @@ -69,7 +69,7 @@ protected void logoutBeforeNewLogin(String sessionName, FoDSessionDescriptor ses @Override protected FoDSessionDescriptor login(String sessionName) { - FoDSessionDescriptor sessionDescriptor; + FoDSessionDescriptor sessionDescriptor = null; IUrlConfig urlConfig = loginOptions.getUrlConfigOptions(); if (loginOptions.hasClientCredentials()) { try { @@ -77,10 +77,7 @@ protected FoDSessionDescriptor login(String sessionName) { loginOptions.getClientCredentialOptions(), loginOptions.getAuthOptions().getScopes()); sessionDescriptor = new FoDSessionDescriptor(urlConfig, createTokenResponse); } catch (UnexpectedHttpResponseException e) { - if (e.getStatus() == 400) { - throw new FcliSimpleException(ERROR_CLIENT_CREDENTIALS); - } - throw new FcliTechnicalException(e.getMessage(), e); + handleUnexpectedHttpResponseException(e, ERROR_CLIENT_CREDENTIALS); } } else if (loginOptions.hasUserCredentials()) { try { @@ -89,15 +86,19 @@ protected FoDSessionDescriptor login(String sessionName) { loginOptions.getAuthOptions().getScopes()); sessionDescriptor = new FoDSessionDescriptor(urlConfig, createTokenResponse); } catch (UnexpectedHttpResponseException e) { - if (e.getStatus() == 400) { - String errorMessage = loginOptions.hasSecurityCode() ? ERROR_WITH_CODE : ERROR_WITHOUT_CODE; - throw new FcliSimpleException(errorMessage); - } - throw new FcliTechnicalException(e.getMessage(), e); + String errorMessage = loginOptions.hasSecurityCode() ? ERROR_WITH_CODE : ERROR_WITHOUT_CODE; + handleUnexpectedHttpResponseException(e, errorMessage); } } else { throw new FcliSimpleException("Either FoD client or user credentials must be provided"); } return sessionDescriptor; } + + private void handleUnexpectedHttpResponseException(UnexpectedHttpResponseException e, String msg) { + if (e.getStatus() == 400) { + throw new FcliSimpleException(msg); + } + throw new FcliTechnicalException(e.getMessage(), e); + } } diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java index 6909e77554a..90ed7e0f0e4 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/helper/oauth/FoDOAuthHelper.java @@ -31,7 +31,7 @@ public class FoDOAuthHelper { public static final FoDTokenCreateResponse createToken(IUrlConfig urlConfig, IFoDUserCredentials uc, String... scopes) { Map formData = generateTokenRequest(uc, null, scopes); - try ( var unirest = UnirestHelper.createUnirestInstance() ) { + try ( var unirest = UnirestHelper.createUnirestInstance() ) { return createToken(unirest, urlConfig, formData); } } @@ -45,7 +45,7 @@ public static final FoDTokenCreateResponse createToken(IUrlConfig urlConfig, IFo public static final FoDTokenCreateResponse createToken(IUrlConfig urlConfig, IFoDClientCredentials cc, String... scopes) { Map formData = generateTokenRequest(cc, scopes); - try ( var unirest = UnirestHelper.createUnirestInstance() ) { + try ( var unirest = UnirestHelper.createUnirestInstance() ) { return createToken(unirest, urlConfig, formData); } } From 6b6648e30a4b4ac83fa07904c653faa323a895e2 Mon Sep 17 00:00:00 2001 From: Sangamesh Vijaykumar Date: Thu, 20 Aug 2026 21:21:26 +0530 Subject: [PATCH 9/9] Handle review comments - avoid wrapping runtime exceptions in FcliTechnicalException, update java.instructions.md also --- .github/instructions/java.instructions.md | 7 +++++-- .../_common/session/cli/cmd/FoDSessionLoginCommand.java | 3 +-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/instructions/java.instructions.md b/.github/instructions/java.instructions.md index 0ddc7742e3c..6b0b54a46d2 100644 --- a/.github/instructions/java.instructions.md +++ b/.github/instructions/java.instructions.md @@ -30,16 +30,19 @@ Always use `headerReplace(name, value)` — never `accept()`, `contentType()`, o - New/updated code should throw only fcli-domain exceptions (`Fcli*Exception`), module-domain exceptions (for example `Aviator*Exception` in Aviator modules), or picocli exceptions (`ParameterException` and related) when integrating with command parsing. - Avoid throwing standard Java runtime exceptions (`IllegalArgumentException`, `IllegalStateException`, `RuntimeException`, and similar) for user-facing or command-flow errors. +- **Checked exceptions** (e.g., `IOException`, `JsonProcessingException`): Wrap in `FcliTechnicalException` to preserve the cause chain. +- **Runtime exceptions** (e.g., `UnexpectedHttpResponseException`): Re-throw as-is unless special handling is needed (e.g., a specific error code requires a user-friendly message). Avoid unnecessary wrapping to keep stack traces short and relevant. | Scenario | Exception | |----------|-----------| | Invalid/missing user input | `FcliSimpleException` | | External resource not found | `FcliSimpleException` with remediation | | User abort | `FcliAbortedByUserException` | -| I/O, network, JSON parse | `FcliTechnicalException` (wrap cause) | +| Checked exception (I/O, JSON parse) | `FcliTechnicalException` (wrap cause) | +| Runtime exception (no special handling needed) | Re-throw as-is | | Invariant violation, unreachable | `FcliBugException` | -Messages: actionable, sentence case, no trailing periods. Preserve root cause in wrapping. +Messages: actionable, sentence case, no trailing periods. Wrap root cause only for checked exceptions. ## Design Patterns diff --git a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/cmd/FoDSessionLoginCommand.java b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/cmd/FoDSessionLoginCommand.java index caaa55cbf94..2daed82ee59 100644 --- a/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/cmd/FoDSessionLoginCommand.java +++ b/fcli-core/fcli-fod/src/main/java/com/fortify/cli/fod/_common/session/cli/cmd/FoDSessionLoginCommand.java @@ -13,7 +13,6 @@ package com.fortify.cli.fod._common.session.cli.cmd; import com.fortify.cli.common.exception.FcliSimpleException; -import com.fortify.cli.common.exception.FcliTechnicalException; import com.fortify.cli.common.output.cli.mixin.OutputHelperMixins; import com.fortify.cli.common.rest.unirest.UnexpectedHttpResponseException; import com.fortify.cli.common.rest.unirest.config.IUrlConfig; @@ -99,6 +98,6 @@ private void handleUnexpectedHttpResponseException(UnexpectedHttpResponseExcepti if (e.getStatus() == 400) { throw new FcliSimpleException(msg); } - throw new FcliTechnicalException(e.getMessage(), e); + throw e; } }