source: josm/trunk/src/org/openstreetmap/josm/gui/oauth/OAuthAuthorizationWizard.java@ 18991

Last change on this file since 18991 was 18991, checked in by taylor.smock, 3 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: 17.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.awt.BorderLayout;
7import java.awt.Component;
8import java.awt.Dimension;
9import java.awt.FlowLayout;
10import java.awt.Font;
11import java.awt.GridBagLayout;
12import java.awt.event.ActionEvent;
13import java.awt.event.ComponentAdapter;
14import java.awt.event.ComponentEvent;
15import java.awt.event.WindowAdapter;
16import java.awt.event.WindowEvent;
17import java.beans.PropertyChangeEvent;
18import java.beans.PropertyChangeListener;
19import java.lang.reflect.InvocationTargetException;
20import java.net.URL;
21import java.util.Objects;
22import java.util.Optional;
23import java.util.concurrent.Executor;
24import java.util.concurrent.FutureTask;
25import java.util.function.Consumer;
26
27import javax.swing.AbstractAction;
28import javax.swing.BorderFactory;
29import javax.swing.JButton;
30import javax.swing.JDialog;
31import javax.swing.JOptionPane;
32import javax.swing.JPanel;
33import javax.swing.JScrollPane;
34import javax.swing.SwingUtilities;
35import javax.swing.UIManager;
36import javax.swing.text.html.HTMLEditorKit;
37
38import org.openstreetmap.josm.data.oauth.IOAuthParameters;
39import org.openstreetmap.josm.data.oauth.IOAuthToken;
40import org.openstreetmap.josm.data.oauth.OAuth20Authorization;
41import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
42import org.openstreetmap.josm.data.oauth.OAuthParameters;
43import org.openstreetmap.josm.data.oauth.OAuthVersion;
44import org.openstreetmap.josm.data.oauth.osm.OsmScopes;
45import org.openstreetmap.josm.gui.MainApplication;
46import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
47import org.openstreetmap.josm.gui.help.HelpUtil;
48import org.openstreetmap.josm.gui.util.GuiHelper;
49import org.openstreetmap.josm.gui.util.WindowGeometry;
50import org.openstreetmap.josm.gui.widgets.HtmlPanel;
51import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
52import org.openstreetmap.josm.io.auth.CredentialsManager;
53import org.openstreetmap.josm.io.remotecontrol.RemoteControl;
54import org.openstreetmap.josm.spi.preferences.Config;
55import org.openstreetmap.josm.tools.GBC;
56import org.openstreetmap.josm.tools.ImageProvider;
57import org.openstreetmap.josm.tools.InputMapUtils;
58import org.openstreetmap.josm.tools.UserCancelException;
59import org.openstreetmap.josm.tools.Utils;
60
61/**
62 * This wizard walks the user to the necessary steps to retrieve an OAuth Access Token which
63 * allows JOSM to access the OSM API on the users behalf.
64 * @since 2746
65 */
66public class OAuthAuthorizationWizard extends JDialog {
67 private boolean canceled;
68 private final AuthorizationProcedure procedure;
69 private final String apiUrl;
70 private final OAuthVersion oAuthVersion;
71
72 private FullyAutomaticAuthorizationUI pnlFullyAutomaticAuthorisationUI;
73 private ManualAuthorizationUI pnlManualAuthorisationUI;
74 private JScrollPane spAuthorisationProcedureUI;
75 private final transient Executor executor;
76
77 /**
78 * Launches the wizard, {@link OAuthAccessTokenHolder#setAccessToken(String, IOAuthToken)} sets the token
79 * and {@link OAuthAccessTokenHolder#setSaveToPreferences(boolean) saves to preferences}.
80 * @param callback Callback to run when authorization is finished
81 * @throws UserCancelException if user cancels the operation
82 */
83 public void showDialog(Consumer<Optional<IOAuthToken>> callback) throws UserCancelException {
84 if ((this.oAuthVersion == OAuthVersion.OAuth20 || this.oAuthVersion == OAuthVersion.OAuth21)
85 && this.procedure == AuthorizationProcedure.FULLY_AUTOMATIC) {
86 authorize(true, callback, this.apiUrl, this.oAuthVersion);
87 } else {
88 setVisible(true);
89 if (isCanceled()) {
90 throw new UserCancelException();
91 }
92 }
93 OAuthAccessTokenHolder holder = OAuthAccessTokenHolder.getInstance();
94 holder.setAccessToken(apiUrl, getAccessToken());
95 holder.setSaveToPreferences(isSaveAccessTokenToPreferences());
96 }
97
98 /**
99 * Perform the oauth dance
100 * @param startRemoteControl {@code true} to start remote control if it is not already running
101 * @param callback The callback to use to notify that the OAuth dance succeeded
102 * @param apiUrl The API URL to get the token for
103 * @param oAuthVersion The OAuth version that the authorization dance is force
104 */
105 static void authorize(boolean startRemoteControl, Consumer<Optional<IOAuthToken>> callback, String apiUrl, OAuthVersion oAuthVersion) {
106 final boolean remoteControlIsRunning = Boolean.TRUE.equals(RemoteControl.PROP_REMOTECONTROL_ENABLED.get());
107 // TODO: Ask user if they want to start remote control?
108 if (!remoteControlIsRunning && startRemoteControl) {
109 RemoteControl.start();
110 }
111 new OAuth20Authorization().authorize(OAuthParameters.createDefault(apiUrl, oAuthVersion), token -> {
112 if (!remoteControlIsRunning) {
113 RemoteControl.stop();
114 }
115 OAuthAccessTokenHolder.getInstance().setAccessToken(apiUrl, token.orElse(null));
116 OAuthAccessTokenHolder.getInstance().save(CredentialsManager.getInstance());
117 if (!token.isPresent()) {
118 GuiHelper.runInEDT(() -> JOptionPane.showMessageDialog(MainApplication.getMainPanel(),
119 tr("Authentication failed, please check browser for details."),
120 tr("OAuth Authentication Failed"),
121 JOptionPane.ERROR_MESSAGE));
122 }
123 if (callback != null) {
124 callback.accept(token);
125 }
126 }, OsmScopes.read_gpx, OsmScopes.write_gpx,
127 OsmScopes.read_prefs, OsmScopes.write_prefs,
128 OsmScopes.write_api, OsmScopes.write_notes);
129 }
130
131 /**
132 * Builds the row with the action buttons
133 *
134 * @return panel with buttons
135 */
136 protected JPanel buildButtonRow() {
137 JPanel pnl = new JPanel(new FlowLayout(FlowLayout.CENTER));
138
139 AcceptAccessTokenAction actAcceptAccessToken = new AcceptAccessTokenAction();
140 pnlFullyAutomaticAuthorisationUI.addPropertyChangeListener(actAcceptAccessToken);
141 pnlManualAuthorisationUI.addPropertyChangeListener(actAcceptAccessToken);
142
143 pnl.add(new JButton(actAcceptAccessToken));
144 pnl.add(new JButton(new CancelAction()));
145 pnl.add(new JButton(new ContextSensitiveHelpAction(HelpUtil.ht("/Dialog/OAuthAuthorisationWizard"))));
146
147 return pnl;
148 }
149
150 /**
151 * Builds the panel with general information in the header
152 *
153 * @return panel with information display
154 */
155 protected JPanel buildHeaderInfoPanel() {
156 JPanel pnl = new JPanel(new GridBagLayout());
157 pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
158
159 // OAuth in a nutshell ...
160 HtmlPanel pnlMessage = new HtmlPanel();
161 pnlMessage.setText("<html><body>"
162 + tr("With OAuth you grant JOSM the right to upload map data and GPS tracks "
163 + "on your behalf (<a href=\"{0}\">more info...</a>).", "https://wiki.openstreetmap.org/wiki/OAuth")
164 + "</body></html>"
165 );
166 pnlMessage.enableClickableHyperlinks();
167 pnl.add(pnlMessage, GBC.eol().fill(GBC.HORIZONTAL));
168
169 // the authorisation procedure
170 JMultilineLabel lbl = new JMultilineLabel(AuthorizationProcedure.FULLY_AUTOMATIC.getDescription());
171 lbl.setFont(lbl.getFont().deriveFont(Font.PLAIN));
172 pnl.add(lbl, GBC.std());
173
174 if (!Config.getUrls().getDefaultOsmApiUrl().equals(apiUrl)) {
175 final HtmlPanel pnlWarning = new HtmlPanel();
176 final HTMLEditorKit kit = (HTMLEditorKit) pnlWarning.getEditorPane().getEditorKit();
177 kit.getStyleSheet().addRule(".warning-body {"
178 + "background-color:rgb(253,255,221);padding: 10pt; "
179 + "border-color:rgb(128,128,128);border-style: solid;border-width: 1px;}");
180 kit.getStyleSheet().addRule("ol {margin-left: 1cm}");
181 pnlWarning.setText("<html><body>"
182 + "<p class=\"warning-body\">"
183 + tr("<strong>Warning:</strong> Since you are using not the default OSM API, " +
184 "make sure to set an OAuth consumer key and secret in the <i>Advanced OAuth parameters</i>.")
185 + "</p>"
186 + "</body></html>");
187 pnl.add(pnlWarning, GBC.eop().fill());
188 }
189
190 return pnl;
191 }
192
193 /**
194 * Refreshes the view of the authorisation panel, depending on the authorisation procedure
195 * currently selected
196 */
197 protected void refreshAuthorisationProcedurePanel() {
198 switch(procedure) {
199 case FULLY_AUTOMATIC:
200 spAuthorisationProcedureUI.getViewport().setView(pnlFullyAutomaticAuthorisationUI);
201 pnlFullyAutomaticAuthorisationUI.revalidate();
202 break;
203 case MANUALLY:
204 spAuthorisationProcedureUI.getViewport().setView(pnlManualAuthorisationUI);
205 pnlManualAuthorisationUI.revalidate();
206 break;
207 default:
208 throw new UnsupportedOperationException("Unsupported auth type: " + procedure);
209 }
210 validate();
211 repaint();
212 }
213
214 /**
215 * builds the UI
216 */
217 protected final void build() {
218 getContentPane().setLayout(new BorderLayout());
219 getContentPane().add(buildHeaderInfoPanel(), BorderLayout.NORTH);
220
221 setTitle(tr("Get an Access Token for ''{0}''", apiUrl));
222 this.setMinimumSize(new Dimension(500, 300));
223
224 pnlFullyAutomaticAuthorisationUI = new FullyAutomaticAuthorizationUI(apiUrl, executor, oAuthVersion);
225 pnlManualAuthorisationUI = new ManualAuthorizationUI(apiUrl, executor, oAuthVersion);
226
227 spAuthorisationProcedureUI = GuiHelper.embedInVerticalScrollPane(new JPanel());
228 spAuthorisationProcedureUI.getVerticalScrollBar().addComponentListener(
229 new ComponentAdapter() {
230 @Override
231 public void componentShown(ComponentEvent e) {
232 spAuthorisationProcedureUI.setBorder(UIManager.getBorder("ScrollPane.border"));
233 }
234
235 @Override
236 public void componentHidden(ComponentEvent e) {
237 spAuthorisationProcedureUI.setBorder(null);
238 }
239 }
240 );
241 getContentPane().add(spAuthorisationProcedureUI, BorderLayout.CENTER);
242 getContentPane().add(buildButtonRow(), BorderLayout.SOUTH);
243
244 addWindowListener(new WindowEventHandler());
245 InputMapUtils.addEscapeAction(getRootPane(), new CancelAction());
246
247 refreshAuthorisationProcedurePanel();
248
249 HelpUtil.setHelpContext(getRootPane(), HelpUtil.ht("/Dialog/OAuthAuthorisationWizard"));
250 }
251
252 /**
253 * Creates the wizard.
254 *
255 * @param parent the component relative to which the dialog is displayed
256 * @param procedure the authorization procedure to use
257 * @param apiUrl the API URL. Must not be null.
258 * @param executor the executor used for running the HTTP requests for the authorization
259 * @param oAuthVersion The OAuth version this wizard is for
260 * @throws IllegalArgumentException if apiUrl is null
261 */
262 public OAuthAuthorizationWizard(Component parent, AuthorizationProcedure procedure, String apiUrl,
263 Executor executor, OAuthVersion oAuthVersion) {
264 super(GuiHelper.getFrameForComponent(parent), ModalityType.DOCUMENT_MODAL);
265 this.procedure = Objects.requireNonNull(procedure, "procedure");
266 this.apiUrl = Objects.requireNonNull(apiUrl, "apiUrl");
267 this.executor = executor;
268 this.oAuthVersion = oAuthVersion;
269 build();
270 }
271
272 /**
273 * Replies true if the dialog was canceled
274 *
275 * @return true if the dialog was canceled
276 */
277 public boolean isCanceled() {
278 return canceled;
279 }
280
281 protected AbstractAuthorizationUI getCurrentAuthorisationUI() {
282 switch(procedure) {
283 case FULLY_AUTOMATIC: return pnlFullyAutomaticAuthorisationUI;
284 case MANUALLY: return pnlManualAuthorisationUI;
285 default: return null;
286 }
287 }
288
289 /**
290 * Replies the Access Token entered using the wizard
291 *
292 * @return the access token. May be null if the wizard was canceled.
293 */
294 public IOAuthToken getAccessToken() {
295 return getCurrentAuthorisationUI().getAccessToken();
296 }
297
298 /**
299 * Replies the current OAuth parameters.
300 *
301 * @return the current OAuth parameters.
302 */
303 public IOAuthParameters getOAuthParameters() {
304 return getCurrentAuthorisationUI().getOAuthParameters();
305 }
306
307 /**
308 * Replies true if the currently selected Access Token shall be saved to
309 * the preferences.
310 *
311 * @return true if the currently selected Access Token shall be saved to
312 * the preferences
313 */
314 public boolean isSaveAccessTokenToPreferences() {
315 return getCurrentAuthorisationUI().isSaveAccessTokenToPreferences();
316 }
317
318 /**
319 * Initializes the dialog with values from the preferences
320 *
321 */
322 public void initFromPreferences() {
323 pnlFullyAutomaticAuthorisationUI.initialize(apiUrl);
324 pnlManualAuthorisationUI.initialize(apiUrl);
325 }
326
327 @Override
328 public void setVisible(boolean visible) {
329 if (visible) {
330 pack();
331 new WindowGeometry(
332 getClass().getName() + ".geometry",
333 WindowGeometry.centerInWindow(
334 MainApplication.getMainFrame(),
335 getPreferredSize()
336 )
337 ).applySafe(this);
338 initFromPreferences();
339 } else if (isShowing()) { // Avoid IllegalComponentStateException like in #8775
340 new WindowGeometry(this).remember(getClass().getName() + ".geometry");
341 }
342 super.setVisible(visible);
343 }
344
345 protected void setCanceled(boolean canceled) {
346 this.canceled = canceled;
347 }
348
349 /**
350 * Obtains an OAuth access token for the connection. Afterwards, the token is accessible via {@link OAuthAccessTokenHolder}.
351 * @param serverUrl the URL to OSM server
352 * @throws InterruptedException if we're interrupted while waiting for the event dispatching thread to finish OAuth authorization task
353 * @throws InvocationTargetException if an exception is thrown while running OAuth authorization task
354 * @since 12803
355 */
356 public static void obtainAccessToken(final URL serverUrl) throws InvocationTargetException, InterruptedException {
357 final Runnable authTask = new FutureTask<>(() -> {
358 // Concerning Utils.newDirectExecutor: Main worker cannot be used since this connection is already
359 // executed via main worker. The OAuth connections would block otherwise.
360 final OAuthAuthorizationWizard wizard = new OAuthAuthorizationWizard(
361 MainApplication.getMainFrame(),
362 AuthorizationProcedure.FULLY_AUTOMATIC,
363 serverUrl.toString(), Utils.newDirectExecutor(),
364 OAuthVersion.OAuth20);
365 wizard.showDialog(null);
366 return wizard;
367 });
368 // exception handling differs from implementation at GuiHelper.runInEDTAndWait()
369 if (SwingUtilities.isEventDispatchThread()) {
370 authTask.run();
371 } else {
372 SwingUtilities.invokeAndWait(authTask);
373 }
374 }
375
376 class CancelAction extends AbstractAction {
377
378 /**
379 * Constructs a new {@code CancelAction}.
380 */
381 CancelAction() {
382 putValue(NAME, tr("Cancel"));
383 new ImageProvider("cancel").getResource().attachImageIcon(this);
384 putValue(SHORT_DESCRIPTION, tr("Close the dialog and cancel authorization"));
385 }
386
387 public void cancel() {
388 setCanceled(true);
389 setVisible(false);
390 }
391
392 @Override
393 public void actionPerformed(ActionEvent evt) {
394 cancel();
395 }
396 }
397
398 class AcceptAccessTokenAction extends AbstractAction implements PropertyChangeListener {
399
400 /**
401 * Constructs a new {@code AcceptAccessTokenAction}.
402 */
403 AcceptAccessTokenAction() {
404 putValue(NAME, tr("Accept Access Token"));
405 new ImageProvider("ok").getResource().attachImageIcon(this);
406 putValue(SHORT_DESCRIPTION, tr("Close the dialog and accept the Access Token"));
407 updateEnabledState(null);
408 }
409
410 @Override
411 public void actionPerformed(ActionEvent evt) {
412 setCanceled(false);
413 setVisible(false);
414 }
415
416 /**
417 * Update the enabled state
418 * @param token The token to use
419 * @since 18991
420 */
421 public final void updateEnabledState(IOAuthToken token) {
422 setEnabled(token != null);
423 }
424
425 @Override
426 public void propertyChange(PropertyChangeEvent evt) {
427 if (!evt.getPropertyName().equals(AbstractAuthorizationUI.ACCESS_TOKEN_PROP))
428 return;
429 updateEnabledState((IOAuthToken) evt.getNewValue());
430 }
431 }
432
433 class WindowEventHandler extends WindowAdapter {
434 @Override
435 public void windowClosing(WindowEvent e) {
436 new CancelAction().cancel();
437 }
438 }
439}
Note: See TracBrowser for help on using the repository browser.