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

Last change on this file since 19050 was 19050, checked in by taylor.smock, 4 weeks ago

Revert most var changes from r19048, fix most new compile warnings and checkstyle issues

Also, document why various ErrorProne checks were originally disabled and fix
generic SonarLint issues.

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