source: josm/trunk/test/unit/org/openstreetmap/josm/data/oauth/OAuth20AuthorizationTest.java@ 18786

Last change on this file since 18786 was 18786, checked in by taylor.smock, 10 months ago

Fix #23083: SocketTimeoutException in OAuth20Authorization$OAuth20AuthorizationHandler.handleRequest

File size: 12.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.oauth;
3
4import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
5import static org.junit.jupiter.api.Assertions.assertEquals;
6import static org.junit.jupiter.api.Assertions.assertNotNull;
7import static org.junit.jupiter.api.Assertions.assertNull;
8import static org.junit.jupiter.api.Assertions.assertTrue;
9
10import java.io.IOException;
11import java.net.URL;
12import java.util.HashMap;
13import java.util.Map;
14import java.util.Optional;
15import java.util.concurrent.atomic.AtomicReference;
16import java.util.stream.Collectors;
17import java.util.stream.Stream;
18
19import com.github.tomakehurst.wiremock.client.WireMock;
20import com.github.tomakehurst.wiremock.common.FileSource;
21import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
22import com.github.tomakehurst.wiremock.extension.Parameters;
23import com.github.tomakehurst.wiremock.extension.ResponseTransformer;
24import com.github.tomakehurst.wiremock.http.FixedDelayDistribution;
25import com.github.tomakehurst.wiremock.http.HttpHeader;
26import com.github.tomakehurst.wiremock.http.HttpHeaders;
27import com.github.tomakehurst.wiremock.http.QueryParameter;
28import com.github.tomakehurst.wiremock.http.Request;
29import com.github.tomakehurst.wiremock.http.Response;
30import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
31import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
32import com.github.tomakehurst.wiremock.matching.AnythingPattern;
33import com.github.tomakehurst.wiremock.matching.EqualToPattern;
34import com.github.tomakehurst.wiremock.matching.StringValuePattern;
35import mockit.Mock;
36import mockit.MockUp;
37import org.junit.jupiter.api.AfterEach;
38import org.junit.jupiter.api.BeforeEach;
39import org.junit.jupiter.api.Test;
40import org.junit.jupiter.api.extension.RegisterExtension;
41import org.openstreetmap.josm.data.oauth.osm.OsmScopes;
42import org.openstreetmap.josm.data.preferences.JosmUrls;
43import org.openstreetmap.josm.io.OsmApi;
44import org.openstreetmap.josm.io.remotecontrol.RemoteControl;
45import org.openstreetmap.josm.spi.preferences.Config;
46import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
47import org.openstreetmap.josm.testutils.annotations.HTTP;
48import org.openstreetmap.josm.testutils.mockers.OpenBrowserMocker;
49import org.openstreetmap.josm.tools.HttpClient;
50import org.openstreetmap.josm.tools.Logging;
51
52@BasicPreferences
53@HTTP
54class OAuth20AuthorizationTest {
55 private static final String RESPONSE_TYPE = "response_type";
56 private static final String RESPONSE_TYPE_VALUE = "code";
57 private static final String CLIENT_ID = "client_id";
58 private static final String CLIENT_ID_VALUE = "edPII614Lm0_0zEpc_QzEltA9BUll93-Y-ugRQUoHMI";
59 private static final String REDIRECT_URI = "redirect_uri";
60 private static final String REDIRECT_URI_VALUE = "http://127.0.0.1:8111/oauth_authorization";
61 private static final String SCOPE = "scope";
62 private static final String STATE = "state";
63 private static final String CODE_CHALLENGE_METHOD = "code_challenge_method";
64 private static final String CODE_CHALLENGE_METHOD_VALUE = "S256";
65 private static final String CODE_CHALLENGE = "code_challenge";
66
67 private enum ConnectionProblems {
68 NONE,
69 SOCKET_TIMEOUT
70 }
71
72 private static class OAuthServerWireMock extends ResponseTransformer {
73 String stateToReturn;
74 ConnectionProblems connectionProblems = ConnectionProblems.NONE;
75 @Override
76 public Response transform(Request request, Response response, FileSource files, Parameters parameters) {
77 try {
78 if (request.getUrl().startsWith("/oauth2/authorize")) {
79 return authorizationRequest(request, response);
80 } else if (request.getUrl().startsWith("/oauth2/token")) {
81 return tokenRequest(request, response);
82 }
83 return response;
84 } catch (Exception e) {
85 // Make certain we actually see the exception in logs -- WireMock returns the error, but then our code needs to print it
86 Logging.error(e);
87 throw e;
88 }
89 }
90
91 private Response tokenRequest(Request request, Response response) {
92 Map<String, String> queryParameters = Stream.of(request.getBodyAsString().split("&", -1))
93 .map(string -> string.split("=", -1))
94 .collect(Collectors.toMap(strings -> strings[0], strings -> strings[1]));
95 if (!queryParameters.containsKey("grant_type")
96 || !queryParameters.containsKey(REDIRECT_URI) || !queryParameters.containsKey(CLIENT_ID)
97 || !queryParameters.containsKey("code") || !queryParameters.containsKey("code_verifier")) {
98 return Response.Builder.like(response).but().status(500).build();
99 }
100 switch (connectionProblems) {
101 case SOCKET_TIMEOUT:
102 return Response.Builder.like(response).but().configureDelay(null, null,
103 10_000, new FixedDelayDistribution(0)).build();
104 case NONE:
105 default:
106 return Response.Builder.like(response).but()
107 .body("{\"token_type\": \"bearer\", \"access_token\": \"test_access_token\"}").build();
108 }
109 }
110
111 private Response authorizationRequest(Request request, Response response) {
112 final QueryParameter state = request.queryParameter(STATE);
113 final QueryParameter codeChallenge = request.queryParameter(CODE_CHALLENGE);
114 final QueryParameter redirectUri = request.queryParameter(REDIRECT_URI);
115 final QueryParameter responseType = request.queryParameter(RESPONSE_TYPE);
116 final QueryParameter scope = request.queryParameter(SCOPE);
117 final QueryParameter clientId = request.queryParameter(CLIENT_ID);
118 final QueryParameter codeChallengeMethod = request.queryParameter(CODE_CHALLENGE_METHOD);
119 final boolean badRequest = !(state.isPresent() && state.isSingleValued());
120 if (badRequest || checkQueryParameter(redirectUri, REDIRECT_URI_VALUE) || checkQueryParameter(responseType, RESPONSE_TYPE_VALUE)
121 || checkQueryParameter(clientId, CLIENT_ID_VALUE) || checkQueryParameter(codeChallengeMethod, CODE_CHALLENGE_METHOD_VALUE)
122 || checkQueryParameter(scope, "read_gpx")
123 || !codeChallenge.isPresent()) {
124 return Response.Builder.like(response).but().status(500).build();
125 }
126 return Response.Builder.like(response).but().status(307)
127 .headers(new HttpHeaders(new HttpHeader("Location",
128 redirectUri.values().get(0)
129 + "?state=" + (this.stateToReturn != null ? stateToReturn : state.firstValue())
130 + "&code=test_code"))).build();
131 }
132
133 private static boolean checkQueryParameter(QueryParameter parameter, String expected) {
134 return !parameter.isPresent() || !parameter.isSingleValued() || !parameter.containsValue(expected);
135 }
136
137 @Override
138 public String getName() {
139 return "OAuthServerWireMock";
140 }
141 }
142
143 private static final OAuthServerWireMock oauthServer = new OAuthServerWireMock();
144 @RegisterExtension
145 static WireMockExtension wml = WireMockExtension.newInstance()
146 .options(WireMockConfiguration.wireMockConfig().dynamicPort().dynamicHttpsPort().extensions(oauthServer))
147 .build();
148 @BeforeEach
149 @AfterEach
150 void setup() {
151 // Reset the mocker
152 OpenBrowserMocker.getCalledURIs().clear();
153 RemoteControl.stop(); // Ensure remote control is stopped
154 oauthServer.stateToReturn = null;
155 oauthServer.connectionProblems = ConnectionProblems.NONE;
156 }
157
158 /**
159 * Set up the default wiremock information
160 * @param wireMockRuntimeInfo The info to set up
161 */
162 @BeforeEach
163 void setupWireMock(WireMockRuntimeInfo wireMockRuntimeInfo) {
164 Config.getPref().put("osm-server.url", wireMockRuntimeInfo.getHttpBaseUrl() + "/api/");
165 new MockUp<JosmUrls>() {
166 @Mock
167 public String getDefaultOsmApiUrl() {
168 return wireMockRuntimeInfo.getHttpBaseUrl() + "/api/";
169 }
170 };
171 new OpenBrowserMocker();
172 final Map<String, StringValuePattern> queryParams = new HashMap<>();
173 queryParams.put(RESPONSE_TYPE, new EqualToPattern(RESPONSE_TYPE_VALUE));
174 queryParams.put(CLIENT_ID, new EqualToPattern(CLIENT_ID_VALUE));
175 queryParams.put(REDIRECT_URI, new EqualToPattern(REDIRECT_URI_VALUE));
176 queryParams.put(SCOPE, new EqualToPattern("read_gpx"));
177 queryParams.put(STATE, new AnythingPattern()); // This is generated via a random UUID, and we have to return this in the redirect
178 queryParams.put(CODE_CHALLENGE_METHOD, new EqualToPattern(CODE_CHALLENGE_METHOD_VALUE));
179 queryParams.put(CODE_CHALLENGE, new AnythingPattern()); // This is generated via a random UUID
180 wireMockRuntimeInfo.getWireMock().register(WireMock.get(WireMock.urlPathEqualTo("/oauth2/authorize")).withQueryParams(queryParams));
181 wireMockRuntimeInfo.getWireMock().register(WireMock.post(WireMock.urlPathEqualTo("/oauth2/token")));
182 }
183
184 private HttpClient generateClient(WireMockRuntimeInfo wireMockRuntimeInfo, AtomicReference<Optional<IOAuthToken>> consumer) {
185 final OAuth20Authorization authorization = new OAuth20Authorization();
186 OAuth20Parameters parameters = (OAuth20Parameters) OAuthParameters.createDefault(OsmApi.getOsmApi().getBaseUrl(), OAuthVersion.OAuth20);
187 RemoteControl.start();
188 authorization.authorize(new OAuth20Parameters(parameters.getClientId(), parameters.getClientSecret(),
189 wireMockRuntimeInfo.getHttpBaseUrl() + "/oauth2", wireMockRuntimeInfo.getHttpBaseUrl() + "/api",
190 parameters.getRedirectUri()), consumer::set, OsmScopes.read_gpx);
191 assertEquals(1, OpenBrowserMocker.getCalledURIs().size());
192 final URL url = assertDoesNotThrow(() -> OpenBrowserMocker.getCalledURIs().get(0).toURL());
193 return HttpClient.create(url);
194 }
195
196 @Test
197 void testAuthorize(WireMockRuntimeInfo wireMockRuntimeInfo) throws IOException {
198 final AtomicReference<Optional<IOAuthToken>> consumer = new AtomicReference<>();
199 final HttpClient client = generateClient(wireMockRuntimeInfo, consumer);
200 try {
201 HttpClient.Response response = client.connect();
202 assertEquals(200, response.getResponseCode());
203 } finally {
204 client.disconnect();
205 }
206 assertNotNull(consumer.get());
207 assertTrue(consumer.get().isPresent());
208 assertEquals(OAuthVersion.OAuth20, consumer.get().get().getOAuthType());
209 OAuth20Token token = (OAuth20Token) consumer.get().get();
210 assertEquals("test_access_token", token.getBearerToken());
211 }
212
213 @Test
214 void testAuthorizeBadState(WireMockRuntimeInfo wireMockRuntimeInfo) throws IOException {
215 oauthServer.stateToReturn = "Bad_State";
216 final AtomicReference<Optional<IOAuthToken>> consumer = new AtomicReference<>();
217 final HttpClient client = generateClient(wireMockRuntimeInfo, consumer);
218 try {
219 HttpClient.Response response = client.connect();
220 assertEquals(400, response.getResponseCode());
221 String content = response.fetchContent();
222 assertTrue(content.contains("Unknown state for authorization"));
223 } finally {
224 client.disconnect();
225 }
226 assertNull(consumer.get(), "The OAuth consumer should not be called since the state does not match");
227 }
228
229 @Test
230 void testSocketTimeout(WireMockRuntimeInfo wireMockRuntimeInfo) throws Exception {
231 // 1s before timeout
232 Config.getPref().putInt("socket.timeout.connect", 1);
233 Config.getPref().putInt("socket.timeout.read", 1);
234 oauthServer.connectionProblems = ConnectionProblems.SOCKET_TIMEOUT;
235
236 final AtomicReference<Optional<IOAuthToken>> consumer = new AtomicReference<>();
237 final HttpClient client = generateClient(wireMockRuntimeInfo, consumer)
238 .setConnectTimeout(15_000).setReadTimeout(30_000);
239 try {
240 HttpClient.Response response = client.connect();
241 assertEquals(500, response.getResponseCode());
242 String content = response.fetchContent();
243 assertTrue(content.contains("java.net.SocketTimeoutException: Read timed out"));
244 } finally {
245 client.disconnect();
246 }
247 assertEquals(Optional.empty(), consumer.get());
248 }
249}
Note: See TracBrowser for help on using the repository browser.