source: josm/trunk/src/org/openstreetmap/josm/io/OsmConnection.java

Last change on this file was 18991, checked in by taylor.smock, 2 months 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: 9.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.lang.reflect.InvocationTargetException;
7import java.net.Authenticator.RequestorType;
8import java.net.URL;
9import java.nio.charset.StandardCharsets;
10import java.util.Base64;
11import java.util.Objects;
12import java.util.Optional;
13import java.util.concurrent.CountDownLatch;
14import java.util.concurrent.TimeUnit;
15import java.util.function.Consumer;
16
17import javax.swing.JOptionPane;
18
19import org.openstreetmap.josm.data.oauth.IOAuthParameters;
20import org.openstreetmap.josm.data.oauth.IOAuthToken;
21import org.openstreetmap.josm.data.oauth.OAuth20Authorization;
22import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
23import org.openstreetmap.josm.data.oauth.OAuthParameters;
24import org.openstreetmap.josm.data.oauth.OAuthVersion;
25import org.openstreetmap.josm.data.oauth.osm.OsmScopes;
26import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
27import org.openstreetmap.josm.gui.MainApplication;
28import org.openstreetmap.josm.gui.util.GuiHelper;
29import org.openstreetmap.josm.io.auth.CredentialsAgentException;
30import org.openstreetmap.josm.io.auth.CredentialsAgentResponse;
31import org.openstreetmap.josm.io.auth.CredentialsManager;
32import org.openstreetmap.josm.io.remotecontrol.RemoteControl;
33import org.openstreetmap.josm.tools.HttpClient;
34import org.openstreetmap.josm.tools.JosmRuntimeException;
35import org.openstreetmap.josm.tools.Logging;
36
37/**
38 * Base class that handles common things like authentication for the reader and writer
39 * to the osm server.
40 *
41 * @author imi
42 */
43public class OsmConnection {
44
45 private static final String BASIC_AUTH = "Basic ";
46
47 protected boolean cancel;
48 protected HttpClient activeConnection;
49 protected IOAuthParameters oAuth20Parameters;
50
51 /**
52 * Retrieves OAuth access token.
53 * @since 12803
54 */
55 public interface OAuthAccessTokenFetcher {
56 /**
57 * Obtains an OAuth access token for the connection. Afterwards, the token is accessible via {@link OAuthAccessTokenHolder}.
58 * @param serverUrl the URL to OSM server
59 * @throws InterruptedException if we're interrupted while waiting for the event dispatching thread to finish OAuth authorization task
60 * @throws InvocationTargetException if an exception is thrown while running OAuth authorization task
61 */
62 void obtainAccessToken(URL serverUrl) throws InvocationTargetException, InterruptedException;
63 }
64
65 static volatile OAuthAccessTokenFetcher fetcher = u -> {
66 throw new JosmRuntimeException("OsmConnection.setOAuthAccessTokenFetcher() has not been called");
67 };
68
69 /**
70 * Sets the OAuth access token fetcher.
71 * @param tokenFetcher new OAuth access token fetcher. Cannot be null
72 * @since 12803
73 */
74 public static void setOAuthAccessTokenFetcher(OAuthAccessTokenFetcher tokenFetcher) {
75 fetcher = Objects.requireNonNull(tokenFetcher, "tokenFetcher");
76 }
77
78 /**
79 * Cancels the connection.
80 */
81 public void cancel() {
82 cancel = true;
83 synchronized (this) {
84 if (activeConnection != null) {
85 activeConnection.disconnect();
86 }
87 }
88 }
89
90 /**
91 * Retrieves login from basic authentication header, if set.
92 *
93 * @param con the connection
94 * @return login from basic authentication header, or {@code null}
95 * @throws OsmTransferException if something went wrong. Check for nested exceptions
96 * @since 12992
97 */
98 protected String retrieveBasicAuthorizationLogin(HttpClient con) throws OsmTransferException {
99 String auth = con.getRequestHeader("Authorization");
100 if (auth != null && auth.startsWith(BASIC_AUTH)) {
101 try {
102 String[] token = new String(Base64.getDecoder().decode(auth.substring(BASIC_AUTH.length())),
103 StandardCharsets.UTF_8).split(":", -1);
104 if (token.length == 2) {
105 return token[0];
106 }
107 } catch (IllegalArgumentException e) {
108 Logging.error(e);
109 }
110 }
111 return null;
112 }
113
114 /**
115 * Adds an authentication header for basic authentication
116 *
117 * @param con the connection
118 * @throws OsmTransferException if something went wrong. Check for nested exceptions
119 */
120 protected void addBasicAuthorizationHeader(HttpClient con) throws OsmTransferException {
121 CredentialsAgentResponse response;
122 try {
123 synchronized (CredentialsManager.getInstance()) {
124 response = CredentialsManager.getInstance().getCredentials(RequestorType.SERVER,
125 con.getURL().getHost(), false /* don't know yet whether the credentials will succeed */);
126 }
127 } catch (CredentialsAgentException e) {
128 throw new OsmTransferException(e);
129 }
130 if (response != null) {
131 if (response.isCanceled()) {
132 cancel = true;
133 } else {
134 String username = response.getUsername() == null ? "" : response.getUsername();
135 String password = response.getPassword() == null ? "" : String.valueOf(response.getPassword());
136 String token = username + ':' + password;
137 con.setHeader("Authorization", BASIC_AUTH + Base64.getEncoder().encodeToString(token.getBytes(StandardCharsets.UTF_8)));
138 }
139 }
140 }
141
142 /**
143 * Obtains an OAuth access token for the connection.
144 * Afterwards, the token is accessible via {@link OAuthAccessTokenHolder} / {@link CredentialsManager}.
145 * @throws MissingOAuthAccessTokenException if the process cannot be completed successfully
146 */
147 private void obtainOAuth20Token() throws MissingOAuthAccessTokenException {
148 if (!Boolean.TRUE.equals(GuiHelper.runInEDTAndWaitAndReturn(() ->
149 ConditionalOptionPaneUtil.showConfirmationDialog("oauth.oauth20.obtain.automatically",
150 MainApplication.getMainFrame(),
151 tr("Obtain OAuth 2.0 token for authentication?"),
152 tr("Obtain authentication to OSM servers"),
153 JOptionPane.YES_NO_CANCEL_OPTION,
154 JOptionPane.QUESTION_MESSAGE, JOptionPane.YES_OPTION)))) {
155 return; // User doesn't want to perform auth
156 }
157 final boolean remoteControlIsRunning = Boolean.TRUE.equals(RemoteControl.PROP_REMOTECONTROL_ENABLED.get());
158 if (!remoteControlIsRunning) {
159 RemoteControl.start();
160 }
161 CountDownLatch done = new CountDownLatch(1);
162 Consumer<Optional<IOAuthToken>> consumer = authToken -> {
163 if (!remoteControlIsRunning) {
164 RemoteControl.stop();
165 }
166 // Clean up old token/password
167 OAuthAccessTokenHolder.getInstance().setAccessToken(OsmApi.getOsmApi().getServerUrl(), authToken.orElse(null));
168 OAuthAccessTokenHolder.getInstance().save(CredentialsManager.getInstance());
169 done.countDown();
170 };
171 new OAuth20Authorization().authorize(oAuth20Parameters,
172 consumer, OsmScopes.read_gpx, OsmScopes.write_gpx,
173 OsmScopes.read_prefs, OsmScopes.write_prefs,
174 OsmScopes.write_api, OsmScopes.write_notes);
175 // Only wait at most 5 minutes
176 int counter = 0;
177 while (done.getCount() >= 0 && counter < 5) {
178 try {
179 if (done.await(1, TimeUnit.MINUTES)) {
180 break;
181 }
182 } catch (InterruptedException e) {
183 Thread.currentThread().interrupt();
184 Logging.trace(e);
185 consumer.accept(null);
186 throw new MissingOAuthAccessTokenException(e);
187 }
188 counter++;
189 }
190 }
191
192 /**
193 * Signs the connection with an OAuth authentication header
194 *
195 * @param connection the connection
196 *
197 * @throws MissingOAuthAccessTokenException if there is currently no OAuth Access Token configured
198 * @throws OsmTransferException if signing fails
199 */
200 protected void addOAuth20AuthorizationHeader(HttpClient connection) throws OsmTransferException {
201 if (this.oAuth20Parameters == null) {
202 this.oAuth20Parameters = OAuthParameters.createFromApiUrl(connection.getURL().getHost(), OAuthVersion.OAuth20);
203 }
204 OAuthAccessTokenHolder holder = OAuthAccessTokenHolder.getInstance();
205 IOAuthToken token = holder.getAccessToken(connection.getURL().toExternalForm(), OAuthVersion.OAuth20);
206 if (token == null) {
207 obtainOAuth20Token();
208 token = holder.getAccessToken(connection.getURL().toExternalForm(), OAuthVersion.OAuth20);
209 }
210 if (token == null) { // check if wizard completed
211 throw new MissingOAuthAccessTokenException();
212 }
213 try {
214 token.sign(connection);
215 } catch (org.openstreetmap.josm.data.oauth.OAuthException e) {
216 throw new OsmTransferException(tr("Failed to sign a HTTP connection with an OAuth Authentication header"), e);
217 }
218 }
219
220 protected void addAuth(HttpClient connection) throws OsmTransferException {
221 final String authMethod = OsmApi.getAuthMethod();
222 switch (authMethod) {
223 case "basic":
224 addBasicAuthorizationHeader(connection);
225 return;
226 case "oauth20":
227 addOAuth20AuthorizationHeader(connection);
228 return;
229 default:
230 String msg = tr("Unexpected value for preference ''{0}''. Got ''{1}''.", "osm-server.auth-method", authMethod);
231 Logging.warn(msg);
232 throw new OsmTransferException(msg);
233 }
234 }
235
236 /**
237 * Replies true if this connection is canceled
238 *
239 * @return true if this connection is canceled
240 */
241 public boolean isCanceled() {
242 return cancel;
243 }
244}
Note: See TracBrowser for help on using the repository browser.