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

Last change on this file since 8285 was 8285, checked in by Don-vip, 9 years ago

fix sonar squid:S2039 - Member variable visibility should be specified

  • Property svn:eol-style set to native
File size: 22.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.oauth;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.BufferedReader;
7import java.io.DataOutputStream;
8import java.io.IOException;
9import java.io.InputStream;
10import java.io.InputStreamReader;
11import java.io.UnsupportedEncodingException;
12import java.lang.reflect.Field;
13import java.net.HttpURLConnection;
14import java.net.MalformedURLException;
15import java.net.URL;
16import java.net.URLEncoder;
17import java.nio.charset.StandardCharsets;
18import java.util.HashMap;
19import java.util.Iterator;
20import java.util.List;
21import java.util.Map;
22import java.util.Map.Entry;
23import java.util.regex.Matcher;
24import java.util.regex.Pattern;
25
26import oauth.signpost.OAuth;
27import oauth.signpost.OAuthConsumer;
28import oauth.signpost.OAuthProvider;
29import oauth.signpost.basic.DefaultOAuthProvider;
30import oauth.signpost.exception.OAuthException;
31
32import org.openstreetmap.josm.Main;
33import org.openstreetmap.josm.data.oauth.OAuthParameters;
34import org.openstreetmap.josm.data.oauth.OAuthToken;
35import org.openstreetmap.josm.data.oauth.OsmPrivileges;
36import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
37import org.openstreetmap.josm.gui.progress.ProgressMonitor;
38import org.openstreetmap.josm.io.OsmTransferCanceledException;
39import org.openstreetmap.josm.tools.CheckParameterUtil;
40import org.openstreetmap.josm.tools.Utils;
41
42/**
43 * An OAuth 1.0 authorization client.
44 * @since 2746
45 */
46public class OsmOAuthAuthorizationClient {
47 private final OAuthParameters oauthProviderParameters;
48 private final OAuthConsumer consumer;
49 private final OAuthProvider provider;
50 private boolean canceled;
51 private HttpURLConnection connection;
52
53 private static class SessionId {
54 private String id;
55 private String token;
56 private String userName;
57 }
58
59 /**
60 * Creates a new authorisation client with default OAuth parameters
61 *
62 */
63 public OsmOAuthAuthorizationClient() {
64 oauthProviderParameters = OAuthParameters.createDefault(Main.pref.get("osm-server.url"));
65 consumer = oauthProviderParameters.buildConsumer();
66 provider = oauthProviderParameters.buildProvider(consumer);
67 }
68
69 /**
70 * Creates a new authorisation client with the parameters <code>parameters</code>.
71 *
72 * @param parameters the OAuth parameters. Must not be null.
73 * @throws IllegalArgumentException if parameters is null
74 */
75 public OsmOAuthAuthorizationClient(OAuthParameters parameters) throws IllegalArgumentException {
76 CheckParameterUtil.ensureParameterNotNull(parameters, "parameters");
77 oauthProviderParameters = new OAuthParameters(parameters);
78 consumer = oauthProviderParameters.buildConsumer();
79 provider = oauthProviderParameters.buildProvider(consumer);
80 }
81
82 /**
83 * Creates a new authorisation client with the parameters <code>parameters</code>
84 * and an already known Request Token.
85 *
86 * @param parameters the OAuth parameters. Must not be null.
87 * @param requestToken the request token. Must not be null.
88 * @throws IllegalArgumentException if parameters is null
89 * @throws IllegalArgumentException if requestToken is null
90 */
91 public OsmOAuthAuthorizationClient(OAuthParameters parameters, OAuthToken requestToken) throws IllegalArgumentException {
92 CheckParameterUtil.ensureParameterNotNull(parameters, "parameters");
93 oauthProviderParameters = new OAuthParameters(parameters);
94 consumer = oauthProviderParameters.buildConsumer();
95 provider = oauthProviderParameters.buildProvider(consumer);
96 consumer.setTokenWithSecret(requestToken.getKey(), requestToken.getSecret());
97 }
98
99 /**
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}
Note: See TracBrowser for help on using the repository browser.