source: josm/trunk/src/org/openstreetmap/josm/gui/oauth/OsmOAuthAuthorizationClient.java@ 17196

Last change on this file since 17196 was 17196, checked in by simon04, 4 years ago

see #15102 - see #16637 - get rid of real HTTP calls to https://www.openstreetmap.org/login in OsmOAuthAuthorizationClientTest, mock them

  • Property svn:eol-style set to native
File size: 19.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.oauth;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.BufferedReader;
7import java.io.IOException;
8import java.net.CookieHandler;
9import java.net.HttpURLConnection;
10import java.net.URISyntaxException;
11import java.net.URL;
12import java.nio.charset.StandardCharsets;
13import java.util.Collections;
14import java.util.HashMap;
15import java.util.Iterator;
16import java.util.List;
17import java.util.Map;
18import java.util.Map.Entry;
19import java.util.regex.Matcher;
20import java.util.regex.Pattern;
21
22import org.openstreetmap.josm.data.oauth.OAuthParameters;
23import org.openstreetmap.josm.data.oauth.OAuthToken;
24import org.openstreetmap.josm.data.oauth.OsmPrivileges;
25import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
26import org.openstreetmap.josm.gui.progress.ProgressMonitor;
27import org.openstreetmap.josm.io.OsmTransferCanceledException;
28import org.openstreetmap.josm.tools.CheckParameterUtil;
29import org.openstreetmap.josm.tools.HttpClient;
30import org.openstreetmap.josm.tools.Logging;
31import org.openstreetmap.josm.tools.Utils;
32
33import oauth.signpost.OAuth;
34import oauth.signpost.OAuthConsumer;
35import oauth.signpost.OAuthProvider;
36import oauth.signpost.exception.OAuthException;
37
38/**
39 * An OAuth 1.0 authorization client.
40 * @since 2746
41 */
42public class OsmOAuthAuthorizationClient {
43 private final OAuthParameters oauthProviderParameters;
44 private final OAuthConsumer consumer;
45 private final OAuthProvider provider;
46 private boolean canceled;
47 private HttpClient connection;
48
49 protected static class SessionId {
50 protected String id;
51 protected String token;
52 protected String userName;
53 }
54
55 /**
56 * Creates a new authorisation client with the parameters <code>parameters</code>.
57 *
58 * @param parameters the OAuth parameters. Must not be null.
59 * @throws IllegalArgumentException if parameters is null
60 */
61 public OsmOAuthAuthorizationClient(OAuthParameters parameters) {
62 CheckParameterUtil.ensureParameterNotNull(parameters, "parameters");
63 oauthProviderParameters = new OAuthParameters(parameters);
64 consumer = oauthProviderParameters.buildConsumer();
65 provider = oauthProviderParameters.buildProvider(consumer);
66 }
67
68 /**
69 * Creates a new authorisation client with the parameters <code>parameters</code>
70 * and an already known Request Token.
71 *
72 * @param parameters the OAuth parameters. Must not be null.
73 * @param requestToken the request token. Must not be null.
74 * @throws IllegalArgumentException if parameters is null
75 * @throws IllegalArgumentException if requestToken is null
76 */
77 public OsmOAuthAuthorizationClient(OAuthParameters parameters, OAuthToken requestToken) {
78 CheckParameterUtil.ensureParameterNotNull(parameters, "parameters");
79 oauthProviderParameters = new OAuthParameters(parameters);
80 consumer = oauthProviderParameters.buildConsumer();
81 provider = oauthProviderParameters.buildProvider(consumer);
82 consumer.setTokenWithSecret(requestToken.getKey(), requestToken.getSecret());
83 }
84
85 /**
86 * Cancels the current OAuth operation.
87 */
88 public void cancel() {
89 canceled = true;
90 synchronized (this) {
91 if (connection != null) {
92 connection.disconnect();
93 }
94 }
95 }
96
97 /**
98 * Submits a request for a Request Token to the Request Token Endpoint Url of the OAuth Service
99 * Provider and replies the request token.
100 *
101 * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null
102 * @return the OAuth Request Token
103 * @throws OsmOAuthAuthorizationException if something goes wrong when retrieving the request token
104 * @throws OsmTransferCanceledException if the user canceled the request
105 */
106 public OAuthToken getRequestToken(ProgressMonitor monitor) throws OsmOAuthAuthorizationException, OsmTransferCanceledException {
107 if (monitor == null) {
108 monitor = NullProgressMonitor.INSTANCE;
109 }
110 try {
111 monitor.beginTask("");
112 monitor.indeterminateSubTask(tr("Retrieving OAuth Request Token from ''{0}''", oauthProviderParameters.getRequestTokenUrl()));
113 provider.retrieveRequestToken(consumer, "");
114 return OAuthToken.createToken(consumer);
115 } catch (OAuthException e) {
116 if (canceled)
117 throw new OsmTransferCanceledException(e);
118 throw new OsmOAuthAuthorizationException(e);
119 } finally {
120 monitor.finishTask();
121 }
122 }
123
124 /**
125 * Submits a request for an Access Token to the Access Token Endpoint Url of the OAuth Service
126 * Provider and replies the request token.
127 *
128 * You must have requested a Request Token using {@link #getRequestToken(ProgressMonitor)} first.
129 *
130 * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null
131 * @return the OAuth Access Token
132 * @throws OsmOAuthAuthorizationException if something goes wrong when retrieving the request token
133 * @throws OsmTransferCanceledException if the user canceled the request
134 * @see #getRequestToken(ProgressMonitor)
135 */
136 public OAuthToken getAccessToken(ProgressMonitor monitor) throws OsmOAuthAuthorizationException, OsmTransferCanceledException {
137 if (monitor == null) {
138 monitor = NullProgressMonitor.INSTANCE;
139 }
140 try {
141 monitor.beginTask("");
142 monitor.indeterminateSubTask(tr("Retrieving OAuth Access Token from ''{0}''", oauthProviderParameters.getAccessTokenUrl()));
143 provider.retrieveAccessToken(consumer, null);
144 return OAuthToken.createToken(consumer);
145 } catch (OAuthException e) {
146 if (canceled)
147 throw new OsmTransferCanceledException(e);
148 throw new OsmOAuthAuthorizationException(e);
149 } finally {
150 monitor.finishTask();
151 }
152 }
153
154 /**
155 * Builds the authorise URL for a given Request Token. Users can be redirected to this URL.
156 * There they can login to OSM and authorise the request.
157 *
158 * @param requestToken the request token
159 * @return the authorise URL for this request
160 */
161 public String getAuthoriseUrl(OAuthToken requestToken) {
162 StringBuilder sb = new StringBuilder(32);
163
164 // OSM is an OAuth 1.0 provider and JOSM isn't a web app. We just add the oauth request token to
165 // the authorisation request, no callback parameter.
166 //
167 sb.append(oauthProviderParameters.getAuthoriseUrl()).append('?'+OAuth.OAUTH_TOKEN+'=').append(requestToken.getKey());
168 return sb.toString();
169 }
170
171 protected String extractToken() {
172 try (BufferedReader r = connection.getResponse().getContentReader()) {
173 String c;
174 Pattern p = Pattern.compile(".*authenticity_token.*value=\"([^\"]+)\".*");
175 while ((c = r.readLine()) != null) {
176 Matcher m = p.matcher(c);
177 if (m.find()) {
178 return m.group(1);
179 }
180 }
181 } catch (IOException e) {
182 Logging.error(e);
183 return null;
184 }
185 Logging.warn("No authenticity_token found in response!");
186 return null;
187 }
188
189 protected SessionId extractOsmSession() throws IOException, URISyntaxException {
190 // response headers might not contain the cookie, see #12584
191 final List<String> setCookies = CookieHandler.getDefault()
192 .get(connection.getURL().toURI(), Collections.<String, List<String>>emptyMap())
193 .get("Cookie");
194 if (setCookies == null) {
195 Logging.warn("No 'Set-Cookie' in response header!");
196 return null;
197 }
198
199 for (String setCookie: setCookies) {
200 String[] kvPairs = setCookie.split(";", -1);
201 if (kvPairs.length == 0) {
202 continue;
203 }
204 for (String kvPair : kvPairs) {
205 kvPair = kvPair.trim();
206 String[] kv = kvPair.split("=", -1);
207 if (kv.length != 2) {
208 continue;
209 }
210 if ("_osm_session".equals(kv[0])) {
211 // osm session cookie found
212 String token = extractToken();
213 if (token == null)
214 return null;
215 SessionId si = new SessionId();
216 si.id = kv[1];
217 si.token = token;
218 return si;
219 }
220 }
221 }
222 Logging.warn("No suitable 'Set-Cookie' in response header found! {0}", setCookies);
223 return null;
224 }
225
226 protected static String buildPostRequest(Map<String, String> parameters) {
227 StringBuilder sb = new StringBuilder(32);
228
229 for (Iterator<Entry<String, String>> it = parameters.entrySet().iterator(); it.hasNext();) {
230 Entry<String, String> entry = it.next();
231 String value = entry.getValue();
232 value = (value == null) ? "" : value;
233 sb.append(entry.getKey()).append('=').append(Utils.encodeUrl(value));
234 if (it.hasNext()) {
235 sb.append('&');
236 }
237 }
238 return sb.toString();
239 }
240
241 /**
242 * Submits a request to the OSM website for a login form. The OSM website replies a session ID in
243 * a cookie.
244 *
245 * @return the session ID structure
246 * @throws OsmOAuthAuthorizationException if something went wrong
247 */
248 protected SessionId fetchOsmWebsiteSessionId() throws OsmOAuthAuthorizationException {
249 try {
250 final URL url = new URL(oauthProviderParameters.getOsmLoginUrl() + "?cookie_test=true");
251 synchronized (this) {
252 connection = HttpClient.create(url).useCache(false);
253 connection.connect();
254 }
255 SessionId sessionId = extractOsmSession();
256 if (sessionId == null)
257 throw new OsmOAuthAuthorizationException(
258 tr("OSM website did not return a session cookie in response to ''{0}'',", url.toString()));
259 return sessionId;
260 } catch (IOException | URISyntaxException e) {
261 throw new OsmOAuthAuthorizationException(e);
262 } finally {
263 synchronized (this) {
264 connection = null;
265 }
266 }
267 }
268
269 /**
270 * Submits a request to the OSM website for a OAuth form. The OSM website replies a session token in
271 * a hidden parameter.
272 * @param sessionId session id
273 * @param requestToken request token
274 *
275 * @throws OsmOAuthAuthorizationException if something went wrong
276 */
277 protected void fetchOAuthToken(SessionId sessionId, OAuthToken requestToken) throws OsmOAuthAuthorizationException {
278 try {
279 URL url = new URL(getAuthoriseUrl(requestToken));
280 synchronized (this) {
281 connection = HttpClient.create(url)
282 .useCache(false)
283 .setHeader("Cookie", "_osm_session=" + sessionId.id + "; _osm_username=" + sessionId.userName);
284 connection.connect();
285 }
286 sessionId.token = extractToken();
287 if (sessionId.token == null)
288 throw new OsmOAuthAuthorizationException(tr("OSM website did not return a session cookie in response to ''{0}'',",
289 url.toString()));
290 } catch (IOException e) {
291 throw new OsmOAuthAuthorizationException(e);
292 } finally {
293 synchronized (this) {
294 connection = null;
295 }
296 }
297 }
298
299 protected void authenticateOsmSession(SessionId sessionId, String userName, String password) throws OsmLoginFailedException {
300 try {
301 final URL url = new URL(oauthProviderParameters.getOsmLoginUrl());
302 final HttpClient client = HttpClient.create(url, "POST").useCache(false);
303
304 Map<String, String> parameters = new HashMap<>();
305 parameters.put("username", userName);
306 parameters.put("password", password);
307 parameters.put("referer", "/");
308 parameters.put("commit", "Login");
309 parameters.put("authenticity_token", sessionId.token);
310 client.setRequestBody(buildPostRequest(parameters).getBytes(StandardCharsets.UTF_8));
311
312 client.setHeader("Content-Type", "application/x-www-form-urlencoded");
313 client.setHeader("Cookie", "_osm_session=" + sessionId.id);
314 // make sure we can catch 302 Moved Temporarily below
315 client.setMaxRedirects(-1);
316
317 synchronized (this) {
318 connection = client;
319 connection.connect();
320 }
321
322 // after a successful login the OSM website sends a redirect to a follow up page. Everything
323 // else, including a 200 OK, is a failed login. A 200 OK is replied if the login form with
324 // an error page is sent to back to the user.
325 //
326 int retCode = connection.getResponse().getResponseCode();
327 if (retCode != HttpURLConnection.HTTP_MOVED_TEMP)
328 throw new OsmOAuthAuthorizationException(tr("Failed to authenticate user ''{0}'' with password ''***'' as OAuth user",
329 userName));
330 } catch (OsmOAuthAuthorizationException | IOException e) {
331 throw new OsmLoginFailedException(e);
332 } finally {
333 synchronized (this) {
334 connection = null;
335 }
336 }
337 }
338
339 protected void logoutOsmSession(SessionId sessionId) throws OsmOAuthAuthorizationException {
340 try {
341 URL url = new URL(oauthProviderParameters.getOsmLogoutUrl());
342 synchronized (this) {
343 connection = HttpClient.create(url).setMaxRedirects(-1);
344 connection.connect();
345 }
346 } catch (IOException e) {
347 throw new OsmOAuthAuthorizationException(e);
348 } finally {
349 synchronized (this) {
350 connection = null;
351 }
352 }
353 }
354
355 protected void sendAuthorisationRequest(SessionId sessionId, OAuthToken requestToken, OsmPrivileges privileges)
356 throws OsmOAuthAuthorizationException {
357 Map<String, String> parameters = new HashMap<>();
358 fetchOAuthToken(sessionId, requestToken);
359 parameters.put("oauth_token", requestToken.getKey());
360 parameters.put("oauth_callback", "");
361 parameters.put("authenticity_token", sessionId.token);
362 if (privileges.isAllowWriteApi()) {
363 parameters.put("allow_write_api", "yes");
364 }
365 if (privileges.isAllowWriteGpx()) {
366 parameters.put("allow_write_gpx", "yes");
367 }
368 if (privileges.isAllowReadGpx()) {
369 parameters.put("allow_read_gpx", "yes");
370 }
371 if (privileges.isAllowWritePrefs()) {
372 parameters.put("allow_write_prefs", "yes");
373 }
374 if (privileges.isAllowReadPrefs()) {
375 parameters.put("allow_read_prefs", "yes");
376 }
377 if (privileges.isAllowModifyNotes()) {
378 parameters.put("allow_write_notes", "yes");
379 }
380
381 parameters.put("commit", "Save changes");
382
383 String request = buildPostRequest(parameters);
384 try {
385 URL url = new URL(oauthProviderParameters.getAuthoriseUrl());
386 final HttpClient client = HttpClient.create(url, "POST").useCache(false);
387 client.setHeader("Content-Type", "application/x-www-form-urlencoded");
388 client.setHeader("Cookie", "_osm_session=" + sessionId.id + "; _osm_username=" + sessionId.userName);
389 client.setMaxRedirects(-1);
390 client.setRequestBody(request.getBytes(StandardCharsets.UTF_8));
391
392 synchronized (this) {
393 connection = client;
394 connection.connect();
395 }
396
397 int retCode = connection.getResponse().getResponseCode();
398 if (retCode != HttpURLConnection.HTTP_OK)
399 throw new OsmOAuthAuthorizationException(tr("Failed to authorize OAuth request ''{0}''", requestToken.getKey()));
400 } catch (IOException e) {
401 throw new OsmOAuthAuthorizationException(e);
402 } finally {
403 synchronized (this) {
404 connection = null;
405 }
406 }
407 }
408
409 /**
410 * Automatically authorises a request token for a set of privileges.
411 *
412 * @param requestToken the request token. Must not be null.
413 * @param userName the OSM user name. Must not be null.
414 * @param password the OSM password. Must not be null.
415 * @param privileges the set of privileges. Must not be null.
416 * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null
417 * @throws IllegalArgumentException if requestToken is null
418 * @throws IllegalArgumentException if osmUserName is null
419 * @throws IllegalArgumentException if osmPassword is null
420 * @throws IllegalArgumentException if privileges is null
421 * @throws OsmOAuthAuthorizationException if the authorisation fails
422 * @throws OsmTransferCanceledException if the task is canceled by the user
423 */
424 public void authorise(OAuthToken requestToken, String userName, String password, OsmPrivileges privileges, ProgressMonitor monitor)
425 throws OsmOAuthAuthorizationException, OsmTransferCanceledException {
426 CheckParameterUtil.ensureParameterNotNull(requestToken, "requestToken");
427 CheckParameterUtil.ensureParameterNotNull(userName, "userName");
428 CheckParameterUtil.ensureParameterNotNull(password, "password");
429 CheckParameterUtil.ensureParameterNotNull(privileges, "privileges");
430
431 if (monitor == null) {
432 monitor = NullProgressMonitor.INSTANCE;
433 }
434 try {
435 monitor.beginTask(tr("Authorizing OAuth Request token ''{0}'' at the OSM website ...", requestToken.getKey()));
436 monitor.setTicksCount(4);
437 monitor.indeterminateSubTask(tr("Initializing a session at the OSM website..."));
438 SessionId sessionId = fetchOsmWebsiteSessionId();
439 sessionId.userName = userName;
440 if (canceled)
441 throw new OsmTransferCanceledException("Authorization canceled");
442 monitor.worked(1);
443
444 monitor.indeterminateSubTask(tr("Authenticating the session for user ''{0}''...", userName));
445 authenticateOsmSession(sessionId, userName, password);
446 if (canceled)
447 throw new OsmTransferCanceledException("Authorization canceled");
448 monitor.worked(1);
449
450 monitor.indeterminateSubTask(tr("Authorizing request token ''{0}''...", requestToken.getKey()));
451 sendAuthorisationRequest(sessionId, requestToken, privileges);
452 if (canceled)
453 throw new OsmTransferCanceledException("Authorization canceled");
454 monitor.worked(1);
455
456 monitor.indeterminateSubTask(tr("Logging out session ''{0}''...", sessionId));
457 logoutOsmSession(sessionId);
458 if (canceled)
459 throw new OsmTransferCanceledException("Authorization canceled");
460 monitor.worked(1);
461 } catch (OsmOAuthAuthorizationException e) {
462 if (canceled)
463 throw new OsmTransferCanceledException(e);
464 throw e;
465 } finally {
466 monitor.finishTask();
467 }
468 }
469}
Note: See TracBrowser for help on using the repository browser.