Skip to content

Commit 8f230fe

Browse files
author
Mark A. Matney, Jr
authored
[SERV-454] Detect client IP address when deployed behind a reverse proxy (#40)
1 parent 74fb75c commit 8f230fe

File tree

4 files changed

+86
-10
lines changed

4 files changed

+86
-10
lines changed

src/main/java/edu/ucla/library/iiif/auth/verticles/MainVerticle.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import io.vertx.core.http.HttpServer;
3535
import io.vertx.core.http.HttpServerOptions;
3636
import io.vertx.core.json.JsonObject;
37+
import io.vertx.ext.web.AllowForwardHeaders;
3738
import io.vertx.ext.web.Router;
3839
import io.vertx.ext.web.handler.APIKeyHandler;
3940
import io.vertx.ext.web.handler.ErrorHandler;
@@ -162,6 +163,9 @@ public Future<Router> createRouter(final JsonObject aConfig) {
162163
.failureHandler(new AdminAuthenticationErrorHandler()) //
163164
.failureHandler(new HtmlRenderingErrorHandler());
164165

166+
// Enable deployment behind a reverse proxy
167+
router.allowForward(AllowForwardHeaders.X_FORWARD);
168+
165169
return Future.succeededFuture(router);
166170
});
167171
}

src/test/java/edu/ucla/library/iiif/auth/handlers/AbstractHandlerIT.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import edu.ucla.library.iiif.auth.services.DatabaseService;
1818
import edu.ucla.library.iiif.auth.utils.TestUtils;
1919

