source: josm/trunk/src/org/openstreetmap/josm/data/oauth/OAuthParameters.java

Last change on this file was 19253, checked in by taylor.smock, 2 weeks ago

Fix #23988: Add api.openhistoricalmap.org as a possible endpoint for OHM when getting the default client id (patch by Rub21)

  • Property svn:eol-style set to native
File size: 9.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.oauth;
3
4import java.io.BufferedReader;
5import java.io.IOException;
6import java.net.URI;
7import java.net.URISyntaxException;
8import java.net.URL;
9import java.util.HashMap;
10import java.util.Map;
11
12import jakarta.json.stream.JsonParsingException;
13import org.openstreetmap.josm.io.NetworkManager;
14import org.openstreetmap.josm.io.OsmApi;
15import org.openstreetmap.josm.io.auth.CredentialsAgentException;
16import org.openstreetmap.josm.io.auth.CredentialsManager;
17import org.openstreetmap.josm.spi.preferences.Config;
18import org.openstreetmap.josm.spi.preferences.IUrls;
19import org.openstreetmap.josm.tools.HttpClient;
20import org.openstreetmap.josm.tools.JosmRuntimeException;
21import org.openstreetmap.josm.tools.Logging;
22import org.openstreetmap.josm.tools.Utils;
23
24import jakarta.json.Json;
25import jakarta.json.JsonObject;
26import jakarta.json.JsonReader;
27import jakarta.json.JsonStructure;
28import jakarta.json.JsonValue;
29
30/**
31 * This class manages an immutable set of OAuth parameters.
32 * @since 2747 (static factory class since 18991)
33 */
34public final class OAuthParameters {
35 private static final Map<String, JsonObject> RFC8414_RESPONSES = new HashMap<>(1);
36 private static final String OSM_API_DEFAULT = "https://api.openstreetmap.org/api";
37 private static final String OSM_API_DEV = "https://api06.dev.openstreetmap.org/api";
38 private static final String OSM_API_MASTER = "https://master.apis.dev.openstreetmap.org/api";
39
40 private OAuthParameters() {
41 // Hide constructor
42 }
43
44 /**
45 * Replies a set of default parameters for a consumer accessing the standard OSM server
46 * at {@link IUrls#getDefaultOsmApiUrl}.
47 * <p>
48 * Note that this may make network requests for RFC 8414 compliant endpoints.
49 * @return a set of default parameters
50 */
51 public static IOAuthParameters createDefault() {
52 return createDefault(Config.getUrls().getDefaultOsmApiUrl(), OAuthVersion.OAuth20);
53 }
54
55 /**
56 * Replies a set of default parameters for a consumer accessing an OSM server
57 * at the given API url. URL parameters are only set if the URL equals {@link IUrls#getDefaultOsmApiUrl}
58 * or references the domain "dev.openstreetmap.org", otherwise they may be <code>null</code>.
59 * <p>
60 * Note that this may make network requests for RFC 8414 compliant endpoints.
61 *
62 * @param apiUrl The API URL for which the OAuth default parameters are created. If null or empty, the default OSM API url is used.
63 * @param oAuthVersion The OAuth version to create default parameters for
64 * @return a set of default parameters for the given {@code apiUrl}
65 * @since 18650
66 */
67 public static IOAuthParameters createDefault(String apiUrl, OAuthVersion oAuthVersion) {
68 if (!Utils.isValidUrl(apiUrl)) {
69 apiUrl = null;
70 }
71
72 switch (oAuthVersion) {
73 case OAuth20:
74 case OAuth21: // For now, OAuth 2.1 (draft) is just OAuth 2.0 with mandatory extensions, which we implement.
75 return getDefaultOAuth20Parameters(apiUrl);
76 default:
77 throw new IllegalArgumentException("Unknown OAuth version: " + oAuthVersion);
78 }
79 }
80
81 private static JsonObject getRFC8414Parameters(String apiUrl) {
82 HttpClient client = null;
83 try {
84 final URI apiURI = new URI(apiUrl);
85 final URL rfc8414URL = new URI(apiURI.getScheme(), apiURI.getHost(),
86 "/.well-known/oauth-authorization-server", null).toURL();
87 client = HttpClient.create(rfc8414URL);
88 HttpClient.Response response = client.connect();
89 if (response.getResponseCode() == 200) {
90 try (BufferedReader reader = response.getContentReader();
91 JsonReader jsonReader = Json.createReader(reader)) {
92 JsonStructure structure = jsonReader.read();
93 if (structure.getValueType() == JsonValue.ValueType.OBJECT) {
94 return structure.asJsonObject();
95 }
96 }
97 }
98 } catch (JsonParsingException | URISyntaxException | IOException e) {
99 throw new JosmRuntimeException(e);
100 } finally {
101 if (client != null) {
102 client.disconnect();
103 }
104 }
105 return Json.createObjectBuilder().build();
106 }
107
108 /**
109 * Get the default OAuth 2.0 parameters
110 * @param apiUrl The API url
111 * @return The default parameters
112 */
113 private static OAuth20Parameters getDefaultOAuth20Parameters(String apiUrl) {
114 final String clientId;
115 final String clientSecret;
116 final String redirectUri = "http://127.0.0.1:8111/oauth_authorization";
117 final String baseUrl;
118 apiUrl = apiUrl == null ? OsmApi.getOsmApi().getServerUrl() : apiUrl;
119 switch (apiUrl) {
120 case OSM_API_DEV:
121 case OSM_API_MASTER:
122 // This clientId/clientSecret are provided by taylor.smock. Feel free to change if needed, but
123 // do let one of the maintainers with server access know so that they can update the test OAuth
124 // token.
125 clientId = "-QZt6n1btDfqrfJNGUIMZjzcyqTgIV6sy79_W4kmQLM";
126 // Keep secret for dev apis, just in case we want to test something that needs it.
127 clientSecret = "SWnmRD4AdLO-2-ttHE5TR3eLF2McNf7dh0_Z2WNzJdI";
128 break;
129 case OSM_API_DEFAULT:
130 clientId = "edPII614Lm0_0zEpc_QzEltA9BUll93-Y-ugRQUoHMI";
131 // We don't actually use the client secret in our authorization flow.
132 clientSecret = null;
133 break;
134 case "https://www.openhistoricalmap.org/api":
135 case "https://api.openhistoricalmap.org/api":
136 // clientId provided by 1ec5 (Minh Nguyễn)
137 clientId = "Hl5yIhFS-Egj6aY7A35ouLOuZl0EHjj8JJQQ46IO96E";
138 clientSecret = null;
139 break;
140 default:
141 clientId = "";
142 clientSecret = null;
143 }
144 baseUrl = apiUrl;
145 // Check if the server is RFC 8414 compliant
146 try {
147 synchronized (RFC8414_RESPONSES) {
148 final JsonObject data;
149 if (NetworkManager.isOffline(apiUrl)) {
150 data = null;
151 } else {
152 data = RFC8414_RESPONSES.computeIfAbsent(apiUrl, OAuthParameters::getRFC8414Parameters);
153 }
154 if (data == null || data.isEmpty()) {
155 RFC8414_RESPONSES.remove(apiUrl);
156 } else {
157 return parseAuthorizationServerMetadataResponse(clientId, clientSecret, apiUrl,
158 redirectUri, data);
159 }
160 }
161 } catch (JosmRuntimeException e) {
162 if (e.getCause() instanceof URISyntaxException || e.getCause() instanceof IOException
163 || e.getCause() instanceof JsonParsingException) {
164 Logging.trace(e);
165 } else {
166 throw e;
167 }
168 } catch (OAuthException e) {
169 Logging.trace(e);
170 }
171 // Fall back to guessing the parameters.
172 return new OAuth20Parameters(clientId, clientSecret, baseUrl, apiUrl, redirectUri);
173 }
174
175 /**
176 * Parse the response from <a href="https://www.rfc-editor.org/rfc/rfc8414.html">RFC 8414</a>
177 * (OAuth 2.0 Authorization Server Metadata)
178 * @return The parameters for the server metadata
179 */
180 private static OAuth20Parameters parseAuthorizationServerMetadataResponse(String clientId, String clientSecret,
181 String apiUrl, String redirectUri,
182 JsonObject serverMetadata)
183 throws OAuthException {
184 final String authorizationEndpoint = serverMetadata.getString("authorization_endpoint", null);
185 final String tokenEndpoint = serverMetadata.getString("token_endpoint", null);
186 // This may also have additional documentation like what the endpoints allow (e.g. scopes, algorithms, etc.)
187 if (authorizationEndpoint == null || tokenEndpoint == null) {
188 throw new OAuth20Exception("Either token endpoint or authorization endpoints are missing");
189 }
190 return new OAuth20Parameters(clientId, clientSecret, tokenEndpoint, authorizationEndpoint, apiUrl, redirectUri);
191 }
192
193 /**
194 * Replies a set of parameters as defined in the preferences.
195 *
196 * @param oAuthVersion The OAuth version to use.
197 * @param apiUrl the API URL. Must not be {@code null}.
198 * @return the parameters
199 * @since 18650
200 */
201 public static IOAuthParameters createFromApiUrl(String apiUrl, OAuthVersion oAuthVersion) {
202 // We actually need the host
203 if (apiUrl.startsWith("https://") || apiUrl.startsWith("http://")) {
204 try {
205 apiUrl = new URI(apiUrl).getHost();
206 } catch (URISyntaxException syntaxException) {
207 Logging.trace(apiUrl);
208 }
209 }
210 switch (oAuthVersion) {
211 case OAuth20:
212 case OAuth21: // Right now, OAuth 2.1 will work with our OAuth 2.0 implementation
213 try {
214 IOAuthToken storedToken = CredentialsManager.getInstance().lookupOAuthAccessToken(apiUrl);
215 if (storedToken != null) {
216 return storedToken.getParameters();
217 }
218 } catch (CredentialsAgentException e) {
219 Logging.trace(e);
220 }
221 return createDefault(apiUrl, oAuthVersion);
222 default:
223 throw new IllegalArgumentException("Unknown OAuth version: " + oAuthVersion);
224 }
225 }
226}
Note: See TracBrowser for help on using the repository browser.