Skip to content

Commit 2cd2357

Browse files
committed
wip
1 parent b77b891 commit 2cd2357

File tree

5 files changed

+141
-22
lines changed

5 files changed

+141
-22
lines changed

docs/_docs/refs/configurations.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,21 @@ Providers and properties:
225225
- `l10n.resttemplate.stateless.msal.client-secret=<client-credential-client-secret-id>`
226226
- `l10n.resttemplate.stateless.msal.scopes=<Application ID URI>/.default`
227227

228+
### CLI Header Auth (e.g., Cloudflare Access)
229+
230+
Configure the CLI to attach static headers (such as Cloudflare Access credentials) to each request
231+
instead of performing the legacy form login or MSAL flows.
232+
233+
```
234+
l10n.resttemplate.authentication-mode=HEADER
235+
l10n.resttemplate.header.headers.CF-Access-Client-Id=<client-id>
236+
l10n.resttemplate.header.headers.CF-Access-Client-Secret=<client-secret>
237+
```
238+
239+
Add additional headers as needed by appending more `headers.<Name>=<value>` properties. Values can
240+
also be provided through environment variables using Spring Boot's relaxed binding, for example
241+
`L10N_RESTTEMPLATE_HEADER_HEADERS_CF-ACCESS-CLIENT-SECRET`.
242+
228243
## Box Platform Integration
229244

230245
### Custom Configurations

restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/AuthenticatedRestTemplate.java

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -72,28 +72,41 @@ protected void init() {
7272
makeRestTemplateWithCustomObjectMapper(restTemplate);
7373
setErrorHandlerWithLogging(restTemplate);
7474

75-
boolean stateless =
76-
resttemplateConfig
77-
.getAuthenticationMode()
78-
.equals(ResttemplateConfig.AuthenticationMode.STATELESS);
79-
80-
if (stateless) {
81-
logger.info("Using stateless Bearer token interceptor");
82-
if (bearerTokenInterceptor == null) {
83-
throw new IllegalStateException(
84-
"Stateless auth selected but BearerTokenInterceptor is not available. Ensure MSAL config is set.");
75+
ResttemplateConfig.AuthenticationMode mode = resttemplateConfig.getAuthenticationMode();
76+
77+
switch (mode) {
78+
case STATELESS -> {
79+
logger.info("Using stateless Bearer token interceptor");
80+
if (bearerTokenInterceptor == null) {
81+
throw new IllegalStateException(
82+
"Stateless auth selected but BearerTokenInterceptor is not available. Ensure MSAL config is set.");
83+
}
84+
List<ClientHttpRequestInterceptor> interceptors =
85+
Collections.<ClientHttpRequestInterceptor>singletonList(bearerTokenInterceptor);
86+
restTemplate.setRequestFactory(
87+
new InterceptingClientHttpRequestFactory(restTemplate.getRequestFactory(), interceptors));
88+
}
89+
case HEADER -> {
90+
logger.info("Using header-based authentication");
91+
Map<String, String> headers = resttemplateConfig.getHeader().getHeaders();
92+
if (headers == null || headers.isEmpty()) {
93+
throw new IllegalStateException(
94+
"Header authentication selected but no headers have been configured (l10n.resttemplate.header.headers)");
95+
}
96+
List<ClientHttpRequestInterceptor> interceptors =
97+
Collections.<ClientHttpRequestInterceptor>singletonList(
98+
new HeaderAuthenticationInterceptor(headers));
99+
restTemplate.setRequestFactory(
100+
new InterceptingClientHttpRequestFactory(restTemplate.getRequestFactory(), interceptors));
101+
}
102+
case STATEFUL -> {
103+
logger.info("Using stateful CSRF interceptor");
104+
List<ClientHttpRequestInterceptor> interceptors =
105+
Collections.<ClientHttpRequestInterceptor>singletonList(
106+
formLoginAuthenticationCsrfTokenInterceptor);
107+
restTemplate.setRequestFactory(
108+
new InterceptingClientHttpRequestFactory(restTemplate.getRequestFactory(), interceptors));
85109
}
86-
List<ClientHttpRequestInterceptor> interceptors =
87-
Collections.<ClientHttpRequestInterceptor>singletonList(bearerTokenInterceptor);
88-
restTemplate.setRequestFactory(
89-
new InterceptingClientHttpRequestFactory(restTemplate.getRequestFactory(), interceptors));
90-
} else {
91-
logger.info("Using stateful CSRF interceptor");
92-
List<ClientHttpRequestInterceptor> interceptors =
93-
Collections.<ClientHttpRequestInterceptor>singletonList(
94-
formLoginAuthenticationCsrfTokenInterceptor);
95-
restTemplate.setRequestFactory(
96-
new InterceptingClientHttpRequestFactory(restTemplate.getRequestFactory(), interceptors));
97110
}
98111
}
99112

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.box.l10n.mojito.rest.resttemplate;
2+
3+
import java.io.IOException;
4+
import java.util.Map;
5+
import org.springframework.http.HttpRequest;
6+
import org.springframework.http.client.ClientHttpRequestExecution;
7+
import org.springframework.http.client.ClientHttpRequestInterceptor;
8+
import org.springframework.http.client.ClientHttpResponse;
9+
10+
class HeaderAuthenticationInterceptor implements ClientHttpRequestInterceptor {
11+
12+
private final Map<String, String> headers;
13+
14+
HeaderAuthenticationInterceptor(Map<String, String> headers) {
15+
this.headers = headers;
16+
}
17+
18+
@Override
19+
public ClientHttpResponse intercept(
20+
HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
21+
headers.forEach((name, value) -> {
22+
if (name != null && value != null) {
23+
request.getHeaders().set(name, value);
24+
}
25+
});
26+
return execution.execute(request, body);
27+
}
28+
}

restclient/src/main/java/com/box/l10n/mojito/rest/resttemplate/ResttemplateConfig.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.box.l10n.mojito.rest.resttemplate;
22

3+
import java.util.HashMap;
4+
import java.util.Map;
35
import java.util.Set;
46
import org.springframework.boot.context.properties.ConfigurationProperties;
57
import org.springframework.stereotype.Component;
@@ -22,6 +24,8 @@ public class ResttemplateConfig {
2224

2325
StatelessAuthentication stateless = new StatelessAuthentication();
2426

27+
HeaderAuthentication header = new HeaderAuthentication();
28+
2529
AuthenticationMode authenticationMode = AuthenticationMode.STATEFUL;
2630

2731
public static class Authentication {
@@ -63,7 +67,21 @@ public enum CredentialProvider {
6367

6468
public enum AuthenticationMode {
6569
STATEFUL,
66-
STATELESS
70+
STATELESS,
71+
HEADER
72+
}
73+
74+
public static class HeaderAuthentication {
75+
76+
Map<String, String> headers = new HashMap<>();
77+
78+
public Map<String, String> getHeaders() {
79+
return headers;
80+
}
81+
82+
public void setHeaders(Map<String, String> headers) {
83+
this.headers = headers;
84+
}
6785
}
6886

6987
public static class StatelessAuthentication {
@@ -184,6 +202,14 @@ public void setStateless(StatelessAuthentication stateless) {
184202
this.stateless = stateless;
185203
}
186204

205+
public HeaderAuthentication getHeader() {
206+
return header;
207+
}
208+
209+
public void setHeader(HeaderAuthentication header) {
210+
this.header = header;
211+
}
212+
187213
public AuthenticationMode getAuthenticationMode() {
188214
return authenticationMode;
189215
}

restclient/src/test/java/com/box/l10n/mojito/rest/resttemplate/AuthenticatedRestTemplateTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ public void before() {
5656

5757
// resets the mock server that was set inside the rest template
5858
authenticatedRestTemplate.restTemplate = new CookieStoreRestTemplate();
59+
resttemplateConfig.setAuthenticationMode(ResttemplateConfig.AuthenticationMode.STATEFUL);
60+
resttemplateConfig.getHeader().getHeaders().clear();
5961
authenticatedRestTemplate.init();
6062

6163
// if port was 0, then the server will randomize it on start up, and now we
@@ -226,6 +228,41 @@ public void testAuthRestTemplateFor401() {
226228
.withHeader("X-CSRF-TOKEN", WireMock.matching("madeup-csrf-value")));
227229
}
228230

231+
@Test
232+
public void testHeaderAuthenticationAddsConfiguredHeaders() {
233+
Map<String, String> originalHeaders =
234+
new HashMap<>(resttemplateConfig.getHeader().getHeaders());
235+
ResttemplateConfig.AuthenticationMode originalMode =
236+
resttemplateConfig.getAuthenticationMode();
237+
238+
try {
239+
resttemplateConfig.setAuthenticationMode(ResttemplateConfig.AuthenticationMode.HEADER);
240+
resttemplateConfig.getHeader().getHeaders().clear();
241+
resttemplateConfig.getHeader().getHeaders().put("CF-Access-Client-Id", "client-id");
242+
resttemplateConfig.getHeader().getHeaders().put("CF-Access-Client-Secret", "client-secret");
243+
244+
authenticatedRestTemplate.restTemplate = new CookieStoreRestTemplate();
245+
authenticatedRestTemplate.init();
246+
247+
WireMock.stubFor(
248+
WireMock.get(WireMock.urlEqualTo("/header-protected"))
249+
.willReturn(WireMock.aResponse().withStatus(HttpStatus.OK.value())));
250+
251+
authenticatedRestTemplate.getForObject("header-protected", String.class);
252+
253+
WireMock.verify(
254+
WireMock.getRequestedFor(WireMock.urlEqualTo("/header-protected"))
255+
.withHeader("CF-Access-Client-Id", WireMock.equalTo("client-id"))
256+
.withHeader("CF-Access-Client-Secret", WireMock.equalTo("client-secret")));
257+
} finally {
258+
resttemplateConfig.setAuthenticationMode(originalMode);
259+
resttemplateConfig.getHeader().getHeaders().clear();
260+
resttemplateConfig.getHeader().getHeaders().putAll(originalHeaders);
261+
authenticatedRestTemplate.restTemplate = new CookieStoreRestTemplate();
262+
authenticatedRestTemplate.init();
263+
}
264+
}
265+
229266
protected void initialAuthenticationMock() {
230267
String expectedResponse = "expected content";
231268

0 commit comments

Comments
 (0)