20+
import info.freelibrary.util.StringUtils;
21+
2022
import io.vertx.config.ConfigRetriever;
2123
import io.vertx.core.CompositeFuture;
2224
import io.vertx.core.Future;
@@ -64,6 +66,27 @@ public abstract class AbstractHandlerIT {
6466
*/
6567
protected static final String POST_ITEMS_PATH = "/items";
6668

69+
/**
70+
* The name of the HTTP request header used by the reverse proxy to carry the client IP address.
71+
*/
72+
protected static final String X_FORWARDED_FOR = "X-Forwarded-For";
73+
74+
/**
75+
* The fictitious client IP address that we'll pretend a reverse proxy sent through.
76+
*/
77+
protected static final String FORWARDED_CLIENT_IP = "10.1.1.1";
78+
79+
/**
80+
* The fictitious proxy IP address that we'll pretend a reverse proxy sent through.
81+
*/
82+
protected static final String FORWARDED_PROXY_IP = "10.2.2.2";
83+
84+
/**
85+
* The value to use for the {@link CLIENT_IP_HEADER} header.
86+
*/
87+
protected static final String FORWARDED_IP_ADDRESSES =
88+
StringUtils.format("{}, {}", FORWARDED_CLIENT_IP, FORWARDED_PROXY_IP);
89+
6790
/**
6891
* A test id for an item with open access.
6992
*/

src/test/java/edu/ucla/library/iiif/auth/handlers/AccessCookieHandlerIT.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
import java.net.URLEncoder;
77
import java.nio.charset.StandardCharsets;
88

9+
import org.jsoup.Jsoup;
910
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.params.ParameterizedTest;
12+
import org.junit.jupiter.params.provider.ValueSource;
1013

1114
import edu.ucla.library.iiif.auth.utils.MediaType;
1215
import edu.ucla.library.iiif.auth.utils.TestConstants;
@@ -27,20 +30,32 @@ public final class AccessCookieHandlerIT extends AbstractHandlerIT {
2730
/**
2831
* Tests that a client can obtain an access cookie.
2932
*
33+
* @param aReverseProxyDeployment Whether or not to simulate app deployment behind a reverse proxy
3034
* @param aVertx A Vert.x instance
3135
* @param aContext A test context
3236
*/
33-
@Test
34-
public void testGetCookie(final Vertx aVertx, final VertxTestContext aContext) {
37+
@ParameterizedTest
38+
@ValueSource(booleans = { true, false })
39+
public void testGetCookie(final boolean aReverseProxyDeployment, final Vertx aVertx,
40+
final VertxTestContext aContext) {
3541
final String requestURI =
3642
StringUtils.format(GET_COOKIE_PATH, URLEncoder.encode(TEST_ORIGIN, StandardCharsets.UTF_8));
3743
final HttpRequest<?> getCookie = myWebClient.get(myPort, TestConstants.INADDR_ANY, requestURI);
3844

45+
if (aReverseProxyDeployment) {
46+
getCookie.putHeader(X_FORWARDED_FOR, FORWARDED_IP_ADDRESSES);
47+
}
48+
3949
getCookie.send().onSuccess(response -> {
4050
assertEquals(HTTP.OK, response.statusCode());
4151
assertEquals(MediaType.TEXT_HTML.toString(), response.headers().get(HttpHeaders.CONTENT_TYPE));
4252
assertEquals(1, response.cookies().size());
4353

54+
if (aReverseProxyDeployment) {
55+
assertEquals(FORWARDED_CLIENT_IP,
56+
Jsoup.parse(response.bodyAsString()).getElementById("client-ip-address").text());
57+
}
58+
4459
aContext.completeNow();
4560
}).onFailure(aContext::failNow);
4661
}

src/test/java/edu/ucla/library/iiif/auth/handlers/AccessTokenHandlerIT.java

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
import org.jsoup.Jsoup;
1515
import org.junit.jupiter.api.Test;
16+
import org.junit.jupiter.params.ParameterizedTest;
17+
import org.junit.jupiter.params.provider.ValueSource;
1618

1719
import edu.ucla.library.iiif.auth.AccessTokenError;
1820
import edu.ucla.library.iiif.auth.Config;
@@ -51,26 +53,42 @@ public final class AccessTokenHandlerIT extends AbstractAccessTokenHandlerIT {
5153
/**
5254
* Tests that a browser client can use a valid access cookie to obtain an access token.
5355
*
56+
* @param aReverseProxyDeployment Whether or not to simulate app deployment behind a reverse proxy
5457
* @param aVertx A Vert.x instance
5558
* @param aContext A test context
5659
*/
57-
@Test
58-
public void testGetTokenBrowser(final Vertx aVertx, final VertxTestContext aContext) {
60+
@ParameterizedTest
61+
@ValueSource(booleans = { true, false })
62+
public void testGetTokenBrowser(final boolean aReverseProxyDeployment, final Vertx aVertx,
63+
final VertxTestContext aContext) {
5964
final String getCookieRequestURI =
6065
StringUtils.format(GET_COOKIE_PATH, URLEncoder.encode(TEST_ORIGIN, StandardCharsets.UTF_8));
6166
final HttpRequest<?> getCookie = myWebClient.get(myPort, TestConstants.INADDR_ANY, getCookieRequestURI);
6267

68+
if (aReverseProxyDeployment) {
69+
getCookie.putHeader(X_FORWARDED_FOR, FORWARDED_IP_ADDRESSES);
70+
}
71+
6372
getCookie.send().compose(result -> {
6473
final String cookieHeader = result.cookies().get(0);
6574
final String cookieValue = cookieHeader.split(EQUALS)[1];
66-
final String clientIpAddress =
67-
Jsoup.parse(result.bodyAsString()).getElementById(myClientIpAddressID).text();
75+
final String clientIpAddress;
76+
77+
if (aReverseProxyDeployment) {
78+
clientIpAddress = FORWARDED_CLIENT_IP;
79+
} else {
80+
clientIpAddress = Jsoup.parse(result.bodyAsString()).getElementById(myClientIpAddressID).text();
81+
}
6882

6983
return myAccessCookieService.decryptCookie(cookieValue, clientIpAddress).compose(cookie -> {
7084
final String getTokenRequestURI = StringUtils.format(GET_TOKEN_PATH, myGetTokenRequestQuery);
7185
final HttpRequest<?> getToken = myWebClient.get(myPort, TestConstants.INADDR_ANY, getTokenRequestURI)
7286
.putHeader(HttpHeaders.COOKIE.toString(), cookieHeader);
7387

88+
if (aReverseProxyDeployment) {
89+
getToken.putHeader(X_FORWARDED_FOR, FORWARDED_IP_ADDRESSES);
90+
}
91+
7492
return getToken.send().onSuccess(response -> {
7593
final JsonObject expectedAccessTokenDecoded =
7694
new JsonObject().put(TokenJsonKeys.VERSION, myConfig.getString(Config.HAUTH_VERSION)).put(
@@ -106,26 +124,42 @@ public void testGetTokenBrowser(final Vertx aVertx, final VertxTestContext aCont
106124
/**
107125
* Tests that a non-browser client can use a valid access cookie to obtain an access token.
108126
*
127+
* @param aReverseProxyDeployment Whether or not to simulate app deployment behind a reverse proxy
109128
* @param aVertx A Vert.x instance
110129
* @param aContext A test context
111130
*/
112-
@Test
113-
public void testGetTokenNonBrowser(final Vertx aVertx, final VertxTestContext aContext) {
131+
@ParameterizedTest
132+
@ValueSource(booleans = { true, false })
133+
public void testGetTokenNonBrowser(final boolean aReverseProxyDeployment, final Vertx aVertx,
134+
final VertxTestContext aContext) {
114135
final String getCookieRequestURI =
115136
StringUtils.format(GET_COOKIE_PATH, URLEncoder.encode(TEST_ORIGIN, StandardCharsets.UTF_8));
116137
final HttpRequest<?> getCookie = myWebClient.get(myPort, TestConstants.INADDR_ANY, getCookieRequestURI);
117138

139+
if (aReverseProxyDeployment) {
140+
getCookie.putHeader(X_FORWARDED_FOR, FORWARDED_IP_ADDRESSES);
141+
}
142+
118143
getCookie.send().compose(result -> {
119144
final String cookieHeader = result.cookies().get(0);
120145
final String cookieValue = cookieHeader.split(EQUALS)[1];
121-
final String clientIpAddress =
122-
Jsoup.parse(result.bodyAsString()).getElementById(myClientIpAddressID).text();
146+
final String clientIpAddress;
147+
148+
if (aReverseProxyDeployment) {
149+
clientIpAddress = FORWARDED_CLIENT_IP;
150+
} else {
151+
clientIpAddress = Jsoup.parse(result.bodyAsString()).getElementById(myClientIpAddressID).text();
152+
}
123153

124154
return myAccessCookieService.decryptCookie(cookieValue, clientIpAddress).compose(cookie -> {
125155
final String getTokenRequestURI = StringUtils.format(GET_TOKEN_PATH, EMPTY);
126156
final HttpRequest<?> getToken = myWebClient.get(myPort, TestConstants.INADDR_ANY, getTokenRequestURI)
127157
.putHeader(HttpHeaders.COOKIE.toString(), cookieHeader);
128158

159+
if (aReverseProxyDeployment) {
160+
getToken.putHeader(X_FORWARDED_FOR, FORWARDED_IP_ADDRESSES);
161+
}
162+
129163
return getToken.send().onSuccess(response -> {
130164
final JsonObject expectedAccessTokenDecoded =
131165
new JsonObject().put(TokenJsonKeys.VERSION, myConfig.getString(Config.HAUTH_VERSION)).put(

0 commit comments

Comments
 (0)