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.IOException; 008import java.net.CookieHandler; 009import java.net.HttpURLConnection; 010import java.net.URISyntaxException; 011import java.net.URL; 012import java.nio.charset.StandardCharsets; 013import java.util.Collections; 014import java.util.HashMap; 015import java.util.Iterator; 016import java.util.List; 017import java.util.Map; 018import java.util.Map.Entry; 019import java.util.regex.Matcher; 020import java.util.regex.Pattern; 021 022import org.openstreetmap.josm.data.oauth.OAuthParameters; 023import org.openstreetmap.josm.data.oauth.OAuthToken; 024import org.openstreetmap.josm.data.oauth.OsmPrivileges; 025import org.openstreetmap.josm.gui.progress.NullProgressMonitor; 026import org.openstreetmap.josm.gui.progress.ProgressMonitor; 027import org.openstreetmap.josm.io.OsmTransferCanceledException; 028import org.openstreetmap.josm.tools.CheckParameterUtil; 029import org.openstreetmap.josm.tools.HttpClient; 030import org.openstreetmap.josm.tools.Logging; 031import org.openstreetmap.josm.tools.Utils; 032 033import oauth.signpost.OAuth; 034import oauth.signpost.OAuthConsumer; 035import oauth.signpost.OAuthProvider; 036import oauth.signpost.exception.OAuthException; 037 038/** 039 * An OAuth 1.0 authorization client. 040 * @since 2746 041 */ 042public class OsmOAuthAuthorizationClient { 043 private final OAuthParameters oauthProviderParameters; 044 private final OAuthConsumer consumer; 045 private final OAuthProvider provider; 046 private boolean canceled; 047 private HttpClient connection; 048 049 protected static class SessionId { 050 protected String id; 051 protected String token; 052 protected String userName; 053 } 054 055 /** 056 * Creates a new authorisation client with the parameters <code>parameters</code>. 057 * 058 * @param parameters the OAuth parameters. Must not be null. 059 * @throws IllegalArgumentException if parameters is null 060 */ 061 public OsmOAuthAuthorizationClient(OAuthParameters parameters) { 062 CheckParameterUtil.ensureParameterNotNull(parameters, "parameters"); 063 oauthProviderParameters = new OAuthParameters(parameters); 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 * and an already known Request Token. 071 * 072 * @param parameters the OAuth parameters. Must not be null. 073 * @param requestToken the request token. Must not be null. 074 * @throws IllegalArgumentException if parameters is null 075 * @throws IllegalArgumentException if requestToken is null 076 */ 077 public OsmOAuthAuthorizationClient(OAuthParameters parameters, OAuthToken requestToken) { 078 CheckParameterUtil.ensureParameterNotNull(parameters, "parameters"); 079 oauthProviderParameters = new OAuthParameters(parameters); 080 consumer = oauthProviderParameters.buildConsumer(); 081 provider = oauthProviderParameters.buildProvider(consumer); 082 consumer.setTokenWithSecret(requestToken.getKey(), requestToken.getSecret()); 083 } 084 085 /** 086 * Cancels the current OAuth operation. 087 */ 088 public void cancel() { 089 canceled = true; 090 synchronized (this) { 091 if (connection != null) { 092 connection.disconnect(); 093 } 094 } 095 } 096 097 /** 098 * Submits a request for a Request Token to the Request Token Endpoint Url of the OAuth Service 099 * Provider and replies the request token. 100 * 101 * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null 102 * @return the OAuth Request Token 103 * @throws OsmOAuthAuthorizationException if something goes wrong when retrieving the request token 104 * @throws OsmTransferCanceledException if the user canceled the request 105 */ 106 public OAuthToken getRequestToken(ProgressMonitor monitor) throws OsmOAuthAuthorizationException, OsmTransferCanceledException { 107 if (monitor == null) { 108 monitor = NullProgressMonitor.INSTANCE; 109 } 110 try { 111 monitor.beginTask(""); 112 monitor.indeterminateSubTask(tr("Retrieving OAuth Request Token from ''{0}''", oauthProviderParameters.getRequestTokenUrl())); 113 provider.retrieveRequestToken(consumer, ""); 114 return OAuthToken.createToken(consumer); 115 } catch (OAuthException e) { 116 if (canceled) 117 throw new OsmTransferCanceledException(e); 118 throw new OsmOAuthAuthorizationException(e); 119 } finally { 120 monitor.finishTask(); 121 } 122 } 123 124 /** 125 * Submits a request for an Access Token to the Access Token Endpoint Url of the OAuth Service 126 * Provider and replies the request token. 127 * 128 * You must have requested a Request Token using {@link #getRequestToken(ProgressMonitor)} first. 129 * 130 * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null 131 * @return the OAuth Access Token 132 * @throws OsmOAuthAuthorizationException if something goes wrong when retrieving the request token 133 * @throws OsmTransferCanceledException if the user canceled the request 134 * @see #getRequestToken(ProgressMonitor) 135 */ 136 public OAuthToken getAccessToken(ProgressMonitor monitor) throws OsmOAuthAuthorizationException, OsmTransferCanceledException { 137 if (monitor == null) { 138 monitor = NullProgressMonitor.INSTANCE; 139 } 140 try { 141 monitor.beginTask(""); 142 monitor.indeterminateSubTask(tr("Retrieving OAuth Access Token from ''{0}''", oauthProviderParameters.getAccessTokenUrl())); 143 provider.retrieveAccessToken(consumer, null); 144 return OAuthToken.createToken(consumer); 145 } catch (OAuthException e) { 146 if (canceled) 147 throw new OsmTransferCanceledException(e); 148 throw new OsmOAuthAuthorizationException(e); 149 } finally { 150 monitor.finishTask(); 151 } 152 } 153 154 /** 155 * Builds the authorise URL for a given Request Token. Users can be redirected to this URL. 156 * There they can login to OSM and authorise the request. 157 * 158 * @param requestToken the request token 159 * @return the authorise URL for this request 160 */ 161 public String getAuthoriseUrl(OAuthToken requestToken) { 162 StringBuilder sb = new StringBuilder(32); 163 164 // OSM is an OAuth 1.0 provider and JOSM isn't a web app. We just add the oauth request token to 165 // the authorisation request, no callback parameter. 166 // 167 sb.append(oauthProviderParameters.getAuthoriseUrl()).append('?'+OAuth.OAUTH_TOKEN+'=').append(requestToken.getKey()); 168 return sb.toString(); 169 } 170 171 protected String extractToken() { 172 try (BufferedReader r = connection.getResponse().getContentReader()) { 173 String c; 174 Pattern p = Pattern.compile(".*authenticity_token.*value=\"([^\"]+)\".*"); 175 while ((c = r.readLine()) != null) { 176 Matcher m = p.matcher(c); 177 if (m.find()) { 178 return m.group(1); 179 } 180 } 181 } catch (IOException e) { 182 Logging.error(e); 183 return null; 184 } 185 Logging.warn("No authenticity_token found in response!"); 186 return null; 187 } 188 189 protected SessionId extractOsmSession() throws IOException, URISyntaxException { 190 // response headers might not contain the cookie, see #12584 191 final List<String> setCookies = CookieHandler.getDefault() 192 .get(connection.getURL().toURI(), Collections.<String, List<String>>emptyMap()) 193 .get("Cookie"); 194 if (setCookies == null) { 195 Logging.warn("No 'Set-Cookie' in response header!"); 196 return null; 197 } 198 199 for (String setCookie: setCookies) { 200 String[] kvPairs = setCookie.split(";", -1); 201 if (kvPairs.length == 0) { 202 continue; 203 } 204 for (String kvPair : kvPairs) { 205 kvPair = kvPair.trim(); 206 String[] kv = kvPair.split("=", -1); 207 if (kv.length != 2) { 208 continue; 209 } 210 if ("_osm_session".equals(kv[0])) { 211 // osm session cookie found 212 String token = extractToken(); 213 if (token == null) 214 return null; 215 SessionId si = new SessionId(); 216 si.id = kv[1]; 217 si.token = token; 218 return si; 219 } 220 } 221 } 222 Logging.warn("No suitable 'Set-Cookie' in response header found! {0}", setCookies); 223 return null; 224 } 225 226 protected static String buildPostRequest(Map<String, String> parameters) { 227 StringBuilder sb = new StringBuilder(32); 228 229 for (Iterator<Entry<String, String>> it = parameters.entrySet().iterator(); it.hasNext();) { 230 Entry<String, String> entry = it.next(); 231 String value = entry.getValue(); 232 value = (value == null) ? "" : value; 233 sb.append(entry.getKey()).append('=').append(Utils.encodeUrl(value)); 234 if (it.hasNext()) { 235 sb.append('&'); 236 } 237 } 238 return sb.toString(); 239 } 240 241 /** 242 * Submits a request to the OSM website for a login form. The OSM website replies a session ID in 243 * a cookie. 244 * 245 * @return the session ID structure 246 * @throws OsmOAuthAuthorizationException if something went wrong 247 */ 248 protected SessionId fetchOsmWebsiteSessionId() throws OsmOAuthAuthorizationException { 249 try { 250 final URL url = new URL(oauthProviderParameters.getOsmLoginUrl() + "?cookie_test=true"); 251 synchronized (this) { 252 connection = HttpClient.create(url).useCache(false); 253 connection.connect(); 254 } 255 SessionId sessionId = extractOsmSession(); 256 if (sessionId == null) 257 throw new OsmOAuthAuthorizationException( 258 tr("OSM website did not return a session cookie in response to ''{0}'',", url.toString())); 259 return sessionId; 260 } catch (IOException | URISyntaxException e) { 261 throw new OsmOAuthAuthorizationException(e); 262 } finally { 263 synchronized (this) { 264 connection = null; 265 } 266 } 267 } 268 269 /** 270 * Submits a request to the OSM website for a OAuth form. The OSM website replies a session token in 271 * a hidden parameter. 272 * @param sessionId session id 273 * @param requestToken request token 274 * 275 * @throws OsmOAuthAuthorizationException if something went wrong 276 */ 277 protected void fetchOAuthToken(SessionId sessionId, OAuthToken requestToken) throws OsmOAuthAuthorizationException { 278 try { 279 URL url = new URL(getAuthoriseUrl(requestToken)); 280 synchronized (this) { 281 connection = HttpClient.create(url) 282 .useCache(false) 283 .setHeader("Cookie", "_osm_session=" + sessionId.id + "; _osm_username=" + sessionId.userName); 284 connection.connect(); 285 } 286 sessionId.token = extractToken(); 287 if (sessionId.token == null) 288 throw new OsmOAuthAuthorizationException(tr("OSM website did not return a session cookie in response to ''{0}'',", 289 url.toString())); 290 } catch (IOException e) { 291 throw new OsmOAuthAuthorizationException(e); 292 } finally { 293 synchronized (this) { 294 connection = null; 295 } 296 } 297 } 298 299 protected void authenticateOsmSession(SessionId sessionId, String userName, String password) throws OsmLoginFailedException { 300 try { 301 final URL url = new URL(oauthProviderParameters.getOsmLoginUrl()); 302 final HttpClient client = HttpClient.create(url, "POST").useCache(false); 303 304 Map<String, String> parameters = new HashMap<>(); 305 parameters.put("username", userName); 306 parameters.put("password", password); 307 parameters.put("referer", "/"); 308 parameters.put("commit", "Login"); 309 parameters.put("authenticity_token", sessionId.token); 310 client.setRequestBody(buildPostRequest(parameters).getBytes(StandardCharsets.UTF_8)); 311 312 client.setHeader("Content-Type", "application/x-www-form-urlencoded"); 313 client.setHeader("Cookie", "_osm_session=" + sessionId.id); 314 // make sure we can catch 302 Moved Temporarily below 315 client.setMaxRedirects(-1); 316 317 synchronized (this) { 318 connection = client; 319 connection.connect(); 320 } 321 322 // after a successful login the OSM website sends a redirect to a follow up page. Everything 323 // else, including a 200 OK, is a failed login. A 200 OK is replied if the login form with 324 // an error page is sent to back to the user. 325 // 326 int retCode = connection.getResponse().getResponseCode(); 327 if (retCode != HttpURLConnection.HTTP_MOVED_TEMP) 328 throw new OsmOAuthAuthorizationException(tr("Failed to authenticate user ''{0}'' with password ''***'' as OAuth user", 329 userName)); 330 } catch (OsmOAuthAuthorizationException | IOException e) { 331 throw new OsmLoginFailedException(e); 332 } finally { 333 synchronized (this) { 334 connection = null; 335 } 336 } 337 } 338 339 protected void logoutOsmSession(SessionId sessionId) throws OsmOAuthAuthorizationException { 340 try { 341 URL url = new URL(oauthProviderParameters.getOsmLogoutUrl()); 342 synchronized (this) { 343 connection = HttpClient.create(url).setMaxRedirects(-1); 344 connection.connect(); 345 } 346 } catch (IOException e) { 347 throw new OsmOAuthAuthorizationException(e); 348 } finally { 349 synchronized (this) { 350 connection = null; 351 } 352 } 353 } 354 355 protected void sendAuthorisationRequest(SessionId sessionId, OAuthToken requestToken, OsmPrivileges privileges) 356 throws OsmOAuthAuthorizationException { 357 Map<String, String> parameters = new HashMap<>(); 358 fetchOAuthToken(sessionId, requestToken); 359 parameters.put("oauth_token", requestToken.getKey()); 360 parameters.put("oauth_callback", ""); 361 parameters.put("authenticity_token", sessionId.token); 362 parameters.put("allow_write_api", booleanParam(privileges.isAllowWriteApi())); 363 parameters.put("allow_write_gpx", booleanParam(privileges.isAllowWriteGpx())); 364 parameters.put("allow_read_gpx", booleanParam(privileges.isAllowReadGpx())); 365 parameters.put("allow_write_prefs", booleanParam(privileges.isAllowWritePrefs())); 366 parameters.put("allow_read_prefs", booleanParam(privileges.isAllowReadPrefs())); 367 parameters.put("allow_write_notes", booleanParam(privileges.isAllowModifyNotes())); 368 parameters.put("allow_write_diary", booleanParam(privileges.isAllowWriteDiary())); 369 370 String request = buildPostRequest(parameters); 371 try { 372 URL url = new URL(oauthProviderParameters.getAuthoriseUrl()); 373 final HttpClient client = HttpClient.create(url, "POST").useCache(false); 374 client.setHeader("Content-Type", "application/x-www-form-urlencoded"); 375 client.setHeader("Cookie", "_osm_session=" + sessionId.id + "; _osm_username=" + sessionId.userName); 376 client.setMaxRedirects(-1); 377 client.setRequestBody(request.getBytes(StandardCharsets.UTF_8)); 378 379 synchronized (this) { 380 connection = client; 381 connection.connect(); 382 } 383 384 int retCode = connection.getResponse().getResponseCode(); 385 if (retCode != HttpURLConnection.HTTP_OK) 386 throw new OsmOAuthAuthorizationException(tr("Failed to authorize OAuth request ''{0}''", requestToken.getKey())); 387 } catch (IOException e) { 388 throw new OsmOAuthAuthorizationException(e); 389 } finally { 390 synchronized (this) { 391 connection = null; 392 } 393 } 394 } 395 396 private static String booleanParam(boolean param) { 397 return param ? "1" : "0"; 398 } 399 400 /** 401 * Automatically authorises a request token for a set of privileges. 402 * 403 * @param requestToken the request token. Must not be null. 404 * @param userName the OSM user name. Must not be null. 405 * @param password the OSM password. Must not be null. 406 * @param privileges the set of privileges. Must not be null. 407 * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null 408 * @throws IllegalArgumentException if requestToken is null 409 * @throws IllegalArgumentException if osmUserName is null 410 * @throws IllegalArgumentException if osmPassword is null 411 * @throws IllegalArgumentException if privileges is null 412 * @throws OsmOAuthAuthorizationException if the authorisation fails 413 * @throws OsmTransferCanceledException if the task is canceled by the user 414 */ 415 public void authorise(OAuthToken requestToken, String userName, String password, OsmPrivileges privileges, ProgressMonitor monitor) 416 throws OsmOAuthAuthorizationException, OsmTransferCanceledException { 417 CheckParameterUtil.ensureParameterNotNull(requestToken, "requestToken"); 418 CheckParameterUtil.ensureParameterNotNull(userName, "userName"); 419 CheckParameterUtil.ensureParameterNotNull(password, "password"); 420 CheckParameterUtil.ensureParameterNotNull(privileges, "privileges"); 421 422 if (monitor == null) { 423 monitor = NullProgressMonitor.INSTANCE; 424 } 425 try { 426 monitor.beginTask(tr("Authorizing OAuth Request token ''{0}'' at the OSM website ...", requestToken.getKey())); 427 monitor.setTicksCount(4); 428 monitor.indeterminateSubTask(tr("Initializing a session at the OSM website...")); 429 SessionId sessionId = fetchOsmWebsiteSessionId(); 430 sessionId.userName = userName; 431 if (canceled) 432 throw new OsmTransferCanceledException("Authorization canceled"); 433 monitor.worked(1); 434 435 monitor.indeterminateSubTask(tr("Authenticating the session for user ''{0}''...", userName)); 436 authenticateOsmSession(sessionId, userName, password); 437 if (canceled) 438 throw new OsmTransferCanceledException("Authorization canceled"); 439 monitor.worked(1); 440 441 monitor.indeterminateSubTask(tr("Authorizing request token ''{0}''...", requestToken.getKey())); 442 sendAuthorisationRequest(sessionId, requestToken, privileges); 443 if (canceled) 444 throw new OsmTransferCanceledException("Authorization canceled"); 445 monitor.worked(1); 446 447 monitor.indeterminateSubTask(tr("Logging out session ''{0}''...", sessionId)); 448 logoutOsmSession(sessionId); 449 if (canceled) 450 throw new OsmTransferCanceledException("Authorization canceled"); 451 monitor.worked(1); 452 } catch (OsmOAuthAuthorizationException e) { 453 if (canceled) 454 throw new OsmTransferCanceledException(e); 455 throw e; 456 } finally { 457 monitor.finishTask(); 458 } 459 } 460}