001 // License: GPL. For details, see LICENSE file.
002 package org.openstreetmap.josm.gui.oauth;
003
004 import static org.openstreetmap.josm.tools.I18n.tr;
005
006 import java.io.BufferedReader;
007 import java.io.DataOutputStream;
008 import java.io.IOException;
009 import java.io.InputStreamReader;
010 import java.io.UnsupportedEncodingException;
011 import java.lang.reflect.Field;
012 import java.net.HttpURLConnection;
013 import java.net.MalformedURLException;
014 import java.net.URL;
015 import java.net.URLEncoder;
016 import java.util.HashMap;
017 import java.util.Iterator;
018 import java.util.List;
019 import java.util.Map;
020 import java.util.Map.Entry;
021 import java.util.regex.Matcher;
022 import java.util.regex.Pattern;
023
024 import oauth.signpost.OAuth;
025 import oauth.signpost.OAuthConsumer;
026 import oauth.signpost.OAuthProvider;
027 import oauth.signpost.basic.DefaultOAuthProvider;
028 import oauth.signpost.exception.OAuthCommunicationException;
029 import oauth.signpost.exception.OAuthException;
030
031 import org.openstreetmap.josm.Main;
032 import org.openstreetmap.josm.data.Version;
033 import org.openstreetmap.josm.data.oauth.OAuthParameters;
034 import org.openstreetmap.josm.data.oauth.OAuthToken;
035 import org.openstreetmap.josm.data.oauth.OsmPrivileges;
036 import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
037 import org.openstreetmap.josm.gui.progress.ProgressMonitor;
038 import org.openstreetmap.josm.io.OsmTransferCanceledException;
039 import org.openstreetmap.josm.tools.CheckParameterUtil;
040
041 /**
042 * An OAuth 1.0 authorization client.
043 * @since 2746
044 */
045 public class OsmOAuthAuthorizationClient {
046 private final OAuthParameters oauthProviderParameters;
047 private final OAuthConsumer consumer;
048 private final OAuthProvider provider;
049 private boolean canceled;
050 private HttpURLConnection connection;
051
052 private static class SessionId {
053 String id;
054 String token;
055 String userName;
056 }
057
058 /**
059 * Creates a new authorisation client with default OAuth parameters
060 *
061 */
062 public OsmOAuthAuthorizationClient() {
063 oauthProviderParameters = OAuthParameters.createDefault(Main.pref.get("osm-server.url"));
064 consumer = oauthProviderParameters.buildConsumer();
065 provider = oauthProviderParameters.buildProvider(consumer);
066 }
067
068 /**
069 * Creates a new authorisation client with the parameters <code>parameters</code>.
070 *
071 * @param parameters the OAuth parameters. Must not be null.
072 * @throws IllegalArgumentException if parameters is null
073 */
074 public OsmOAuthAuthorizationClient(OAuthParameters parameters) throws IllegalArgumentException {
075 CheckParameterUtil.ensureParameterNotNull(parameters, "parameters");
076 oauthProviderParameters = new OAuthParameters(parameters);
077 consumer = oauthProviderParameters.buildConsumer();
078 provider = oauthProviderParameters.buildProvider(consumer);
079 }
080
081 /**
082 * Creates a new authorisation client with the parameters <code>parameters</code>
083 * and an already known Request Token.
084 *
085 * @param parameters the OAuth parameters. Must not be null.
086 * @param requestToken the request token. Must not be null.
087 * @throws IllegalArgumentException if parameters is null
088 * @throws IllegalArgumentException if requestToken is null
089 */
090 public OsmOAuthAuthorizationClient(OAuthParameters parameters, OAuthToken requestToken) throws IllegalArgumentException {
091 CheckParameterUtil.ensureParameterNotNull(parameters, "parameters");
092 oauthProviderParameters = new OAuthParameters(parameters);
093 consumer = oauthProviderParameters.buildConsumer();
094 provider = oauthProviderParameters.buildProvider(consumer);
095 consumer.setTokenWithSecret(requestToken.getKey(), requestToken.getSecret());
096 }
097
098 /**
099 * Cancels the current OAuth operation.
100 */
101 public void cancel() {
102 DefaultOAuthProvider p = (DefaultOAuthProvider)provider;
103 canceled = true;
104 if (p != null) {
105 try {
106 Field f = p.getClass().getDeclaredField("connection");
107 f.setAccessible(true);
108 HttpURLConnection con = (HttpURLConnection)f.get(p);
109 if (con != null) {
110 con.disconnect();
111 }
112 } catch(NoSuchFieldException e) {
113 e.printStackTrace();
114 System.err.println(tr("Warning: failed to cancel running OAuth operation"));
115 } catch(SecurityException e) {
116 e.printStackTrace();
117 System.err.println(tr("Warning: failed to cancel running OAuth operation"));
118 } catch(IllegalAccessException e) {
119 e.printStackTrace();
120 System.err.println(tr("Warning: failed to cancel running OAuth operation"));
121 }
122 }
123 synchronized(this) {
124 if (connection != null) {
125 connection.disconnect();
126 }
127 }
128 }
129
130 /**
131 * Submits a request for a Request Token to the Request Token Endpoint Url of the OAuth Service
132 * Provider and replies the request token.
133 *
134 * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null
135 * @return the OAuth Request Token
136 * @throws OsmOAuthAuthorizationException if something goes wrong when retrieving the request token
137 * @throws OsmTransferCanceledException if the user canceled the request
138 */
139 public OAuthToken getRequestToken(ProgressMonitor monitor) throws OsmOAuthAuthorizationException, OsmTransferCanceledException {
140 if (monitor == null) {
141 monitor = NullProgressMonitor.INSTANCE;
142 }
143 try {
144 monitor.beginTask("");
145 monitor.indeterminateSubTask(tr("Retrieving OAuth Request Token from ''{0}''", oauthProviderParameters.getRequestTokenUrl()));
146 provider.retrieveRequestToken(consumer, "");
147 return OAuthToken.createToken(consumer);
148 } catch(OAuthCommunicationException e){
149 if (canceled)
150 throw new OsmTransferCanceledException();
151 throw new OsmOAuthAuthorizationException(e);
152 } catch(OAuthException e){
153 if (canceled)
154 throw new OsmTransferCanceledException();
155 throw new OsmOAuthAuthorizationException(e);
156 } finally {
157 monitor.finishTask();
158 }
159 }
160
161 /**
162 * Submits a request for an Access Token to the Access Token Endpoint Url of the OAuth Service
163 * Provider and replies the request token.
164 *
165 * You must have requested a Request Token using {@link #getRequestToken(ProgressMonitor)} first.
166 *
167 * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null
168 * @return the OAuth Access Token
169 * @throws OsmOAuthAuthorizationException if something goes wrong when retrieving the request token
170 * @throws OsmTransferCanceledException if the user canceled the request
171 * @see #getRequestToken(ProgressMonitor)
172 */
173 public OAuthToken getAccessToken(ProgressMonitor monitor) throws OsmOAuthAuthorizationException, OsmTransferCanceledException {
174 if (monitor == null) {
175 monitor = NullProgressMonitor.INSTANCE;
176 }
177 try {
178 monitor.beginTask("");
179 monitor.indeterminateSubTask(tr("Retrieving OAuth Access Token from ''{0}''", oauthProviderParameters.getAccessTokenUrl()));
180 provider.retrieveAccessToken(consumer, null);
181 return OAuthToken.createToken(consumer);
182 } catch(OAuthCommunicationException e){
183 if (canceled)
184 throw new OsmTransferCanceledException();
185 throw new OsmOAuthAuthorizationException(e);
186 } catch(OAuthException e){
187 if (canceled)
188 throw new OsmTransferCanceledException();
189 throw new OsmOAuthAuthorizationException(e);
190 } finally {
191 monitor.finishTask();
192 }
193 }
194
195 /**
196 * Builds the authorise URL for a given Request Token. Users can be redirected to this URL.
197 * There they can login to OSM and authorise the request.
198 *
199 * @param requestToken the request token
200 * @return the authorise URL for this request
201 */
202 public String getAuthoriseUrl(OAuthToken requestToken) {
203 StringBuilder sb = new StringBuilder();
204
205 // OSM is an OAuth 1.0 provider and JOSM isn't a web app. We just add the oauth request token to
206 // the authorisation request, no callback parameter.
207 //
208 sb.append(oauthProviderParameters.getAuthoriseUrl()).append("?")
209 .append(OAuth.OAUTH_TOKEN).append("=").append(requestToken.getKey());
210 return sb.toString();
211 }
212
213 protected String extractToken(HttpURLConnection connection) {
214 try {
215 BufferedReader r = new BufferedReader(new InputStreamReader(connection.getInputStream()));
216 String c;
217 Pattern p = Pattern.compile(".*authenticity_token.*value=\"([^\"]+)\".*");
218 while((c = r.readLine()) != null) {
219 Matcher m = p.matcher(c);
220 if(m.find())
221 return m.group(1);
222 }
223 } catch (IOException e) {
224 return null;
225 }
226 return null;
227 }
228
229 protected SessionId extractOsmSession(HttpURLConnection connection) {
230 List<String> setCookies = connection.getHeaderFields().get("Set-Cookie");
231 if (setCookies == null)
232 // no cookies set
233 return null;
234
235 for (String setCookie: setCookies) {
236 String[] kvPairs = setCookie.split(";");
237 if (kvPairs == null || kvPairs.length == 0) {
238 continue;
239 }
240 for (String kvPair : kvPairs) {
241 kvPair = kvPair.trim();
242 String [] kv = kvPair.split("=");
243 if (kv == null || kv.length != 2) {
244 continue;
245 }
246 if (kv[0].equals("_osm_session")) {
247 // osm session cookie found
248 String token = extractToken(connection);
249 if(token == null)
250 return null;
251 SessionId si = new SessionId();
252 si.id = kv[1];
253 si.token = token;
254 return si;
255 }
256 }
257 }
258 return null;
259 }
260
261 protected String buildPostRequest(Map<String,String> parameters) throws OsmOAuthAuthorizationException {
262 try {
263 StringBuilder sb = new StringBuilder();
264
265 for(Iterator<Entry<String,String>> it = parameters.entrySet().iterator(); it.hasNext();) {
266 Entry<String,String> entry = it.next();
267 String value = entry.getValue();
268 value = (value == null) ? "" : value;
269 sb.append(entry.getKey()).append("=").append(URLEncoder.encode(value, "UTF-8"));
270 if (it.hasNext()) {
271 sb.append("&");
272 }
273 }
274 return sb.toString();
275 } catch(UnsupportedEncodingException e) {
276 throw new OsmOAuthAuthorizationException(e);
277 }
278 }
279
280 /**
281 * Derives the OSM login URL from the OAuth Authorization Website URL
282 *
283 * @return the OSM login URL
284 * @throws OsmOAuthAuthorizationException if something went wrong, in particular if the
285 * URLs are malformed
286 */
287 public String buildOsmLoginUrl() throws OsmOAuthAuthorizationException{
288 try {
289 URL autUrl = new URL(oauthProviderParameters.getAuthoriseUrl());
290 URL url = new URL(Main.pref.get("oauth.protocol", "https"), autUrl.getHost(), autUrl.getPort(), "/login");
291 return url.toString();
292 } catch(MalformedURLException e) {
293 throw new OsmOAuthAuthorizationException(e);
294 }
295 }
296
297 /**
298 * Derives the OSM logout URL from the OAuth Authorization Website URL
299 *
300 * @return the OSM logout URL
301 * @throws OsmOAuthAuthorizationException if something went wrong, in particular if the
302 * URLs are malformed
303 */
304 protected String buildOsmLogoutUrl() throws OsmOAuthAuthorizationException{
305 try {
306 URL autUrl = new URL(oauthProviderParameters.getAuthoriseUrl());
307 URL url = new URL("http", autUrl.getHost(), autUrl.getPort(), "/logout");
308 return url.toString();
309 } catch(MalformedURLException e) {
310 throw new OsmOAuthAuthorizationException(e);
311 }
312 }
313
314 /**
315 * Submits a request to the OSM website for a login form. The OSM website replies a session ID in
316 * a cookie.
317 *
318 * @return the session ID structure
319 * @throws OsmOAuthAuthorizationException if something went wrong
320 */
321 protected SessionId fetchOsmWebsiteSessionId() throws OsmOAuthAuthorizationException {
322 try {
323 StringBuilder sb = new StringBuilder();
324 sb.append(buildOsmLoginUrl()).append("?cookie_test=true");
325 URL url = new URL(sb.toString());
326 synchronized(this) {
327 connection = (HttpURLConnection)url.openConnection();
328 }
329 connection.setRequestMethod("GET");
330 connection.setDoInput(true);
331 connection.setDoOutput(false);
332 setHttpRequestParameters(connection);
333 connection.connect();
334 SessionId sessionId = extractOsmSession(connection);
335 if (sessionId == null)
336 throw new OsmOAuthAuthorizationException(tr("OSM website did not return a session cookie in response to ''{0}'',", url.toString()));
337 return sessionId;
338 } catch(IOException e) {
339 throw new OsmOAuthAuthorizationException(e);
340 } finally {
341 synchronized(this) {
342 connection = null;
343 }
344 }
345 }
346
347 /**
348 * Submits a request to the OSM website for a OAuth form. The OSM website replies a session token in
349 * a hidden parameter.
350 *
351 * @throws OsmOAuthAuthorizationException if something went wrong
352 */
353 protected void fetchOAuthToken(SessionId sessionId, OAuthToken requestToken) throws OsmOAuthAuthorizationException {
354 try {
355 URL url = new URL(getAuthoriseUrl(requestToken));
356 synchronized(this) {
357 connection = (HttpURLConnection)url.openConnection();
358 }
359 connection.setRequestMethod("GET");
360 connection.setDoInput(true);
361 connection.setDoOutput(false);
362 connection.setRequestProperty("Cookie", "_osm_session=" + sessionId.id + "; _osm_username=" + sessionId.userName);
363 setHttpRequestParameters(connection);
364 connection.connect();
365 sessionId.token = extractToken(connection);
366 if (sessionId.token == null)
367 throw new OsmOAuthAuthorizationException(tr("OSM website did not return a session cookie in response to ''{0}'',", url.toString()));
368 } catch(IOException e) {
369 throw new OsmOAuthAuthorizationException(e);
370 } finally {
371 synchronized(this) {
372 connection = null;
373 }
374 }
375 }
376
377 protected void authenticateOsmSession(SessionId sessionId, String userName, String password) throws OsmLoginFailedException {
378 DataOutputStream dout = null;
379 try {
380 URL url = new URL(buildOsmLoginUrl());
381 synchronized(this) {
382 connection = (HttpURLConnection)url.openConnection();
383 }
384 connection.setRequestMethod("POST");
385 connection.setDoInput(true);
386 connection.setDoOutput(true);
387 connection.setUseCaches(false);
388
389 Map<String,String> parameters = new HashMap<String, String>();
390 parameters.put("username", userName);
391 parameters.put("password", password);
392 parameters.put("referer", "/");
393 parameters.put("commit", "Login");
394 parameters.put("authenticity_token", sessionId.token);
395
396 String request = buildPostRequest(parameters);
397
398 connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
399 connection.setRequestProperty("Content-Length", Integer.toString(request.length()));
400 connection.setRequestProperty("Cookie", "_osm_session=" + sessionId.id);
401 // make sure we can catch 302 Moved Temporarily below
402 connection.setInstanceFollowRedirects(false);
403 setHttpRequestParameters(connection);
404
405 connection.connect();
406
407 dout = new DataOutputStream(connection.getOutputStream());
408 dout.writeBytes(request);
409 dout.flush();
410 dout.close();
411
412 // after a successful login the OSM website sends a redirect to a follow up page. Everything
413 // else, including a 200 OK, is a failed login. A 200 OK is replied if the login form with
414 // an error page is sent to back to the user.
415 //
416 int retCode = connection.getResponseCode();
417 if (retCode != HttpURLConnection.HTTP_MOVED_TEMP)
418 throw new OsmOAuthAuthorizationException(tr("Failed to authenticate user ''{0}'' with password ''***'' as OAuth user", userName));
419 } catch(OsmOAuthAuthorizationException e) {
420 throw new OsmLoginFailedException(e.getCause());
421 } catch(IOException e) {
422 throw new OsmLoginFailedException(e);
423 } finally {
424 if (dout != null) {
425 try {
426 dout.close();
427 } catch(IOException e) { /* ignore */ }
428 }
429 synchronized(this) {
430 connection = null;
431 }
432 }
433 }
434
435 protected void logoutOsmSession(SessionId sessionId) throws OsmOAuthAuthorizationException {
436 try {
437 URL url = new URL(buildOsmLogoutUrl());
438 synchronized(this) {
439 connection = (HttpURLConnection)url.openConnection();
440 }
441 connection.setRequestMethod("GET");
442 connection.setDoInput(true);
443 connection.setDoOutput(false);
444 setHttpRequestParameters(connection);
445 connection.connect();
446 }catch(MalformedURLException e) {
447 throw new OsmOAuthAuthorizationException(e);
448 } catch(IOException e) {
449 throw new OsmOAuthAuthorizationException(e);
450 } finally {
451 synchronized(this) {
452 connection = null;
453 }
454 }
455 }
456
457 protected void sendAuthorisationRequest(SessionId sessionId, OAuthToken requestToken, OsmPrivileges privileges) throws OsmOAuthAuthorizationException {
458 Map<String, String> parameters = new HashMap<String, String>();
459 fetchOAuthToken(sessionId, requestToken);
460 parameters.put("oauth_token", requestToken.getKey());
461 parameters.put("oauth_callback", "");
462 parameters.put("authenticity_token", sessionId.token);
463 if (privileges.isAllowWriteApi()) {
464 parameters.put("allow_write_api", "yes");
465 }
466 if (privileges.isAllowWriteGpx()) {
467 parameters.put("allow_write_gpx", "yes");
468 }
469 if (privileges.isAllowReadGpx()) {
470 parameters.put("allow_read_gpx", "yes");
471 }
472 if (privileges.isAllowWritePrefs()) {
473 parameters.put("allow_write_prefs", "yes");
474 }
475 if (privileges.isAllowReadPrefs()) {
476 parameters.put("allow_read_prefs", "yes");
477 }
478
479 parameters.put("commit", "Save changes");
480
481 String request = buildPostRequest(parameters);
482 DataOutputStream dout = null;
483 try {
484 URL url = new URL(oauthProviderParameters.getAuthoriseUrl());
485 synchronized(this) {
486 connection = (HttpURLConnection)url.openConnection();
487 }
488 connection.setRequestMethod("POST");
489 connection.setDoInput(true);
490 connection.setDoOutput(true);
491 connection.setUseCaches(false);
492 connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
493 connection.setRequestProperty("Content-Length", Integer.toString(request.length()));
494 connection.setRequestProperty("Cookie", "_osm_session=" + sessionId.id + "; _osm_username=" + sessionId.userName);
495 connection.setInstanceFollowRedirects(false);
496 setHttpRequestParameters(connection);
497
498 connection.connect();
499
500 dout = new DataOutputStream(connection.getOutputStream());
501 dout.writeBytes(request);
502 dout.flush();
503 dout.close();
504
505 int retCode = connection.getResponseCode();
506 if (retCode != HttpURLConnection.HTTP_OK)
507 throw new OsmOAuthAuthorizationException(tr("Failed to authorize OAuth request ''{0}''", requestToken.getKey()));
508 } catch(MalformedURLException e) {
509 throw new OsmOAuthAuthorizationException(e);
510 } catch(IOException e) {
511 throw new OsmOAuthAuthorizationException(e);
512 } finally {
513 if (dout != null) {
514 try {
515 dout.close();
516 } catch(IOException e) { /* ignore */ }
517 }
518 synchronized(this) {
519 connection = null;
520 }
521 }
522 }
523
524 protected void setHttpRequestParameters(HttpURLConnection connection) {
525 connection.setRequestProperty("User-Agent", Version.getInstance().getAgentString());
526 connection.setRequestProperty("Host", connection.getURL().getHost());
527 }
528
529 /**
530 * Automatically authorises a request token for a set of privileges.
531 *
532 * @param requestToken the request token. Must not be null.
533 * @param osmUserName the OSM user name. Must not be null.
534 * @param osmPassword the OSM password. Must not be null.
535 * @param privileges the set of privileges. Must not be null.
536 * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null
537 * @throws IllegalArgumentException if requestToken is null
538 * @throws IllegalArgumentException if osmUserName is null
539 * @throws IllegalArgumentException if osmPassword is null
540 * @throws IllegalArgumentException if privileges is null
541 * @throws OsmOAuthAuthorizationException if the authorisation fails
542 * @throws OsmTransferCanceledException if the task is canceled by the user
543 */
544 public void authorise(OAuthToken requestToken, String osmUserName, String osmPassword, OsmPrivileges privileges, ProgressMonitor monitor) throws IllegalArgumentException, OsmOAuthAuthorizationException, OsmTransferCanceledException{
545 CheckParameterUtil.ensureParameterNotNull(requestToken, "requestToken");
546 CheckParameterUtil.ensureParameterNotNull(osmUserName, "osmUserName");
547 CheckParameterUtil.ensureParameterNotNull(osmPassword, "osmPassword");
548 CheckParameterUtil.ensureParameterNotNull(privileges, "privileges");
549
550 if (monitor == null) {
551 monitor = NullProgressMonitor.INSTANCE;
552 }
553 try {
554 monitor.beginTask(tr("Authorizing OAuth Request token ''{0}'' at the OSM website ...", requestToken.getKey()));
555 monitor.setTicksCount(4);
556 monitor.indeterminateSubTask(tr("Initializing a session at the OSM website..."));
557 SessionId sessionId = fetchOsmWebsiteSessionId();
558 sessionId.userName = osmUserName;
559 if (canceled)
560 throw new OsmTransferCanceledException();
561 monitor.worked(1);
562
563 monitor.indeterminateSubTask(tr("Authenticating the session for user ''{0}''...", osmUserName));
564 authenticateOsmSession(sessionId, osmUserName, osmPassword);
565 if (canceled)
566 throw new OsmTransferCanceledException();
567 monitor.worked(1);
568
569 monitor.indeterminateSubTask(tr("Authorizing request token ''{0}''...", requestToken.getKey()));
570 sendAuthorisationRequest(sessionId, requestToken, privileges);
571 if (canceled)
572 throw new OsmTransferCanceledException();
573 monitor.worked(1);
574
575 monitor.indeterminateSubTask(tr("Logging out session ''{0}''...", sessionId));
576 logoutOsmSession(sessionId);
577 if (canceled)
578 throw new OsmTransferCanceledException();
579 monitor.worked(1);
580 } catch(OsmOAuthAuthorizationException e) {
581 if (canceled)
582 throw new OsmTransferCanceledException();
583 throw e;
584 } finally {
585 monitor.finishTask();
586 }
587 }
588 }