source: josm/trunk/src/org/openstreetmap/josm/gui/oauth/TestAccessTokenTask.java

Last change on this file was 18991, checked in by taylor.smock, 2 years ago

Fix #22810: OSM OAuth 1.0a/Basic auth deprecation and removal

As of 2024-02-15, something changed in the OSM server configuration. This broke
our OAuth 1.0a implementation (see #23475). As such, we are removing OAuth 1.0a
from JOSM now instead of when the OSM server removes support in June 2024.

For third-party OpenStreetMap servers, the Basic Authentication method has been
kept. However, they should be made aware that it may be removed if a non-trivial
bug occurs with it. We highly recommend that the third-party servers update to
the current OpenStreetMap website implementation (if only for their own security).

Failing that, the third-party server can implement RFC8414. As of this commit,
we currently use the authorization_endpoint and token_endpoint fields.
To check and see if their third-party server implements RFC8414, they can go
to <server host>/.well-known/oauth-authorization-server.

Prominent third-party OpenStreetMap servers may give us a client id for their
specific server. That client id may be added to the hard-coded client id list
at maintainer discretion. At a minimum, the server must be publicly
available and have a significant user base.

  • Property svn:eol-style set to native
File size: 11.2 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.awt.Component;
7import java.io.IOException;
8import java.net.HttpURLConnection;
9import java.net.URL;
10
11import javax.swing.JOptionPane;
12import javax.xml.parsers.ParserConfigurationException;
13
14import org.openstreetmap.josm.data.oauth.IOAuthToken;
15import org.openstreetmap.josm.data.oauth.OAuth20Token;
16import org.openstreetmap.josm.data.oauth.OAuthException;
17import org.openstreetmap.josm.data.osm.UserInfo;
18import org.openstreetmap.josm.gui.HelpAwareOptionPane;
19import org.openstreetmap.josm.gui.PleaseWaitRunnable;
20import org.openstreetmap.josm.gui.help.HelpUtil;
21import org.openstreetmap.josm.io.OsmApiException;
22import org.openstreetmap.josm.io.OsmServerUserInfoReader;
23import org.openstreetmap.josm.io.OsmTransferException;
24import org.openstreetmap.josm.io.auth.DefaultAuthenticator;
25import org.openstreetmap.josm.tools.CheckParameterUtil;
26import org.openstreetmap.josm.tools.HttpClient;
27import org.openstreetmap.josm.tools.Logging;
28import org.openstreetmap.josm.tools.Utils;
29import org.openstreetmap.josm.tools.XmlParsingException;
30import org.openstreetmap.josm.tools.XmlUtils;
31import org.w3c.dom.Document;
32import org.xml.sax.SAXException;
33
34/**
35 * Checks whether an OSM API server can be accessed with a specific Access Token.
36 * <p>
37 * It retrieves the user details for the user which is authorized to access the server with
38 * this token.
39 *
40 */
41public class TestAccessTokenTask extends PleaseWaitRunnable {
42 private final IOAuthToken tokenOAuth2;
43 private boolean canceled;
44 private final Component parent;
45 private final String apiUrl;
46 private HttpClient connection;
47
48 /**
49 * Create the task
50 *
51 * @param parent the parent component relative to which the {@link PleaseWaitRunnable}-Dialog is displayed
52 * @param apiUrl the API URL. Must not be null.
53 * @param accessToken the Access Token. Must not be null.
54 * @since 18991
55 */
56 public TestAccessTokenTask(Component parent, String apiUrl, IOAuthToken accessToken) {
57 super(parent, tr("Testing OAuth Access Token"), false /* don't ignore exceptions */);
58 CheckParameterUtil.ensureParameterNotNull(apiUrl, "apiUrl");
59 CheckParameterUtil.ensureParameterNotNull(accessToken, "accessToken");
60 this.tokenOAuth2 = accessToken;
61 this.parent = parent;
62 this.apiUrl = apiUrl;
63 }
64
65 @Override
66 protected void cancel() {
67 canceled = true;
68 synchronized (this) {
69 if (connection != null) {
70 connection.disconnect();
71 }
72 }
73 }
74
75 @Override
76 protected void finish() {
77 // Do nothing
78 }
79
80 protected void sign(HttpClient con) throws OAuthException {
81 this.tokenOAuth2.sign(con);
82 }
83
84 protected String normalizeApiUrl(String url) {
85 // remove leading and trailing white space
86 url = url.trim();
87
88 // remove trailing slashes
89 while (url.endsWith("/")) {
90 url = url.substring(0, url.lastIndexOf('/'));
91 }
92 return url;
93 }
94
95 protected UserInfo getUserDetails() throws OsmOAuthAuthorizationException, XmlParsingException, OsmTransferException {
96 boolean authenticatorEnabled = true;
97 try {
98 URL url = new URL(normalizeApiUrl(apiUrl) + "/0.6/user/details");
99 authenticatorEnabled = DefaultAuthenticator.getInstance().isEnabled();
100 DefaultAuthenticator.getInstance().setEnabled(false);
101
102 final HttpClient client = HttpClient.create(url);
103 sign(client);
104 synchronized (this) {
105 connection = client;
106 connection.connect();
107 }
108
109 final String oauthKey = getAuthKey();
110 if (connection.getResponse().getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED)
111 throw new OsmApiException(HttpURLConnection.HTTP_UNAUTHORIZED,
112 tr("Retrieving user details with Access Token Key ''{0}'' was rejected.",
113 oauthKey), null);
114
115 if (connection.getResponse().getResponseCode() == HttpURLConnection.HTTP_FORBIDDEN)
116 throw new OsmApiException(HttpURLConnection.HTTP_FORBIDDEN,
117 tr("Retrieving user details with Access Token Key ''{0}'' was forbidden.", oauthKey), null);
118
119 if (connection.getResponse().getResponseCode() != HttpURLConnection.HTTP_OK)
120 throw new OsmApiException(connection.getResponse().getResponseCode(),
121 connection.getResponse().getHeaderField("Error"), null);
122 Document d = XmlUtils.parseSafeDOM(connection.getResponse().getContent());
123 return OsmServerUserInfoReader.buildFromXML(d);
124 } catch (SAXException | ParserConfigurationException e) {
125 throw new XmlParsingException(e);
126 } catch (IOException e) {
127 throw new OsmTransferException(e);
128 } catch (OAuthException e) {
129 throw new OsmOAuthAuthorizationException(e);
130 } finally {
131 DefaultAuthenticator.getInstance().setEnabled(authenticatorEnabled);
132 }
133 }
134
135 protected void notifySuccess(UserInfo userInfo) {
136 HelpAwareOptionPane.showMessageDialogInEDT(
137 parent,
138 tr("<html>"
139 + "Successfully used the Access Token ''{0}'' to<br>"
140 + "access the OSM server at ''{1}''.<br>"
141 + "You are accessing the OSM server as user ''{2}'' with id ''{3}''."
142 + "</html>",
143 getAuthKey(),
144 apiUrl,
145 Utils.escapeReservedCharactersHTML(userInfo.getDisplayName()),
146 userInfo.getId()
147 ),
148 tr("Success"),
149 JOptionPane.INFORMATION_MESSAGE,
150 HelpUtil.ht("/Dialog/OAuthAuthorisationWizard#AccessTokenOK")
151 );
152 }
153
154 protected void alertFailedAuthentication() {
155 HelpAwareOptionPane.showMessageDialogInEDT(
156 parent,
157 tr("<html>"
158 + "Failed to access the OSM server ''{0}''<br>"
159 + "with the Access Token ''{1}''.<br>"
160 + "The server rejected the Access Token as unauthorized. You will not<br>"
161 + "be able to access any protected resource on this server using this token."
162 +"</html>",
163 apiUrl,
164 getAuthKey()
165 ),
166 tr("Test failed"),
167 JOptionPane.ERROR_MESSAGE,
168 HelpUtil.ht("/Dialog/OAuthAuthorisationWizard#AccessTokenFailed")
169 );
170 }
171
172 protected void alertFailedAuthorisation() {
173 HelpAwareOptionPane.showMessageDialogInEDT(
174 parent,
175 tr("<html>"
176 + "The Access Token ''{1}'' is known to the OSM server ''{0}''.<br>"
177 + "The test to retrieve the user details for this token failed, though.<br>"
178 + "Depending on what rights are granted to this token you may nevertheless use it<br>"
179 + "to upload data, upload GPS traces, and/or access other protected resources."
180 +"</html>",
181 apiUrl,
182 getAuthKey()
183 ),
184 tr("Token allows restricted access"),
185 JOptionPane.WARNING_MESSAGE,
186 HelpUtil.ht("/Dialog/OAuthAuthorisationWizard#AccessTokenFailed")
187 );
188 }
189
190 protected void alertFailedConnection() {
191 HelpAwareOptionPane.showMessageDialogInEDT(
192 parent,
193 tr("<html>"
194 + "Failed to retrieve information about the current user"
195 + " from the OSM server ''{0}''.<br>"
196 + "This is probably not a problem caused by the tested Access Token, but<br>"
197 + "rather a problem with the server configuration. Carefully check the server<br>"
198 + "URL and your Internet connection."
199 +"</html>",
200 apiUrl,
201 getAuthKey()
202 ),
203 tr("Test failed"),
204 JOptionPane.ERROR_MESSAGE,
205 HelpUtil.ht("/Dialog/OAuthAuthorisationWizard#AccessTokenFailed")
206 );
207 }
208
209 protected void alertFailedSigning() {
210 HelpAwareOptionPane.showMessageDialogInEDT(
211 parent,
212 tr("<html>"
213 + "Failed to sign the request for the OSM server ''{0}'' with the "
214 + "token ''{1}''.<br>"
215 + "The token ist probably invalid."
216 +"</html>",
217 apiUrl,
218 getAuthKey()
219 ),
220 tr("Test failed"),
221 JOptionPane.ERROR_MESSAGE,
222 HelpUtil.ht("/Dialog/OAuthAuthorisationWizard#AccessTokenFailed")
223 );
224 }
225
226 protected void alertInternalError() {
227 HelpAwareOptionPane.showMessageDialogInEDT(
228 parent,
229 tr("<html>"
230 + "The test failed because the server responded with an internal error.<br>"
231 + "JOSM could not decide whether the token is valid. Please try again later."
232 + "</html>",
233 apiUrl,
234 getAuthKey()
235 ),
236 tr("Test failed"),
237 JOptionPane.WARNING_MESSAGE,
238 HelpUtil.ht("/Dialog/OAuthAuthorisationWizard#AccessTokenFailed")
239 );
240 }
241
242 @Override
243 protected void realRun() throws SAXException, IOException, OsmTransferException {
244 try {
245 getProgressMonitor().indeterminateSubTask(tr("Retrieving user info..."));
246 UserInfo userInfo = getUserDetails();
247 if (canceled) return;
248 notifySuccess(userInfo);
249 } catch (OsmOAuthAuthorizationException e) {
250 if (canceled) return;
251 Logging.error(e);
252 alertFailedSigning();
253 } catch (OsmApiException e) {
254 if (canceled) return;
255 Logging.error(e);
256 if (e.getResponseCode() == HttpURLConnection.HTTP_INTERNAL_ERROR) {
257 alertInternalError();
258 return;
259 } else if (e.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
260 alertFailedAuthentication();
261 return;
262 } else if (e.getResponseCode() == HttpURLConnection.HTTP_FORBIDDEN) {
263 alertFailedAuthorisation();
264 return;
265 }
266 alertFailedConnection();
267 } catch (OsmTransferException e) {
268 if (canceled) return;
269 Logging.error(e);
270 alertFailedConnection();
271 }
272 }
273
274 private String getAuthKey() {
275 if (this.tokenOAuth2 instanceof OAuth20Token) {
276 return ((OAuth20Token) this.tokenOAuth2).getBearerToken();
277 }
278 throw new IllegalArgumentException("Only OAuth2 tokens are understood: " + this.tokenOAuth2);
279 }
280}
Note: See TracBrowser for help on using the repository browser.