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

Last change on this file since 7004 was 7004, checked in by Don-vip, 10 years ago

see #8465 - use multi-catch where applicable

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