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

Last change on this file since 4874 was 4874, checked in by jttt, 12 years ago

Use static class were appropriate

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