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 }