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

Last change on this file was 19235, checked in by taylor.smock, 21 months ago

Fix #23956: JsonParsingException when the destination server returns 200 and a malformed document

In this case, the destination server performed a redirect via javascript in the
response.

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