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

Last change on this file since 17318 was 17196, checked in by simon04, 4 years ago

see #15102 - see #16637 - get rid of real HTTP calls to https://www.openstreetmap.org/login in OsmOAuthAuthorizationClientTest, mock them

  • Property svn:eol-style set to native
File size: 19.3 KB
RevLine 
[2801]1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.oauth;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
[4729]6import java.io.BufferedReader;
[4874]7import java.io.IOException;
[9935]8import java.net.CookieHandler;
[2801]9import java.net.HttpURLConnection;
[9935]10import java.net.URISyntaxException;
[2801]11import java.net.URL;
[7082]12import java.nio.charset.StandardCharsets;
[9935]13import java.util.Collections;
[2801]14import java.util.HashMap;
15import java.util.Iterator;
16import java.util.List;
17import java.util.Map;
18import java.util.Map.Entry;
[4729]19import java.util.regex.Matcher;
20import java.util.regex.Pattern;
[2801]21
22import org.openstreetmap.josm.data.oauth.OAuthParameters;
23import org.openstreetmap.josm.data.oauth.OAuthToken;
24import org.openstreetmap.josm.data.oauth.OsmPrivileges;
25import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
26import org.openstreetmap.josm.gui.progress.ProgressMonitor;
[4310]27import org.openstreetmap.josm.io.OsmTransferCanceledException;
[2801]28import org.openstreetmap.josm.tools.CheckParameterUtil;
[9172]29import org.openstreetmap.josm.tools.HttpClient;
[12620]30import org.openstreetmap.josm.tools.Logging;
[5587]31import org.openstreetmap.josm.tools.Utils;
[2801]32
[9913]33import oauth.signpost.OAuth;
34import oauth.signpost.OAuthConsumer;
35import oauth.signpost.OAuthProvider;
36import oauth.signpost.exception.OAuthException;
37
[5422]38/**
[6070]39 * An OAuth 1.0 authorization client.
[5422]40 * @since 2746
41 */
[2861]42public class OsmOAuthAuthorizationClient {
[5422]43 private final OAuthParameters oauthProviderParameters;
44 private final OAuthConsumer consumer;
45 private final OAuthProvider provider;
[2801]46 private boolean canceled;
[9309]47 private HttpClient connection;
[2801]48
[17196]49 protected static class SessionId {
50 protected String id;
51 protected String token;
52 protected String userName;
[4729]53 }
54
[2801]55 /**
56 * Creates a new authorisation client with the parameters <code>parameters</code>.
[3095]57 *
[2801]58 * @param parameters the OAuth parameters. Must not be null.
[5422]59 * @throws IllegalArgumentException if parameters is null
[2801]60 */
[8291]61 public OsmOAuthAuthorizationClient(OAuthParameters parameters) {
[2801]62 CheckParameterUtil.ensureParameterNotNull(parameters, "parameters");
63 oauthProviderParameters = new OAuthParameters(parameters);
64 consumer = oauthProviderParameters.buildConsumer();
65 provider = oauthProviderParameters.buildProvider(consumer);
66 }
67
68 /**
69 * Creates a new authorisation client with the parameters <code>parameters</code>
70 * and an already known Request Token.
[3095]71 *
[2801]72 * @param parameters the OAuth parameters. Must not be null.
73 * @param requestToken the request token. Must not be null.
[5422]74 * @throws IllegalArgumentException if parameters is null
75 * @throws IllegalArgumentException if requestToken is null
[2801]76 */
[8291]77 public OsmOAuthAuthorizationClient(OAuthParameters parameters, OAuthToken requestToken) {
[2801]78 CheckParameterUtil.ensureParameterNotNull(parameters, "parameters");
79 oauthProviderParameters = new OAuthParameters(parameters);
80 consumer = oauthProviderParameters.buildConsumer();
81 provider = oauthProviderParameters.buildProvider(consumer);
82 consumer.setTokenWithSecret(requestToken.getKey(), requestToken.getSecret());
83 }
84
[5422]85 /**
86 * Cancels the current OAuth operation.
87 */
[2801]88 public void cancel() {
89 canceled = true;
[8510]90 synchronized (this) {
[2801]91 if (connection != null) {
92 connection.disconnect();
93 }
94 }
95 }
96
97 /**
98 * Submits a request for a Request Token to the Request Token Endpoint Url of the OAuth Service
99 * Provider and replies the request token.
[3095]100 *
[5266]101 * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null
[2801]102 * @return the OAuth Request Token
[5422]103 * @throws OsmOAuthAuthorizationException if something goes wrong when retrieving the request token
104 * @throws OsmTransferCanceledException if the user canceled the request
[2801]105 */
[4310]106 public OAuthToken getRequestToken(ProgressMonitor monitor) throws OsmOAuthAuthorizationException, OsmTransferCanceledException {
[2801]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()));
[3494]113 provider.retrieveRequestToken(consumer, "");
[2801]114 return OAuthToken.createToken(consumer);
[8510]115 } catch (OAuthException e) {
[2801]116 if (canceled)
[6987]117 throw new OsmTransferCanceledException(e);
[2861]118 throw new OsmOAuthAuthorizationException(e);
[2801]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.
[3095]127 *
[5266]128 * You must have requested a Request Token using {@link #getRequestToken(ProgressMonitor)} first.
[3095]129 *
[5266]130 * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null
[2801]131 * @return the OAuth Access Token
[5422]132 * @throws OsmOAuthAuthorizationException if something goes wrong when retrieving the request token
133 * @throws OsmTransferCanceledException if the user canceled the request
[2801]134 * @see #getRequestToken(ProgressMonitor)
135 */
[4310]136 public OAuthToken getAccessToken(ProgressMonitor monitor) throws OsmOAuthAuthorizationException, OsmTransferCanceledException {
[2801]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()));
[3425]143 provider.retrieveAccessToken(consumer, null);
[2801]144 return OAuthToken.createToken(consumer);
[8510]145 } catch (OAuthException e) {
[2801]146 if (canceled)
[6987]147 throw new OsmTransferCanceledException(e);
[2861]148 throw new OsmOAuthAuthorizationException(e);
[2801]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.
[3095]157 *
[2801]158 * @param requestToken the request token
159 * @return the authorise URL for this request
160 */
161 public String getAuthoriseUrl(OAuthToken requestToken) {
[8390]162 StringBuilder sb = new StringBuilder(32);
[2801]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 //
[8390]167 sb.append(oauthProviderParameters.getAuthoriseUrl()).append('?'+OAuth.OAUTH_TOKEN+'=').append(requestToken.getKey());
[2801]168 return sb.toString();
169 }
170
[9172]171 protected String extractToken() {
[9309]172 try (BufferedReader r = connection.getResponse().getContentReader()) {
[4729]173 String c;
174 Pattern p = Pattern.compile(".*authenticity_token.*value=\"([^\"]+)\".*");
[6268]175 while ((c = r.readLine()) != null) {
[4729]176 Matcher m = p.matcher(c);
[6268]177 if (m.find()) {
[4729]178 return m.group(1);
[6268]179 }
[4729]180 }
181 } catch (IOException e) {
[12620]182 Logging.error(e);
[4729]183 return null;
184 }
[12620]185 Logging.warn("No authenticity_token found in response!");
[4729]186 return null;
187 }
188
[9935]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");
[9904]194 if (setCookies == null) {
[12620]195 Logging.warn("No 'Set-Cookie' in response header!");
[2801]196 return null;
[9904]197 }
[2801]198
199 for (String setCookie: setCookies) {
[16643]200 String[] kvPairs = setCookie.split(";", -1);
[11381]201 if (kvPairs.length == 0) {
[2801]202 continue;
203 }
204 for (String kvPair : kvPairs) {
205 kvPair = kvPair.trim();
[16643]206 String[] kv = kvPair.split("=", -1);
[11381]207 if (kv.length != 2) {
[2801]208 continue;
209 }
[6990]210 if ("_osm_session".equals(kv[0])) {
[2801]211 // osm session cookie found
[9172]212 String token = extractToken();
[8510]213 if (token == null)
[4729]214 return null;
215 SessionId si = new SessionId();
216 si.id = kv[1];
217 si.token = token;
218 return si;
219 }
[2801]220 }
221 }
[12620]222 Logging.warn("No suitable 'Set-Cookie' in response header found! {0}", setCookies);
[2801]223 return null;
224 }
225
[9227]226 protected static String buildPostRequest(Map<String, String> parameters) {
[8390]227 StringBuilder sb = new StringBuilder(32);
[2801]228
[8510]229 for (Iterator<Entry<String, String>> it = parameters.entrySet().iterator(); it.hasNext();) {
230 Entry<String, String> entry = it.next();
[8304]231 String value = entry.getValue();
232 value = (value == null) ? "" : value;
[8390]233 sb.append(entry.getKey()).append('=').append(Utils.encodeUrl(value));
[8304]234 if (it.hasNext()) {
[8390]235 sb.append('&');
[2801]236 }
237 }
[8304]238 return sb.toString();
[2801]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.
[3095]244 *
[4729]245 * @return the session ID structure
[5422]246 * @throws OsmOAuthAuthorizationException if something went wrong
[2801]247 */
[4729]248 protected SessionId fetchOsmWebsiteSessionId() throws OsmOAuthAuthorizationException {
[2801]249 try {
[9355]250 final URL url = new URL(oauthProviderParameters.getOsmLoginUrl() + "?cookie_test=true");
[8510]251 synchronized (this) {
[9913]252 connection = HttpClient.create(url).useCache(false);
[9309]253 connection.connect();
[2801]254 }
[9172]255 SessionId sessionId = extractOsmSession();
[2801]256 if (sessionId == null)
[8540]257 throw new OsmOAuthAuthorizationException(
258 tr("OSM website did not return a session cookie in response to ''{0}'',", url.toString()));
[2801]259 return sessionId;
[9935]260 } catch (IOException | URISyntaxException e) {
[2861]261 throw new OsmOAuthAuthorizationException(e);
[2801]262 } finally {
[8510]263 synchronized (this) {
[2801]264 connection = null;
265 }
266 }
267 }
268
[4729]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.
[9227]272 * @param sessionId session id
273 * @param requestToken request token
[4729]274 *
[5422]275 * @throws OsmOAuthAuthorizationException if something went wrong
[4729]276 */
277 protected void fetchOAuthToken(SessionId sessionId, OAuthToken requestToken) throws OsmOAuthAuthorizationException {
278 try {
279 URL url = new URL(getAuthoriseUrl(requestToken));
[8510]280 synchronized (this) {
[9172]281 connection = HttpClient.create(url)
[9913]282 .useCache(false)
[9309]283 .setHeader("Cookie", "_osm_session=" + sessionId.id + "; _osm_username=" + sessionId.userName);
284 connection.connect();
[4729]285 }
[9172]286 sessionId.token = extractToken();
[4729]287 if (sessionId.token == null)
[8540]288 throw new OsmOAuthAuthorizationException(tr("OSM website did not return a session cookie in response to ''{0}'',",
289 url.toString()));
[8510]290 } catch (IOException e) {
[4729]291 throw new OsmOAuthAuthorizationException(e);
292 } finally {
[8510]293 synchronized (this) {
[4729]294 connection = null;
295 }
296 }
297 }
298
299 protected void authenticateOsmSession(SessionId sessionId, String userName, String password) throws OsmLoginFailedException {
[2801]300 try {
[9355]301 final URL url = new URL(oauthProviderParameters.getOsmLoginUrl());
[9172]302 final HttpClient client = HttpClient.create(url, "POST").useCache(false);
[2801]303
[8510]304 Map<String, String> parameters = new HashMap<>();
[4147]305 parameters.put("username", userName);
306 parameters.put("password", password);
[2801]307 parameters.put("referer", "/");
308 parameters.put("commit", "Login");
[4729]309 parameters.put("authenticity_token", sessionId.token);
[9172]310 client.setRequestBody(buildPostRequest(parameters).getBytes(StandardCharsets.UTF_8));
[2801]311
[9172]312 client.setHeader("Content-Type", "application/x-www-form-urlencoded");
313 client.setHeader("Cookie", "_osm_session=" + sessionId.id);
[2801]314 // make sure we can catch 302 Moved Temporarily below
[9172]315 client.setMaxRedirects(-1);
[2801]316
[9172]317 synchronized (this) {
[9309]318 connection = client;
319 connection.connect();
[7037]320 }
[2801]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 //
[9309]326 int retCode = connection.getResponse().getResponseCode();
[2801]327 if (retCode != HttpURLConnection.HTTP_MOVED_TEMP)
[8540]328 throw new OsmOAuthAuthorizationException(tr("Failed to authenticate user ''{0}'' with password ''***'' as OAuth user",
329 userName));
[13207]330 } catch (OsmOAuthAuthorizationException | IOException e) {
[2801]331 throw new OsmLoginFailedException(e);
332 } finally {
[8510]333 synchronized (this) {
[2801]334 connection = null;
335 }
336 }
337 }
338
[4729]339 protected void logoutOsmSession(SessionId sessionId) throws OsmOAuthAuthorizationException {
[2801]340 try {
[9355]341 URL url = new URL(oauthProviderParameters.getOsmLogoutUrl());
[8510]342 synchronized (this) {
[9309]343 connection = HttpClient.create(url).setMaxRedirects(-1);
344 connection.connect();
[2801]345 }
[8510]346 } catch (IOException e) {
[2861]347 throw new OsmOAuthAuthorizationException(e);
[10378]348 } finally {
[8510]349 synchronized (this) {
[2801]350 connection = null;
351 }
352 }
353 }
354
[8509]355 protected void sendAuthorisationRequest(SessionId sessionId, OAuthToken requestToken, OsmPrivileges privileges)
356 throws OsmOAuthAuthorizationException {
[7005]357 Map<String, String> parameters = new HashMap<>();
[4729]358 fetchOAuthToken(sessionId, requestToken);
[2801]359 parameters.put("oauth_token", requestToken.getKey());
360 parameters.put("oauth_callback", "");
[4729]361 parameters.put("authenticity_token", sessionId.token);
[2801]362 if (privileges.isAllowWriteApi()) {
363 parameters.put("allow_write_api", "yes");
364 }
365 if (privileges.isAllowWriteGpx()) {
366 parameters.put("allow_write_gpx", "yes");
367 }
368 if (privileges.isAllowReadGpx()) {
369 parameters.put("allow_read_gpx", "yes");
370 }
371 if (privileges.isAllowWritePrefs()) {
372 parameters.put("allow_write_prefs", "yes");
373 }
374 if (privileges.isAllowReadPrefs()) {
375 parameters.put("allow_read_prefs", "yes");
376 }
[7037]377 if (privileges.isAllowModifyNotes()) {
[6066]378 parameters.put("allow_write_notes", "yes");
379 }
[2801]380
381 parameters.put("commit", "Save changes");
382
383 String request = buildPostRequest(parameters);
384 try {
385 URL url = new URL(oauthProviderParameters.getAuthoriseUrl());
[9172]386 final HttpClient client = HttpClient.create(url, "POST").useCache(false);
387 client.setHeader("Content-Type", "application/x-www-form-urlencoded");
388 client.setHeader("Cookie", "_osm_session=" + sessionId.id + "; _osm_username=" + sessionId.userName);
389 client.setMaxRedirects(-1);
390 client.setRequestBody(request.getBytes(StandardCharsets.UTF_8));
391
[8510]392 synchronized (this) {
[9309]393 connection = client;
394 connection.connect();
[2801]395 }
396
[9309]397 int retCode = connection.getResponse().getResponseCode();
[3748]398 if (retCode != HttpURLConnection.HTTP_OK)
[2877]399 throw new OsmOAuthAuthorizationException(tr("Failed to authorize OAuth request ''{0}''", requestToken.getKey()));
[7037]400 } catch (IOException e) {
[2861]401 throw new OsmOAuthAuthorizationException(e);
[2801]402 } finally {
[8510]403 synchronized (this) {
[2801]404 connection = null;
405 }
406 }
407 }
408
409 /**
410 * Automatically authorises a request token for a set of privileges.
[3095]411 *
[2801]412 * @param requestToken the request token. Must not be null.
[8509]413 * @param userName the OSM user name. Must not be null.
414 * @param password the OSM password. Must not be null.
[2801]415 * @param privileges the set of privileges. Must not be null.
[5266]416 * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null
[5422]417 * @throws IllegalArgumentException if requestToken is null
418 * @throws IllegalArgumentException if osmUserName is null
419 * @throws IllegalArgumentException if osmPassword is null
420 * @throws IllegalArgumentException if privileges is null
421 * @throws OsmOAuthAuthorizationException if the authorisation fails
422 * @throws OsmTransferCanceledException if the task is canceled by the user
[2801]423 */
[8509]424 public void authorise(OAuthToken requestToken, String userName, String password, OsmPrivileges privileges, ProgressMonitor monitor)
425 throws OsmOAuthAuthorizationException, OsmTransferCanceledException {
[2801]426 CheckParameterUtil.ensureParameterNotNull(requestToken, "requestToken");
[8509]427 CheckParameterUtil.ensureParameterNotNull(userName, "userName");
428 CheckParameterUtil.ensureParameterNotNull(password, "password");
[2801]429 CheckParameterUtil.ensureParameterNotNull(privileges, "privileges");
430
431 if (monitor == null) {
432 monitor = NullProgressMonitor.INSTANCE;
433 }
434 try {
[2877]435 monitor.beginTask(tr("Authorizing OAuth Request token ''{0}'' at the OSM website ...", requestToken.getKey()));
[2801]436 monitor.setTicksCount(4);
437 monitor.indeterminateSubTask(tr("Initializing a session at the OSM website..."));
[4729]438 SessionId sessionId = fetchOsmWebsiteSessionId();
[8509]439 sessionId.userName = userName;
[2801]440 if (canceled)
[8415]441 throw new OsmTransferCanceledException("Authorization canceled");
[2801]442 monitor.worked(1);
443
[8509]444 monitor.indeterminateSubTask(tr("Authenticating the session for user ''{0}''...", userName));
445 authenticateOsmSession(sessionId, userName, password);
[2801]446 if (canceled)
[8415]447 throw new OsmTransferCanceledException("Authorization canceled");
[2801]448 monitor.worked(1);
449
[2877]450 monitor.indeterminateSubTask(tr("Authorizing request token ''{0}''...", requestToken.getKey()));
[2801]451 sendAuthorisationRequest(sessionId, requestToken, privileges);
452 if (canceled)
[8415]453 throw new OsmTransferCanceledException("Authorization canceled");
[2801]454 monitor.worked(1);
455
456 monitor.indeterminateSubTask(tr("Logging out session ''{0}''...", sessionId));
457 logoutOsmSession(sessionId);
458 if (canceled)
[8415]459 throw new OsmTransferCanceledException("Authorization canceled");
[2801]460 monitor.worked(1);
[8510]461 } catch (OsmOAuthAuthorizationException e) {
[2801]462 if (canceled)
[6987]463 throw new OsmTransferCanceledException(e);
[2801]464 throw e;
465 } finally {
466 monitor.finishTask();
467 }
468 }
469}
Note: See TracBrowser for help on using the repository browser.