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

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

findbugs - RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE

  • 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 Field f = provider.getClass().getDeclaredField("connection");
94 Utils.setObjectsAccessible(f);
95 HttpURLConnection con = (HttpURLConnection) f.get(provider);
96 if (con != null) {
97 con.disconnect();
98 }
99 } catch (NoSuchFieldException | SecurityException | IllegalAccessException e) {
100 Main.error(e);
101 Main.warn(tr("Failed to cancel running OAuth operation"));
102 }
103 }
104 synchronized (this) {
105 if (connection != null) {
106 connection.disconnect();
107 }
108 }
109 }
110
111 /**
112 * Submits a request for a Request Token to the Request Token Endpoint Url of the OAuth Service
113 * Provider and replies the request token.
114 *
115 * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null
116 * @return the OAuth Request Token
117 * @throws OsmOAuthAuthorizationException if something goes wrong when retrieving the request token
118 * @throws OsmTransferCanceledException if the user canceled the request
119 */
120 public OAuthToken getRequestToken(ProgressMonitor monitor) throws OsmOAuthAuthorizationException, OsmTransferCanceledException {
121 if (monitor == null) {
122 monitor = NullProgressMonitor.INSTANCE;
123 }
124 try {
125 monitor.beginTask("");
126 monitor.indeterminateSubTask(tr("Retrieving OAuth Request Token from ''{0}''", oauthProviderParameters.getRequestTokenUrl()));
127 provider.retrieveRequestToken(consumer, "");
128 return OAuthToken.createToken(consumer);
129 } catch (OAuthException e) {
130 if (canceled)
131 throw new OsmTransferCanceledException(e);
132 throw new OsmOAuthAuthorizationException(e);
133 } finally {
134 monitor.finishTask();
135 }
136 }
137
138 /**
139 * Submits a request for an Access Token to the Access Token Endpoint Url of the OAuth Service
140 * Provider and replies the request token.
141 *
142 * You must have requested a Request Token using {@link #getRequestToken(ProgressMonitor)} first.
143 *
144 * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null
145 * @return the OAuth Access Token
146 * @throws OsmOAuthAuthorizationException if something goes wrong when retrieving the request token
147 * @throws OsmTransferCanceledException if the user canceled the request
148 * @see #getRequestToken(ProgressMonitor)
149 */
150 public OAuthToken getAccessToken(ProgressMonitor monitor) throws OsmOAuthAuthorizationException, OsmTransferCanceledException {
151 if (monitor == null) {
152 monitor = NullProgressMonitor.INSTANCE;
153 }
154 try {
155 monitor.beginTask("");
156 monitor.indeterminateSubTask(tr("Retrieving OAuth Access Token from ''{0}''", oauthProviderParameters.getAccessTokenUrl()));
157 provider.retrieveAccessToken(consumer, null);
158 return OAuthToken.createToken(consumer);
159 } catch (OAuthException e) {
160 if (canceled)
161 throw new OsmTransferCanceledException(e);
162 throw new OsmOAuthAuthorizationException(e);
163 } finally {
164 monitor.finishTask();
165 }
166 }
167
168 /**
169 * Builds the authorise URL for a given Request Token. Users can be redirected to this URL.
170 * There they can login to OSM and authorise the request.
171 *
172 * @param requestToken the request token
173 * @return the authorise URL for this request
174 */
175 public String getAuthoriseUrl(OAuthToken requestToken) {
176 StringBuilder sb = new StringBuilder(32);
177
178 // OSM is an OAuth 1.0 provider and JOSM isn't a web app. We just add the oauth request token to
179 // the authorisation request, no callback parameter.
180 //
181 sb.append(oauthProviderParameters.getAuthoriseUrl()).append('?'+OAuth.OAUTH_TOKEN+'=').append(requestToken.getKey());
182 return sb.toString();
183 }
184
185 protected String extractToken() {
186 try (BufferedReader r = connection.getResponse().getContentReader()) {
187 String c;
188 Pattern p = Pattern.compile(".*authenticity_token.*value=\"([^\"]+)\".*");
189 while ((c = r.readLine()) != null) {
190 Matcher m = p.matcher(c);
191 if (m.find()) {
192 return m.group(1);
193 }
194 }
195 } catch (IOException e) {
196 Main.error(e);
197 return null;
198 }
199 Main.warn("No authenticity_token found in response!");
200 return null;
201 }
202
203 protected SessionId extractOsmSession() throws IOException, URISyntaxException {
204 // response headers might not contain the cookie, see #12584
205 final List<String> setCookies = CookieHandler.getDefault()
206 .get(connection.getURL().toURI(), Collections.<String, List<String>>emptyMap())
207 .get("Cookie");
208 if (setCookies == null) {
209 Main.warn("No 'Set-Cookie' in response header!");
210 return null;
211 }
212
213 for (String setCookie: setCookies) {
214 String[] kvPairs = setCookie.split(";");
215 if (kvPairs.length == 0) {
216 continue;
217 }
218 for (String kvPair : kvPairs) {
219 kvPair = kvPair.trim();
220 String[] kv = kvPair.split("=");
221 if (kv.length != 2) {
222 continue;
223 }
224 if ("_osm_session".equals(kv[0])) {
225 // osm session cookie found
226 String token = extractToken();
227 if (token == null)
228 return null;
229 SessionId si = new SessionId();
230 si.id = kv[1];
231 si.token = token;
232 return si;
233 }
234 }
235 }
236 Main.warn("No suitable 'Set-Cookie' in response header found! {0}", setCookies);
237 return null;
238 }
239
240 protected static String buildPostRequest(Map<String, String> parameters) {
241 StringBuilder sb = new StringBuilder(32);
242
243 for (Iterator<Entry<String, String>> it = parameters.entrySet().iterator(); it.hasNext();) {
244 Entry<String, String> entry = it.next();
245 String value = entry.getValue();
246 value = (value == null) ? "" : value;
247 sb.append(entry.getKey()).append('=').append(Utils.encodeUrl(value));
248 if (it.hasNext()) {
249 sb.append('&');
250 }
251 }
252 return sb.toString();
253 }
254
255 /**
256 * Submits a request to the OSM website for a login form. The OSM website replies a session ID in
257 * a cookie.
258 *
259 * @return the session ID structure
260 * @throws OsmOAuthAuthorizationException if something went wrong
261 */
262 protected SessionId fetchOsmWebsiteSessionId() throws OsmOAuthAuthorizationException {
263 try {
264 final URL url = new URL(oauthProviderParameters.getOsmLoginUrl() + "?cookie_test=true");
265 synchronized (this) {
266 connection = HttpClient.create(url).useCache(false);
267 connection.connect();
268 }
269 SessionId sessionId = extractOsmSession();
270 if (sessionId == null)
271 throw new OsmOAuthAuthorizationException(
272 tr("OSM website did not return a session cookie in response to ''{0}'',", url.toString()));
273 return sessionId;
274 } catch (IOException | URISyntaxException e) {
275 throw new OsmOAuthAuthorizationException(e);
276 } finally {
277 synchronized (this) {
278 connection = null;
279 }
280 }
281 }
282
283 /**
284 * Submits a request to the OSM website for a OAuth form. The OSM website replies a session token in
285 * a hidden parameter.
286 * @param sessionId session id
287 * @param requestToken request token
288 *
289 * @throws OsmOAuthAuthorizationException if something went wrong
290 */
291 protected void fetchOAuthToken(SessionId sessionId, OAuthToken requestToken) throws OsmOAuthAuthorizationException {
292 try {
293 URL url = new URL(getAuthoriseUrl(requestToken));
294 synchronized (this) {
295 connection = HttpClient.create(url)
296 .useCache(false)
297 .setHeader("Cookie", "_osm_session=" + sessionId.id + "; _osm_username=" + sessionId.userName);
298 connection.connect();
299 }
300 sessionId.token = extractToken();
301 if (sessionId.token == null)
302 throw new OsmOAuthAuthorizationException(tr("OSM website did not return a session cookie in response to ''{0}'',",
303 url.toString()));
304 } catch (IOException e) {
305 throw new OsmOAuthAuthorizationException(e);
306 } finally {
307 synchronized (this) {
308 connection = null;
309 }
310 }
311 }
312
313 protected void authenticateOsmSession(SessionId sessionId, String userName, String password) throws OsmLoginFailedException {
314 try {
315 final URL url = new URL(oauthProviderParameters.getOsmLoginUrl());
316 final HttpClient client = HttpClient.create(url, "POST").useCache(false);
317
318 Map<String, String> parameters = new HashMap<>();
319 parameters.put("username", userName);
320 parameters.put("password", password);
321 parameters.put("referer", "/");
322 parameters.put("commit", "Login");
323 parameters.put("authenticity_token", sessionId.token);
324 client.setRequestBody(buildPostRequest(parameters).getBytes(StandardCharsets.UTF_8));
325
326 client.setHeader("Content-Type", "application/x-www-form-urlencoded");
327 client.setHeader("Cookie", "_osm_session=" + sessionId.id);
328 // make sure we can catch 302 Moved Temporarily below
329 client.setMaxRedirects(-1);
330
331 synchronized (this) {
332 connection = client;
333 connection.connect();
334 }
335
336 // after a successful login the OSM website sends a redirect to a follow up page. Everything
337 // else, including a 200 OK, is a failed login. A 200 OK is replied if the login form with
338 // an error page is sent to back to the user.
339 //
340 int retCode = connection.getResponse().getResponseCode();
341 if (retCode != HttpURLConnection.HTTP_MOVED_TEMP)
342 throw new OsmOAuthAuthorizationException(tr("Failed to authenticate user ''{0}'' with password ''***'' as OAuth user",
343 userName));
344 } catch (OsmOAuthAuthorizationException e) {
345 Main.debug(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.