Ignore:
Timestamp:
2023-02-08T18:31:58+01:00 (3 years ago)
Author:
taylor.smock
Message:

Fix #20768: Add OAuth 2.0 support

This also fixes #21607: authentication buttons are unavailable when credentials
are set.

Location:
trunk/src/org/openstreetmap/josm/data/oauth
Files:
12 added
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/org/openstreetmap/josm/data/oauth/OAuthAccessTokenHolder.java

    r13173 r18650  
    44import static org.openstreetmap.josm.tools.I18n.tr;
    55
     6import java.net.URI;
     7import java.util.EnumMap;
     8import java.util.HashMap;
     9import java.util.Map;
     10import java.util.Objects;
     11import java.util.Optional;
     12
    613import org.openstreetmap.josm.io.auth.CredentialsAgent;
    714import org.openstreetmap.josm.io.auth.CredentialsAgentException;
     15import org.openstreetmap.josm.io.auth.CredentialsManager;
    816import org.openstreetmap.josm.spi.preferences.Config;
    917import org.openstreetmap.josm.tools.CheckParameterUtil;
     
    3240    private String accessTokenSecret;
    3341
     42    private final Map<String, Map<OAuthVersion, IOAuthToken>> tokenMap = new HashMap<>();
     43
    3444    /**
    3545     * Replies true if current access token should be saved to the preferences file.
     
    101111            return null;
    102112        return new OAuthToken(accessTokenKey, accessTokenSecret);
     113    }
     114
     115    /**
     116     * Replies the access token.
     117     * @param api The api the token is for
     118     * @param version The OAuth version the token is for
     119     * @return the access token, can be {@code null}
     120     * @since 18650
     121     */
     122    public IOAuthToken getAccessToken(String api, OAuthVersion version) {
     123        api = URI.create(api).getHost();
     124        if (this.tokenMap.containsKey(api)) {
     125            Map<OAuthVersion, IOAuthToken> map = this.tokenMap.get(api);
     126            return map.get(version);
     127        }
     128        try {
     129            IOAuthToken token = CredentialsManager.getInstance().lookupOAuthAccessToken(api);
     130            // We *do* want to set the API token to null, if it doesn't exist. Just to avoid unnecessary lookups.
     131            this.setAccessToken(api, token);
     132            return token;
     133        } catch (CredentialsAgentException exception) {
     134            Logging.trace(exception);
     135        }
     136        return null;
    103137    }
    104138
     
    126160            this.accessTokenKey = token.getKey();
    127161            this.accessTokenSecret = token.getSecret();
     162        }
     163    }
     164
     165    /**
     166     * Sets the access token hold by this holder.
     167     *
     168     * @param api The api the token is for
     169     * @param token the access token. Can be null to clear the content in this holder.
     170     * @since 18650
     171     */
     172    public void setAccessToken(String api, IOAuthToken token) {
     173        Objects.requireNonNull(api, "api url");
     174        // Sometimes the api might be sent as the host
     175        api = Optional.ofNullable(URI.create(api).getHost()).orElse(api);
     176        if (token == null) {
     177            if (this.tokenMap.containsKey(api)) {
     178                this.tokenMap.get(api).clear();
     179            }
     180        } else {
     181            this.tokenMap.computeIfAbsent(api, key -> new EnumMap<>(OAuthVersion.class)).put(token.getOAuthType(), token);
    128182        }
    129183    }
     
    176230            if (!saveToPreferences) {
    177231                cm.storeOAuthAccessToken(null);
     232                for (String host : this.tokenMap.keySet()) {
     233                    cm.storeOAuthAccessToken(host, null);
     234                }
    178235            } else {
    179                 cm.storeOAuthAccessToken(new OAuthToken(accessTokenKey, accessTokenSecret));
     236                if (this.accessTokenKey != null && this.accessTokenSecret != null) {
     237                    cm.storeOAuthAccessToken(new OAuthToken(accessTokenKey, accessTokenSecret));
     238                }
     239                for (Map.Entry<String, Map<OAuthVersion, IOAuthToken>> entry : this.tokenMap.entrySet()) {
     240                    if (entry.getValue().isEmpty()) {
     241                        cm.storeOAuthAccessToken(entry.getKey(), null);
     242                        continue;
     243                    }
     244                    for (OAuthVersion version : OAuthVersion.values()) {
     245                        if (entry.getValue().containsKey(version)) {
     246                            cm.storeOAuthAccessToken(entry.getKey(), entry.getValue().get(version));
     247                        }
     248                    }
     249                }
    180250            }
    181251        } catch (CredentialsAgentException e) {
  • trunk/src/org/openstreetmap/josm/data/oauth/OAuthParameters.java

    r15009 r18650  
    22package org.openstreetmap.josm.data.oauth;
    33
     4import java.io.BufferedReader;
     5import java.io.IOException;
     6import java.net.URL;
    47import java.util.Objects;
    58
     9import javax.json.Json;
     10import javax.json.JsonObject;
     11import javax.json.JsonReader;
     12import javax.json.JsonStructure;
     13import javax.json.JsonValue;
     14
     15import org.openstreetmap.josm.io.OsmApi;
     16import org.openstreetmap.josm.io.auth.CredentialsAgentException;
     17import org.openstreetmap.josm.io.auth.CredentialsManager;
    618import org.openstreetmap.josm.spi.preferences.Config;
    719import org.openstreetmap.josm.spi.preferences.IUrls;
    820import org.openstreetmap.josm.tools.CheckParameterUtil;
     21import org.openstreetmap.josm.tools.HttpClient;
     22import org.openstreetmap.josm.tools.Logging;
    923import org.openstreetmap.josm.tools.Utils;
    1024
     
    1630 * @since 2747
    1731 */
    18 public class OAuthParameters {
     32public class OAuthParameters implements IOAuthParameters {
    1933
    2034    /**
     
    4761     */
    4862    public static OAuthParameters createDefault(String apiUrl) {
     63        return (OAuthParameters) createDefault(apiUrl, OAuthVersion.OAuth10a);
     64    }
     65
     66    /**
     67     * Replies a set of default parameters for a consumer accessing an OSM server
     68     * at the given API url. URL parameters are only set if the URL equals {@link IUrls#getDefaultOsmApiUrl}
     69     * or references the domain "dev.openstreetmap.org", otherwise they may be <code>null</code>.
     70     *
     71     * @param apiUrl The API URL for which the OAuth default parameters are created. If null or empty, the default OSM API url is used.
     72     * @param oAuthVersion The OAuth version to create default parameters for
     73     * @return a set of default parameters for the given {@code apiUrl}
     74     * @since 18650
     75     */
     76    public static IOAuthParameters createDefault(String apiUrl, OAuthVersion oAuthVersion) {
     77        if (!Utils.isValidUrl(apiUrl)) {
     78            apiUrl = null;
     79        }
     80
     81        switch (oAuthVersion) {
     82            case OAuth10a:
     83                return getDefaultOAuth10Parameters(apiUrl);
     84            case OAuth20:
     85            case OAuth21: // For now, OAuth 2.1 (draft) is just OAuth 2.0 with mandatory extensions, which we implement.
     86                return getDefaultOAuth20Parameters(apiUrl);
     87            default:
     88                throw new IllegalArgumentException("Unknown OAuth version: " + oAuthVersion);
     89        }
     90    }
     91
     92    /**
     93     * Get the default OAuth 2.0 parameters
     94     * @param apiUrl The API url
     95     * @return The default parameters
     96     */
     97    private static OAuth20Parameters getDefaultOAuth20Parameters(String apiUrl) {
     98        final String clientId;
     99        final String clientSecret;
     100        final String redirectUri;
     101        final String baseUrl;
     102        if (apiUrl != null && !Config.getUrls().getDefaultOsmApiUrl().equals(apiUrl)) {
     103            clientId = "";
     104            clientSecret = "";
     105            baseUrl = apiUrl;
     106            HttpClient client = null;
     107            redirectUri = "";
     108            // Check if the server is RFC 8414 compliant
     109            try {
     110                client = HttpClient.create(new URL(apiUrl + (apiUrl.endsWith("/") ? "" : "/") + ".well-known/oauth-authorization-server"));
     111                HttpClient.Response response = client.connect();
     112                if (response.getResponseCode() == 200) {
     113                    try (BufferedReader reader = response.getContentReader();
     114                         JsonReader jsonReader = Json.createReader(reader)) {
     115                        JsonStructure structure = jsonReader.read();
     116                        if (structure.getValueType() == JsonValue.ValueType.OBJECT) {
     117                            return parseAuthorizationServerMetadataResponse(clientId, clientSecret, apiUrl,
     118                                    redirectUri, structure.asJsonObject());
     119                        }
     120                    }
     121                }
     122            } catch (IOException | OAuthException e) {
     123                Logging.trace(e);
     124            } finally {
     125                if (client != null) client.disconnect();
     126            }
     127        } else {
     128            clientId = "edPII614Lm0_0zEpc_QzEltA9BUll93-Y-ugRQUoHMI";
     129            // We don't actually use the client secret in our authorization flow.
     130            clientSecret = null;
     131            baseUrl = "https://www.openstreetmap.org/oauth2";
     132            redirectUri = "http://127.0.0.1:8111/oauth_authorization";
     133            apiUrl = OsmApi.getOsmApi().getBaseUrl();
     134        }
     135        return new OAuth20Parameters(clientId, clientSecret, baseUrl, apiUrl, redirectUri);
     136    }
     137
     138    /**
     139     * Parse the response from <a href="https://www.rfc-editor.org/rfc/rfc8414.html">RFC 8414</a>
     140     * (OAuth 2.0 Authorization Server Metadata)
     141     * @return The parameters for the server metadata
     142     */
     143    private static OAuth20Parameters parseAuthorizationServerMetadataResponse(String clientId, String clientSecret,
     144                                                                              String apiUrl, String redirectUri,
     145                                                                              JsonObject serverMetadata)
     146            throws OAuthException {
     147        final String authorizationEndpoint = serverMetadata.getString("authorization_endpoint", null);
     148        final String tokenEndpoint = serverMetadata.getString("token_endpoint", null);
     149        // This may also have additional documentation like what the endpoints allow (e.g. scopes, algorithms, etc.)
     150        if (authorizationEndpoint == null || tokenEndpoint == null) {
     151            throw new OAuth20Exception("Either token endpoint or authorization endpoints are missing");
     152        }
     153        return new OAuth20Parameters(clientId, clientSecret, tokenEndpoint, authorizationEndpoint, apiUrl, redirectUri);
     154    }
     155
     156    /**
     157     * Get the default OAuth 1.0a parameters
     158     * @param apiUrl The api url
     159     * @return The default parameters
     160     */
     161    private static OAuthParameters getDefaultOAuth10Parameters(String apiUrl) {
    49162        final String consumerKey;
    50163        final String consumerSecret;
    51164        final String serverUrl;
    52 
    53         if (!Utils.isValidUrl(apiUrl)) {
    54             apiUrl = null;
    55         }
    56165
    57166        if (apiUrl != null && !Config.getUrls().getDefaultOsmApiUrl().equals(apiUrl)) {
     
    82191     */
    83192    public static OAuthParameters createFromApiUrl(String apiUrl) {
    84         OAuthParameters parameters = createDefault(apiUrl);
    85         return new OAuthParameters(
    86                 Config.getPref().get("oauth.settings.consumer-key", parameters.getConsumerKey()),
    87                 Config.getPref().get("oauth.settings.consumer-secret", parameters.getConsumerSecret()),
    88                 Config.getPref().get("oauth.settings.request-token-url", parameters.getRequestTokenUrl()),
    89                 Config.getPref().get("oauth.settings.access-token-url", parameters.getAccessTokenUrl()),
    90                 Config.getPref().get("oauth.settings.authorise-url", parameters.getAuthoriseUrl()),
    91                 Config.getPref().get("oauth.settings.osm-login-url", parameters.getOsmLoginUrl()),
    92                 Config.getPref().get("oauth.settings.osm-logout-url", parameters.getOsmLogoutUrl()));
     193        return (OAuthParameters) createFromApiUrl(apiUrl, OAuthVersion.OAuth10a);
     194    }
     195
     196    /**
     197     * Replies a set of parameters as defined in the preferences.
     198     *
     199     * @param oAuthVersion The OAuth version to use.
     200     * @param apiUrl the API URL. Must not be {@code null}.
     201     * @return the parameters
     202     * @since 18650
     203     */
     204    public static IOAuthParameters createFromApiUrl(String apiUrl, OAuthVersion oAuthVersion) {
     205        IOAuthParameters parameters = createDefault(apiUrl, oAuthVersion);
     206        switch (oAuthVersion) {
     207            case OAuth10a:
     208                OAuthParameters oauth10aParameters = (OAuthParameters) parameters;
     209                return new OAuthParameters(
     210                    Config.getPref().get("oauth.settings.consumer-key", oauth10aParameters.getConsumerKey()),
     211                    Config.getPref().get("oauth.settings.consumer-secret", oauth10aParameters.getConsumerSecret()),
     212                    Config.getPref().get("oauth.settings.request-token-url", oauth10aParameters.getRequestTokenUrl()),
     213                    Config.getPref().get("oauth.settings.access-token-url", oauth10aParameters.getAccessTokenUrl()),
     214                    Config.getPref().get("oauth.settings.authorise-url", oauth10aParameters.getAuthoriseUrl()),
     215                    Config.getPref().get("oauth.settings.osm-login-url", oauth10aParameters.getOsmLoginUrl()),
     216                    Config.getPref().get("oauth.settings.osm-logout-url", oauth10aParameters.getOsmLogoutUrl()));
     217            case OAuth20:
     218            case OAuth21: // Right now, OAuth 2.1 will work with our OAuth 2.0 implementation
     219                OAuth20Parameters oAuth20Parameters = (OAuth20Parameters) parameters;
     220                try {
     221                    IOAuthToken storedToken = CredentialsManager.getInstance().lookupOAuthAccessToken(apiUrl);
     222                    return storedToken != null ? storedToken.getParameters() : oAuth20Parameters;
     223                } catch (CredentialsAgentException e) {
     224                    Logging.trace(e);
     225                }
     226                return oAuth20Parameters;
     227            default:
     228                throw new IllegalArgumentException("Unknown OAuth version: " + oAuthVersion);
     229        }
    93230    }
    94231
     
    96233     * Remembers the current values in the preferences.
    97234     */
     235    @Override
    98236    public void rememberPreferences() {
    99237        Config.getPref().put("oauth.settings.consumer-key", getConsumerKey());
     
    183321     * @return The access token URL
    184322     */
     323    @Override
    185324    public String getAccessTokenUrl() {
    186325        return accessTokenUrl;
    187326    }
    188327
     328    @Override
     329    public String getAuthorizationUrl() {
     330        return this.authoriseUrl;
     331    }
     332
     333    @Override
     334    public OAuthVersion getOAuthVersion() {
     335        return OAuthVersion.OAuth10a;
     336    }
     337
     338    @Override
     339    public String getClientId() {
     340        return this.consumerKey;
     341    }
     342
     343    @Override
     344    public String getClientSecret() {
     345        return this.consumerSecret;
     346    }
     347
    189348    /**
    190349     * Gets the authorise URL.
     
    192351     */
    193352    public String getAuthoriseUrl() {
    194         return authoriseUrl;
     353        return this.getAuthorizationUrl();
    195354    }
    196355
Note: See TracChangeset for help on using the changeset viewer.