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

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

remove extra whitespaces

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