001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.oauth; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.io.BufferedReader; 007import java.io.DataOutputStream; 008import java.io.IOException; 009import java.io.InputStream; 010import java.io.InputStreamReader; 011import java.io.UnsupportedEncodingException; 012import java.lang.reflect.Field; 013import java.net.HttpURLConnection; 014import java.net.MalformedURLException; 015import java.net.URL; 016import java.net.URLEncoder; 017import java.nio.charset.StandardCharsets; 018import java.util.HashMap; 019import java.util.Iterator; 020import java.util.List; 021import java.util.Map; 022import java.util.Map.Entry; 023import java.util.regex.Matcher; 024import java.util.regex.Pattern; 025 026import oauth.signpost.OAuth; 027import oauth.signpost.OAuthConsumer; 028import oauth.signpost.OAuthProvider; 029import oauth.signpost.basic.DefaultOAuthProvider; 030import oauth.signpost.exception.OAuthException; 031 032import org.openstreetmap.josm.Main; 033import org.openstreetmap.josm.data.oauth.OAuthParameters; 034import org.openstreetmap.josm.data.oauth.OAuthToken; 035import org.openstreetmap.josm.data.oauth.OsmPrivileges; 036import org.openstreetmap.josm.gui.progress.NullProgressMonitor; 037import org.openstreetmap.josm.gui.progress.ProgressMonitor; 038import org.openstreetmap.josm.io.OsmTransferCanceledException; 039import org.openstreetmap.josm.tools.CheckParameterUtil; 040import org.openstreetmap.josm.tools.Utils; 041 042/** 043 * An OAuth 1.0 authorization client. 044 * @since 2746 045 */ 046public class OsmOAuthAuthorizationClient { 047 private final OAuthParameters oauthProviderParameters; 048 private final OAuthConsumer consumer; 049 private final OAuthProvider provider; 050 private boolean canceled; 051 private HttpURLConnection connection; 052 053 private static class SessionId { 054 String id; 055 String token; 056 String userName; 057 } 058 059 /** 060 * Creates a new authorisation client with default OAuth parameters 061 * 062 */ 063 public OsmOAuthAuthorizationClient() { 064 oauthProviderParameters = OAuthParameters.createDefault(Main.pref.get("osm-server.url")); 065 consumer = oauthProviderParameters.buildConsumer(); 066 provider = oauthProviderParameters.buildProvider(consumer); 067 } 068 069 /** 070 * Creates a new authorisation client with the parameters <code>parameters</code>. 071 * 072 * @param parameters the OAuth parameters. Must not be null. 073 * @throws IllegalArgumentException if parameters is null 074 */ 075 public OsmOAuthAuthorizationClient(OAuthParameters parameters) throws IllegalArgumentException { 076 CheckParameterUtil.ensureParameterNotNull(parameters, "parameters"); 077 oauthProviderParameters = new OAuthParameters(parameters); 078 consumer = oauthProviderParameters.buildConsumer(); 079 provider = oauthProviderParameters.buildProvider(consumer); 080 } 081 082 /** 083 * Creates a new authorisation client with the parameters <code>parameters</code> 084 * and an already known Request Token. 085 * 086 * @param parameters the OAuth parameters. Must not be null. 087 * @param requestToken the request token. Must not be null. 088 * @throws IllegalArgumentException if parameters is null 089 * @throws IllegalArgumentException if requestToken is null 090 */ 091 public OsmOAuthAuthorizationClient(OAuthParameters parameters, OAuthToken requestToken) throws IllegalArgumentException { 092 CheckParameterUtil.ensureParameterNotNull(parameters, "parameters"); 093 oauthProviderParameters = new OAuthParameters(parameters); 094 consumer = oauthProviderParameters.buildConsumer(); 095 provider = oauthProviderParameters.buildProvider(consumer); 096 consumer.setTokenWithSecret(requestToken.getKey(), requestToken.getSecret()); 097 } 098 099 /** 100 * Cancels the current OAuth operation. 101 */ 102 public void cancel() { 103 DefaultOAuthProvider p = (DefaultOAuthProvider)provider; 104 canceled = true; 105 if (p != null) { 106 try { 107 Field f = p.getClass().getDeclaredField("connection"); 108 f.setAccessible(true); 109 HttpURLConnection con = (HttpURLConnection)f.get(p); 110 if (con != null) { 111 con.disconnect(); 112 } 113 } catch (NoSuchFieldException | SecurityException | IllegalAccessException e) { 114 Main.error(e); 115 Main.warn(tr("Failed to cancel running OAuth operation")); 116 } 117 } 118 synchronized(this) { 119 if (connection != null) { 120 connection.disconnect(); 121 } 122 } 123 } 124 125 /** 126 * Submits a request for a Request Token to the Request Token Endpoint Url of the OAuth Service 127 * Provider and replies the request token. 128 * 129 * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null 130 * @return the OAuth Request Token 131 * @throws OsmOAuthAuthorizationException if something goes wrong when retrieving the request token 132 * @throws OsmTransferCanceledException if the user canceled the request 133 */ 134 public OAuthToken getRequestToken(ProgressMonitor monitor) throws OsmOAuthAuthorizationException, OsmTransferCanceledException { 135 if (monitor == null) { 136 monitor = NullProgressMonitor.INSTANCE; 137 } 138 try { 139 monitor.beginTask(""); 140 monitor.indeterminateSubTask(tr("Retrieving OAuth Request Token from ''{0}''", oauthProviderParameters.getRequestTokenUrl())); 141 provider.retrieveRequestToken(consumer, ""); 142 return OAuthToken.createToken(consumer); 143 } catch(OAuthException e){ 144 if (canceled) 145 throw new OsmTransferCanceledException(e); 146 throw new OsmOAuthAuthorizationException(e); 147 } finally { 148 monitor.finishTask(); 149 } 150 } 151 152 /** 153 * Submits a request for an Access Token to the Access Token Endpoint Url of the OAuth Service 154 * Provider and replies the request token. 155 * 156 * You must have requested a Request Token using {@link #getRequestToken(ProgressMonitor)} first. 157 * 158 * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null 159 * @return the OAuth Access Token 160 * @throws OsmOAuthAuthorizationException if something goes wrong when retrieving the request token 161 * @throws OsmTransferCanceledException if the user canceled the request 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(OAuthException e){ 174 if (canceled) 175 throw new OsmTransferCanceledException(e); 176 throw new OsmOAuthAuthorizationException(e); 177 } finally { 178 monitor.finishTask(); 179 } 180 } 181 182 /** 183 * Builds the authorise URL for a given Request Token. Users can be redirected to this URL. 184 * There they can login to OSM and authorise the request. 185 * 186 * @param requestToken the request token 187 * @return the authorise URL for this request 188 */ 189 public String getAuthoriseUrl(OAuthToken requestToken) { 190 StringBuilder sb = new StringBuilder(); 191 192 // OSM is an OAuth 1.0 provider and JOSM isn't a web app. We just add the oauth request token to 193 // the authorisation request, no callback parameter. 194 // 195 sb.append(oauthProviderParameters.getAuthoriseUrl()).append("?") 196 .append(OAuth.OAUTH_TOKEN).append("=").append(requestToken.getKey()); 197 return sb.toString(); 198 } 199 200 protected String extractToken(HttpURLConnection connection) { 201 try ( 202 InputStream is = connection.getInputStream(); 203 BufferedReader r = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)) 204 ) { 205 String c; 206 Pattern p = Pattern.compile(".*authenticity_token.*value=\"([^\"]+)\".*"); 207 while ((c = r.readLine()) != null) { 208 Matcher m = p.matcher(c); 209 if (m.find()) { 210 return m.group(1); 211 } 212 } 213 } catch (IOException e) { 214 Main.error(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 ("_osm_session".equals(kv[0])) { 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 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 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 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 = Utils.openHttpConnection(url); 319 } 320 connection.setRequestMethod("GET"); 321 connection.setDoInput(true); 322 connection.setDoOutput(false); 323 connection.connect(); 324 SessionId sessionId = extractOsmSession(connection); 325 if (sessionId == null) 326 throw new OsmOAuthAuthorizationException(tr("OSM website did not return a session cookie in response to ''{0}'',", url.toString())); 327 return sessionId; 328 } catch(IOException e) { 329 throw new OsmOAuthAuthorizationException(e); 330 } finally { 331 synchronized(this) { 332 connection = null; 333 } 334 } 335 } 336 337 /** 338 * Submits a request to the OSM website for a OAuth form. The OSM website replies a session token in 339 * a hidden parameter. 340 * 341 * @throws OsmOAuthAuthorizationException if something went wrong 342 */ 343 protected void fetchOAuthToken(SessionId sessionId, OAuthToken requestToken) throws OsmOAuthAuthorizationException { 344 try { 345 URL url = new URL(getAuthoriseUrl(requestToken)); 346 synchronized(this) { 347 connection = Utils.openHttpConnection(url); 348 } 349 connection.setRequestMethod("GET"); 350 connection.setDoInput(true); 351 connection.setDoOutput(false); 352 connection.setRequestProperty("Cookie", "_osm_session=" + sessionId.id + "; _osm_username=" + sessionId.userName); 353 connection.connect(); 354 sessionId.token = extractToken(connection); 355 if (sessionId.token == null) 356 throw new OsmOAuthAuthorizationException(tr("OSM website did not return a session cookie in response to ''{0}'',", url.toString())); 357 } catch(IOException e) { 358 throw new OsmOAuthAuthorizationException(e); 359 } finally { 360 synchronized(this) { 361 connection = null; 362 } 363 } 364 } 365 366 protected void authenticateOsmSession(SessionId sessionId, String userName, String password) throws OsmLoginFailedException { 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<>(); 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 try (DataOutputStream dout = new DataOutputStream(connection.getOutputStream())) { 395 dout.writeBytes(request); 396 dout.flush(); 397 } 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 synchronized(this) { 412 connection = null; 413 } 414 } 415 } 416 417 protected void logoutOsmSession(SessionId sessionId) throws OsmOAuthAuthorizationException { 418 try { 419 URL url = new URL(buildOsmLogoutUrl()); 420 synchronized(this) { 421 connection = Utils.openHttpConnection(url); 422 } 423 connection.setRequestMethod("GET"); 424 connection.setDoInput(true); 425 connection.setDoOutput(false); 426 connection.connect(); 427 } catch(IOException e) { 428 throw new OsmOAuthAuthorizationException(e); 429 } finally { 430 synchronized(this) { 431 connection = null; 432 } 433 } 434 } 435 436 protected void sendAuthorisationRequest(SessionId sessionId, OAuthToken requestToken, OsmPrivileges privileges) throws OsmOAuthAuthorizationException { 437 Map<String, String> parameters = new HashMap<>(); 438 fetchOAuthToken(sessionId, requestToken); 439 parameters.put("oauth_token", requestToken.getKey()); 440 parameters.put("oauth_callback", ""); 441 parameters.put("authenticity_token", sessionId.token); 442 if (privileges.isAllowWriteApi()) { 443 parameters.put("allow_write_api", "yes"); 444 } 445 if (privileges.isAllowWriteGpx()) { 446 parameters.put("allow_write_gpx", "yes"); 447 } 448 if (privileges.isAllowReadGpx()) { 449 parameters.put("allow_read_gpx", "yes"); 450 } 451 if (privileges.isAllowWritePrefs()) { 452 parameters.put("allow_write_prefs", "yes"); 453 } 454 if (privileges.isAllowReadPrefs()) { 455 parameters.put("allow_read_prefs", "yes"); 456 } 457 if (privileges.isAllowModifyNotes()) { 458 parameters.put("allow_write_notes", "yes"); 459 } 460 461 parameters.put("commit", "Save changes"); 462 463 String request = buildPostRequest(parameters); 464 try { 465 URL url = new URL(oauthProviderParameters.getAuthoriseUrl()); 466 synchronized(this) { 467 connection = Utils.openHttpConnection(url); 468 } 469 connection.setRequestMethod("POST"); 470 connection.setDoInput(true); 471 connection.setDoOutput(true); 472 connection.setUseCaches(false); 473 connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); 474 connection.setRequestProperty("Content-Length", Integer.toString(request.length())); 475 connection.setRequestProperty("Cookie", "_osm_session=" + sessionId.id + "; _osm_username=" + sessionId.userName); 476 connection.setInstanceFollowRedirects(false); 477 478 connection.connect(); 479 480 try (DataOutputStream dout = new DataOutputStream(connection.getOutputStream())) { 481 dout.writeBytes(request); 482 dout.flush(); 483 } 484 485 int retCode = connection.getResponseCode(); 486 if (retCode != HttpURLConnection.HTTP_OK) 487 throw new OsmOAuthAuthorizationException(tr("Failed to authorize OAuth request ''{0}''", requestToken.getKey())); 488 } catch (IOException e) { 489 throw new OsmOAuthAuthorizationException(e); 490 } finally { 491 synchronized(this) { 492 connection = null; 493 } 494 } 495 } 496 497 /** 498 * Automatically authorises a request token for a set of privileges. 499 * 500 * @param requestToken the request token. Must not be null. 501 * @param osmUserName the OSM user name. Must not be null. 502 * @param osmPassword the OSM password. Must not be null. 503 * @param privileges the set of privileges. Must not be null. 504 * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null 505 * @throws IllegalArgumentException if requestToken is null 506 * @throws IllegalArgumentException if osmUserName is null 507 * @throws IllegalArgumentException if osmPassword is null 508 * @throws IllegalArgumentException if privileges is null 509 * @throws OsmOAuthAuthorizationException if the authorisation fails 510 * @throws OsmTransferCanceledException if the task is canceled by the user 511 */ 512 public void authorise(OAuthToken requestToken, String osmUserName, String osmPassword, OsmPrivileges privileges, ProgressMonitor monitor) throws IllegalArgumentException, OsmOAuthAuthorizationException, OsmTransferCanceledException{ 513 CheckParameterUtil.ensureParameterNotNull(requestToken, "requestToken"); 514 CheckParameterUtil.ensureParameterNotNull(osmUserName, "osmUserName"); 515 CheckParameterUtil.ensureParameterNotNull(osmPassword, "osmPassword"); 516 CheckParameterUtil.ensureParameterNotNull(privileges, "privileges"); 517 518 if (monitor == null) { 519 monitor = NullProgressMonitor.INSTANCE; 520 } 521 try { 522 monitor.beginTask(tr("Authorizing OAuth Request token ''{0}'' at the OSM website ...", requestToken.getKey())); 523 monitor.setTicksCount(4); 524 monitor.indeterminateSubTask(tr("Initializing a session at the OSM website...")); 525 SessionId sessionId = fetchOsmWebsiteSessionId(); 526 sessionId.userName = osmUserName; 527 if (canceled) 528 throw new OsmTransferCanceledException(); 529 monitor.worked(1); 530 531 monitor.indeterminateSubTask(tr("Authenticating the session for user ''{0}''...", osmUserName)); 532 authenticateOsmSession(sessionId, osmUserName, osmPassword); 533 if (canceled) 534 throw new OsmTransferCanceledException(); 535 monitor.worked(1); 536 537 monitor.indeterminateSubTask(tr("Authorizing request token ''{0}''...", requestToken.getKey())); 538 sendAuthorisationRequest(sessionId, requestToken, privileges); 539 if (canceled) 540 throw new OsmTransferCanceledException(); 541 monitor.worked(1); 542 543 monitor.indeterminateSubTask(tr("Logging out session ''{0}''...", sessionId)); 544 logoutOsmSession(sessionId); 545 if (canceled) 546 throw new OsmTransferCanceledException(); 547 monitor.worked(1); 548 } catch(OsmOAuthAuthorizationException e) { 549 if (canceled) 550 throw new OsmTransferCanceledException(e); 551 throw e; 552 } finally { 553 monitor.finishTask(); 554 } 555 } 556}