Skip to content
Open
Show file tree
Hide file tree
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 @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<code>", arity = "0..1", interactive = true, echo = false)
@Getter private char[] securityCode;
@Option(names = {"--totp" })
@Getter private boolean isTotp;
}

public static class FoDClientCredentialOptions implements IFoDClientCredentials {
Expand All @@ -72,7 +76,7 @@ public FoDUserCredentialOptions getUserCredentialOptions() {
.map(FoDCredentialOptions::getUserCredentialOptions)
.orElse(null);
}

public FoDClientCredentialOptions getClientCredentialOptions() {
return Optional.ofNullable(authOptions)
.map(FoDAuthOptions::getCredentialOptions)
Expand All @@ -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;
Expand All @@ -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; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,41 @@
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;

// 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 <code> (or -c <code>) 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<String,Object> formData = generateTokenRequest(uc, scopes);
try ( var unirest = UnirestHelper.createUnirestInstance() ) {
Expand All @@ -41,6 +63,26 @@ 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) {
String errorMessage = loginOptions.hasSecurityCode() ? ERROR_WITH_CODE : ERROR_WITHOUT_CODE;
throw new FcliSimpleException(errorMessage);
}
throw new FcliSimpleException(e.getMessage(), e);
}
}

private static final FoDTokenCreateResponse createToken(UnirestInstance unirest, IUrlConfig urlConfig, Map<String, Object> formData) {
configureUnirest(unirest, urlConfig);
Expand All @@ -66,6 +108,10 @@ private static final Map<String, Object> 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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ public interface IFoDUserCredentials {
String getUser();
char[] getPassword();
String getTenant();
default String getSecurityCode() { return null; }
default boolean isTotp() { return false; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.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 \
Expand Down
Loading