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

Last change on this file since 3188 was 3188, checked in by stoecker, 14 years ago

close #4790 - OAuth uses HTTPS

  • Property svn:eol-style set to native
File size: 21.5 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.DataOutputStream;
7import java.io.IOException;
8import java.io.UnsupportedEncodingException;
9import java.lang.reflect.Field;
10import java.net.HttpURLConnection;
11import java.net.MalformedURLException;
12import java.net.URL;
13import java.net.URLEncoder;
14import java.util.HashMap;
15import java.util.Iterator;
16import java.util.List;
17import java.util.Map;
18import java.util.Map.Entry;
19import java.util.logging.Logger;
20
21import oauth.signpost.OAuth;
22import oauth.signpost.OAuthConsumer;
23import oauth.signpost.OAuthProvider;
24import oauth.signpost.basic.DefaultOAuthProvider;
25import oauth.signpost.exception.OAuthCommunicationException;
26import oauth.signpost.exception.OAuthException;
27
28import org.openstreetmap.josm.Main;
29import org.openstreetmap.josm.data.Version;
30import org.openstreetmap.josm.data.oauth.OAuthParameters;
31import org.openstreetmap.josm.data.oauth.OAuthToken;
32import org.openstreetmap.josm.data.oauth.OsmPrivileges;
33import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
34import org.openstreetmap.josm.gui.progress.ProgressMonitor;
35import org.openstreetmap.josm.io.OsmTransferCancelledException;
36import org.openstreetmap.josm.tools.CheckParameterUtil;
37
38public class OsmOAuthAuthorizationClient {
39 @SuppressWarnings("unused")
40 static private final Logger logger = Logger.getLogger(OsmOAuthAuthorizationClient.class.getName());
41
42 private OAuthParameters oauthProviderParameters;
43 private OAuthConsumer consumer;
44 private OAuthProvider provider;
45 private boolean canceled;
46 private HttpURLConnection connection;
47
48 /**
49 * Creates a new authorisation client with default OAuth parameters
50 *
51 */
52 public OsmOAuthAuthorizationClient() {
53 oauthProviderParameters = OAuthParameters.createDefault();
54 consumer = oauthProviderParameters.buildConsumer();
55 provider = oauthProviderParameters.buildProvider(consumer);
56 }
57
58 /**
59 * Creates a new authorisation client with the parameters <code>parameters</code>.
60 *
61 * @param parameters the OAuth parameters. Must not be null.
62 * @throws IllegalArgumentException thrown if parameters is null
63 */
64 public OsmOAuthAuthorizationClient(OAuthParameters parameters) throws IllegalArgumentException {
65 CheckParameterUtil.ensureParameterNotNull(parameters, "parameters");
66 oauthProviderParameters = new OAuthParameters(parameters);
67 consumer = oauthProviderParameters.buildConsumer();
68 provider = oauthProviderParameters.buildProvider(consumer);
69 }
70
71 /**
72 * Creates a new authorisation client with the parameters <code>parameters</code>
73 * and an already known Request Token.
74 *
75 * @param parameters the OAuth parameters. Must not be null.
76 * @param requestToken the request token. Must not be null.
77 * @throws IllegalArgumentException thrown if parameters is null
78 * @throws IllegalArgumentException thrown if requestToken is null
79 */
80 public OsmOAuthAuthorizationClient(OAuthParameters parameters, OAuthToken requestToken) throws IllegalArgumentException {
81 CheckParameterUtil.ensureParameterNotNull(parameters, "parameters");
82 oauthProviderParameters = new OAuthParameters(parameters);
83 consumer = oauthProviderParameters.buildConsumer();
84 provider = oauthProviderParameters.buildProvider(consumer);
85 consumer.setTokenWithSecret(requestToken.getKey(), requestToken.getSecret());
86 }
87
88 public void cancel() {
89 DefaultOAuthProvider p = (DefaultOAuthProvider)provider;
90 canceled = true;
91 if (p != null) {
92 try {
93 Field f = p.getClass().getDeclaredField("connection");
94 f.setAccessible(true);
95 HttpURLConnection con = (HttpURLConnection)f.get(p);
96 if (con != null) {
97 con.disconnect();
98 }
99 } catch(NoSuchFieldException e) {
100 e.printStackTrace();
101 System.err.println(tr("Warning: failed to cancel running OAuth operation"));
102 } catch(SecurityException e) {
103 e.printStackTrace();
104 System.err.println(tr("Warning: failed to cancel running OAuth operation"));
105 } catch(IllegalAccessException e) {
106 e.printStackTrace();
107 System.err.println(tr("Warning: failed to cancel running OAuth operation"));
108 }
109 }
110 synchronized(this) {
111 if (connection != null) {
112 connection.disconnect();
113 }
114 }
115 }
116
117 /**
118 * Submits a request for a Request Token to the Request Token Endpoint Url of the OAuth Service
119 * Provider and replies the request token.
120 *
121 * @param monitor a progress monitor. Defaults to {@see NullProgressMonitor#INSTANCE} if null
122 * @return the OAuth Request Token
123 * @throws OsmOAuthAuthorizationException thrown if something goes wrong when retrieving the request token
124 */
125 public OAuthToken getRequestToken(ProgressMonitor monitor) throws OsmOAuthAuthorizationException, OsmTransferCancelledException {
126 if (monitor == null) {
127 monitor = NullProgressMonitor.INSTANCE;
128 }
129 try {
130 monitor.beginTask("");
131 monitor.indeterminateSubTask(tr("Retrieving OAuth Request Token from ''{0}''", oauthProviderParameters.getRequestTokenUrl()));
132 provider.retrieveRequestToken(null);
133 return OAuthToken.createToken(consumer);
134 } catch(OAuthCommunicationException e){
135 if (canceled)
136 throw new OsmTransferCancelledException();
137 throw new OsmOAuthAuthorizationException(e);
138 } catch(OAuthException e){
139 if (canceled)
140 throw new OsmTransferCancelledException();
141 throw new OsmOAuthAuthorizationException(e);
142 } finally {
143 monitor.finishTask();
144 }
145 }
146
147 /**
148 * Submits a request for an Access Token to the Access Token Endpoint Url of the OAuth Service
149 * Provider and replies the request token.
150 *
151 * You must have requested a Request Token using {@see #getRequestToken(ProgressMonitor)} first.
152 *
153 * @param monitor a progress monitor. Defaults to {@see NullProgressMonitor#INSTANCE} if null
154 * @return the OAuth Access Token
155 * @throws OsmOAuthAuthorizationException thrown if something goes wrong when retrieving the request token
156 * @see #getRequestToken(ProgressMonitor)
157 */
158 public OAuthToken getAccessToken(ProgressMonitor monitor) throws OsmOAuthAuthorizationException, OsmTransferCancelledException {
159 if (monitor == null) {
160 monitor = NullProgressMonitor.INSTANCE;
161 }
162 try {
163 monitor.beginTask("");
164 monitor.indeterminateSubTask(tr("Retrieving OAuth Access Token from ''{0}''", oauthProviderParameters.getAccessTokenUrl()));
165 provider.retrieveAccessToken(null);
166 return OAuthToken.createToken(consumer);
167 } catch(OAuthCommunicationException e){
168 if (canceled)
169 throw new OsmTransferCancelledException();
170 throw new OsmOAuthAuthorizationException(e);
171 } catch(OAuthException e){
172 if (canceled)
173 throw new OsmTransferCancelledException();
174 throw new OsmOAuthAuthorizationException(e);
175 } finally {
176 monitor.finishTask();
177 }
178 }
179
180 /**
181 * Builds the authorise URL for a given Request Token. Users can be redirected to this URL.
182 * There they can login to OSM and authorise the request.
183 *
184 * @param requestToken the request token
185 * @return the authorise URL for this request
186 */
187 public String getAuthoriseUrl(OAuthToken requestToken) {
188 StringBuilder sb = new StringBuilder();
189
190 // OSM is an OAuth 1.0 provider and JOSM isn't a web app. We just add the oauth request token to
191 // the authorisation request, no callback parameter.
192 //
193 sb.append(oauthProviderParameters.getAuthoriseUrl()).append("?")
194 .append(OAuth.OAUTH_TOKEN).append("=").append(requestToken.getKey());
195 return sb.toString();
196 }
197
198 protected String extractOsmSession(HttpURLConnection connection) {
199 List<String> setCookies = connection.getHeaderFields().get("Set-Cookie");
200 if (setCookies == null)
201 // no cookies set
202 return null;
203
204 for (String setCookie: setCookies) {
205 String[] kvPairs = setCookie.split(";");
206 if (kvPairs == null || kvPairs.length == 0) {
207 continue;
208 }
209 for (String kvPair : kvPairs) {
210 kvPair = kvPair.trim();
211 String [] kv = kvPair.split("=");
212 if (kv == null || kv.length != 2) {
213 continue;
214 }
215 if (kv[0].equals("_osm_session"))
216 // osm session cookie found
217 return kv[1];
218 }
219 }
220 return null;
221 }
222
223 protected String buildPostRequest(Map<String,String> parameters) throws OsmOAuthAuthorizationException {
224 try {
225 StringBuilder sb = new StringBuilder();
226
227 for(Iterator<Entry<String,String>> it = parameters.entrySet().iterator(); it.hasNext();) {
228 Entry<String,String> entry = it.next();
229 String value = entry.getValue();
230 value = (value == null) ? "" : value;
231 sb.append(entry.getKey()).append("=").append(URLEncoder.encode(value, "UTF-8"));
232 if (it.hasNext()) {
233 sb.append("&");
234 }
235 }
236 return sb.toString();
237 } catch(UnsupportedEncodingException e) {
238 throw new OsmOAuthAuthorizationException(e);
239 }
240 }
241
242 /**
243 * Derives the OSM login URL from the OAuth Authorization Website URL
244 *
245 * @return the OSM login URL
246 * @throws OsmOAuthAuthorizationException thrown if something went wrong, in particular if the
247 * URLs are malformed
248 */
249 public String buildOsmLoginUrl() throws OsmOAuthAuthorizationException{
250 try {
251 URL autUrl = new URL(oauthProviderParameters.getAuthoriseUrl());
252 URL url = new URL(Main.pref.get("oauth.protocol", "https"), autUrl.getHost(), autUrl.getPort(), "/login");
253 return url.toString();
254 } catch(MalformedURLException e) {
255 throw new OsmOAuthAuthorizationException(e);
256 }
257 }
258
259 /**
260 * Derives the OSM logout URL from the OAuth Authorization Website URL
261 *
262 * @return the OSM logout URL
263 * @throws OsmOAuthAuthorizationException thrown if something went wrong, in particular if the
264 * URLs are malformed
265 */
266 protected String buildOsmLogoutUrl() throws OsmOAuthAuthorizationException{
267 try {
268 URL autUrl = new URL(oauthProviderParameters.getAuthoriseUrl());
269 URL url = new URL("http", autUrl.getHost(), autUrl.getPort(), "/logout");
270 return url.toString();
271 } catch(MalformedURLException e) {
272 throw new OsmOAuthAuthorizationException(e);
273 }
274 }
275
276 /**
277 * Submits a request to the OSM website for a login form. The OSM website replies a session ID in
278 * a cookie.
279 *
280 * @return the session ID
281 * @throws OsmOAuthAuthorizationException thrown if something went wrong
282 */
283 protected String fetchOsmWebsiteSessionId() throws OsmOAuthAuthorizationException {
284 try {
285 StringBuilder sb = new StringBuilder();
286 sb.append(buildOsmLoginUrl()).append("?cookie_test=true");
287 URL url = new URL(sb.toString());
288 synchronized(this) {
289 connection = (HttpURLConnection)url.openConnection();
290 }
291 connection.setRequestMethod("GET");
292 connection.setDoInput(true);
293 connection.setDoOutput(false);
294 setHttpRequestParameters(connection);
295 connection.connect();
296 String sessionId = extractOsmSession(connection);
297 if (sessionId == null)
298 throw new OsmOAuthAuthorizationException(tr("OSM website did not return a session cookie in response to ''{0}'',", url.toString()));
299 return sessionId;
300 } catch(IOException e) {
301 throw new OsmOAuthAuthorizationException(e);
302 } finally {
303 synchronized(this) {
304 connection = null;
305 }
306 }
307 }
308
309 protected void authenticateOsmSession(String sessionId, String userName, String password) throws OsmLoginFailedException {
310 DataOutputStream dout = null;
311 try {
312 URL url = new URL(buildOsmLoginUrl());
313 synchronized(this) {
314 connection = (HttpURLConnection)url.openConnection();
315 }
316 connection.setRequestMethod("POST");
317 connection.setDoInput(true);
318 connection.setDoOutput(true);
319 connection.setUseCaches(false);
320
321 Map<String,String> parameters = new HashMap<String, String>();
322 parameters.put("user[email]", userName);
323 parameters.put("user[password]", password);
324 parameters.put("referer", "/");
325 parameters.put("commit", "Login");
326
327 String request = buildPostRequest(parameters);
328
329 connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
330 connection.setRequestProperty("Content-Length", Integer.toString(request.length()));
331 connection.setRequestProperty("Cookie", "_osm_session=" + sessionId);
332 // make sure we can catch 302 Moved Temporarily below
333 connection.setInstanceFollowRedirects(false);
334 setHttpRequestParameters(connection);
335
336 connection.connect();
337
338 dout = new DataOutputStream(connection.getOutputStream());
339 dout.writeBytes(request);
340 dout.flush();
341 dout.close();
342
343 // after a successful login the OSM website sends a redirect to a follow up page. Everything
344 // else, including a 200 OK, is a failed login. A 200 OK is replied if the login form with
345 // an error page is sent to back to the user.
346 //
347 int retCode = connection.getResponseCode();
348 if (retCode != HttpURLConnection.HTTP_MOVED_TEMP)
349 throw new OsmOAuthAuthorizationException(tr("Failed to authenticate user ''{0}'' with password ''***'' as OAuth user", userName));
350 } catch(OsmOAuthAuthorizationException e) {
351 throw new OsmLoginFailedException(e.getCause());
352 } catch(IOException e) {
353 throw new OsmLoginFailedException(e);
354 } finally {
355 if (dout != null) {
356 try {
357 dout.close();
358 } catch(IOException e) { /* ignore */ }
359 }
360 synchronized(this) {
361 connection = null;
362 }
363 }
364 }
365
366 protected void logoutOsmSession(String sessionId) throws OsmOAuthAuthorizationException {
367 try {
368 URL url = new URL(buildOsmLogoutUrl());
369 synchronized(this) {
370 connection = (HttpURLConnection)url.openConnection();
371 }
372 connection.setRequestMethod("GET");
373 connection.setDoInput(true);
374 connection.setDoOutput(false);
375 setHttpRequestParameters(connection);
376 connection.connect();
377 }catch(MalformedURLException e) {
378 throw new OsmOAuthAuthorizationException(e);
379 } catch(IOException e) {
380 throw new OsmOAuthAuthorizationException(e);
381 } finally {
382 synchronized(this) {
383 connection = null;
384 }
385 }
386 }
387
388 protected void sendAuthorisationRequest(String sessionId, OAuthToken requestToken, OsmPrivileges privileges) throws OsmOAuthAuthorizationException {
389 Map<String, String> parameters = new HashMap<String, String>();
390 parameters.put("oauth_token", requestToken.getKey());
391 parameters.put("oauth_callback", "");
392 if (privileges.isAllowWriteApi()) {
393 parameters.put("allow_write_api", "yes");
394 }
395 if (privileges.isAllowWriteGpx()) {
396 parameters.put("allow_write_gpx", "yes");
397 }
398 if (privileges.isAllowReadGpx()) {
399 parameters.put("allow_read_gpx", "yes");
400 }
401 if (privileges.isAllowWritePrefs()) {
402 parameters.put("allow_write_prefs", "yes");
403 }
404 if (privileges.isAllowReadPrefs()) {
405 parameters.put("allow_read_prefs", "yes");
406 }
407
408 parameters.put("commit", "Save changes");
409
410 String request = buildPostRequest(parameters);
411 DataOutputStream dout = null;
412 try {
413 URL url = new URL(oauthProviderParameters.getAuthoriseUrl());
414 synchronized(this) {
415 connection = (HttpURLConnection)url.openConnection();
416 }
417 connection.setRequestMethod("POST");
418 connection.setDoInput(true);
419 connection.setDoOutput(true);
420 connection.setUseCaches(false);
421 connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
422 connection.setRequestProperty("Content-Length", Integer.toString(request.length()));
423 connection.setRequestProperty("Cookie", "_osm_session=" + sessionId);
424 connection.setInstanceFollowRedirects(false);
425 setHttpRequestParameters(connection);
426
427 connection.connect();
428
429 dout = new DataOutputStream(connection.getOutputStream());
430 dout.writeBytes(request);
431 dout.flush();
432 dout.close();
433
434 int retCode = connection.getResponseCode();
435 if (retCode != HttpURLConnection.HTTP_MOVED_TEMP)
436 throw new OsmOAuthAuthorizationException(tr("Failed to authorize OAuth request ''{0}''", requestToken.getKey()));
437 } catch(MalformedURLException e) {
438 throw new OsmOAuthAuthorizationException(e);
439 } catch(IOException e) {
440 throw new OsmOAuthAuthorizationException(e);
441 } finally {
442 if (dout != null) {
443 try {
444 dout.close();
445 } catch(IOException e) { /* ignore */ }
446 }
447 synchronized(this) {
448 connection = null;
449 }
450 }
451 }
452
453 protected void setHttpRequestParameters(HttpURLConnection connection) {
454 connection.setRequestProperty("User-Agent", Version.getInstance().getAgentString());
455 connection.setRequestProperty("Host", connection.getURL().getHost());
456 }
457
458 /**
459 * Automatically authorises a request token for a set of privileges.
460 *
461 * @param requestToken the request token. Must not be null.
462 * @param osmUserName the OSM user name. Must not be null.
463 * @param osmPassword the OSM password. Must not be null.
464 * @param privileges the set of privileges. Must not be null.
465 * @param monitor a progress monitor. Defaults to {@see NullProgressMonitor#INSTANCE} if null
466 * @throws IllegalArgumentException thrown if requestToken is null
467 * @throws IllegalArgumentException thrown if osmUserName is null
468 * @throws IllegalArgumentException thrown if osmPassword is null
469 * @throws IllegalArgumentException thrown if privileges is null
470 * @throws OsmOAuthAuthorizationException thrown if the authorisation fails
471 * @throws OsmTransferCancelledException thrown if the task is cancelled by the user
472 */
473 public void authorise(OAuthToken requestToken, String osmUserName, String osmPassword, OsmPrivileges privileges, ProgressMonitor monitor) throws IllegalArgumentException, OsmOAuthAuthorizationException, OsmTransferCancelledException{
474 CheckParameterUtil.ensureParameterNotNull(requestToken, "requestToken");
475 CheckParameterUtil.ensureParameterNotNull(osmUserName, "osmUserName");
476 CheckParameterUtil.ensureParameterNotNull(osmPassword, "osmPassword");
477 CheckParameterUtil.ensureParameterNotNull(privileges, "privileges");
478
479 if (monitor == null) {
480 monitor = NullProgressMonitor.INSTANCE;
481 }
482 try {
483 monitor.beginTask(tr("Authorizing OAuth Request token ''{0}'' at the OSM website ...", requestToken.getKey()));
484 monitor.setTicksCount(4);
485 monitor.indeterminateSubTask(tr("Initializing a session at the OSM website..."));
486 String sessionId = fetchOsmWebsiteSessionId();
487 if (canceled)
488 throw new OsmTransferCancelledException();
489 monitor.worked(1);
490
491 monitor.indeterminateSubTask(tr("Authenticating the session for user ''{0}''...", osmUserName));
492 authenticateOsmSession(sessionId, osmUserName, osmPassword);
493 if (canceled)
494 throw new OsmTransferCancelledException();
495 monitor.worked(1);
496
497 monitor.indeterminateSubTask(tr("Authorizing request token ''{0}''...", requestToken.getKey()));
498 sendAuthorisationRequest(sessionId, requestToken, privileges);
499 if (canceled)
500 throw new OsmTransferCancelledException();
501 monitor.worked(1);
502
503 monitor.indeterminateSubTask(tr("Logging out session ''{0}''...", sessionId));
504 logoutOsmSession(sessionId);
505 if (canceled)
506 throw new OsmTransferCancelledException();
507 monitor.worked(1);
508 } catch(OsmOAuthAuthorizationException e) {
509 if (canceled)
510 throw new OsmTransferCancelledException();
511 throw e;
512 } finally {
513 monitor.finishTask();
514 }
515 }
516}
Note: See TracBrowser for help on using the repository browser.