Ticket #24925: oauth-auth-on-upload.patch
| File oauth-auth-on-upload.patch, 13.8 KB (added by , 6 weeks ago) |
|---|
-
src/org/openstreetmap/josm/data/oauth/OAuthParameters.java
diff --git src/org/openstreetmap/josm/data/oauth/OAuthParameters.java src/org/openstreetmap/josm/data/oauth/OAuthParameters.java index 73ed6ce10c..e82301e7ec 100644
public final class OAuthParameters { 262 262 if (remembered != null) { 263 263 return remembered; 264 264 } 265 return createDefault(apiUrl, oAuthVersion); 265 // Use the URL as it was given: apiUrl is the host by now, which is never a valid URL, so 266 // createDefault() would silently fall back to the API currently configured in the preferences. 267 return createDefault(originalApiUrl, oAuthVersion); 266 268 default: 267 269 throw new IllegalArgumentException("Unknown OAuth version: " + oAuthVersion); 268 270 } -
src/org/openstreetmap/josm/io/OsmConnection.java
diff --git src/org/openstreetmap/josm/io/OsmConnection.java src/org/openstreetmap/josm/io/OsmConnection.java index b2703c69e2..184cf2272d 100644
import org.openstreetmap.josm.io.remotecontrol.RemoteControl; 33 33 import org.openstreetmap.josm.tools.HttpClient; 34 34 import org.openstreetmap.josm.tools.JosmRuntimeException; 35 35 import org.openstreetmap.josm.tools.Logging; 36 import org.openstreetmap.josm.tools.Utils; 36 37 37 38 /** 38 39 * Base class that handles common things like authentication for the reader and writer … … public class OsmConnection { 146 147 * @throws MissingOAuthAccessTokenException if the process cannot be completed successfully 147 148 */ 148 149 private void obtainOAuth20Token() throws MissingOAuthAccessTokenException { 150 if (Utils.isEmpty(this.oAuth20Parameters.getClientId())) { 151 // Without a client id the authorization request is rejected by the server with "Missing required 152 // parameter: client_id", so do not send the user to the browser at all. The message of the exception 153 // is for the log only; what the user is shown already points at the preferences, which is where the 154 // client id of an OAuth application registered on this server has to be entered. 155 throw new MissingOAuthAccessTokenException( 156 "No OAuth client id for " + OsmApi.getOsmApi().getServerUrl() + ", cannot request a token"); 157 } 149 158 if (!Boolean.TRUE.equals(GuiHelper.runInEDTAndWaitAndReturn(() -> 150 159 ConditionalOptionPaneUtil.showConfirmationDialog("oauth.oauth20.obtain.automatically", 151 160 MainApplication.getMainFrame(), … … public class OsmConnection { 200 209 */ 201 210 protected void addOAuth20AuthorizationHeader(HttpClient connection) throws OsmTransferException { 202 211 if (this.oAuth20Parameters == null) { 203 this.oAuth20Parameters = OAuthParameters.createFromApiUrl(connection.getURL().getHost(), OAuthVersion.OAuth20); 212 // The parameters are stored under the API URL, so look them up with the API URL and not with the 213 // host of the request: OAuthParameters.createFromApiUrl() reduces it to the host itself where that 214 // is what it needs. 215 this.oAuth20Parameters = OAuthParameters.createFromApiUrl(OsmApi.getOsmApi().getServerUrl(), OAuthVersion.OAuth20); 204 216 } 205 217 OAuthAccessTokenHolder holder = OAuthAccessTokenHolder.getInstance(); 206 218 IOAuthToken token = holder.getAccessToken(connection.getURL().toExternalForm(), OAuthVersion.OAuth20); -
test/unit/org/openstreetmap/josm/data/oauth/OAuthParametersTest.java
diff --git test/unit/org/openstreetmap/josm/data/oauth/OAuthParametersTest.java test/unit/org/openstreetmap/josm/data/oauth/OAuthParametersTest.java index a5ab49d8e9..24ab07f7fd 100644
import static org.junit.jupiter.api.Assertions.assertEquals; 5 5 import static org.junit.jupiter.api.Assertions.assertNotEquals; 6 6 import static org.junit.jupiter.api.Assertions.assertNotNull; 7 7 8 import org.junit.jupiter.api.AfterEach; 8 9 import org.junit.jupiter.api.Test; 9 10 import org.openstreetmap.josm.TestUtils; 11 import org.openstreetmap.josm.io.NetworkManager; 12 import org.openstreetmap.josm.io.OnlineResource; 10 13 import org.openstreetmap.josm.spi.preferences.Config; 14 import org.openstreetmap.josm.testutils.annotations.BasicPreferences; 11 15 import org.openstreetmap.josm.tools.Logging; 12 16 13 17 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; … … import nl.jqno.equalsverifier.EqualsVerifier; 16 20 /** 17 21 * Unit tests for class {@link OAuthParameters}. 18 22 */ 23 @BasicPreferences 19 24 class OAuthParametersTest { 25 26 @AfterEach 27 void tearDown() { 28 NetworkManager.setOnline(OnlineResource.ALL); 29 // the API URL is static state shared with the other tests of this class 30 Config.getPref().put("osm-server.url", null); 31 } 32 33 /** 34 * {@link OAuthParameters#createFromApiUrl} answers for the server it is asked about, whichever server is 35 * configured in the preferences, and finds the parameters the user entered in the advanced OAuth settings. 36 * <p> 37 * Non-regression test for #24925: the URL was reduced to the host before the fallback to 38 * {@link OAuthParameters#createDefault}, and a host is never a valid URL, so the defaults of the configured 39 * API were returned instead of those of the server which was asked about. 40 */ 41 @Test 42 void testCreateFromApiUrlUsesTheRequestedServer() { 43 // the RFC 8414 lookup must not make a network request 44 NetworkManager.setOffline(OnlineResource.ALL); 45 final String thirdParty = "https://nonprod-mapops.example.org/api"; 46 Config.getPref().put("osm-server.url", thirdParty); 47 48 // a server JOSM has a client id for is answered with that client id, not with the configured server's 49 assertEquals("Hl5yIhFS-Egj6aY7A35ouLOuZl0EHjj8JJQQ46IO96E", 50 OAuthParameters.createFromApiUrl("https://api.openhistoricalmap.org/api", OAuthVersion.OAuth20).getClientId()); 51 // a server JOSM knows nothing about has no client id until the user enters one 52 assertEquals("", OAuthParameters.createFromApiUrl(thirdParty, OAuthVersion.OAuth20).getClientId()); 53 54 // once the parameters are remembered - as the authentication preferences do, under the API URL - they 55 // are found both by the API URL and by the host, which is what a connection has at hand 56 new OAuth20Parameters("entered-client-id", null, thirdParty, thirdParty, "http://127.0.0.1:8111/oauth_authorization") 57 .rememberPreferences(); 58 assertEquals("entered-client-id", 59 OAuthParameters.createFromApiUrl(thirdParty, OAuthVersion.OAuth20).getClientId()); 60 assertEquals("https://api.openhistoricalmap.org/api", 61 OAuthParameters.createFromApiUrl("https://api.openhistoricalmap.org/api", OAuthVersion.OAuth20).getApiUrl()); 62 } 63 20 64 /** 21 65 * Unit test of method {@link OAuthParameters#createDefault}. 22 66 */ -
new file test/unit/org/openstreetmap/josm/io/OsmConnectionTest.java
diff --git test/unit/org/openstreetmap/josm/io/OsmConnectionTest.java test/unit/org/openstreetmap/josm/io/OsmConnectionTest.java new file mode 100644 index 0000000000..f742a38422
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.io; 3 4 import static org.junit.jupiter.api.Assertions.assertEquals; 5 import static org.junit.jupiter.api.Assertions.assertNotNull; 6 import static org.junit.jupiter.api.Assertions.assertNull; 7 import static org.junit.jupiter.api.Assertions.assertThrows; 8 import static org.junit.jupiter.api.Assertions.assertTrue; 9 10 import java.net.MalformedURLException; 11 import java.net.URL; 12 13 import javax.swing.JOptionPane; 14 15 import org.junit.jupiter.api.AfterEach; 16 import org.junit.jupiter.api.BeforeEach; 17 import org.junit.jupiter.api.Test; 18 import org.openstreetmap.josm.data.oauth.IOAuthParameters; 19 import org.openstreetmap.josm.data.oauth.OAuth20Exception; 20 import org.openstreetmap.josm.data.oauth.OAuth20Parameters; 21 import org.openstreetmap.josm.data.oauth.OAuth20Token; 22 import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder; 23 import org.openstreetmap.josm.data.oauth.OAuthVersion; 24 import org.openstreetmap.josm.spi.preferences.Config; 25 import org.openstreetmap.josm.testutils.annotations.BasicPreferences; 26 import org.openstreetmap.josm.tools.HttpClient; 27 28 /** 29 * Unit tests of {@link OsmConnection}. 30 */ 31 @BasicPreferences 32 class OsmConnectionTest { 33 34 private static final String API_URL = "https://nonprod-mapops.example.org/api"; 35 private static final String CLIENT_ID = "a-client-id-of-an-application-registered-on-that-server"; 36 private static final String REDIRECT_URI = "http://127.0.0.1:8111/oauth_authorization"; 37 38 /** Gives the test access to the protected members of {@link OsmConnection} */ 39 private static final class TestConnection extends OsmConnection { 40 IOAuthParameters parameters() { 41 return this.oAuth20Parameters; 42 } 43 } 44 45 @BeforeEach 46 void setUp() { 47 // the RFC 8414 lookup of OAuthParameters must not make a network request 48 NetworkManager.setOffline(OnlineResource.ALL); 49 Config.getPref().put("osm-server.url", API_URL); 50 } 51 52 @AfterEach 53 void tearDown() { 54 NetworkManager.setOnline(OnlineResource.ALL); 55 OAuthAccessTokenHolder.getInstance().clear(); 56 // preferences and the token holder are static state shared with the other tests of this class 57 Config.getPref().put("oauth.access-token.parameters." + OAuthVersion.OAuth20 + "." + API_URL, null); 58 Config.getPref().put("osm-server.url", null); 59 } 60 61 private static HttpClient apiRequest() throws MalformedURLException { 62 return HttpClient.create(new URL(API_URL + "/0.6/changeset/create"), "PUT"); 63 } 64 65 /** 66 * Stores the advanced OAuth parameters the way the authentication preferences do, that is under the API URL. 67 */ 68 private static OAuth20Parameters rememberParameters() { 69 OAuth20Parameters parameters = new OAuth20Parameters(CLIENT_ID, null, API_URL, API_URL, REDIRECT_URI); 70 parameters.rememberPreferences(); 71 return parameters; 72 } 73 74 /** 75 * The OAuth parameters of a connection are the ones the user entered for the configured API. 76 * <p> 77 * Non-regression test for #24925: they were looked up with the host of the request although they are stored 78 * under the API URL, so the lookup missed and the client id was empty - which made the authorization request 79 * on upload fail with "Missing required parameter: client_id". 80 * @throws Exception if the connection cannot be signed 81 */ 82 @Test 83 void testOAuthParametersComeFromTheApiUrl() throws Exception { 84 OAuth20Parameters parameters = rememberParameters(); 85 // a token is needed, otherwise the connection would try to obtain one 86 OAuthAccessTokenHolder.getInstance().setAccessToken(API_URL, new OAuth20Token(parameters, 87 "{\"token_type\":\"bearer\", \"access_token\":\"a-token\"}")); 88 89 TestConnection connection = new TestConnection(); 90 assertNull(connection.parameters()); 91 HttpClient client = apiRequest(); 92 connection.addOAuth20AuthorizationHeader(client); 93 94 assertEquals(CLIENT_ID, connection.parameters().getClientId()); 95 assertEquals(API_URL, connection.parameters().getApiUrl()); 96 assertEquals("Bearer a-token", client.getRequestHeader("Authorization")); 97 } 98 99 /** 100 * Without a client id there is nothing to authorize with, so the user is told to configure one instead of 101 * being sent to a browser which the server then rejects with "Missing required parameter: client_id". 102 * <p> 103 * The confirmation dialog is pre-answered with "No" so that the test still terminates if the check is ever 104 * removed; what tells the two apart is that only the check names the server it gave up on. 105 * @throws MalformedURLException never 106 */ 107 @Test 108 void testNoClientIdFailsBeforeAskingToOpenABrowser() throws MalformedURLException { 109 Config.getPref().putBoolean("message.oauth.oauth20.obtain.automatically", false); 110 Config.getPref().putInt("message.oauth.oauth20.obtain.automatically.value", JOptionPane.NO_OPTION); 111 112 // no parameters remembered for this API, and it has no client id built into JOSM 113 HttpClient client = apiRequest(); 114 MissingOAuthAccessTokenException thrown = assertThrows(MissingOAuthAccessTokenException.class, 115 () -> new TestConnection().addOAuth20AuthorizationHeader(client)); 116 assertNotNull(thrown.getMessage(), "gave up only after asking the user, rather than for the missing client id"); 117 assertTrue(thrown.getMessage().contains(API_URL), thrown.getMessage()); 118 assertNull(client.getRequestHeader("Authorization")); 119 } 120 121 /** 122 * An existing token is used without the parameters having to be stored, since the token carries its own. 123 * @throws OAuth20Exception if the token cannot be built 124 * @throws Exception if the connection cannot be signed 125 */ 126 @Test 127 void testExistingTokenSignsTheConnection() throws Exception { 128 OAuth20Parameters parameters = new OAuth20Parameters(CLIENT_ID, null, API_URL, API_URL, REDIRECT_URI); 129 OAuthAccessTokenHolder.getInstance().setAccessToken(API_URL, new OAuth20Token(parameters, 130 "{\"token_type\":\"bearer\", \"access_token\":\"another-token\"}")); 131 132 HttpClient client = apiRequest(); 133 new TestConnection().addOAuth20AuthorizationHeader(client); 134 assertEquals("Bearer another-token", client.getRequestHeader("Authorization")); 135 } 136 }
