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

Last change on this file since 9232 was 9232, checked in by simon04, 8 years ago

fix #12265 - Use HttpClient for imagery requests

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