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

Last change on this file since 18801 was 18801, checked in by taylor.smock, 9 months ago

Fix #22832: Code cleanup and some simplification, documentation fixes (patch by gaben)

There should not be any functional changes in this patch; it is intended to do
the following:

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