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

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

fix remaining checkstyle issues

  • Property svn:eol-style set to native
File size: 22.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.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(
320 tr("OSM website did not return a session cookie in response to ''{0}'',", url.toString()));
321 return sessionId;
322 } catch (IOException e) {
323 throw new OsmOAuthAuthorizationException(e);
324 } finally {
325 synchronized (this) {
326 connection = null;
327 }
328 }
329 }
330
331 /**
332 * Submits a request to the OSM website for a OAuth form. The OSM website replies a session token in
333 * a hidden parameter.
334 *
335 * @throws OsmOAuthAuthorizationException if something went wrong
336 */
337 protected void fetchOAuthToken(SessionId sessionId, OAuthToken requestToken) throws OsmOAuthAuthorizationException {
338 try {
339 URL url = new URL(getAuthoriseUrl(requestToken));
340 synchronized (this) {
341 connection = Utils.openHttpConnection(url);
342 }
343 connection.setRequestMethod("GET");
344 connection.setDoInput(true);
345 connection.setDoOutput(false);
346 connection.setRequestProperty("Cookie", "_osm_session=" + sessionId.id + "; _osm_username=" + sessionId.userName);
347 connection.connect();
348 sessionId.token = extractToken(connection);
349 if (sessionId.token == null)
350 throw new OsmOAuthAuthorizationException(tr("OSM website did not return a session cookie in response to ''{0}'',",
351 url.toString()));
352 } catch (IOException e) {
353 throw new OsmOAuthAuthorizationException(e);
354 } finally {
355 synchronized (this) {
356 connection = null;
357 }
358 }
359 }
360
361 protected void authenticateOsmSession(SessionId sessionId, String userName, String password) throws OsmLoginFailedException {
362 try {
363 URL url = new URL(buildOsmLoginUrl());
364 synchronized (this) {
365 connection = Utils.openHttpConnection(url);
366 }
367 connection.setRequestMethod("POST");
368 connection.setDoInput(true);
369 connection.setDoOutput(true);
370 connection.setUseCaches(false);
371
372 Map<String, String> parameters = new HashMap<>();
373 parameters.put("username", userName);
374 parameters.put("password", password);
375 parameters.put("referer", "/");
376 parameters.put("commit", "Login");
377 parameters.put("authenticity_token", sessionId.token);
378
379 String request = buildPostRequest(parameters);
380
381 connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
382 connection.setRequestProperty("Content-Length", Integer.toString(request.length()));
383 connection.setRequestProperty("Cookie", "_osm_session=" + sessionId.id);
384 // make sure we can catch 302 Moved Temporarily below
385 connection.setInstanceFollowRedirects(false);
386
387 connection.connect();
388
389 try (DataOutputStream dout = new DataOutputStream(connection.getOutputStream())) {
390 dout.writeBytes(request);
391 dout.flush();
392 }
393
394 // after a successful login the OSM website sends a redirect to a follow up page. Everything
395 // else, including a 200 OK, is a failed login. A 200 OK is replied if the login form with
396 // an error page is sent to back to the user.
397 //
398 int retCode = connection.getResponseCode();
399 if (retCode != HttpURLConnection.HTTP_MOVED_TEMP)
400 throw new OsmOAuthAuthorizationException(tr("Failed to authenticate user ''{0}'' with password ''***'' as OAuth user",
401 userName));
402 } catch (OsmOAuthAuthorizationException e) {
403 throw new OsmLoginFailedException(e.getCause());
404 } catch (IOException e) {
405 throw new OsmLoginFailedException(e);
406 } finally {
407 synchronized (this) {
408 connection = null;
409 }
410 }
411 }
412
413 protected void logoutOsmSession(SessionId sessionId) throws OsmOAuthAuthorizationException {
414 try {
415 URL url = new URL(buildOsmLogoutUrl());
416 synchronized (this) {
417 connection = Utils.openHttpConnection(url);
418 }
419 connection.setRequestMethod("GET");
420 connection.setDoInput(true);
421 connection.setDoOutput(false);
422 connection.connect();
423 } catch (IOException e) {
424 throw new OsmOAuthAuthorizationException(e);
425 } finally {
426 synchronized (this) {
427 connection = null;
428 }
429 }
430 }
431
432 protected void sendAuthorisationRequest(SessionId sessionId, OAuthToken requestToken, OsmPrivileges privileges)
433 throws OsmOAuthAuthorizationException {
434 Map<String, String> parameters = new HashMap<>();
435 fetchOAuthToken(sessionId, requestToken);
436 parameters.put("oauth_token", requestToken.getKey());
437 parameters.put("oauth_callback", "");
438 parameters.put("authenticity_token", sessionId.token);
439 if (privileges.isAllowWriteApi()) {
440 parameters.put("allow_write_api", "yes");
441 }
442 if (privileges.isAllowWriteGpx()) {
443 parameters.put("allow_write_gpx", "yes");
444 }
445 if (privileges.isAllowReadGpx()) {
446 parameters.put("allow_read_gpx", "yes");
447 }
448 if (privileges.isAllowWritePrefs()) {
449 parameters.put("allow_write_prefs", "yes");
450 }
451 if (privileges.isAllowReadPrefs()) {
452 parameters.put("allow_read_prefs", "yes");
453 }
454 if (privileges.isAllowModifyNotes()) {
455 parameters.put("allow_write_notes", "yes");
456 }
457
458 parameters.put("commit", "Save changes");
459
460 String request = buildPostRequest(parameters);
461 try {
462 URL url = new URL(oauthProviderParameters.getAuthoriseUrl());
463 synchronized (this) {
464 connection = Utils.openHttpConnection(url);
465 }
466 connection.setRequestMethod("POST");
467 connection.setDoInput(true);
468 connection.setDoOutput(true);
469 connection.setUseCaches(false);
470 connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
471 connection.setRequestProperty("Content-Length", Integer.toString(request.length()));
472 connection.setRequestProperty("Cookie", "_osm_session=" + sessionId.id + "; _osm_username=" + sessionId.userName);
473 connection.setInstanceFollowRedirects(false);
474
475 connection.connect();
476
477 try (DataOutputStream dout = new DataOutputStream(connection.getOutputStream())) {
478 dout.writeBytes(request);
479 dout.flush();
480 }
481
482 int retCode = connection.getResponseCode();
483 if (retCode != HttpURLConnection.HTTP_OK)
484 throw new OsmOAuthAuthorizationException(tr("Failed to authorize OAuth request ''{0}''", requestToken.getKey()));
485 } catch (IOException e) {
486 throw new OsmOAuthAuthorizationException(e);
487 } finally {
488 synchronized (this) {
489 connection = null;
490 }
491 }
492 }
493
494 /**
495 * Automatically authorises a request token for a set of privileges.
496 *
497 * @param requestToken the request token. Must not be null.
498 * @param userName the OSM user name. Must not be null.
499 * @param password the OSM password. Must not be null.
500 * @param privileges the set of privileges. Must not be null.
501 * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null
502 * @throws IllegalArgumentException if requestToken is null
503 * @throws IllegalArgumentException if osmUserName is null
504 * @throws IllegalArgumentException if osmPassword is null
505 * @throws IllegalArgumentException if privileges is null
506 * @throws OsmOAuthAuthorizationException if the authorisation fails
507 * @throws OsmTransferCanceledException if the task is canceled by the user
508 */
509 public void authorise(OAuthToken requestToken, String userName, String password, OsmPrivileges privileges, ProgressMonitor monitor)
510 throws OsmOAuthAuthorizationException, OsmTransferCanceledException {
511 CheckParameterUtil.ensureParameterNotNull(requestToken, "requestToken");
512 CheckParameterUtil.ensureParameterNotNull(userName, "userName");
513 CheckParameterUtil.ensureParameterNotNull(password, "password");
514 CheckParameterUtil.ensureParameterNotNull(privileges, "privileges");
515
516 if (monitor == null) {
517 monitor = NullProgressMonitor.INSTANCE;
518 }
519 try {
520 monitor.beginTask(tr("Authorizing OAuth Request token ''{0}'' at the OSM website ...", requestToken.getKey()));
521 monitor.setTicksCount(4);
522 monitor.indeterminateSubTask(tr("Initializing a session at the OSM website..."));
523 SessionId sessionId = fetchOsmWebsiteSessionId();
524 sessionId.userName = userName;
525 if (canceled)
526 throw new OsmTransferCanceledException("Authorization canceled");
527 monitor.worked(1);
528
529 monitor.indeterminateSubTask(tr("Authenticating the session for user ''{0}''...", userName));
530 authenticateOsmSession(sessionId, userName, password);
531 if (canceled)
532 throw new OsmTransferCanceledException("Authorization canceled");
533 monitor.worked(1);
534
535 monitor.indeterminateSubTask(tr("Authorizing request token ''{0}''...", requestToken.getKey()));
536 sendAuthorisationRequest(sessionId, requestToken, privileges);
537 if (canceled)
538 throw new OsmTransferCanceledException("Authorization canceled");
539 monitor.worked(1);
540
541 monitor.indeterminateSubTask(tr("Logging out session ''{0}''...", sessionId));
542 logoutOsmSession(sessionId);
543 if (canceled)
544 throw new OsmTransferCanceledException("Authorization canceled");
545 monitor.worked(1);
546 } catch (OsmOAuthAuthorizationException e) {
547 if (canceled)
548 throw new OsmTransferCanceledException(e);
549 throw e;
550 } finally {
551 monitor.finishTask();
552 }
553 }
554}
Note: See TracBrowser for help on using the repository browser.