Skip to content

Commit

Permalink
I18N-1384: Add proxy functionality to CLI
Browse files Browse the repository at this point in the history
* Create service to perform proxy health check
* Create configuration POJO for proxy details
* Use proxy configuration if enabled.
* If the proxy is not healthy, default to direct access
  • Loading branch information
byronantak authored Jan 3, 2025
1 parent 6040d66 commit 1a3dcb6
Show file tree
Hide file tree
Showing 10 changed files with 463 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down Expand Up @@ -49,17 +51,19 @@ public class AuthenticatedRestTemplate {
/** logger */
static Logger logger = LoggerFactory.getLogger(AuthenticatedRestTemplate.class);

@Autowired ResttemplateConfig resttemplateConfig;
@Autowired ResttemplateConfig restTemplateConfig;

/** Will delegate calls to the {@link RestTemplate} instance that was configured */
@Autowired CookieStoreRestTemplate restTemplate;

/** Used to intercept requests and inject CSRF token */
@Autowired
FormLoginAuthenticationCsrfTokenInterceptor formLoginAuthenticationCsrfTokenInterceptor;
@Autowired LoginAuthenticationCsrfTokenInterceptor loginAuthenticationCsrfTokenInterceptor;

@Autowired RestTemplateUtil restTemplateUtil;

@Autowired(required = false)
ProxyOutboundRequestInterceptor proxyOutboundRequestInterceptor;

/** Initialize the internal restTemplate instance */
@PostConstruct
protected void init() {
Expand All @@ -70,8 +74,10 @@ protected void init() {

logger.debug("Set interceptor for authentication");
List<ClientHttpRequestInterceptor> interceptors =
Collections.<ClientHttpRequestInterceptor>singletonList(
formLoginAuthenticationCsrfTokenInterceptor);
Collections.<ClientHttpRequestInterceptor>unmodifiableList(
Stream.of(proxyOutboundRequestInterceptor, loginAuthenticationCsrfTokenInterceptor)
.filter(Objects::nonNull)
.toList());

restTemplate.setRequestFactory(
new InterceptingClientHttpRequestFactory(restTemplate.getRequestFactory(), interceptors));
Expand Down Expand Up @@ -121,7 +127,7 @@ protected void makeRestTemplateWithCustomObjectMapper(RestTemplate restTemplate)

ObjectMapper objectMapper = jackson2ObjectMapperFactoryBean.getObject();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// To keep backward compatibility with the Joda output, disable write/reading nano seconds
// To keep backward compatibility with the Joda output, disable write/reading nanoseconds
// with
// Java time and ZonedDateTime
// also see {@link com.box.l10n.mojito.json.ObjectMapper}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public void setCookieStoreAndUpdateRequestFactory(CookieStore cookieStore) {
HttpClientBuilder.create()
.setDefaultCookieStore(cookieStore)
// we have to turn off auto redirect in the rest template because
// when session expires, it will return a 302 and resttemplate
// when session expires, it will return a 302 and restTemplate
// will automatically redirect to /login even before returning
// the ClientHttpResponse in the interceptor
.disableRedirectHandling()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
import jakarta.annotation.PostConstruct;
import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.apache.hc.client5.http.cookie.Cookie;
import org.apache.hc.client5.http.cookie.CookieStore;
import org.slf4j.Logger;
Expand Down Expand Up @@ -36,10 +37,10 @@
* @author wyau
*/
@Component
public class FormLoginAuthenticationCsrfTokenInterceptor implements ClientHttpRequestInterceptor {
public class LoginAuthenticationCsrfTokenInterceptor implements ClientHttpRequestInterceptor {

/** logger */
Logger logger = LoggerFactory.getLogger(FormLoginAuthenticationCsrfTokenInterceptor.class);
Logger logger = LoggerFactory.getLogger(LoginAuthenticationCsrfTokenInterceptor.class);

public static final String CSRF_PARAM_NAME = "_csrf";
public static final String CSRF_HEADER_NAME = "X-CSRF-TOKEN";
Expand Down Expand Up @@ -77,6 +78,9 @@ public class FormLoginAuthenticationCsrfTokenInterceptor implements ClientHttpRe
/** Will delegate calls to the {@link RestTemplate} instance that was configured */
@Autowired CookieStoreRestTemplate restTemplate;

@Autowired(required = false)
ProxyOutboundRequestInterceptor proxyOutboundRequestInterceptor;

/** Init */
@PostConstruct
protected void init() {
Expand All @@ -89,19 +93,22 @@ protected void init() {
restTemplate.setCookieStoreAndUpdateRequestFactory(cookieStore);

List<ClientHttpRequestInterceptor> interceptors =
Collections.singletonList(
new ClientHttpRequestInterceptor() {
@Override
public ClientHttpResponse intercept(
HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
if (latestCsrfToken != null) {
// At the beginning of auth flow, there's no token yet
injectCsrfTokenIntoHeader(request, latestCsrfToken);
}
return execution.execute(request, body);
}
});
Stream.of(
proxyOutboundRequestInterceptor,
new ClientHttpRequestInterceptor() {
@Override
public ClientHttpResponse intercept(
HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
if (latestCsrfToken != null) {
// At the beginning of auth flow, there's no token yet
injectCsrfTokenIntoHeader(request, latestCsrfToken);
}
return execution.execute(request, body);
}
})
.filter(Objects::nonNull)
.toList();

restTemplateForAuthenticationFlow.setRequestFactory(
new InterceptingClientHttpRequestFactory(
Expand Down Expand Up @@ -140,8 +147,8 @@ public ClientHttpResponse intercept(

/**
* Handle http response from the intercept. It will check to see if the initial response was
* successful (ie. error status such as 301, 403). If so, it'll try the authentication flow again.
* If it further encounters an unsuccessful response, then it'll throw a {@link
* successful (i.e. error status such as 301, 403). If so, it'll try the authentication flow
* again. If it further encounters an unsuccessful response, then it'll throw a {@link
* RestClientException}
*
* @param request
Expand Down Expand Up @@ -202,7 +209,8 @@ private void reauthenticate(HttpRequest request) {
protected void startAuthenticationAndInjectCsrfToken(HttpRequest request) {
logger.debug(
"Authenticate because no session is found in cookie store or it doesn't match with the one used to get the CSRF token we have.");
startAuthenticationFlow();
// TODO: Remove logic once PreAuth logic is configured on all environments
startAuthenticationFlow(resttemplateConfig.usesLoginAuthentication());

logger.debug("Injecting CSRF token");
injectCsrfTokenIntoHeader(request, latestCsrfToken);
Expand Down Expand Up @@ -248,72 +256,87 @@ protected void injectCsrfTokenIntoHeader(HttpRequest request, CsrfToken csrfToke
}

logger.debug(
"Injecting CSRF token into request {} header: {}", request.getURI(), csrfToken.getToken());
"Injecting CSRF token into request {} token: {}", request.getURI(), csrfToken.getToken());
request.getHeaders().add(csrfToken.getHeaderName(), csrfToken.getToken());
}

/**
* Starts the traditioanl form login authentication flow handshake. Consequencially, the cookie
* store (which contains the session id) and the CSRF token will be updated.
* If preAuthentication is enabled, then we merely get the CSRF token If preAuthentication is not
* enabled, start the traditional form login authentication flow handshake.
*
* <p>Consequentially, the cookie store (which contains the session id) and the CSRF token will be
* updated.
*
* @throws AuthenticationException
*/
protected synchronized void startAuthenticationFlow() throws AuthenticationException {
protected synchronized void startAuthenticationFlow(boolean usesLoginAuthentication)
throws AuthenticationException {
logger.debug("Getting authenticated session");

logger.debug(
"Start by loading up the login form to get a valid unauthenticated session and CSRF token");
ResponseEntity<String> loginResponseEntity =
restTemplateForAuthenticationFlow.getForEntity(
restTemplateUtil.getURIForResource(formLoginConfig.getLoginFormPath()), String.class);

latestCsrfToken = getCsrfTokenFromLoginHtml(loginResponseEntity.getBody());
latestSessionIdForLatestCsrfToken = getAuthenticationSessionIdFromCookieStore();
logger.debug(
"Update CSRF token for interceptor ({}) from login form", latestCsrfToken.getToken());

MultiValueMap<String, Object> loginPostParams = new LinkedMultiValueMap<>();
loginPostParams.add("username", credentialProvider.getUsername());
loginPostParams.add("password", credentialProvider.getPassword());
if (usesLoginAuthentication) {
logger.debug(
"Start by loading up the login form to get a valid unauthenticated session and CSRF token");
ResponseEntity<String> loginResponseEntity =
restTemplateForAuthenticationFlow.getForEntity(
restTemplateUtil.getURIForResource(formLoginConfig.getLoginFormPath()), String.class);

logger.debug("login Resonse status code {}", loginResponseEntity.getStatusCode());
if (!loginResponseEntity.hasBody()) {
throw new SessionAuthenticationException(
"Authentication failed: no CSRF token could be found. GET login status code = "
+ loginResponseEntity.getStatusCode());
}

logger.debug(
"Post to login url to startAuthenticationFlow with user={}, pwd={}",
credentialProvider.getUsername(),
credentialProvider.getPassword());
ResponseEntity<String> postLoginResponseEntity =
restTemplateForAuthenticationFlow.postForEntity(
restTemplateUtil.getURIForResource(formLoginConfig.getLoginFormPath()),
loginPostParams,
String.class);

// TODO(P1) This current way of checking if authentication is successful is somewhat
// hacky. Bascailly it says that authentication is successful if a 302 is returned
// and the redirect (from location header) maps to the login redirect path from the config.
URI locationURI = URI.create(postLoginResponseEntity.getHeaders().get("Location").get(0));
String expectedLocation =
resttemplateConfig.getContextPath() + "/" + formLoginConfig.getLoginRedirectPath();

if (postLoginResponseEntity.getStatusCode().equals(HttpStatus.FOUND)
&& expectedLocation.equals(locationURI.getPath())) {

latestCsrfToken =
getCsrfTokenFromEndpoint(
restTemplateUtil.getURIForResource(formLoginConfig.getCsrfTokenPath()));
latestCsrfToken = getCsrfTokenFromLoginHtml(loginResponseEntity.getBody());
latestSessionIdForLatestCsrfToken = getAuthenticationSessionIdFromCookieStore();

logger.debug(
"Update CSRF token interceptor in AuthRestTempplate ({})", latestCsrfToken.getToken());
"Update CSRF token for interceptor ({}) from login form", latestCsrfToken.getToken());

} else {
throw new SessionAuthenticationException(
"Authentication failed. Post login status code = "
+ postLoginResponseEntity.getStatusCode()
+ ", location = ["
+ locationURI.getPath()
+ "], expected location = ["
+ expectedLocation
+ "]");
MultiValueMap<String, Object> loginPostParams = new LinkedMultiValueMap<>();
loginPostParams.add("username", credentialProvider.getUsername());
loginPostParams.add("password", credentialProvider.getPassword());
loginPostParams.add("_csrf", latestCsrfToken.getToken());

logger.debug(
"Post to login url to startAuthenticationFlow with user={}, pwd={}",
credentialProvider.getUsername(),
credentialProvider.getPassword());
ResponseEntity<String> postLoginResponseEntity =
restTemplateForAuthenticationFlow.postForEntity(
restTemplateUtil.getURIForResource(formLoginConfig.getLoginFormPath()),
loginPostParams,
String.class);

// TODO(P1) This current way of checking if authentication is successful is somewhat
// hacky. Basically it says that authentication is successful if a 302 is returned
// and the redirect (from location header) maps to the login redirect path from the config.
URI locationURI = URI.create(postLoginResponseEntity.getHeaders().get("Location").get(0));
String expectedLocation =
resttemplateConfig.getContextPath() + "/" + formLoginConfig.getLoginRedirectPath();

boolean isAuthenticated =
postLoginResponseEntity.getStatusCode().equals(HttpStatus.FOUND)
&& expectedLocation.equals(locationURI.getPath());

if (!isAuthenticated) {
throw new SessionAuthenticationException(
"Authentication failed. Post login status code = "
+ postLoginResponseEntity.getStatusCode()
+ ", location = ["
+ locationURI.getPath()
+ "], expected location = ["
+ expectedLocation
+ "]");
}
}

latestCsrfToken =
getCsrfTokenFromEndpoint(
restTemplateUtil.getURIForResource(formLoginConfig.getCsrfTokenPath()));
latestSessionIdForLatestCsrfToken = getAuthenticationSessionIdFromCookieStore();

logger.debug(
"Update CSRF token interceptor in AuthRestTemplate ({})", latestCsrfToken.getToken());
}

/**
Expand All @@ -322,7 +345,7 @@ protected synchronized void startAuthenticationFlow() throws AuthenticationExcep
*
* @param loginHtml The login page HTML which contains the csrf token. It is assumed that the CSRF
* token is embedded on the page inside an input field with name matching {@link
* com.box.l10n.mojito.rest.resttemplate.FormLoginAuthenticationCsrfTokenInterceptor#CSRF_PARAM_NAME}
* LoginAuthenticationCsrfTokenInterceptor#CSRF_PARAM_NAME}
* @return
* @throws AuthenticationException
*/
Expand All @@ -346,10 +369,20 @@ protected CsrfToken getCsrfTokenFromLoginHtml(String loginHtml) throws Authentic
* @param csrfTokenUrl The full URL to which the CSRF token can be obtained
* @return
*/
protected CsrfToken getCsrfTokenFromEndpoint(String csrfTokenUrl) {
protected CsrfToken getCsrfTokenFromEndpoint(String csrfTokenUrl)
throws SessionAuthenticationException {
ResponseEntity<String> csrfTokenEntity =
restTemplateForAuthenticationFlow.getForEntity(csrfTokenUrl, String.class, "");
logger.debug("CSRF token from {} is {}", csrfTokenUrl, csrfTokenEntity.getBody());
if (csrfTokenEntity.getStatusCode().isError()) {
throw new SessionAuthenticationException(
"Authentication failed. GET login status code = "
+ csrfTokenEntity.getStatusCode()
+ ", location = ["
+ csrfTokenUrl
+ "]");
}

return new DefaultCsrfToken(CSRF_HEADER_NAME, CSRF_PARAM_NAME, csrfTokenEntity.getBody());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.box.l10n.mojito.rest.resttemplate;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ProxyConfig {

@Value("${l10n.proxy.host:}")
String host;

@Value("${l10n.proxy.scheme:http}")
String scheme;

@Value("${l10n.proxy.port:19193}")
Integer port;

public String getHost() {
return host;
}

public void setHost(String host) {
this.host = host;
}

public String getScheme() {
return scheme;
}

public void setScheme(String scheme) {
this.scheme = scheme;
}

public Integer getPort() {
return port;
}

public void setPort(Integer port) {
this.port = port;
}

public boolean isValidConfiguration() {
return this.host != null
&& this.scheme != null
&& this.port != null
&& !this.host.isEmpty()
&& !this.scheme.isEmpty()
&& this.port > 0;
}
}
Loading

0 comments on commit 1a3dcb6

Please sign in to comment.