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

Last change on this file since 9935 was 9935, checked in by simon04, 8 years ago

see #12584 - Attempt to obtain the session cookie via CookieManager

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