Ticket #20768: 20768.patch

File 20768.patch, 156.3 KB (added by taylor.smock, 3 years ago)
  • new file src/org/openstreetmap/josm/data/oauth/IOAuthAuthorization.java

    Subject: [PATCH] Fix #20768: Add OAuth 2.0 support
    ---
    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/oauth/IOAuthAuthorization.java b/src/org/openstreetmap/josm/data/oauth/IOAuthAuthorization.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.oauth;
     3
     4import java.util.function.Consumer;
     5
     6/**
     7 * Interface for OAuth authorization classes
     8 * @author Taylor Smock
     9 * @since xxx
     10 */
     11public interface IOAuthAuthorization {
     12    /**
     13     * Perform the authorization dance
     14     * @param parameters The OAuth parameters
     15     * @param consumer The callback for the generated token
     16     * @param scopes The scopes to ask for
     17     */
     18    void authorize(IOAuthParameters parameters, Consumer<IOAuthToken> consumer, Enum<?>... scopes);
     19}
  • new file src/org/openstreetmap/josm/data/oauth/IOAuthParameters.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/oauth/IOAuthParameters.java b/src/org/openstreetmap/josm/data/oauth/IOAuthParameters.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.oauth;
     3
     4import java.util.stream.Stream;
     5
     6/**
     7 * A generic interface for OAuth Parameters
     8 * @author Taylor Smock
     9 * @since xxx
     10 */
     11public interface IOAuthParameters {
     12    /**
     13     * Get the access token URL
     14     * @return The URL to use to switch the code to a token
     15     */
     16    String getAccessTokenUrl();
     17
     18    /**
     19     * Get the base authorization URL to open in a browser
     20     * @return The base URL to send to the browser
     21     */
     22    String getAuthorizationUrl();
     23
     24    /**
     25     * Get the authorization URL to open in a browser
     26     * @param state The state to prevent attackers from providing their own token
     27     * @param scopes The scopes to request
     28     * @return The URL to send to the browser
     29     */
     30    default String getAuthorizationUrl(String state, Enum<?>... scopes) {
     31        return this.getAuthorizationUrl(state, Stream.of(scopes).map(Enum::toString).toArray(String[]::new));
     32    }
     33
     34    /**
     35     * Get the authorization URL to open in a browser
     36     * @param state The state to prevent attackers from providing their own token
     37     * @param scopes The scopes to request
     38     * @return The URL to send to the browser
     39     */
     40    default String getAuthorizationUrl(String state, String... scopes) {
     41        // response_type = code | token, but token is deprecated in the draft oauth 2.1 spec
     42        // 2.1 is adding code_challenge, code_challenge_method
     43        // code_challenge requires a code_verifier
     44        return this.getAuthorizationUrl() + "?response_type=code&client_id=" + this.getClientId()
     45                + "&redirect_uri=" + this.getRedirectUri()
     46                + "&scope=" + String.join(" ", scopes)
     47                // State is used to detect/prevent cross-site request forgery
     48                + "&state=" + state;
     49    }
     50
     51    /**
     52     * Get the OAuth version that the API expects
     53     * @return The oauth version
     54     */
     55    OAuthVersion getOAuthVersion();
     56
     57    /**
     58     * Get the client id
     59     * @return The client id
     60     */
     61    String getClientId();
     62
     63    /**
     64     * Get the client secret
     65     * @return The client secret
     66     */
     67    String getClientSecret();
     68
     69    /**
     70     * Get the redirect URI
     71     * @return The redirect URI
     72     */
     73    default String getRedirectUri() {
     74        return null;
     75    }
     76
     77    /**
     78     * Convert to a preference string
     79     * @return the preference string
     80     */
     81    default String toPreferencesString() {
     82        return null;
     83    }
     84
     85    /**
     86     * Get the actual API URL
     87     * @return The API URl
     88     */
     89    default String getApiUrl() {
     90        return null;
     91    }
     92
     93    void rememberPreferences();
     94}
  • new file src/org/openstreetmap/josm/data/oauth/IOAuthToken.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/oauth/IOAuthToken.java b/src/org/openstreetmap/josm/data/oauth/IOAuthToken.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.oauth;
     3
     4import org.openstreetmap.josm.tools.HttpClient;
     5
     6/**
     7 * An interface for oauth tokens
     8 * @author Taylor Smock
     9 * @since xxx
     10 */
     11public interface IOAuthToken {
     12    /**
     13     * Sign a client
     14     * @param client The client to sign
     15     */
     16    void sign(HttpClient client) throws OAuthException;
     17
     18    /**
     19     * Get the preferences string of this auth token.
     20     * This should match the expected return body from the authentication server.
     21     * For OAuth, this is typically JSON.
     22     * @return The preferences string
     23     */
     24    String toPreferencesString();
     25
     26    /**
     27     * Get the auth type of this token
     28     * @return The auth type
     29     */
     30    OAuthVersion getOAuthType();
     31
     32    /**
     33     * Get the OAuth parameters
     34     * @return The OAuth parameters
     35     */
     36    IOAuthParameters getParameters();
     37}
  • new file src/org/openstreetmap/josm/data/oauth/OAuth20Authorization.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/oauth/OAuth20Authorization.java b/src/org/openstreetmap/josm/data/oauth/OAuth20Authorization.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.oauth;
     3
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5
     6import java.io.IOException;
     7import java.net.MalformedURLException;
     8import java.net.URL;
     9import java.nio.charset.StandardCharsets;
     10import java.security.MessageDigest;
     11import java.security.NoSuchAlgorithmException;
     12import java.util.Base64;
     13import java.util.Map;
     14import java.util.Objects;
     15import java.util.UUID;
     16import java.util.function.Consumer;
     17
     18import org.openstreetmap.josm.io.remotecontrol.handler.AuthorizationHandler;
     19import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler;
     20import org.openstreetmap.josm.tools.HttpClient;
     21import org.openstreetmap.josm.tools.JosmRuntimeException;
     22import org.openstreetmap.josm.tools.OpenBrowser;
     23
     24/**
     25 * Authorize the application
     26 * @since xxx
     27 */
     28public class OAuth20Authorization implements IOAuthAuthorization {
     29    /**
     30     * See <a href="https://www.rfc-editor.org/rfc/rfc7636">RFC7636</a>: PKCE
     31     * @param cryptographicallyRandomString A cryptographically secure string
     32     * @return The S256 bytes
     33     */
     34    private static String getPKCES256CodeChallenge(String cryptographicallyRandomString) {
     35        // S256: code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
     36        try {
     37            byte[] encodedBytes = cryptographicallyRandomString.getBytes(StandardCharsets.US_ASCII);
     38            MessageDigest digest = MessageDigest.getInstance("SHA-256");
     39            return new String(Base64.getUrlEncoder().encode(digest.digest(encodedBytes)), StandardCharsets.US_ASCII)
     40                    .replace("=", "").replace("+", "-").replace("/", "_");
     41        } catch (NoSuchAlgorithmException e) {
     42            throw new JosmRuntimeException(e);
     43        }
     44    }
     45
     46    @Override
     47    public void authorize(IOAuthParameters parameters, Consumer<IOAuthToken> consumer, Enum<?>... scopes) {
     48        final String state = UUID.randomUUID().toString();
     49        final String codeVerifier = UUID.randomUUID().toString(); // Cryptographically random string (ASCII)
     50        final String s256CodeChallenge = getPKCES256CodeChallenge(codeVerifier);
     51
     52        // Enable authorization remote control
     53        new AuthorizationHandler().getPermissionPreference().put(true);
     54        String url = parameters.getAuthorizationUrl(state, scopes)
     55                + "&code_challenge_method=S256&code_challenge=" + s256CodeChallenge;
     56        AuthorizationHandler.addAuthorizationConsumer(state, new OAuth20AuthorizationHandler(state, codeVerifier, parameters, consumer));
     57        OpenBrowser.displayUrl(url);
     58    }
     59
     60    private static class OAuth20AuthorizationHandler implements AuthorizationHandler.AuthorizationConsumer {
     61
     62        private final String state;
     63        private final IOAuthParameters parameters;
     64        private final Consumer<IOAuthToken> consumer;
     65        private final String codeVerifier;
     66
     67        OAuth20AuthorizationHandler(String state, String codeVerifier, IOAuthParameters parameters, Consumer<IOAuthToken> consumer) {
     68            this.state = state;
     69            this.parameters = parameters;
     70            this.consumer = consumer;
     71            this.codeVerifier = codeVerifier;
     72        }
     73
     74        @Override
     75        public void validateRequest(String sender, String request, Map<String, String> args)
     76                throws RequestHandler.RequestHandlerBadRequestException {
     77            String argState = args.get("state");
     78            if (!Objects.equals(this.state, argState)) {
     79                throw new RequestHandler.RequestHandlerBadRequestException(
     80                        tr("Mismatched state: Expected {0} but got {1}", this.state, argState));
     81            }
     82        }
     83
     84        @Override
     85        public AuthorizationHandler.ResponseRecord handleRequest(String sender, String request, Map<String, String> args)
     86                throws RequestHandler.RequestHandlerErrorException, RequestHandler.RequestHandlerBadRequestException {
     87            String code = args.get("code");
     88            try {
     89                HttpClient tradeCodeForToken = HttpClient.create(new URL(parameters.getAccessTokenUrl()), "POST");
     90                tradeCodeForToken.setRequestBody(("grant_type=authorization_code&client_id=" + parameters.getClientId()
     91                        + "&redirect_uri=" + parameters.getRedirectUri()
     92                        + "&code=" + code
     93                        + (this.codeVerifier != null ? "&code_verifier=" + this.codeVerifier : "")
     94                ).getBytes(StandardCharsets.UTF_8));
     95                try {
     96                    tradeCodeForToken.connect();
     97                    HttpClient.Response response = tradeCodeForToken.getResponse();
     98                    OAuth20Token oAuth20Token = new OAuth20Token(parameters, response.getContentReader());
     99                    consumer.accept(oAuth20Token);
     100                } catch (IOException | OAuth20Exception e) {
     101                    consumer.accept(null);
     102                    throw new JosmRuntimeException(e);
     103                } finally {
     104                    tradeCodeForToken.disconnect();
     105                }
     106            } catch (MalformedURLException e) {
     107                throw new JosmRuntimeException(e);
     108            }
     109            return null;
     110        }
     111    }
     112}
  • new file src/org/openstreetmap/josm/data/oauth/OAuth20Exception.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/oauth/OAuth20Exception.java b/src/org/openstreetmap/josm/data/oauth/OAuth20Exception.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.oauth;
     3
     4import javax.json.JsonObject;
     5
     6/**
     7 * A generic OAuth 2.0 exception
     8 * @since xxx
     9 */
     10public final class OAuth20Exception extends OAuthException {
     11    private static final long serialVersionUID = -203910656089454886L;
     12
     13    /**
     14     * Invalid request types
     15     */
     16    public enum Type {
     17        invalid_request,
     18        invalid_client,
     19        invalid_grant,
     20        unauthorized_client,
     21        unsupported_grant_type,
     22        invalid_scope,
     23        unknown
     24    }
     25
     26    private final Type type;
     27
     28    /**
     29     * Create a new exception with a specified cause
     30     * @param cause The cause leading to this exception
     31     */
     32    public OAuth20Exception(Exception cause) {
     33        super(cause);
     34        this.type = Type.unknown;
     35    }
     36
     37    /**
     38     * Create a new exception with a given message
     39     * @param message The message to use
     40     */
     41    public OAuth20Exception(String message) {
     42        super(message);
     43        this.type = Type.unknown;
     44    }
     45
     46    /**
     47     * Create an exception from a server message
     48     * @param serverMessage The server message. Should conform to
     49     *                      <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2">RFC 6747 Section 4.2.2</a>, but in JSON
     50     *                      format.
     51     */
     52    OAuth20Exception(JsonObject serverMessage) {
     53        super(serverMessage != null
     54                ? serverMessage.getString("error_description", serverMessage.getString("error", "Unknown error"))
     55                : "Unknown error");
     56        if (serverMessage != null && serverMessage.containsKey("error")) {
     57            switch(serverMessage.getString("error")) {
     58                case "invalid_request":
     59                case "invalid_client":
     60                case "invalid_grant":
     61                case "unauthorized_client":
     62                case "unsupported_grant_type":
     63                case "invalid_scope":
     64                    this.type = Type.valueOf(serverMessage.getString("error"));
     65                    break;
     66                default:
     67                    this.type = Type.unknown;
     68            }
     69        } else {
     70            this.type = Type.unknown;
     71        }
     72    }
     73
     74    @Override
     75    OAuthVersion[] getOAuthVersions() {
     76        return new OAuthVersion[] {OAuthVersion.OAuth20};
     77    }
     78
     79    @Override
     80    public String getMessage() {
     81        String message = super.getMessage();
     82        if (message == null) {
     83            return "OAuth error " + this.type;
     84        }
     85        return "OAuth error " + this.type + ": " + message;
     86    }
     87}
  • new file src/org/openstreetmap/josm/data/oauth/OAuth20Parameters.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/oauth/OAuth20Parameters.java b/src/org/openstreetmap/josm/data/oauth/OAuth20Parameters.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.oauth;
     3
     4import java.io.ByteArrayInputStream;
     5import java.io.IOException;
     6import java.io.UncheckedIOException;
     7import java.nio.charset.StandardCharsets;
     8import java.util.Objects;
     9
     10import javax.json.Json;
     11import javax.json.JsonObject;
     12import javax.json.JsonObjectBuilder;
     13import javax.json.JsonReader;
     14import javax.json.JsonStructure;
     15import javax.json.JsonValue;
     16
     17import org.openstreetmap.josm.spi.preferences.Config;
     18
     19/**
     20 * Parameters for OAuth 2.0
     21 * @author Taylor Smock
     22 * @since xxx
     23 */
     24public final class OAuth20Parameters implements IOAuthParameters {
     25    private static final String REDIRECT_URI = "redirect_uri";
     26    private static final String CLIENT_ID = "client_id";
     27    private static final String CLIENT_SECRET = "client_secret";
     28    private static final String TOKEN_URL = "token_url";
     29    private static final String AUTHORIZE_URL = "authorize_url";
     30    private static final String API_URL = "api_url";
     31    private final String redirectUri;
     32    private final String clientSecret;
     33    private final String clientId;
     34    private final String tokenUrl;
     35    private final String authorizeUrl;
     36    private final String apiUrl;
     37
     38    /**
     39     * Recreate a parameter object from a JSON string
     40     * @param jsonString The JSON string with the required data
     41     */
     42    public OAuth20Parameters(String jsonString) {
     43        try (ByteArrayInputStream bais = new ByteArrayInputStream(jsonString.getBytes(StandardCharsets.UTF_8));
     44             JsonReader reader = Json.createReader(bais)) {
     45            JsonStructure structure = reader.read();
     46            if (structure.getValueType() != JsonValue.ValueType.OBJECT) {
     47                throw new IllegalArgumentException("Invalid JSON object: " + jsonString);
     48            }
     49            JsonObject jsonObject = structure.asJsonObject();
     50            this.redirectUri = jsonObject.getString(REDIRECT_URI);
     51            this.clientId = jsonObject.getString(CLIENT_ID);
     52            this.clientSecret = jsonObject.getString(CLIENT_SECRET, null);
     53            this.tokenUrl = jsonObject.getString(TOKEN_URL);
     54            this.authorizeUrl = jsonObject.getString(AUTHORIZE_URL);
     55            this.apiUrl = jsonObject.getString(API_URL);
     56        } catch (IOException e) {
     57            // This should literally never happen -- ByteArrayInputStream does not do *anything* in the close method.
     58            throw new UncheckedIOException(e);
     59        }
     60    }
     61
     62    /**
     63     * Create a new OAuth parameter object
     64     * @param clientId The client id. May not be {@code null}.
     65     * @param clientSecret The client secret. May be {@code null}. Not currently used.
     66     * @param baseUrl The base url. This assumes that the endpoints are {@code /token} and {@code /authorize}.
     67     * @param apiUrl The API url
     68     * @param redirectUri The redirect URI for the client.
     69     */
     70    public OAuth20Parameters(String clientId, String clientSecret, String baseUrl, String apiUrl, String redirectUri) {
     71        this(clientId, clientSecret, baseUrl + "/token", baseUrl + "/authorize", apiUrl, redirectUri);
     72    }
     73
     74    /**
     75     * Create a new OAuth parameter object
     76     * @param clientId The client id.
     77     * @param clientSecret The client secret. May be {@code null}. Not currently used.
     78     * @param tokenUrl The token request URL (RFC6749 4.4.2)
     79     * @param authorizeUrl The authorization request URL (RFC6749 4.1.1)
     80     * @param apiUrl The API url
     81     * @param redirectUri The redirect URI for the client.
     82     */
     83    public OAuth20Parameters(String clientId, String clientSecret, String tokenUrl, String authorizeUrl, String apiUrl, String redirectUri) {
     84        Objects.requireNonNull(authorizeUrl, "authorizeUrl");
     85        Objects.requireNonNull(clientId, "clientId");
     86        Objects.requireNonNull(redirectUri, "redirectUri");
     87        Objects.requireNonNull(tokenUrl, "tokenUrl");
     88        Objects.requireNonNull(apiUrl, "apiUrl");
     89        // Alternatively, we could try using rfc8414 ( /.well-known/oauth-authorization-server ), but OSM (doorkeeper) doesn't support it.
     90        this.redirectUri = redirectUri;
     91        this.clientId = clientId;
     92        this.clientSecret = clientSecret;
     93        this.tokenUrl = tokenUrl;
     94        this.authorizeUrl = authorizeUrl;
     95        this.apiUrl = apiUrl;
     96    }
     97
     98    @Override
     99    public String getAccessTokenUrl() {
     100        return this.tokenUrl;
     101    }
     102
     103    @Override
     104    public String getAuthorizationUrl() {
     105        return this.authorizeUrl;
     106    }
     107
     108    @Override
     109    public OAuthVersion getOAuthVersion() {
     110        return OAuthVersion.OAuth20;
     111    }
     112
     113    @Override
     114    public String getClientId() {
     115        return this.clientId;
     116    }
     117
     118    @Override
     119    public String getClientSecret() {
     120        return this.clientSecret;
     121    }
     122
     123    @Override
     124    public String getRedirectUri() {
     125        return this.redirectUri;
     126    }
     127
     128    @Override
     129    public String getApiUrl() {
     130        return this.apiUrl;
     131    }
     132
     133    @Override
     134    public void rememberPreferences() {
     135        Config.getPref().put("oauth.access-token.parameters." + OAuthVersion.OAuth20 + "." + this.apiUrl,
     136                this.toPreferencesString());
     137    }
     138
     139    @Override
     140    public String toPreferencesString() {
     141        JsonObjectBuilder builder = Json.createObjectBuilder();
     142        builder.add(CLIENT_ID, this.clientId);
     143        builder.add(REDIRECT_URI, this.redirectUri);
     144        if (this.apiUrl != null) builder.add(API_URL, this.apiUrl);
     145        if (this.authorizeUrl != null) builder.add(AUTHORIZE_URL, this.authorizeUrl);
     146        if (this.clientSecret != null) builder.add(CLIENT_SECRET, this.clientSecret);
     147        if (this.tokenUrl != null) builder.add(TOKEN_URL, this.tokenUrl);
     148        return builder.build().toString();
     149    }
     150}
  • new file src/org/openstreetmap/josm/data/oauth/OAuth20Token.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/oauth/OAuth20Token.java b/src/org/openstreetmap/josm/data/oauth/OAuth20Token.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.oauth;
     3
     4import java.io.ByteArrayInputStream;
     5import java.io.IOException;
     6import java.io.InputStreamReader;
     7import java.io.Reader;
     8import java.net.URI;
     9import java.net.URL;
     10import java.nio.charset.StandardCharsets;
     11import java.time.Instant;
     12
     13import javax.json.Json;
     14import javax.json.JsonObject;
     15import javax.json.JsonObjectBuilder;
     16import javax.json.JsonReader;
     17import javax.json.JsonStructure;
     18import javax.json.JsonValue;
     19
     20import org.openstreetmap.josm.tools.HttpClient;
     21import org.openstreetmap.josm.tools.JosmRuntimeException;
     22
     23/**
     24 * Token holder for OAuth 2.0
     25 * @author Taylor Smock
     26 * @since xxx
     27 */
     28public final class OAuth20Token implements IOAuthToken {
     29    private static final String ACCESS_TOKEN = "access_token";
     30    private static final String CREATED_AT = "created_at";
     31    private static final String EXPIRES_IN = "expires_in";
     32    private static final String REFRESH_TOKEN = "refresh_token";
     33    private static final String SCOPE = "scope";
     34    private static final String TOKEN_TYPE = "token_type";
     35    private final String accessToken;
     36    private final String tokenType;
     37    private final int expiresIn;
     38    private final String refreshToken;
     39    private final String[] scopes;
     40    private final Instant createdAt;
     41    private final IOAuthParameters oauthParameters;
     42
     43    /**
     44     * Create a new OAuth token
     45     * @param oauthParameters The parameters for the OAuth token
     46     * @param json The stored JSON for the token
     47     * @throws OAuth20Exception If the JSON creates an invalid token
     48     */
     49    public OAuth20Token(IOAuthParameters oauthParameters, String json) throws OAuth20Exception {
     50        this(oauthParameters, new InputStreamReader(new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8));
     51    }
     52
     53    OAuth20Token(IOAuthParameters oauthParameters, Reader bufferedReader) throws OAuth20Exception {
     54        this.oauthParameters = oauthParameters;
     55        try (JsonReader reader = Json.createReader(bufferedReader)) {
     56            JsonStructure structure = reader.read();
     57            if (structure.getValueType() != JsonValue.ValueType.OBJECT
     58            || !structure.asJsonObject().containsKey(ACCESS_TOKEN)
     59            || !structure.asJsonObject().containsKey(TOKEN_TYPE)) {
     60                if (structure.getValueType() == JsonValue.ValueType.OBJECT
     61                && structure.asJsonObject().containsKey("error")) {
     62                    throw new OAuth20Exception(structure.asJsonObject());
     63                } else {
     64                    throw new OAuth20Exception("Either " + ACCESS_TOKEN + " or " + TOKEN_TYPE + " is not present: " + structure);
     65                }
     66            }
     67            JsonObject object = structure.asJsonObject();
     68            this.accessToken = object.getString(ACCESS_TOKEN);
     69            this.tokenType = object.getString(TOKEN_TYPE);
     70            this.expiresIn = object.getInt(EXPIRES_IN, Integer.MAX_VALUE);
     71            this.refreshToken = object.getString(REFRESH_TOKEN, null);
     72            this.scopes = object.getString(SCOPE, "").split(" ");
     73            if (object.containsKey(CREATED_AT)) {
     74                this.createdAt = Instant.ofEpochSecond(object.getJsonNumber(CREATED_AT).longValue());
     75            } else {
     76                this.createdAt = Instant.now();
     77            }
     78        }
     79    }
     80
     81    @Override
     82    public void sign(HttpClient client) throws OAuthException {
     83        if (!this.oauthParameters.getApiUrl().contains(client.getURL().getHost())) {
     84            String host = URI.create(this.oauthParameters.getAccessTokenUrl()).getHost();
     85            throw new IllegalArgumentException("Cannot sign URL with token for different host: Expected " + host
     86                + " but got " + client.getURL().getHost());
     87        }
     88        if (this.getBearerToken() != null) {
     89            client.setHeader("Authorization", "Bearer " + this.getBearerToken());
     90            return;
     91        }
     92        throw new OAuth20Exception("Unknown token type: " + this.tokenType);
     93    }
     94
     95    /**
     96     * Get the OAuth 2.0 bearer token
     97     * @return The bearer token. May return {@code null} if the token type is not a bearer type.
     98     */
     99    public String getBearerToken() {
     100        if ("bearer".equalsIgnoreCase(this.tokenType)) {
     101            return this.accessToken;
     102        }
     103        return null;
     104    }
     105
     106    @Override
     107    public String toPreferencesString() {
     108        final OAuth20Token tokenToSave;
     109        if (shouldRefresh()) {
     110            tokenToSave = refresh();
     111        } else {
     112            tokenToSave = this;
     113        }
     114        JsonObjectBuilder jsonObjectBuilder = Json.createObjectBuilder();
     115        jsonObjectBuilder.add(ACCESS_TOKEN, tokenToSave.accessToken);
     116        jsonObjectBuilder.add(TOKEN_TYPE, tokenToSave.tokenType);
     117        if (tokenToSave.createdAt != null) {
     118            jsonObjectBuilder.add(CREATED_AT, tokenToSave.createdAt.getEpochSecond());
     119        }
     120        if (tokenToSave.expiresIn != Integer.MAX_VALUE) {
     121            jsonObjectBuilder.add(EXPIRES_IN, tokenToSave.expiresIn);
     122        }
     123        if (tokenToSave.refreshToken != null) {
     124            jsonObjectBuilder.add(REFRESH_TOKEN, tokenToSave.refreshToken);
     125        }
     126        if (tokenToSave.scopes.length > 0) {
     127            jsonObjectBuilder.add(SCOPE, String.join(" ", tokenToSave.scopes));
     128        }
     129        return jsonObjectBuilder.build().toString();
     130    }
     131
     132    @Override
     133    public OAuthVersion getOAuthType() {
     134        return OAuthVersion.OAuth20;
     135    }
     136
     137    @Override
     138    public IOAuthParameters getParameters() {
     139        return this.oauthParameters;
     140    }
     141
     142    /**
     143     * Check if the token should be refreshed
     144     * @return {@code true} if the token should be refreshed
     145     */
     146    boolean shouldRefresh() {
     147        return this.refreshToken != null && this.expiresIn != Integer.MAX_VALUE
     148                // We should refresh the token when 10% of its lifespan has been spent.
     149                // We aren't an application that will be used every day by every user.
     150                && this.createdAt.getEpochSecond() + this.expiresIn < Instant.now().getEpochSecond() - this.expiresIn * 9L / 10;
     151    }
     152
     153    /**
     154     * Refresh the OAuth 2.0 token
     155     * @return The new token to use
     156     */
     157    OAuth20Token refresh() {
     158        // This bit isn't necessarily OAuth 2.1 compliant. Spec isn't finished yet, but
     159        // refresh tokens will either be sender constrained or some kind of rotation or both.
     160        // This refresh code handles rotation, mostly by creating a new OAuth20Token. :)
     161        // For sender constrained, it will likely allow self-signed certificates (RFC8705).
     162        // Note: OSM doesn't have age limits on their tokens, at time of writing.
     163        String refresh = "grant_type=refresh_token&refresh_token=" + this.refreshToken;
     164        if (this.scopes.length > 0) {
     165            refresh += "&scope=" + String.join(" ", this.scopes);
     166        }
     167        HttpClient client = null;
     168        try {
     169            client = HttpClient.create(new URL(this.oauthParameters.getAccessTokenUrl()), "POST");
     170            client.setRequestBody(refresh.getBytes(StandardCharsets.UTF_8));
     171            client.connect();
     172            HttpClient.Response response = client.getResponse();
     173            return new OAuth20Token(this.oauthParameters, response.getContentReader());
     174        } catch (IOException | OAuth20Exception e) {
     175            throw new JosmRuntimeException(e);
     176        } finally {
     177            if (client != null) {
     178                client.disconnect();
     179            }
     180        }
     181    }
     182}
  • src/org/openstreetmap/josm/data/oauth/OAuthAccessTokenHolder.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/oauth/OAuthAccessTokenHolder.java b/src/org/openstreetmap/josm/data/oauth/OAuthAccessTokenHolder.java
    a b  
    33
    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;
    1018import org.openstreetmap.josm.tools.Logging;
     
    3139    private String accessTokenKey;
    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.
    3646     *
     
    102112        return new OAuthToken(accessTokenKey, accessTokenSecret);
    103113    }
    104114
     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 xxx
     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;
     137    }
     138
    105139    /**
    106140     * Sets the access token hold by this holder.
    107141     *
     
    128162        }
    129163    }
    130164
     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 xxx
     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);
     182        }
     183    }
     184
    131185    /**
    132186     * Replies true if this holder contains an complete access token, consisting of an
    133187     * Access Token Key and an Access Token Secret.
     
    175229        try {
    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) {
    182252            Logging.error(e);
  • new file src/org/openstreetmap/josm/data/oauth/OAuthException.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/oauth/OAuthException.java b/src/org/openstreetmap/josm/data/oauth/OAuthException.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.oauth;
     3
     4/**
     5 * Base OAuth exception
     6 * @author Taylor Smock
     7 * @since xxx
     8 */
     9public abstract class OAuthException extends Exception {
     10    OAuthException(Exception cause) {
     11        super(cause);
     12    }
     13
     14    OAuthException(String message) {
     15        super(message);
     16    }
     17
     18    abstract OAuthVersion[] getOAuthVersions();
     19}
  • src/org/openstreetmap/josm/data/oauth/OAuthParameters.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/oauth/OAuthParameters.java b/src/org/openstreetmap/josm/data/oauth/OAuthParameters.java
    a b  
    11// License: GPL. For details, see LICENSE file.
    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
    1125import oauth.signpost.OAuthConsumer;
     
    1529 * This class manages an immutable set of OAuth parameters.
    1630 * @since 2747
    1731 */
    18 public class OAuthParameters {
     32public class OAuthParameters implements IOAuthParameters {
    1933
    2034    /**
    2135     * The default JOSM OAuth consumer key (created by user josmeditor).
     
    4660     * @since 5422
    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 xxx
     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;
    52165
    53         if (!Utils.isValidUrl(apiUrl)) {
    54             apiUrl = null;
    55         }
    56 
    57166        if (apiUrl != null && !Config.getUrls().getDefaultOsmApiUrl().equals(apiUrl)) {
    58167            consumerKey = ""; // a custom consumer key is required
    59168            consumerSecret = ""; // a custom consumer secret is requireds
     
    81190     * @return the parameters
    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 xxx
     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
    95232    /**
    96233     * Remembers the current values in the preferences.
    97234     */
     235    @Override
    98236    public void rememberPreferences() {
    99237        Config.getPref().put("oauth.settings.consumer-key", getConsumerKey());
    100238        Config.getPref().put("oauth.settings.consumer-secret", getConsumerSecret());
     
    182320     * Gets the access token URL.
    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.
    191350     * @return The authorise URL
    192351     */
    193352    public String getAuthoriseUrl() {
    194         return authoriseUrl;
     353        return this.getAuthorizationUrl();
    195354    }
    196355
    197356    /**
  • new file src/org/openstreetmap/josm/data/oauth/OAuthVersion.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/oauth/OAuthVersion.java b/src/org/openstreetmap/josm/data/oauth/OAuthVersion.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.oauth;
     3
     4/**
     5 * The OAuth versions ordered oldest to newest
     6 * @author Taylor Smock
     7 * @since xxx
     8 */
     9public enum OAuthVersion {
     10    /** <a href="https://oauth.net/core/1.0a/">OAuth 1.0a</a> */
     11    OAuth10a,
     12    /** <a href="https://datatracker.ietf.org/doc/html/rfc6749">OAuth 2.0</a> */
     13    OAuth20,
     14    /** <a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-06">OAuth 2.1 (draft)</a> */
     15    OAuth21
     16}
  • new file src/org/openstreetmap/josm/data/oauth/osm/OsmScopes.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/oauth/osm/OsmScopes.java b/src/org/openstreetmap/josm/data/oauth/osm/OsmScopes.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.oauth.osm;
     3
     4/**
     5 * The possible scopes for OSM
     6 * @author Taylor Smock
     7 * @since xxx
     8 */
     9public enum OsmScopes {
     10    /** Read user preferences */
     11    read_prefs,
     12    /** Modify user preferences */
     13    write_prefs,
     14    /** Write diary posts */
     15    write_diary,
     16    /** Modify the map */
     17    write_api,
     18    /** Read private GPS traces */
     19    read_gpx,
     20    /** Upload GPS traces */
     21    write_gpx,
     22    /** Modify notes */
     23    write_notes
     24}
  • new file src/org/openstreetmap/josm/data/oauth/osm/package-info.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/oauth/osm/package-info.java b/src/org/openstreetmap/josm/data/oauth/osm/package-info.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2
     3/**
     4 * Provides the classes for OAuth authentication to OSM.
     5 */
     6package org.openstreetmap.josm.data.oauth.osm;
  • src/org/openstreetmap/josm/data/UserIdentityManager.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/UserIdentityManager.java b/src/org/openstreetmap/josm/data/UserIdentityManager.java
    a b  
    44import static org.openstreetmap.josm.tools.I18n.tr;
    55
    66import java.text.MessageFormat;
     7import java.util.Arrays;
     8import java.util.Objects;
    79
    810import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
     11import org.openstreetmap.josm.data.oauth.OAuthVersion;
    912import org.openstreetmap.josm.data.osm.User;
    1013import org.openstreetmap.josm.data.osm.UserInfo;
    1114import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
     
    6871    public static synchronized UserIdentityManager getInstance() {
    6972        if (instance == null) {
    7073            instance = new UserIdentityManager();
    71             if (OsmApi.isUsingOAuth() && OAuthAccessTokenHolder.getInstance().containsAccessToken() &&
    72                     !NetworkManager.isOffline(OnlineResource.OSM_API)) {
     74            if (OsmApi.isUsingOAuthAndOAuthSetUp(OsmApi.getOsmApi()) && !NetworkManager.isOffline(OnlineResource.OSM_API)) {
    7375                try {
    7476                    instance.initFromOAuth();
    7577                } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
     
    305307            accessTokenSecretChanged = true;
    306308            break;
    307309        default: // Do nothing
     310            if (evt.getKey() != null && evt.getKey().equals("oauth.access-token.parameters.OAuth20." + OsmApi.getOsmApi().getHost())) {
     311                accessTokenKeyChanged = true;
     312                accessTokenSecretChanged = true;
     313            }
    308314        }
     315        // oauth.access-token.parameters.OAuth20.api.openstreetmap.org
    309316
    310317        if (accessTokenKeyChanged && accessTokenSecretChanged) {
    311318            accessTokenKeyChanged = false;
  • src/org/openstreetmap/josm/gui/oauth/AbstractAuthorizationUI.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/oauth/AbstractAuthorizationUI.java b/src/org/openstreetmap/josm/gui/oauth/AbstractAuthorizationUI.java
    a b  
    33
    44import java.util.Objects;
    55
     6import org.openstreetmap.josm.data.oauth.IOAuthParameters;
    67import org.openstreetmap.josm.data.oauth.OAuthParameters;
    78import org.openstreetmap.josm.data.oauth.OAuthToken;
     9import org.openstreetmap.josm.data.oauth.OAuthVersion;
    810import org.openstreetmap.josm.gui.widgets.VerticallyScrollablePanel;
    911import org.openstreetmap.josm.tools.CheckParameterUtil;
    1012
     
    2022    public static final String ACCESS_TOKEN_PROP = AbstractAuthorizationUI.class.getName() + ".accessToken";
    2123
    2224    private String apiUrl;
    23     private final AdvancedOAuthPropertiesPanel pnlAdvancedProperties = new AdvancedOAuthPropertiesPanel();
     25    private final AdvancedOAuthPropertiesPanel pnlAdvancedProperties = new AdvancedOAuthPropertiesPanel(OAuthVersion.OAuth10a);
    2426    private transient OAuthToken accessToken;
    2527
    2628    /**
     
    7981     *
    8082     * @return the current set of advanced OAuth parameters in this UI
    8183     */
    82     public OAuthParameters getOAuthParameters() {
     84    public IOAuthParameters getOAuthParameters() {
    8385        return pnlAdvancedProperties.getAdvancedParameters();
    8486    }
    8587
  • src/org/openstreetmap/josm/gui/oauth/AdvancedOAuthPropertiesPanel.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/oauth/AdvancedOAuthPropertiesPanel.java b/src/org/openstreetmap/josm/gui/oauth/AdvancedOAuthPropertiesPanel.java
    a b  
    1515import javax.swing.JLabel;
    1616import javax.swing.JOptionPane;
    1717
     18import org.openstreetmap.josm.data.oauth.IOAuthParameters;
     19import org.openstreetmap.josm.data.oauth.OAuth20Parameters;
    1820import org.openstreetmap.josm.data.oauth.OAuthParameters;
     21import org.openstreetmap.josm.data.oauth.OAuthVersion;
    1922import org.openstreetmap.josm.gui.HelpAwareOptionPane;
    2023import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
    2124import org.openstreetmap.josm.gui.help.HelpUtil;
     
    5154    private final JosmTextField tfAuthoriseURL = new JosmTextField();
    5255    private final JosmTextField tfOsmLoginURL = new JosmTextField();
    5356    private final JosmTextField tfOsmLogoutURL = new JosmTextField();
     57    private final OAuthVersion oauthVersion;
    5458    private transient UseDefaultItemListener ilUseDefault;
    5559    private String apiUrl;
    5660
    5761    /**
    5862     * Constructs a new {@code AdvancedOAuthPropertiesPanel}.
     63     * @param oauthVersion The OAuth version to make the panel for
    5964     */
    60     public AdvancedOAuthPropertiesPanel() {
     65    public AdvancedOAuthPropertiesPanel(OAuthVersion oauthVersion) {
     66        this.oauthVersion = oauthVersion;
    6167        build();
    6268    }
    6369
     
    7076        gc.fill = GridBagConstraints.HORIZONTAL;
    7177        gc.weightx = 1.0;
    7278        gc.insets = new Insets(0, 0, 3, 3);
    73         gc.gridwidth = 2;
     79        gc.gridwidth = 3;
    7480        add(cbUseDefaults, gc);
    7581
    7682        // -- consumer key
    7783        gc.gridy = 1;
    7884        gc.weightx = 0.0;
    7985        gc.gridwidth = 1;
    80         add(new JLabel(tr("Consumer Key:")), gc);
     86        if (this.oauthVersion == OAuthVersion.OAuth10a) {
     87            add(new JLabel(tr("Consumer Key:")), gc);
     88        } else {
     89            add(new JLabel(tr("Client ID:")), gc);
     90        }
    8191
    8292        gc.gridx = 1;
    8393        gc.weightx = 1.0;
     
    8595        SelectAllOnFocusGainedDecorator.decorate(tfConsumerKey);
    8696
    8797        // -- consumer secret
    88         gc.gridy = 2;
     98        gc.gridy++;
    8999        gc.gridx = 0;
    90100        gc.weightx = 0.0;
    91         add(new JLabel(tr("Consumer Secret:")), gc);
     101        if (this.oauthVersion == OAuthVersion.OAuth10a) {
     102            add(new JLabel(tr("Consumer Secret:")), gc);
     103        } else {
     104            add(new JLabel(tr("Client Secret:")), gc);
     105        }
    92106
    93107        gc.gridx = 1;
    94108        gc.weightx = 1.0;
     
    96110        SelectAllOnFocusGainedDecorator.decorate(tfConsumerSecret);
    97111
    98112        // -- request token URL
    99         gc.gridy = 3;
     113        gc.gridy++;
    100114        gc.gridx = 0;
    101115        gc.weightx = 0.0;
    102         add(new JLabel(tr("Request Token URL:")), gc);
     116        if (this.oauthVersion == OAuthVersion.OAuth10a) {
     117            add(new JLabel(tr("Request Token URL:")), gc);
     118        } else {
     119            add(new JLabel(tr("Redirect URL:")), gc);
     120        }
    103121
    104122        gc.gridx = 1;
    105123        gc.weightx = 1.0;
     
    107125        SelectAllOnFocusGainedDecorator.decorate(tfRequestTokenURL);
    108126
    109127        // -- access token URL
    110         gc.gridy = 4;
     128        gc.gridy++;
    111129        gc.gridx = 0;
    112130        gc.weightx = 0.0;
    113131        add(new JLabel(tr("Access Token URL:")), gc);
     
    118136        SelectAllOnFocusGainedDecorator.decorate(tfAccessTokenURL);
    119137
    120138        // -- authorise URL
    121         gc.gridy = 5;
     139        gc.gridy++;
    122140        gc.gridx = 0;
    123141        gc.weightx = 0.0;
    124142        add(new JLabel(tr("Authorize URL:")), gc);
     
    128146        add(tfAuthoriseURL, gc);
    129147        SelectAllOnFocusGainedDecorator.decorate(tfAuthoriseURL);
    130148
    131         // -- OSM login URL
    132         gc.gridy = 6;
    133         gc.gridx = 0;
    134         gc.weightx = 0.0;
    135         add(new JLabel(tr("OSM login URL:")), gc);
     149        if (this.oauthVersion == OAuthVersion.OAuth10a) {
     150            // -- OSM login URL
     151            gc.gridy++;
     152            gc.gridx = 0;
     153            gc.weightx = 0.0;
     154            add(new JLabel(tr("OSM login URL:")), gc);
    136155
    137         gc.gridx = 1;
    138         gc.weightx = 1.0;
    139         add(tfOsmLoginURL, gc);
    140         SelectAllOnFocusGainedDecorator.decorate(tfOsmLoginURL);
     156            gc.gridx = 1;
     157            gc.weightx = 1.0;
     158            add(tfOsmLoginURL, gc);
     159            SelectAllOnFocusGainedDecorator.decorate(tfOsmLoginURL);
    141160
    142         // -- OSM logout URL
    143         gc.gridy = 7;
    144         gc.gridx = 0;
    145         gc.weightx = 0.0;
    146         add(new JLabel(tr("OSM logout URL:")), gc);
     161            // -- OSM logout URL
     162            gc.gridy++;
     163            gc.gridx = 0;
     164            gc.weightx = 0.0;
     165            add(new JLabel(tr("OSM logout URL:")), gc);
    147166
    148         gc.gridx = 1;
    149         gc.weightx = 1.0;
    150         add(tfOsmLogoutURL, gc);
    151         SelectAllOnFocusGainedDecorator.decorate(tfOsmLogoutURL);
     167            gc.gridx = 1;
     168            gc.weightx = 1.0;
     169            add(tfOsmLogoutURL, gc);
     170            SelectAllOnFocusGainedDecorator.decorate(tfOsmLogoutURL);
     171        }
    152172
    153173        ilUseDefault = new UseDefaultItemListener();
    154174        cbUseDefaults.addItemListener(ilUseDefault);
     
    191211
    192212    protected void resetToDefaultSettings() {
    193213        cbUseDefaults.setSelected(true);
    194         OAuthParameters params = OAuthParameters.createDefault(apiUrl);
    195         tfConsumerKey.setText(params.getConsumerKey());
    196         tfConsumerSecret.setText(params.getConsumerSecret());
    197         tfRequestTokenURL.setText(params.getRequestTokenUrl());
    198         tfAccessTokenURL.setText(params.getAccessTokenUrl());
    199         tfAuthoriseURL.setText(params.getAuthoriseUrl());
    200         tfOsmLoginURL.setText(params.getOsmLoginUrl());
    201         tfOsmLogoutURL.setText(params.getOsmLogoutUrl());
     214        IOAuthParameters iParams = OAuthParameters.createDefault(apiUrl, this.oauthVersion);
     215        switch (this.oauthVersion) {
     216            case OAuth10a:
     217                OAuthParameters params = (OAuthParameters) iParams;
     218                tfConsumerKey.setText(params.getConsumerKey());
     219                tfConsumerSecret.setText(params.getConsumerSecret());
     220                tfRequestTokenURL.setText(params.getRequestTokenUrl());
     221                tfAccessTokenURL.setText(params.getAccessTokenUrl());
     222                tfAuthoriseURL.setText(params.getAuthoriseUrl());
     223                tfOsmLoginURL.setText(params.getOsmLoginUrl());
     224                tfOsmLogoutURL.setText(params.getOsmLogoutUrl());
     225                break;
     226            case OAuth20:
     227            case OAuth21:
     228                OAuth20Parameters params20 = (OAuth20Parameters) iParams;
     229                tfConsumerKey.setText(params20.getClientId());
     230                tfConsumerSecret.setText(params20.getClientSecret());
     231                tfAccessTokenURL.setText(params20.getAccessTokenUrl());
     232                tfAuthoriseURL.setText(params20.getAuthorizationUrl());
     233                tfRequestTokenURL.setText(params20.getRedirectUri());
     234        }
    202235
    203236        setChildComponentsEnabled(false);
    204237    }
     
    216249     *
    217250     * @return the OAuth parameters
    218251     */
    219     public OAuthParameters getAdvancedParameters() {
     252    public IOAuthParameters getAdvancedParameters() {
    220253        if (cbUseDefaults.isSelected())
    221             return OAuthParameters.createDefault(apiUrl);
    222         return new OAuthParameters(
    223             tfConsumerKey.getText(),
    224             tfConsumerSecret.getText(),
    225             tfRequestTokenURL.getText(),
    226             tfAccessTokenURL.getText(),
    227             tfAuthoriseURL.getText(),
    228             tfOsmLoginURL.getText(),
    229             tfOsmLogoutURL.getText());
     254            return OAuthParameters.createDefault(apiUrl, this.oauthVersion);
     255        if (this.oauthVersion == OAuthVersion.OAuth10a) {
     256            return new OAuthParameters(
     257                    tfConsumerKey.getText(),
     258                    tfConsumerSecret.getText(),
     259                    tfRequestTokenURL.getText(),
     260                    tfAccessTokenURL.getText(),
     261                    tfAuthoriseURL.getText(),
     262                    tfOsmLoginURL.getText(),
     263                    tfOsmLogoutURL.getText());
     264        }
     265        return new OAuth20Parameters(
     266                tfConsumerKey.getText(),
     267                tfConsumerSecret.getText(),
     268                tfAuthoriseURL.getText(),
     269                tfAccessTokenURL.getText(),
     270                tfRequestTokenURL.getText()
     271                );
    230272    }
    231273
    232274    /**
     
    235277     * @param parameters the advanced parameters. Must not be null.
    236278     * @throws IllegalArgumentException if parameters is null.
    237279     */
    238     public void setAdvancedParameters(OAuthParameters parameters) {
     280    public void setAdvancedParameters(IOAuthParameters parameters) {
    239281        CheckParameterUtil.ensureParameterNotNull(parameters, "parameters");
    240         if (parameters.equals(OAuthParameters.createDefault(apiUrl))) {
     282        if (parameters.equals(OAuthParameters.createDefault(apiUrl, parameters.getOAuthVersion()))) {
    241283            cbUseDefaults.setSelected(true);
    242284            setChildComponentsEnabled(false);
    243285        } else {
    244286            cbUseDefaults.setSelected(false);
    245287            setChildComponentsEnabled(true);
    246             tfConsumerKey.setText(parameters.getConsumerKey() == null ? "" : parameters.getConsumerKey());
    247             tfConsumerSecret.setText(parameters.getConsumerSecret() == null ? "" : parameters.getConsumerSecret());
    248             tfRequestTokenURL.setText(parameters.getRequestTokenUrl() == null ? "" : parameters.getRequestTokenUrl());
    249             tfAccessTokenURL.setText(parameters.getAccessTokenUrl() == null ? "" : parameters.getAccessTokenUrl());
    250             tfAuthoriseURL.setText(parameters.getAuthoriseUrl() == null ? "" : parameters.getAuthoriseUrl());
    251             tfOsmLoginURL.setText(parameters.getOsmLoginUrl() == null ? "" : parameters.getOsmLoginUrl());
    252             tfOsmLogoutURL.setText(parameters.getOsmLogoutUrl() == null ? "" : parameters.getOsmLogoutUrl());
     288            if (parameters instanceof OAuthParameters) {
     289                OAuthParameters parameters10 = (OAuthParameters) parameters;
     290                tfConsumerKey.setText(parameters10.getConsumerKey() == null ? "" : parameters10.getConsumerKey());
     291                tfConsumerSecret.setText(parameters10.getConsumerSecret() == null ? "" : parameters10.getConsumerSecret());
     292                tfRequestTokenURL.setText(parameters10.getRequestTokenUrl() == null ? "" : parameters10.getRequestTokenUrl());
     293                tfAccessTokenURL.setText(parameters10.getAccessTokenUrl() == null ? "" : parameters10.getAccessTokenUrl());
     294                tfAuthoriseURL.setText(parameters10.getAuthoriseUrl() == null ? "" : parameters10.getAuthoriseUrl());
     295                tfOsmLoginURL.setText(parameters10.getOsmLoginUrl() == null ? "" : parameters10.getOsmLoginUrl());
     296                tfOsmLogoutURL.setText(parameters10.getOsmLogoutUrl() == null ? "" : parameters10.getOsmLogoutUrl());
     297            } else if (parameters instanceof OAuth20Parameters) {
     298                OAuth20Parameters parameters20 = (OAuth20Parameters) parameters;
     299                tfConsumerKey.setText(parameters20.getClientId());
     300                tfConsumerSecret.setText(parameters20.getClientSecret());
     301                tfAccessTokenURL.setText(parameters20.getAccessTokenUrl());
     302                tfAuthoriseURL.setText(parameters20.getAuthorizationUrl());
     303                tfRequestTokenURL.setText(parameters20.getRedirectUri());
     304            }
    253305        }
    254306    }
    255307
  • src/org/openstreetmap/josm/gui/oauth/FullyAutomaticAuthorizationUI.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/oauth/FullyAutomaticAuthorizationUI.java b/src/org/openstreetmap/josm/gui/oauth/FullyAutomaticAuthorizationUI.java
    a b  
    2828import javax.swing.text.JTextComponent;
    2929import javax.swing.text.html.HTMLEditorKit;
    3030
     31import org.openstreetmap.josm.data.oauth.OAuthParameters;
    3132import org.openstreetmap.josm.data.oauth.OAuthToken;
    3233import org.openstreetmap.josm.gui.HelpAwareOptionPane;
    3334import org.openstreetmap.josm.gui.PleaseWaitRunnable;
     
    384385            executor.execute(new TestAccessTokenTask(
    385386                    FullyAutomaticAuthorizationUI.this,
    386387                    getApiUrl(),
    387                     getAdvancedPropertiesPanel().getAdvancedParameters(),
     388                    (OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters(),
    388389                    getAccessToken()
    389390            ));
    390391        }
     
    437438                            + "a valid login URL from the OAuth Authorize Endpoint URL ''{0}''.<br><br>"
    438439                            + "Please check your advanced setting and try again."
    439440                            + "</html>",
    440                             getAdvancedPropertiesPanel().getAdvancedParameters().getAuthoriseUrl()),
     441                            ((OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters()).getAuthoriseUrl()),
    441442                    tr("OAuth authorization failed"),
    442443                    JOptionPane.ERROR_MESSAGE,
    443444                    HelpUtil.ht("/Dialog/OAuthAuthorisationWizard#FullyAutomaticProcessFailed")
     
    445446        }
    446447
    447448        protected void alertLoginFailed() {
    448             final String loginUrl = getAdvancedPropertiesPanel().getAdvancedParameters().getOsmLoginUrl();
     449            final String loginUrl = ((OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters()).getOsmLoginUrl();
    449450            HelpAwareOptionPane.showOptionDialog(
    450451                    FullyAutomaticAuthorizationUI.this,
    451452                    tr("<html>"
     
    479480            try {
    480481                getProgressMonitor().setTicksCount(3);
    481482                OsmOAuthAuthorizationClient authClient = new OsmOAuthAuthorizationClient(
    482                         getAdvancedPropertiesPanel().getAdvancedParameters()
     483                        (OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters()
    483484                );
    484485                OAuthToken requestToken = authClient.getRequestToken(
    485486                        getProgressMonitor().createSubTaskMonitor(1, false)
  • src/org/openstreetmap/josm/gui/oauth/ManualAuthorizationUI.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/oauth/ManualAuthorizationUI.java b/src/org/openstreetmap/josm/gui/oauth/ManualAuthorizationUI.java
    a b  
    2525import javax.swing.text.JTextComponent;
    2626
    2727import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
     28import org.openstreetmap.josm.data.oauth.OAuthParameters;
    2829import org.openstreetmap.josm.data.oauth.OAuthToken;
    2930import org.openstreetmap.josm.gui.widgets.DefaultTextComponentValidator;
    3031import org.openstreetmap.josm.gui.widgets.HtmlPanel;
     
    232233            TestAccessTokenTask task = new TestAccessTokenTask(
    233234                    ManualAuthorizationUI.this,
    234235                    getApiUrl(),
    235                     getAdvancedPropertiesPanel().getAdvancedParameters(),
     236                    (OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters(),
    236237                    getAccessToken()
    237238            );
    238239            executor.execute(task);
  • src/org/openstreetmap/josm/gui/oauth/OAuthAuthorizationWizard.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/oauth/OAuthAuthorizationWizard.java b/src/org/openstreetmap/josm/gui/oauth/OAuthAuthorizationWizard.java
    a b  
    255255     * @return the current OAuth parameters.
    256256     */
    257257    public OAuthParameters getOAuthParameters() {
    258         return getCurrentAuthorisationUI().getOAuthParameters();
     258        return (OAuthParameters) getCurrentAuthorisationUI().getOAuthParameters();
    259259    }
    260260
    261261    /**
  • src/org/openstreetmap/josm/gui/oauth/SemiAutomaticAuthorizationUI.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/oauth/SemiAutomaticAuthorizationUI.java b/src/org/openstreetmap/josm/gui/oauth/SemiAutomaticAuthorizationUI.java
    a b  
    2222import javax.swing.JPanel;
    2323
    2424import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
     25import org.openstreetmap.josm.data.oauth.OAuthParameters;
    2526import org.openstreetmap.josm.data.oauth.OAuthToken;
    2627import org.openstreetmap.josm.gui.util.GuiHelper;
    2728import org.openstreetmap.josm.gui.widgets.HtmlPanel;
     
    7879
    7980    protected void transitionToRetrieveAccessToken() {
    8081        OsmOAuthAuthorizationClient client = new OsmOAuthAuthorizationClient(
    81                 getAdvancedPropertiesPanel().getAdvancedParameters()
     82                (OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters()
    8283        );
    8384        String authoriseUrl = client.getAuthoriseUrl(requestToken);
    8485        OpenBrowser.displayUrl(authoriseUrl);
     
    183184                    + "Please click on <strong>{0}</strong> to retrieve an OAuth Request Token from "
    184185                    + "''{1}''.</html>",
    185186                    tr("Retrieve Request Token"),
    186                     getAdvancedPropertiesPanel().getAdvancedParameters().getRequestTokenUrl()
     187                    ((OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters()).getRequestTokenUrl()
    187188            ));
    188189            pnl.add(h, gc);
    189190
     
    390391        public void actionPerformed(ActionEvent evt) {
    391392            final RetrieveRequestTokenTask task = new RetrieveRequestTokenTask(
    392393                    SemiAutomaticAuthorizationUI.this,
    393                     getAdvancedPropertiesPanel().getAdvancedParameters()
     394                    (OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters()
    394395            );
    395396            executor.execute(task);
    396397            Runnable r = () -> {
     
    418419        public void actionPerformed(ActionEvent evt) {
    419420            final RetrieveAccessTokenTask task = new RetrieveAccessTokenTask(
    420421                    SemiAutomaticAuthorizationUI.this,
    421                     getAdvancedPropertiesPanel().getAdvancedParameters(),
     422                    (OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters(),
    422423                    requestToken
    423424            );
    424425            executor.execute(task);
     
    450451            TestAccessTokenTask task = new TestAccessTokenTask(
    451452                    SemiAutomaticAuthorizationUI.this,
    452453                    getApiUrl(),
    453                     getAdvancedPropertiesPanel().getAdvancedParameters(),
     454                    (OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters(),
    454455                    getAccessToken()
    455456            );
    456457            executor.execute(task);
  • src/org/openstreetmap/josm/gui/preferences/advanced/AdvancedPreference.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/preferences/advanced/AdvancedPreference.java b/src/org/openstreetmap/josm/gui/preferences/advanced/AdvancedPreference.java
    a b  
    235235        Map<String, Setting<?>> loaded;
    236236        Map<String, Setting<?>> orig = Preferences.main().getAllSettings();
    237237        Map<String, Setting<?>> defaults = tmpPrefs.getAllDefaults();
    238         orig.remove("osm-server.password");
    239         defaults.remove("osm-server.password");
     238        Preferences.main().getSensitive().forEach(orig::remove);
     239        tmpPrefs.getSensitive().forEach(defaults::remove);
    240240        if (tmpPrefs != Preferences.main()) {
    241241            loaded = tmpPrefs.getAllSettings();
    242242            // plugins preference keys may be changed directly later, after plugins are downloaded
  • src/org/openstreetmap/josm/gui/preferences/server/AuthenticationPreferencesPanel.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/preferences/server/AuthenticationPreferencesPanel.java b/src/org/openstreetmap/josm/gui/preferences/server/AuthenticationPreferencesPanel.java
    a b  
    44import static org.openstreetmap.josm.tools.I18n.tr;
    55
    66import java.awt.BorderLayout;
     7import java.awt.FlowLayout;
    78import java.awt.GridBagConstraints;
    89import java.awt.GridBagLayout;
    910import java.awt.Insets;
     
    1718import javax.swing.JRadioButton;
    1819
    1920import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
     21import org.openstreetmap.josm.data.oauth.OAuthVersion;
    2022import org.openstreetmap.josm.gui.help.HelpUtil;
    2123import org.openstreetmap.josm.gui.widgets.VerticallyScrollablePanel;
    2224import org.openstreetmap.josm.io.OsmApi;
    2325import org.openstreetmap.josm.io.auth.CredentialsManager;
    2426import org.openstreetmap.josm.spi.preferences.Config;
     27import org.openstreetmap.josm.tools.GBC;
    2528import org.openstreetmap.josm.tools.Logging;
    2629
    2730/**
     
    3235
    3336    /** indicates whether we use basic authentication */
    3437    private final JRadioButton rbBasicAuthentication = new JRadioButton();
    35     /** indicates whether we use OAuth as authentication scheme */
     38    /** indicates whether we use OAuth 1.0a as authentication scheme */
    3639    private final JRadioButton rbOAuth = new JRadioButton();
     40    /** indicates whether we use OAuth 2.0 as authentication scheme */
     41    private final JRadioButton rbOAuth20 = new JRadioButton();
    3742    /** the panel which contains the authentication parameters for the respective authentication scheme */
    3843    private final JPanel pnlAuthenticationParameters = new JPanel(new BorderLayout());
    3944    /** the panel for the basic authentication parameters */
    4045    private BasicAuthenticationPreferencesPanel pnlBasicAuthPreferences;
    41     /** the panel for the OAuth authentication parameters */
     46    /** the panel for the OAuth 1.0a authentication parameters */
    4247    private OAuthAuthenticationPreferencesPanel pnlOAuthPreferences;
     48    /** the panel for the OAuth 2.0 authentication parameters */
     49    private OAuthAuthenticationPreferencesPanel pnlOAuth20Preferences;
    4350
    4451    /**
    4552     * Constructs a new {@code AuthenticationPreferencesPanel}.
     
    5562     */
    5663    protected final void build() {
    5764        setLayout(new GridBagLayout());
    58         GridBagConstraints gc = new GridBagConstraints();
    5965
    6066        AuthenticationMethodChangeListener authChangeListener = new AuthenticationMethodChangeListener();
    6167
     68        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEADING));
    6269        // -- radio button for basic authentication
    63         gc.anchor = GridBagConstraints.NORTHWEST;
    64         gc.fill = GridBagConstraints.HORIZONTAL;
    65         gc.gridx = 1;
    66         gc.weightx = 1.0;
    67         gc.insets = new Insets(0, 0, 0, 3);
    68         add(rbBasicAuthentication, gc);
     70        buttonPanel.add(rbBasicAuthentication);
    6971        rbBasicAuthentication.setText(tr("Use Basic Authentication"));
    7072        rbBasicAuthentication.setToolTipText(tr("Select to use HTTP basic authentication with your OSM username and password"));
    7173        rbBasicAuthentication.addItemListener(authChangeListener);
    7274
    73         //-- radio button for OAuth
    74         gc.gridx = 0;
    75         gc.weightx = 0.0;
    76         add(rbOAuth, gc);
    77         rbOAuth.setText(tr("Use OAuth"));
    78         rbOAuth.setToolTipText(tr("Select to use OAuth as authentication mechanism"));
     75        //-- radio button for OAuth 1.0a
     76        buttonPanel.add(rbOAuth);
     77        rbOAuth.setText(tr("Use OAuth {0}", "1.0a"));
     78        rbOAuth.setToolTipText(tr("Select to use OAuth {0} as authentication mechanism", "1.0a"));
    7979        rbOAuth.addItemListener(authChangeListener);
    8080
     81        //-- radio button for OAuth 2.0
     82        buttonPanel.add(rbOAuth20);
     83        rbOAuth20.setText(tr("Use OAuth {0}", "2.0"));
     84        rbOAuth20.setToolTipText(tr("Select to use OAuth {0} as authentication mechanism", "2.0"));
     85        rbOAuth20.addItemListener(authChangeListener);
     86
     87        add(buttonPanel, GBC.eol());
    8188        //-- radio button for OAuth
    8289        ButtonGroup bg = new ButtonGroup();
    8390        bg.add(rbBasicAuthentication);
    8491        bg.add(rbOAuth);
     92        bg.add(rbOAuth20);
    8593
    8694        //-- add the panel which will hold the authentication parameters
     95        GridBagConstraints gc = new GridBagConstraints();
     96        gc.anchor = GridBagConstraints.NORTHWEST;
     97        gc.insets = new Insets(0, 0, 0, 3);
    8798        gc.gridx = 0;
    8899        gc.gridy = 1;
    89100        gc.gridwidth = 2;
     
    94105
    95106        //-- the two panels for authentication parameters
    96107        pnlBasicAuthPreferences = new BasicAuthenticationPreferencesPanel();
    97         pnlOAuthPreferences = new OAuthAuthenticationPreferencesPanel();
     108        pnlOAuthPreferences = new OAuthAuthenticationPreferencesPanel(OAuthVersion.OAuth10a);
     109        pnlOAuth20Preferences = new OAuthAuthenticationPreferencesPanel(OAuthVersion.OAuth20);
    98110
    99111        rbBasicAuthentication.setSelected(true);
    100112        pnlAuthenticationParameters.add(pnlBasicAuthPreferences, BorderLayout.CENTER);
     
    109121            rbBasicAuthentication.setSelected(true);
    110122        } else if ("oauth".equals(authMethod)) {
    111123            rbOAuth.setSelected(true);
     124        } else if ("oauth20".equals(authMethod)) {
     125            rbOAuth20.setSelected(true);
    112126        } else {
    113127            Logging.warn(tr("Unsupported value in preference ''{0}'', got ''{1}''. Using authentication method ''Basic Authentication''.",
    114128                    "osm-server.auth-method", authMethod));
     
    116130        }
    117131        pnlBasicAuthPreferences.initFromPreferences();
    118132        pnlOAuthPreferences.initFromPreferences();
     133        pnlOAuth20Preferences.initFromPreferences();
    119134    }
    120135
    121136    /**
     
    126141        String authMethod;
    127142        if (rbBasicAuthentication.isSelected()) {
    128143            authMethod = "basic";
    129         } else {
     144        } else if (rbOAuth.isSelected()) {
    130145            authMethod = "oauth";
     146        } else if (rbOAuth20.isSelected()) {
     147            authMethod = "oauth20";
     148        } else {
     149            throw new IllegalStateException("One of OAuth 2.0, OAuth 1.0a, or Basic authentication must be checked");
    131150        }
    132151        Config.getPref().put("osm-server.auth-method", authMethod);
    133152        if ("basic".equals(authMethod)) {
     
    140159            pnlBasicAuthPreferences.clearPassword();
    141160            pnlBasicAuthPreferences.saveToPreferences();
    142161            pnlOAuthPreferences.saveToPreferences();
     162        } else { // oauth20
     163            // clear the password in the preferences
     164            pnlBasicAuthPreferences.clearPassword();
     165            pnlBasicAuthPreferences.saveToPreferences();
     166            pnlOAuth20Preferences.saveToPreferences();
    143167        }
    144168    }
    145169
     
    149173    class AuthenticationMethodChangeListener implements ItemListener {
    150174        @Override
    151175        public void itemStateChanged(ItemEvent e) {
    152             if (rbBasicAuthentication.isSelected()) {
    153                 pnlAuthenticationParameters.removeAll();
     176            pnlAuthenticationParameters.removeAll();
     177            if (rbBasicAuthentication.isSelected()) {
    154178                pnlAuthenticationParameters.add(pnlBasicAuthPreferences, BorderLayout.CENTER);
    155179                pnlBasicAuthPreferences.revalidate();
    156             } else {
    157                 pnlAuthenticationParameters.removeAll();
     180            } else if (rbOAuth.isSelected()) {
    158181                pnlAuthenticationParameters.add(pnlOAuthPreferences, BorderLayout.CENTER);
    159182                pnlOAuthPreferences.revalidate();
     183            } else if (rbOAuth20.isSelected()) {
     184                pnlAuthenticationParameters.add(pnlOAuth20Preferences, BorderLayout.CENTER);
     185                pnlOAuth20Preferences.revalidate();
    160186            }
    161187            repaint();
    162188        }
  • src/org/openstreetmap/josm/gui/preferences/server/OAuthAuthenticationPreferencesPanel.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/preferences/server/OAuthAuthenticationPreferencesPanel.java b/src/org/openstreetmap/josm/gui/preferences/server/OAuthAuthenticationPreferencesPanel.java
    a b  
    2323import javax.swing.JPanel;
    2424
    2525import org.openstreetmap.josm.actions.ExpertToggleAction;
     26import org.openstreetmap.josm.data.oauth.IOAuthToken;
     27import org.openstreetmap.josm.data.oauth.OAuth20Authorization;
     28import org.openstreetmap.josm.data.oauth.OAuth20Token;
    2629import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
    2730import org.openstreetmap.josm.data.oauth.OAuthParameters;
    2831import org.openstreetmap.josm.data.oauth.OAuthToken;
     32import org.openstreetmap.josm.data.oauth.OAuthVersion;
     33import org.openstreetmap.josm.data.oauth.osm.OsmScopes;
    2934import org.openstreetmap.josm.gui.MainApplication;
    3035import org.openstreetmap.josm.gui.oauth.AdvancedOAuthPropertiesPanel;
    3136import org.openstreetmap.josm.gui.oauth.AuthorizationProcedure;
    3237import org.openstreetmap.josm.gui.oauth.OAuthAuthorizationWizard;
    3338import org.openstreetmap.josm.gui.oauth.TestAccessTokenTask;
     39import org.openstreetmap.josm.gui.util.GuiHelper;
    3440import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
    3541import org.openstreetmap.josm.gui.widgets.JosmTextField;
    3642import org.openstreetmap.josm.io.OsmApi;
    3743import org.openstreetmap.josm.io.auth.CredentialsManager;
     44import org.openstreetmap.josm.io.remotecontrol.RemoteControl;
    3845import org.openstreetmap.josm.tools.GBC;
    3946import org.openstreetmap.josm.tools.ImageProvider;
    4047import org.openstreetmap.josm.tools.Logging;
    4148import org.openstreetmap.josm.tools.UserCancelException;
    4249
    4350/**
    44  * The preferences panel for the OAuth preferences. This just a summary panel
     51 * The preferences panel for the OAuth 1.0a preferences. This just a summary panel
    4552 * showing the current Access Token Key and Access Token Secret, if the
    4653 * user already has an Access Token.
    47  *
     54 * <br>
    4855 * For initial authorisation see {@link OAuthAuthorizationWizard}.
    4956 * @since 2745
    5057 */
     
    5360    private final JCheckBox cbShowAdvancedParameters = new JCheckBox(tr("Display Advanced OAuth Parameters"));
    5461    private final JCheckBox cbSaveToPreferences = new JCheckBox(tr("Save to preferences"));
    5562    private final JPanel pnlAuthorisationMessage = new JPanel(new BorderLayout());
    56     private final NotYetAuthorisedPanel pnlNotYetAuthorised = new NotYetAuthorisedPanel();
    57     private final AdvancedOAuthPropertiesPanel pnlAdvancedProperties = new AdvancedOAuthPropertiesPanel();
    58     private final AlreadyAuthorisedPanel pnlAlreadyAuthorised = new AlreadyAuthorisedPanel();
     63    private final NotYetAuthorisedPanel pnlNotYetAuthorised;
     64    private final AdvancedOAuthPropertiesPanel pnlAdvancedProperties;
     65    private final AlreadyAuthorisedPanel pnlAlreadyAuthorised;
     66    private final OAuthVersion oAuthVersion;
    5967    private String apiUrl;
    6068
    6169    /**
    62      * Create the panel
     70     * Create the panel. Uses {@link OAuthVersion#OAuth10a}.
    6371     */
    6472    public OAuthAuthenticationPreferencesPanel() {
     73        this(OAuthVersion.OAuth10a);
     74    }
     75
     76    /**
     77     * Create the panel.
     78     * @param oAuthVersion The OAuth version to use
     79     */
     80    public OAuthAuthenticationPreferencesPanel(OAuthVersion oAuthVersion) {
     81        this.oAuthVersion = oAuthVersion;
     82        // These must come after we set the oauth version
     83        this.pnlNotYetAuthorised = new NotYetAuthorisedPanel();
     84        this.pnlAdvancedProperties = new AdvancedOAuthPropertiesPanel(this.oAuthVersion);
     85        this.pnlAlreadyAuthorised = new AlreadyAuthorisedPanel();
    6586        build();
    66         refreshView();
    6787    }
    6888
    6989    /**
     
    117137
    118138    protected void refreshView() {
    119139        pnlAuthorisationMessage.removeAll();
    120         if (OAuthAccessTokenHolder.getInstance().containsAccessToken()) {
     140        if ((this.oAuthVersion == OAuthVersion.OAuth10a &&
     141                OAuthAccessTokenHolder.getInstance().containsAccessToken())
     142        || OAuthAccessTokenHolder.getInstance().getAccessToken(this.apiUrl, this.oAuthVersion) != null) {
    121143            pnlAuthorisationMessage.add(pnlAlreadyAuthorised, BorderLayout.CENTER);
    122144            pnlAlreadyAuthorised.refreshView();
    123145            pnlAlreadyAuthorised.revalidate();
     
    180202            lbl.setFont(lbl.getFont().deriveFont(Font.PLAIN));
    181203
    182204            // Action for authorising now
    183             add(new JButton(new AuthoriseNowAction(AuthorizationProcedure.FULLY_AUTOMATIC)), GBC.eol());
     205            if (oAuthVersion == OAuthVersion.OAuth10a) {
     206                add(new JButton(new AuthoriseNowAction(AuthorizationProcedure.FULLY_AUTOMATIC)), GBC.eol());
     207            }
    184208            add(new JButton(new AuthoriseNowAction(AuthorizationProcedure.SEMI_AUTOMATIC)), GBC.eol());
    185             JButton authManually = new JButton(new AuthoriseNowAction(AuthorizationProcedure.MANUALLY));
    186             add(authManually, GBC.eol());
    187             ExpertToggleAction.addVisibilitySwitcher(authManually);
     209            if (oAuthVersion == OAuthVersion.OAuth10a) {
     210                JButton authManually = new JButton(new AuthoriseNowAction(AuthorizationProcedure.MANUALLY));
     211                add(authManually, GBC.eol());
     212                ExpertToggleAction.addVisibilitySwitcher(authManually);
     213            }
    188214
    189215            // filler - grab remaining space
    190216            add(new JPanel(), GBC.std().fill(GBC.BOTH));
     
    253279
    254280            // -- action buttons
    255281            JPanel btns = new JPanel(new FlowLayout(FlowLayout.LEFT));
    256             btns.add(new JButton(new RenewAuthorisationAction(AuthorizationProcedure.FULLY_AUTOMATIC)));
    257             btns.add(new JButton(new TestAuthorisationAction()));
     282            if (oAuthVersion == OAuthVersion.OAuth10a) {
     283                // these want the OAuth 1.0 token information
     284                btns.add(new JButton(new RenewAuthorisationAction(AuthorizationProcedure.FULLY_AUTOMATIC)));
     285                btns.add(new JButton(new TestAuthorisationAction()));
     286            }
     287            btns.add(new JButton(new RemoveAuthorisationAction()));
    258288            gc.gridy = 4;
    259289            gc.gridx = 0;
    260290            gc.gridwidth = 2;
     
    277307        }
    278308
    279309        protected final void refreshView() {
    280             String v = OAuthAccessTokenHolder.getInstance().getAccessTokenKey();
    281             tfAccessTokenKey.setText(v == null ? "" : v);
    282             v = OAuthAccessTokenHolder.getInstance().getAccessTokenSecret();
    283             tfAccessTokenSecret.setText(v == null ? "" : v);
     310            switch (oAuthVersion) {
     311                case OAuth10a:
     312                    String v = OAuthAccessTokenHolder.getInstance().getAccessTokenKey();
     313                    tfAccessTokenKey.setText(v == null ? "" : v);
     314                    v = OAuthAccessTokenHolder.getInstance().getAccessTokenSecret();
     315                    tfAccessTokenSecret.setText(v == null ? "" : v);
     316                    tfAccessTokenSecret.setVisible(true);
     317                    break;
     318                case OAuth20:
     319                case OAuth21:
     320                    String token = "";
     321                    if (apiUrl != null) {
     322                        OAuth20Token bearerToken = (OAuth20Token) OAuthAccessTokenHolder.getInstance().getAccessToken(apiUrl, oAuthVersion);
     323                        token = bearerToken == null ? "" : bearerToken.getBearerToken();
     324                    }
     325                    tfAccessTokenKey.setText(token == null ? "" : token);
     326                    tfAccessTokenSecret.setVisible(false);
     327            }
    284328            cbSaveToPreferences.setSelected(OAuthAccessTokenHolder.getInstance().isSaveToPreferences());
    285329        }
    286330    }
     
    295339            this.procedure = procedure;
    296340            putValue(NAME, tr("{0} ({1})", tr("Authorize now"), procedure.getText()));
    297341            putValue(SHORT_DESCRIPTION, procedure.getDescription());
    298             if (procedure == AuthorizationProcedure.FULLY_AUTOMATIC) {
     342            if (procedure == AuthorizationProcedure.FULLY_AUTOMATIC
     343            || OAuthAuthenticationPreferencesPanel.this.oAuthVersion != OAuthVersion.OAuth10a) {
    299344                new ImageProvider("oauth", "oauth-small").getResource().attachImageIcon(this);
    300345            }
    301346        }
    302347
    303348        @Override
    304349        public void actionPerformed(ActionEvent arg0) {
    305             OAuthAuthorizationWizard wizard = new OAuthAuthorizationWizard(
    306                     OAuthAuthenticationPreferencesPanel.this,
    307                     procedure,
    308                     apiUrl,
    309                     MainApplication.worker);
    310             try {
    311                 wizard.showDialog();
    312             } catch (UserCancelException ignore) {
    313                 Logging.trace(ignore);
    314                 return;
    315             }
    316             pnlAdvancedProperties.setAdvancedParameters(wizard.getOAuthParameters());
     350            if (OAuthAuthenticationPreferencesPanel.this.oAuthVersion == OAuthVersion.OAuth10a) {
     351                OAuthAuthorizationWizard wizard = new OAuthAuthorizationWizard(
     352                        OAuthAuthenticationPreferencesPanel.this,
     353                        procedure,
     354                        apiUrl,
     355                        MainApplication.worker);
     356                try {
     357                    wizard.showDialog();
     358                } catch (UserCancelException ignore) {
     359                    Logging.trace(ignore);
     360                    return;
     361                }
     362                pnlAdvancedProperties.setAdvancedParameters(wizard.getOAuthParameters());
     363                refreshView();
     364            } else {
     365                final boolean remoteControlIsRunning = Boolean.TRUE.equals(RemoteControl.PROP_REMOTECONTROL_ENABLED.get());
     366                // TODO: Ask user if they want to start remote control?
     367                if (!remoteControlIsRunning) {
     368                    RemoteControl.start();
     369                }
     370                new OAuth20Authorization().authorize(OAuthParameters.createDefault(OsmApi.getOsmApi().getServerUrl(), oAuthVersion), token -> {
     371                    if (!remoteControlIsRunning) {
     372                        RemoteControl.stop();
     373                    }
     374                    // Clean up old token/password
     375                    OAuthAccessTokenHolder.getInstance().setAccessToken(null);
     376                    OAuthAccessTokenHolder.getInstance().setAccessToken(OsmApi.getOsmApi().getServerUrl(), token);
     377                    OAuthAccessTokenHolder.getInstance().save(CredentialsManager.getInstance());
     378                    GuiHelper.runInEDT(OAuthAuthenticationPreferencesPanel.this::refreshView);
     379                }, OsmScopes.read_gpx, OsmScopes.write_gpx,
     380                        OsmScopes.read_prefs, OsmScopes.write_prefs,
     381                        OsmScopes.write_api, OsmScopes.write_notes);
     382            }
     383        }
     384    }
     385
     386    /**
     387     * Remove the OAuth authorization token
     388     */
     389    private class RemoveAuthorisationAction extends AbstractAction {
     390        RemoveAuthorisationAction() {
     391            putValue(NAME, tr("Remove token"));
     392            putValue(SHORT_DESCRIPTION, tr("Remove token from JOSM. This does not revoke the token."));
     393            new ImageProvider("cancel").getResource().attachImageIcon(this);
     394        }
     395
     396        @Override
     397        public void actionPerformed(ActionEvent e) {
     398            if (oAuthVersion == OAuthVersion.OAuth10a) {
     399                OAuthAccessTokenHolder.getInstance().setAccessToken(null);
     400            } else {
     401                OAuthAccessTokenHolder.getInstance().setAccessToken(apiUrl, (IOAuthToken) null);
     402            }
     403            OAuthAccessTokenHolder.getInstance().save(CredentialsManager.getInstance());
    317404            refreshView();
    318405        }
    319406    }
  • src/org/openstreetmap/josm/io/auth/CredentialsAgent.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/io/auth/CredentialsAgent.java b/src/org/openstreetmap/josm/io/auth/CredentialsAgent.java
    a b  
    55import java.net.Authenticator.RequestorType;
    66import java.net.PasswordAuthentication;
    77
     8import javax.annotation.Nullable;
     9
     10import org.openstreetmap.josm.data.oauth.IOAuthToken;
    811import org.openstreetmap.josm.data.oauth.OAuthToken;
    912
    1013/**
     
    6467     */
    6568    OAuthToken lookupOAuthAccessToken() throws CredentialsAgentException;
    6669
     70    /**
     71     * Lookup the current OAuth Access Token to access the specified server. Replies null, if no
     72     * Access Token is currently managed by this CredentialAgent.
     73     *
     74     * @param host The host to get OAuth credentials for
     75     * @return the current OAuth Access Token to access the specified server.
     76     * @throws CredentialsAgentException if something goes wrong
     77     * @since xxx
     78     */
     79    @Nullable
     80    IOAuthToken lookupOAuthAccessToken(String host) throws CredentialsAgentException;
     81
    6782    /**
    6883     * Stores the OAuth Access Token <code>accessToken</code>.
    6984     *
     
    7287     */
    7388    void storeOAuthAccessToken(OAuthToken accessToken) throws CredentialsAgentException;
    7489
     90    /**
     91     * Stores the OAuth Access Token <code>accessToken</code>.
     92     *
     93     * @param host The host the access token is for
     94     * @param accessToken the access Token. null, to remove the Access Token. This will remove all IOAuthTokens <i>not</i> managed by
     95     *                    {@link #storeOAuthAccessToken(OAuthToken)}.
     96     * @throws CredentialsAgentException if something goes wrong
     97     * @since xxx
     98     */
     99    void storeOAuthAccessToken(String host, IOAuthToken accessToken) throws CredentialsAgentException;
     100
    75101    /**
    76102     * Purges the internal credentials cache for the given requestor type.
    77103     * @param requestorType the type of service.
  • src/org/openstreetmap/josm/io/auth/CredentialsManager.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/io/auth/CredentialsManager.java b/src/org/openstreetmap/josm/io/auth/CredentialsManager.java
    a b  
    77import java.util.Objects;
    88
    99import org.openstreetmap.josm.data.UserIdentityManager;
     10import org.openstreetmap.josm.data.oauth.IOAuthToken;
    1011import org.openstreetmap.josm.data.oauth.OAuthToken;
    1112import org.openstreetmap.josm.io.OsmApi;
    1213import org.openstreetmap.josm.tools.CheckParameterUtil;
     
    160161        return delegate.lookupOAuthAccessToken();
    161162    }
    162163
     164    @Override
     165    public IOAuthToken lookupOAuthAccessToken(String host) throws CredentialsAgentException {
     166        return delegate.lookupOAuthAccessToken(host);
     167    }
     168
    163169    @Override
    164170    public void storeOAuthAccessToken(OAuthToken accessToken) throws CredentialsAgentException {
    165171        delegate.storeOAuthAccessToken(accessToken);
    166172    }
    167173
     174    @Override
     175    public void storeOAuthAccessToken(String host, IOAuthToken accessToken) throws CredentialsAgentException {
     176        delegate.storeOAuthAccessToken(host, accessToken);
     177    }
     178
    168179    @Override
    169180    public Component getPreferencesDecorationPanel() {
    170181        return delegate.getPreferencesDecorationPanel();
  • src/org/openstreetmap/josm/io/auth/JosmPreferencesCredentialAgent.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/io/auth/JosmPreferencesCredentialAgent.java b/src/org/openstreetmap/josm/io/auth/JosmPreferencesCredentialAgent.java
    a b  
    66import java.awt.Component;
    77import java.net.Authenticator.RequestorType;
    88import java.net.PasswordAuthentication;
     9import java.util.HashSet;
    910import java.util.Objects;
     11import java.util.Set;
    1012
     13import javax.json.JsonException;
    1114import javax.swing.text.html.HTMLEditorKit;
    1215
     16import org.openstreetmap.josm.data.oauth.IOAuthToken;
     17import org.openstreetmap.josm.data.oauth.OAuth20Exception;
     18import org.openstreetmap.josm.data.oauth.OAuth20Parameters;
     19import org.openstreetmap.josm.data.oauth.OAuth20Token;
    1320import org.openstreetmap.josm.data.oauth.OAuthToken;
     21import org.openstreetmap.josm.data.oauth.OAuthVersion;
    1422import org.openstreetmap.josm.gui.widgets.HtmlPanel;
    1523import org.openstreetmap.josm.io.DefaultProxySelector;
    1624import org.openstreetmap.josm.io.OsmApi;
    1725import org.openstreetmap.josm.spi.preferences.Config;
     26import org.openstreetmap.josm.tools.Utils;
    1827
    1928/**
    2029 * This is the default credentials agent in JOSM. It keeps username and password for both
     
    6877        case SERVER:
    6978            if (Objects.equals(OsmApi.getOsmApi().getHost(), host)) {
    7079                Config.getPref().put("osm-server.username", credentials.getUserName());
    71                 if (credentials.getPassword() == null) {
     80                if (credentials.getPassword().length == 0) { // PasswordAuthentication#getPassword cannot be null
    7281                    Config.getPref().put("osm-server.password", null);
    7382                } else {
    7483                    Config.getPref().put("osm-server.password", String.valueOf(credentials.getPassword()));
    7584                }
    7685            } else if (host != null) {
    7786                Config.getPref().put("server.username."+host, credentials.getUserName());
    78                 if (credentials.getPassword() == null) {
     87                if (credentials.getPassword().length == 0) {
    7988                    Config.getPref().put("server.password."+host, null);
    8089                } else {
    8190                    Config.getPref().put("server.password."+host, String.valueOf(credentials.getPassword()));
     
    8493            break;
    8594        case PROXY:
    8695            Config.getPref().put(DefaultProxySelector.PROXY_USER, credentials.getUserName());
    87             if (credentials.getPassword() == null) {
     96            if (credentials.getPassword().length == 0) {
    8897                Config.getPref().put(DefaultProxySelector.PROXY_PASS, null);
    8998            } else {
    9099                Config.getPref().put(DefaultProxySelector.PROXY_PASS, String.valueOf(credentials.getPassword()));
     
    109118        return new OAuthToken(accessTokenKey, accessTokenSecret);
    110119    }
    111120
     121    @Override
     122    public IOAuthToken lookupOAuthAccessToken(String host) throws CredentialsAgentException {
     123        Set<String> keySet = new HashSet<>(Config.getPref().getKeySet());
     124        keySet.addAll(Config.getPref().getSensitive()); // Just in case we decide to not return sensitive keys in getKeySet
     125        for (OAuthVersion oauthType : OAuthVersion.values()) {
     126            final String hostKey = "oauth.access-token.object." + oauthType + "." + host;
     127            final String parametersKey = "oauth.access-token.parameters." + oauthType + "." + host;
     128            if (!keySet.contains(hostKey) || !keySet.contains(parametersKey)) {
     129                continue; // Avoid adding empty temporary entries to preferences
     130            }
     131            String token = Config.getPref().get(hostKey, null);
     132            String parameters = Config.getPref().get(parametersKey, null);
     133            if (!Utils.isBlank(token) && !Utils.isBlank(parameters) && OAuthVersion.OAuth20 == oauthType) {
     134                try {
     135                    OAuth20Parameters oAuth20Parameters = new OAuth20Parameters(parameters);
     136                    return new OAuth20Token(oAuth20Parameters, token);
     137                } catch (OAuth20Exception | JsonException e) {
     138                    throw new CredentialsAgentException(e);
     139                }
     140            }
     141        }
     142        return null;
     143    }
     144
    112145    /**
    113146     * Stores the OAuth Access Token <code>accessToken</code>.
    114147     *
     
    126159        }
    127160    }
    128161
     162    @Override
     163    public void storeOAuthAccessToken(String host, IOAuthToken accessToken) throws CredentialsAgentException {
     164        Objects.requireNonNull(host, "host");
     165        if (accessToken == null) {
     166            Set<String> keySet = new HashSet<>(Config.getPref().getKeySet());
     167            keySet.addAll(Config.getPref().getSensitive()); // Just in case we decide to not return sensitive keys in getKeySet
     168            // Assume we want to remove all access tokens
     169            for (OAuthVersion oauthType : OAuthVersion.values()) {
     170                final String hostKey = "oauth.access-token.parameters." + oauthType + "." + host;
     171                final String parametersKey = "oauth.access-token.parameters." + oauthType + "." + host;
     172                if (keySet.contains(hostKey)) {
     173                    Config.getPref().removeSensitive(hostKey);
     174                }
     175                if (keySet.contains(parametersKey)) {
     176                    Config.getPref().removeSensitive(parametersKey);
     177                }
     178            }
     179        } else {
     180            final String hostKey = "oauth.access-token.object." + accessToken.getOAuthType() + "." + host;
     181            final String parametersKey = "oauth.access-token.parameters." + accessToken.getOAuthType() + "." + host;
     182            Config.getPref().put(hostKey, accessToken.toPreferencesString());
     183            Config.getPref().put(parametersKey, accessToken.getParameters().toPreferencesString());
     184            Config.getPref().addSensitive(this, hostKey);
     185            Config.getPref().addSensitive(this, parametersKey);
     186        }
     187    }
     188
    129189    @Override
    130190    public Component getPreferencesDecorationPanel() {
    131191        HtmlPanel pnlMessage = new HtmlPanel();
  • new file src/org/openstreetmap/josm/io/remotecontrol/handler/AuthorizationHandler.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/io/remotecontrol/handler/AuthorizationHandler.java b/src/org/openstreetmap/josm/io/remotecontrol/handler/AuthorizationHandler.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.io.remotecontrol.handler;
     3
     4import java.util.HashMap;
     5import java.util.Map;
     6import java.util.Objects;
     7import java.util.Optional;
     8import java.util.stream.Collectors;
     9
     10import org.openstreetmap.josm.data.preferences.BooleanProperty;
     11import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
     12
     13/**
     14 * Handle authorization requests (mostly OAuth)
     15 * @since xxx
     16 */
     17public class AuthorizationHandler extends RequestHandler {
     18
     19    /**
     20     * The actual authorization consumer/handler
     21     */
     22    public interface AuthorizationConsumer {
     23        /**
     24         * Validate the request
     25         * @param args The GET request arguments
     26         * @param request The request URL without "GET".
     27         * @param sender who sent the request? the host from referer header or IP of request sender
     28         * @throws RequestHandlerBadRequestException if the request is invalid
     29         * @see RequestHandler#validateRequest()
     30         */
     31        void validateRequest(String sender, String request, Map<String, String> args)
     32                throws RequestHandlerBadRequestException;
     33
     34        /**
     35         * Handle the request. Any time-consuming operation must be performed asynchronously to avoid delaying the HTTP response.
     36         * @param args The GET request arguments
     37         * @param request The request URL without "GET".
     38         * @param sender who sent the request? the host from referer header or IP of request sender
     39         * @return The response to show the user. May be {@code null}.
     40         * @throws RequestHandlerErrorException if an error occurs while processing the request
     41         * @throws RequestHandlerBadRequestException if the request is invalid
     42         * @see RequestHandler#handleRequest()
     43         */
     44        ResponseRecord handleRequest(String sender, String request, Map<String, String> args)
     45                throws RequestHandlerErrorException, RequestHandlerBadRequestException;
     46    }
     47
     48    /**
     49     * A basic record for changing responses
     50     */
     51    public static final class ResponseRecord {
     52        private final String content;
     53        private final String type;
     54
     55        /**
     56         * Create a new record
     57         * @param content The content to show the user
     58         * @param type The content mime type
     59         */
     60        public ResponseRecord(String content, String type) {
     61            this.content = content;
     62            this.type = type;
     63        }
     64
     65        /**
     66         * Get the content for the response
     67         * @return The content as a string
     68         */
     69        public String content() {
     70            return this.content;
     71        }
     72
     73        /**
     74         * Get the type for the response
     75         * @return The response mime type
     76         */
     77        public String type() {
     78            return this.type;
     79        }
     80    }
     81
     82    /**
     83     * The remote control command
     84     */
     85    public static final String command = "oauth_authorization";
     86
     87    private static final BooleanProperty PROPERTY = new BooleanProperty("remotecontrol.permission.authorization", false);
     88    private static final Map<String, AuthorizationConsumer> AUTHORIZATION_CONSUMERS = new HashMap<>();
     89
     90    private AuthorizationConsumer consumer;
     91    /**
     92     * Add an authorization consumer.
     93     * @param state The unique state for each request (for OAuth, this would be the {@code state} parameter)
     94     * @param consumer The consumer of the response
     95     */
     96    public static synchronized void addAuthorizationConsumer(String state, AuthorizationConsumer consumer) {
     97        if (AUTHORIZATION_CONSUMERS.containsKey(state)) {
     98            throw new IllegalArgumentException("Cannot add multiple consumers for one authorization state");
     99        }
     100        AUTHORIZATION_CONSUMERS.put(state, consumer);
     101    }
     102
     103    @Override
     104    protected void validateRequest() throws RequestHandlerBadRequestException {
     105        boolean clearAll = false;
     106        for (Map.Entry<String, AuthorizationConsumer> entry : AUTHORIZATION_CONSUMERS.entrySet()) {
     107            if (Objects.equals(this.args.get("state"), entry.getKey())) {
     108                if (this.consumer == null) {
     109                    this.consumer = entry.getValue();
     110                } else {
     111                    // Remove all authorization consumers. Someone might be playing games.
     112                    clearAll = true;
     113                }
     114            }
     115        }
     116        if (clearAll) {
     117            AUTHORIZATION_CONSUMERS.clear();
     118            this.consumer = null;
     119            throw new RequestHandlerBadRequestException("Multiple states for authorization");
     120        }
     121
     122        if (this.consumer == null) {
     123            throw new RequestHandlerBadRequestException("Unknown state for authorization");
     124        }
     125        this.consumer.validateRequest(this.sender, this.request, this.args);
     126    }
     127
     128    @Override
     129    protected void handleRequest() throws RequestHandlerErrorException, RequestHandlerBadRequestException {
     130        ResponseRecord response = this.consumer.handleRequest(this.sender, this.request, this.args);
     131        if (response != null) {
     132            this.content = Optional.ofNullable(response.content()).orElse(this.content);
     133            this.contentType = Optional.ofNullable(response.type()).orElse(this.contentType);
     134        }
     135        // Only ever allow a consumer to be used once
     136        AUTHORIZATION_CONSUMERS.entrySet().stream().filter(entry -> Objects.equals(this.consumer, entry.getValue()))
     137                .map(Map.Entry::getKey).collect(Collectors.toList()).forEach(AUTHORIZATION_CONSUMERS::remove);
     138        this.consumer = null;
     139    }
     140
     141    @Override
     142    public String getPermissionMessage() {
     143        return "Allow OAuth remote control to set credentials";
     144    }
     145
     146    @Override
     147    public PermissionPrefWithDefault getPermissionPref() {
     148        return null;
     149    }
     150
     151    public BooleanProperty getPermissionPreference() {
     152        return PROPERTY;
     153    }
     154
     155    @Override
     156    public String[] getMandatoryParams() {
     157        return new String[] {"code", "state"};
     158    }
     159}
  • src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java b/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java
    a b  
    3030import org.openstreetmap.josm.gui.help.HelpUtil;
    3131import org.openstreetmap.josm.io.remotecontrol.handler.AddNodeHandler;
    3232import org.openstreetmap.josm.io.remotecontrol.handler.AddWayHandler;
     33import org.openstreetmap.josm.io.remotecontrol.handler.AuthorizationHandler;
    3334import org.openstreetmap.josm.io.remotecontrol.handler.FeaturesHandler;
    3435import org.openstreetmap.josm.io.remotecontrol.handler.ImageryHandler;
    3536import org.openstreetmap.josm.io.remotecontrol.handler.ImportHandler;
     
    171172            addRequestHandlerClass(VersionHandler.command, VersionHandler.class, true);
    172173            addRequestHandlerClass(FeaturesHandler.command, FeaturesHandler.class, true);
    173174            addRequestHandlerClass(OpenApiHandler.command, OpenApiHandler.class, true);
     175            addRequestHandlerClass(AuthorizationHandler.command, AuthorizationHandler.class, true);
    174176        }
    175177    }
    176178
  • src/org/openstreetmap/josm/io/MessageNotifier.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/io/MessageNotifier.java b/src/org/openstreetmap/josm/io/MessageNotifier.java
    a b  
    1010import java.util.concurrent.TimeUnit;
    1111
    1212import org.openstreetmap.josm.data.UserIdentityManager;
     13import org.openstreetmap.josm.data.oauth.OAuthVersion;
    1314import org.openstreetmap.josm.data.osm.UserInfo;
    1415import org.openstreetmap.josm.data.preferences.BooleanProperty;
    1516import org.openstreetmap.josm.data.preferences.IntegerProperty;
     
    143144            CredentialsManager credManager = CredentialsManager.getInstance();
    144145            try {
    145146                if (JosmPreferencesCredentialAgent.class.equals(credManager.getCredentialsAgentClass())) {
    146                     if (OsmApi.isUsingOAuth()) {
     147                    if (OsmApi.isUsingOAuth(OAuthVersion.OAuth10a)) {
    147148                        return credManager.lookupOAuthAccessToken() != null;
     149                    } else if (OsmApi.isUsingOAuth(OAuthVersion.OAuth20) || OsmApi.isUsingOAuth(OAuthVersion.OAuth21)) {
     150                        return credManager.lookupOAuthAccessToken(OsmApi.getOsmApi().getHost()) != null;
     151                    } else if (OsmApi.isUsingOAuth()) {
     152                        // Ensure we do not forget to update this section
     153                        throw new IllegalStateException("Unknown oauth version: " + OsmApi.getAuthMethod());
    148154                    } else {
    149155                        String username = Config.getPref().get("osm-server.username", null);
    150156                        String password = Config.getPref().get("osm-server.password", null);
  • src/org/openstreetmap/josm/io/OsmApi.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/io/OsmApi.java b/src/org/openstreetmap/josm/io/OsmApi.java
    a b  
    2929
    3030import org.openstreetmap.josm.data.coor.LatLon;
    3131import org.openstreetmap.josm.data.notes.Note;
     32import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
     33import org.openstreetmap.josm.data.oauth.OAuthVersion;
    3234import org.openstreetmap.josm.data.osm.Changeset;
    3335import org.openstreetmap.josm.data.osm.IPrimitive;
    3436import org.openstreetmap.josm.data.osm.OsmPrimitive;
     
    3739import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
    3840import org.openstreetmap.josm.gui.progress.ProgressMonitor;
    3941import org.openstreetmap.josm.io.Capabilities.CapabilitiesParser;
     42import org.openstreetmap.josm.io.auth.CredentialsAgentException;
    4043import org.openstreetmap.josm.io.auth.CredentialsManager;
    4144import org.openstreetmap.josm.spi.preferences.Config;
    4245import org.openstreetmap.josm.tools.CheckParameterUtil;
     
    645648     * @since 6349
    646649     */
    647650    public static boolean isUsingOAuth() {
    648         return "oauth".equals(getAuthMethod());
     651        return isUsingOAuth(OAuthVersion.OAuth10a)
     652                || isUsingOAuth(OAuthVersion.OAuth20)
     653                || isUsingOAuth(OAuthVersion.OAuth21);
     654    }
     655
     656    /**
     657     * Determines if JOSM is configured to access OSM API via OAuth
     658     * @param version The OAuth version
     659     * @return {@code true} if JOSM is configured to access OSM API via OAuth, {@code false} otherwise
     660     * @since xxx
     661     */
     662    public static boolean isUsingOAuth(OAuthVersion version) {
     663        if (version == OAuthVersion.OAuth10a) {
     664            return "oauth".equalsIgnoreCase(getAuthMethod());
     665        } else if (version == OAuthVersion.OAuth20 || version == OAuthVersion.OAuth21) {
     666            return "oauth20".equalsIgnoreCase(getAuthMethod());
     667        }
     668        return false;
     669    }
     670
     671    /**
     672     * Ensure that OAuth is set up
     673     * @param api The api for which we need OAuth keys
     674     * @return {@code true} if we are using OAuth and there are keys for the specified API
     675     */
     676    public static boolean isUsingOAuthAndOAuthSetUp(OsmApi api) {
     677        if (OsmApi.isUsingOAuth()) {
     678            if (OsmApi.isUsingOAuth(OAuthVersion.OAuth10a)) {
     679                return OAuthAccessTokenHolder.getInstance().containsAccessToken();
     680            }
     681            if (OsmApi.isUsingOAuth(OAuthVersion.OAuth20)) {
     682                return OAuthAccessTokenHolder.getInstance().getAccessToken(api.getBaseUrl(), OAuthVersion.OAuth20) != null;
     683            }
     684            if (OsmApi.isUsingOAuth(OAuthVersion.OAuth21)) {
     685                return OAuthAccessTokenHolder.getInstance().getAccessToken(api.getBaseUrl(), OAuthVersion.OAuth21) != null;
     686            }
     687        }
     688        return false;
    649689    }
    650690
    651691    /**
     
    653693     * @return the authentication method
    654694     */
    655695    public static String getAuthMethod() {
    656         return Config.getPref().get("osm-server.auth-method", "oauth");
     696        return Config.getPref().get("osm-server.auth-method", "oauth"); // fixme switch to oauth20 by default
    657697    }
    658698
    659699    protected final String sendPostRequest(String urlSuffix, String requestBody, ProgressMonitor monitor) throws OsmTransferException {
  • src/org/openstreetmap/josm/io/OsmConnection.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/io/OsmConnection.java b/src/org/openstreetmap/josm/io/OsmConnection.java
    a b  
    1010import java.nio.charset.StandardCharsets;
    1111import java.util.Base64;
    1212import java.util.Objects;
     13import java.util.concurrent.TimeUnit;
     14import java.util.concurrent.atomic.AtomicBoolean;
     15import java.util.function.Consumer;
    1316
     17import javax.swing.JOptionPane;
     18
     19import org.openstreetmap.josm.data.oauth.IOAuthParameters;
     20import org.openstreetmap.josm.data.oauth.IOAuthToken;
     21import org.openstreetmap.josm.data.oauth.OAuth20Authorization;
    1422import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
    1523import org.openstreetmap.josm.data.oauth.OAuthParameters;
     24import org.openstreetmap.josm.data.oauth.OAuthVersion;
     25import org.openstreetmap.josm.data.oauth.osm.OsmScopes;
     26import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
     27import org.openstreetmap.josm.gui.MainApplication;
     28import org.openstreetmap.josm.gui.util.GuiHelper;
    1629import org.openstreetmap.josm.io.auth.CredentialsAgentException;
    1730import org.openstreetmap.josm.io.auth.CredentialsAgentResponse;
    1831import org.openstreetmap.josm.io.auth.CredentialsManager;
     32import org.openstreetmap.josm.io.remotecontrol.RemoteControl;
    1933import org.openstreetmap.josm.tools.HttpClient;
    2034import org.openstreetmap.josm.tools.JosmRuntimeException;
    2135import org.openstreetmap.josm.tools.Logging;
     
    3650    protected boolean cancel;
    3751    protected HttpClient activeConnection;
    3852    protected OAuthParameters oauthParameters;
     53    protected IOAuthParameters oAuth20Parameters;
    3954
    4055    /**
    4156     * Retrieves OAuth access token.
     
    171186            fetcher.obtainAccessToken(apiUrl);
    172187            OAuthAccessTokenHolder.getInstance().setSaveToPreferences(true);
    173188            OAuthAccessTokenHolder.getInstance().save(CredentialsManager.getInstance());
    174         } catch (MalformedURLException | InterruptedException | InvocationTargetException e) {
     189        } catch (MalformedURLException | InvocationTargetException e) {
    175190            throw new MissingOAuthAccessTokenException(e);
     191        } catch (InterruptedException e) {
     192            Thread.currentThread().interrupt();
     193            throw new MissingOAuthAccessTokenException(e);
     194        }
     195    }
     196
     197    /**
     198     * Obtains an OAuth access token for the connection.
     199     * Afterwards, the token is accessible via {@link OAuthAccessTokenHolder} / {@link CredentialsManager}.
     200     * @throws MissingOAuthAccessTokenException if the process cannot be completed successfully
     201     */
     202    private void obtainOAuth20Token() throws MissingOAuthAccessTokenException {
     203        if (!Boolean.TRUE.equals(GuiHelper.runInEDTAndWaitAndReturn(() ->
     204                ConditionalOptionPaneUtil.showConfirmationDialog("oauth.oauth20.obtain.automatically",
     205                    MainApplication.getMainFrame(),
     206                    tr("Obtain OAuth 2.0 token for authentication?"),
     207                    tr("Obtain authentication to OSM servers"),
     208                    JOptionPane.YES_NO_CANCEL_OPTION,
     209                    JOptionPane.QUESTION_MESSAGE, JOptionPane.YES_OPTION)))) {
     210            return; // User doesn't want to perform auth
     211        }
     212        final boolean remoteControlIsRunning = Boolean.TRUE.equals(RemoteControl.PROP_REMOTECONTROL_ENABLED.get());
     213        if (!remoteControlIsRunning) {
     214            RemoteControl.start();
     215        }
     216        AtomicBoolean done = new AtomicBoolean();
     217        Consumer<IOAuthToken> consumer = authToken -> {
     218                    if (!remoteControlIsRunning) {
     219                        RemoteControl.stop();
     220                    }
     221                    // Clean up old token/password
     222                    OAuthAccessTokenHolder.getInstance().setAccessToken(null);
     223                    OAuthAccessTokenHolder.getInstance().setAccessToken(OsmApi.getOsmApi().getServerUrl(), authToken);
     224                    OAuthAccessTokenHolder.getInstance().save(CredentialsManager.getInstance());
     225                    synchronized (done) {
     226                        done.set(true);
     227                        done.notifyAll();
     228                    }
     229                };
     230        new OAuth20Authorization().authorize(oAuth20Parameters,
     231                consumer, OsmScopes.read_gpx, OsmScopes.write_gpx,
     232                OsmScopes.read_prefs, OsmScopes.write_prefs,
     233                OsmScopes.write_api, OsmScopes.write_notes);
     234        synchronized (done) {
     235            // Only wait at most 5 minutes
     236            int counter = 0;
     237            while (!done.get() && counter < 5) {
     238                try {
     239                    done.wait(TimeUnit.MINUTES.toMillis(1));
     240                } catch (InterruptedException e) {
     241                    Thread.currentThread().interrupt();
     242                    Logging.trace(e);
     243                    consumer.accept(null);
     244                    throw new MissingOAuthAccessTokenException(e);
     245                }
     246                counter++;
     247            }
    176248        }
    177249    }
     250
     251    /**
     252     * Signs the connection with an OAuth authentication header
     253     *
     254     * @param connection the connection
     255     *
     256     * @throws MissingOAuthAccessTokenException if there is currently no OAuth Access Token configured
     257     * @throws OsmTransferException if signing fails
     258     */
     259    protected void addOAuth20AuthorizationHeader(HttpClient connection) throws OsmTransferException {
     260        if (this.oAuth20Parameters == null) {
     261            this.oAuth20Parameters = OAuthParameters.createFromApiUrl(connection.getURL().getHost(), OAuthVersion.OAuth20);
     262        }
     263        OAuthAccessTokenHolder holder = OAuthAccessTokenHolder.getInstance();
     264        IOAuthToken token = holder.getAccessToken(connection.getURL().toExternalForm(), OAuthVersion.OAuth20);
     265        if (token == null) {
     266            obtainOAuth20Token();
     267            token = holder.getAccessToken(connection.getURL().toExternalForm(), OAuthVersion.OAuth20);
     268        }
     269        if (token == null) { // check if wizard completed
     270            throw new MissingOAuthAccessTokenException();
     271        }
     272        try {
     273            token.sign(connection);
     274        } catch (org.openstreetmap.josm.data.oauth.OAuthException e) {
     275            throw new OsmTransferException(tr("Failed to sign a HTTP connection with an OAuth Authentication header"), e);
     276        }
     277    }
    178278
    179279    protected void addAuth(HttpClient connection) throws OsmTransferException {
    180280        final String authMethod = OsmApi.getAuthMethod();
    181         if ("basic".equals(authMethod)) {
    182             addBasicAuthorizationHeader(connection);
    183         } else if ("oauth".equals(authMethod)) {
    184             addOAuthAuthorizationHeader(connection);
    185         } else {
    186             String msg = tr("Unexpected value for preference ''{0}''. Got ''{1}''.", "osm-server.auth-method", authMethod);
    187             Logging.warn(msg);
    188             throw new OsmTransferException(msg);
     281        switch (authMethod) {
     282            case "basic":
     283                addBasicAuthorizationHeader(connection);
     284                return;
     285            case "oauth":
     286                addOAuthAuthorizationHeader(connection);
     287                return;
     288            case "oauth20":
     289                addOAuth20AuthorizationHeader(connection);
     290                return;
     291            default:
     292                String msg = tr("Unexpected value for preference ''{0}''. Got ''{1}''.", "osm-server.auth-method", authMethod);
     293                Logging.warn(msg);
     294                throw new OsmTransferException(msg);
    189295        }
    190296    }
    191297
  • src/org/openstreetmap/josm/io/OsmServerReader.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/io/OsmServerReader.java b/src/org/openstreetmap/josm/io/OsmServerReader.java
    a b  
    1010import java.net.MalformedURLException;
    1111import java.net.URL;
    1212import java.util.List;
     13import java.util.Objects;
    1314
    1415import javax.xml.parsers.ParserConfigurationException;
    1516
    1617import org.openstreetmap.josm.data.gpx.GpxData;
    1718import org.openstreetmap.josm.data.notes.Note;
     19import org.openstreetmap.josm.data.oauth.OAuthVersion;
    1820import org.openstreetmap.josm.data.osm.DataSet;
    1921import org.openstreetmap.josm.gui.progress.ProgressMonitor;
    2022import org.openstreetmap.josm.io.auth.CredentialsAgentException;
     
    3840 * @author imi
    3941 */
    4042public abstract class OsmServerReader extends OsmConnection {
    41     private final OsmApi api = OsmApi.getOsmApi();
     43    private final OsmApi api;
    4244    private boolean doAuthenticate;
    4345    protected boolean gpxParsedProperly;
    4446    protected String contentType;
     
    4749     * Constructs a new {@code OsmServerReader}.
    4850     */
    4951    protected OsmServerReader() {
    50         try {
    51             doAuthenticate = OsmApi.isUsingOAuth()
    52                     && CredentialsManager.getInstance().lookupOAuthAccessToken() != null
    53                     && OsmApi.USE_OAUTH_FOR_ALL_REQUESTS.get();
    54         } catch (CredentialsAgentException e) {
    55             Logging.warn(e);
    56         }
     52        this(OsmApi.getOsmApi());
     53    }
     54
     55    /**
     56     * Constructs a new {@code OsmServerReader}.
     57     * @param osmApi The API to use for this call
     58     * @since xxx
     59     */
     60    protected OsmServerReader(OsmApi osmApi) {
     61        this.api = osmApi;
     62        this.doAuthenticate = OsmApi.isUsingOAuthAndOAuthSetUp(osmApi) && OsmApi.USE_OAUTH_FOR_ALL_REQUESTS.get();
    5763    }
    5864
    5965    /**
     
    184190                    .setRequestBody(requestBody);
    185191            activeConnection = client;
    186192            adaptRequest(client);
    187             if (doAuthenticate) {
     193            if (doAuthenticate && Objects.equals(this.api.getHost(), client.getURL().getHost())) {
    188194                addAuth(client);
    189195            }
    190196            if (cancel)
  • src/org/openstreetmap/josm/spi/preferences/AbstractPreferences.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/spi/preferences/AbstractPreferences.java b/src/org/openstreetmap/josm/spi/preferences/AbstractPreferences.java
    a b  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.spi.preferences;
    33
     4import java.util.Arrays;
     5import java.util.Collection;
     6import java.util.Collections;
     7import java.util.HashSet;
    48import java.util.LinkedList;
    59import java.util.List;
    610import java.util.Map;
    711import java.util.Map.Entry;
     12import java.util.Set;
    813import java.util.TreeMap;
    914import java.util.stream.Collectors;
    1015
     16import org.openstreetmap.josm.io.DefaultProxySelector;
     17import org.openstreetmap.josm.io.auth.CredentialsAgent;
     18import org.openstreetmap.josm.io.auth.CredentialsManager;
    1119import org.openstreetmap.josm.tools.Logging;
    1220import org.openstreetmap.josm.tools.Utils;
    1321
     
    1624 * @since 12847
    1725 */
    1826public abstract class AbstractPreferences implements IPreferences {
     27    /** The preference key for sensitive keys */
     28    private static final String KEY_SENSITIVE_KEYS = "sensitive.keys";
     29
     30    /** A set of sensitive keys that should not be seen/distributed outside of specific callers (like a {@link CredentialsAgent}) */
     31    private static final Set<String> SENSITIVE_KEYS = new HashSet<>();
    1932
    2033    @Override
    2134    public synchronized String get(final String key, final String def) {
     
    175188                .map(Entry::getKey)
    176189                .collect(Collectors.toCollection(LinkedList::new));
    177190    }
     191
     192    @Override
     193    public void addSensitive(CredentialsAgent caller, String key) {
     194        if (SENSITIVE_KEYS.isEmpty()) {
     195            populateSensitiveKeys();
     196        }
     197        if (CredentialsManager.getInstance().getCredentialsAgentClass().equals(caller.getClass())) {
     198            SENSITIVE_KEYS.add(key);
     199            putList("sensitive.keys", SENSITIVE_KEYS.stream().sorted().collect(Collectors.toList()));
     200        }
     201    }
     202
     203    @Override
     204    public Collection<String> getSensitive() {
     205        if (SENSITIVE_KEYS.isEmpty()) {
     206            populateSensitiveKeys();
     207        }
     208        return Collections.unmodifiableSet(SENSITIVE_KEYS);
     209    }
     210
     211    @Override
     212    public void removeSensitive(String key) {
     213        if (KEY_SENSITIVE_KEYS.equals(key)) {
     214            throw new IllegalArgumentException(KEY_SENSITIVE_KEYS + " cannot be removed from the sensitive key list.");
     215        }
     216        // Reset the key first -- avoid race conditions where a sensitive value might be visible if we start restricting access in the future.
     217        put(key, null);
     218        SENSITIVE_KEYS.remove(key);
     219        putList(KEY_SENSITIVE_KEYS, SENSITIVE_KEYS.stream().sorted().collect(Collectors.toList()));
     220    }
     221
     222    /**
     223     * Populate the sensitive key set from preferences
     224     */
     225    private void populateSensitiveKeys() {
     226        SENSITIVE_KEYS.addAll(getList(KEY_SENSITIVE_KEYS, Arrays.asList("sensitive.keys", "osm-server.username", "osm-server.password",
     227                DefaultProxySelector.PROXY_USER, DefaultProxySelector.PROXY_PASS,
     228                "oauth.access-token.key", "oauth.access-token.secret")));
     229    }
    178230}
  • src/org/openstreetmap/josm/spi/preferences/IPreferences.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/spi/preferences/IPreferences.java b/src/org/openstreetmap/josm/spi/preferences/IPreferences.java
    a b  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.spi.preferences;
    33
     4import java.util.Collection;
    45import java.util.Collections;
    56import java.util.List;
    67import java.util.Map;
    78import java.util.Set;
    89
     10import org.openstreetmap.josm.io.auth.CredentialsAgent;
     11
    912/**
    1013 * Interface for preference handling.
    1114 *
     
    239242     * @return the set of all keys
    240243     */
    241244    Set<String> getKeySet();
     245
     246    /**
     247     * Add sensitive keys
     248     * @param caller The calling agent
     249     * @param key The key that may contain sensitive information
     250     * @since xxx
     251     */
     252    void addSensitive(CredentialsAgent caller, String key);
     253
     254    /**
     255     * Get sensitive keys
     256     * @return The sensitive keys
     257     * @since xxx
     258     */
     259    Collection<String> getSensitive();
     260
     261    /**
     262     * Remove sensitive keys. This removes the key from the sensitive list <i>and</i>
     263     * removes the stored preference value.
     264     * @param key The key to remove
     265     * @since xxx
     266     */
     267    void removeSensitive(String key);
    242268}
  • new file test/unit/org/openstreetmap/josm/data/oauth/OAuth20AuthorizationTest.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/test/unit/org/openstreetmap/josm/data/oauth/OAuth20AuthorizationTest.java b/test/unit/org/openstreetmap/josm/data/oauth/OAuth20AuthorizationTest.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.oauth;
     3
     4import static org.junit.jupiter.api.Assertions.assertEquals;
     5import static org.junit.jupiter.api.Assertions.assertNotNull;
     6import static org.junit.jupiter.api.Assertions.assertNull;
     7import static org.junit.jupiter.api.Assertions.assertTrue;
     8
     9import java.io.IOException;
     10import java.util.HashMap;
     11import java.util.Map;
     12import java.util.concurrent.atomic.AtomicReference;
     13import java.util.stream.Collectors;
     14import java.util.stream.Stream;
     15
     16import com.github.tomakehurst.wiremock.client.WireMock;
     17import com.github.tomakehurst.wiremock.common.FileSource;
     18import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
     19import com.github.tomakehurst.wiremock.extension.Parameters;
     20import com.github.tomakehurst.wiremock.extension.ResponseTransformer;
     21import com.github.tomakehurst.wiremock.http.HttpHeader;
     22import com.github.tomakehurst.wiremock.http.HttpHeaders;
     23import com.github.tomakehurst.wiremock.http.QueryParameter;
     24import com.github.tomakehurst.wiremock.http.Request;
     25import com.github.tomakehurst.wiremock.http.Response;
     26import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
     27import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
     28import com.github.tomakehurst.wiremock.matching.AnythingPattern;
     29import com.github.tomakehurst.wiremock.matching.EqualToPattern;
     30import com.github.tomakehurst.wiremock.matching.StringValuePattern;
     31import mockit.Mock;
     32import mockit.MockUp;
     33import org.junit.jupiter.api.AfterEach;
     34import org.junit.jupiter.api.BeforeEach;
     35import org.junit.jupiter.api.Test;
     36import org.junit.jupiter.api.extension.RegisterExtension;
     37import org.openstreetmap.josm.data.oauth.osm.OsmScopes;
     38import org.openstreetmap.josm.data.preferences.JosmUrls;
     39import org.openstreetmap.josm.io.OsmApi;
     40import org.openstreetmap.josm.io.remotecontrol.RemoteControl;
     41import org.openstreetmap.josm.spi.preferences.Config;
     42import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
     43import org.openstreetmap.josm.testutils.annotations.HTTP;
     44import org.openstreetmap.josm.testutils.mockers.OpenBrowserMocker;
     45import org.openstreetmap.josm.tools.HttpClient;
     46import org.openstreetmap.josm.tools.Logging;
     47
     48@BasicPreferences
     49@HTTP
     50class OAuth20AuthorizationTest {
     51    private static final String RESPONSE_TYPE = "response_type";
     52    private static final String RESPONSE_TYPE_VALUE = "code";
     53    private static final String CLIENT_ID = "client_id";
     54    private static final String CLIENT_ID_VALUE = "edPII614Lm0_0zEpc_QzEltA9BUll93-Y-ugRQUoHMI";
     55    private static final String REDIRECT_URI = "redirect_uri";
     56    private static final String REDIRECT_URI_VALUE = "http://127.0.0.1:8111/oauth_authorization";
     57    private static final String SCOPE = "scope";
     58    private static final String STATE = "state";
     59    private static final String CODE_CHALLENGE_METHOD = "code_challenge_method";
     60    private static final String CODE_CHALLENGE_METHOD_VALUE = "S256";
     61    private static final String CODE_CHALLENGE = "code_challenge";
     62
     63    private static class OAuthServerWireMock extends ResponseTransformer {
     64        String stateToReturn;
     65        @Override
     66        public Response transform(Request request, Response response, FileSource files, Parameters parameters) {
     67            try {
     68                if (request.getUrl().startsWith("/oauth2/authorize")) {
     69                    return authorizationRequest(request, response);
     70                } else if (request.getUrl().startsWith("/oauth2/token")) {
     71                    return tokenRequest(request, response);
     72                }
     73                return response;
     74            } catch (Exception e) {
     75                // Make certain we actually see the exception in logs -- WireMock returns the error, but then our code needs to print it
     76                Logging.error(e);
     77                throw e;
     78            }
     79        }
     80
     81        private Response tokenRequest(Request request, Response response) {
     82            Map<String, String> queryParameters = Stream.of(request.getBodyAsString().split("&", -1))
     83                    .map(string -> string.split("=", -1))
     84                    .collect(Collectors.toMap(strings -> strings[0], strings -> strings[1]));
     85            if (!queryParameters.containsKey("grant_type")
     86                    || !queryParameters.containsKey(REDIRECT_URI) || !queryParameters.containsKey(CLIENT_ID)
     87                    || !queryParameters.containsKey("code") || !queryParameters.containsKey("code_verifier")) {
     88                return Response.Builder.like(response).but().status(500).build();
     89            }
     90            return Response.Builder.like(response).but().body("{\"token_type\": \"bearer\", \"access_token\": \"test_access_token\"}").build();
     91        }
     92
     93        private Response authorizationRequest(Request request, Response response) {
     94            final QueryParameter state = request.queryParameter(STATE);
     95            final QueryParameter codeChallenge = request.queryParameter(CODE_CHALLENGE);
     96            final QueryParameter redirectUri = request.queryParameter(REDIRECT_URI);
     97            final QueryParameter responseType = request.queryParameter(RESPONSE_TYPE);
     98            final QueryParameter scope = request.queryParameter(SCOPE);
     99            final QueryParameter clientId = request.queryParameter(CLIENT_ID);
     100            final QueryParameter codeChallengeMethod = request.queryParameter(CODE_CHALLENGE_METHOD);
     101            final boolean badRequest = !(state.isPresent() && state.isSingleValued());
     102            if (badRequest || checkQueryParameter(redirectUri, REDIRECT_URI_VALUE) || checkQueryParameter(responseType, RESPONSE_TYPE_VALUE)
     103                    || checkQueryParameter(clientId, CLIENT_ID_VALUE) || checkQueryParameter(codeChallengeMethod, CODE_CHALLENGE_METHOD_VALUE)
     104                    || checkQueryParameter(scope, "read_gpx")
     105                    || !codeChallenge.isPresent()) {
     106                return Response.Builder.like(response).but().status(500).build();
     107            }
     108            return Response.Builder.like(response).but().status(307)
     109                    .headers(new HttpHeaders(new HttpHeader("Location",
     110                            redirectUri.values().get(0)
     111                                    + "?state=" + (this.stateToReturn != null ? stateToReturn : state.firstValue())
     112                                    + "&code=test_code"))).build();
     113        }
     114
     115        private static boolean checkQueryParameter(QueryParameter parameter, String expected) {
     116            return !parameter.isPresent() || !parameter.isSingleValued() || !parameter.containsValue(expected);
     117        }
     118
     119        @Override
     120        public String getName() {
     121            return "OAuthServerWireMock";
     122        }
     123    }
     124
     125    private static final OAuthServerWireMock oauthServer = new OAuthServerWireMock();
     126    @RegisterExtension
     127    static WireMockExtension wml = WireMockExtension.newInstance()
     128            .options(WireMockConfiguration.wireMockConfig().dynamicPort().dynamicHttpsPort().extensions(oauthServer))
     129            .build();
     130    @BeforeEach
     131    @AfterEach
     132    void setup() {
     133        // Reset the mocker
     134        OpenBrowserMocker.getCalledURIs().clear();
     135        RemoteControl.stop(); // Ensure remote control is stopped
     136        oauthServer.stateToReturn = null;
     137    }
     138
     139    /**
     140     * Set up the default wiremock information
     141     * @param wireMockRuntimeInfo The info to set up
     142     */
     143    @BeforeEach
     144    void setupWireMock(WireMockRuntimeInfo wireMockRuntimeInfo) {
     145        Config.getPref().put("osm-server.url", wireMockRuntimeInfo.getHttpBaseUrl() + "/api/");
     146        new MockUp<JosmUrls>() {
     147            @Mock
     148            public String getDefaultOsmApiUrl() {
     149                return wireMockRuntimeInfo.getHttpBaseUrl() + "/api/";
     150            }
     151        };
     152        new OpenBrowserMocker();
     153        final Map<String, StringValuePattern> queryParams = new HashMap<>();
     154        queryParams.put(RESPONSE_TYPE, new EqualToPattern(RESPONSE_TYPE_VALUE));
     155        queryParams.put(CLIENT_ID, new EqualToPattern(CLIENT_ID_VALUE));
     156        queryParams.put(REDIRECT_URI, new EqualToPattern(REDIRECT_URI_VALUE));
     157        queryParams.put(SCOPE, new EqualToPattern("read_gpx"));
     158        queryParams.put(STATE, new AnythingPattern()); // This is generated via a random UUID, and we have to return this in the redirect
     159        queryParams.put(CODE_CHALLENGE_METHOD, new EqualToPattern(CODE_CHALLENGE_METHOD_VALUE));
     160        queryParams.put(CODE_CHALLENGE, new AnythingPattern()); // This is generated via a random UUID
     161        wireMockRuntimeInfo.getWireMock().register(WireMock.get(WireMock.urlPathEqualTo("/oauth2/authorize")).withQueryParams(queryParams));
     162        wireMockRuntimeInfo.getWireMock().register(WireMock.post(WireMock.urlPathEqualTo("/oauth2/token")));
     163    }
     164
     165    @Test
     166    void testAuthorize(WireMockRuntimeInfo wireMockRuntimeInfo) throws IOException {
     167        final OAuth20Authorization authorization = new OAuth20Authorization();
     168        final AtomicReference<IOAuthToken> consumer = new AtomicReference<>();
     169        OAuth20Parameters parameters = (OAuth20Parameters) OAuthParameters.createDefault(OsmApi.getOsmApi().getBaseUrl(), OAuthVersion.OAuth20);
     170        RemoteControl.start();
     171        authorization.authorize(new OAuth20Parameters(parameters.getClientId(), parameters.getClientSecret(),
     172                wireMockRuntimeInfo.getHttpBaseUrl() + "/oauth2", wireMockRuntimeInfo.getHttpBaseUrl() + "/api",
     173                parameters.getRedirectUri()), consumer::set, OsmScopes.read_gpx);
     174        assertEquals(1, OpenBrowserMocker.getCalledURIs().size());
     175        HttpClient client = HttpClient.create(OpenBrowserMocker.getCalledURIs().get(0).toURL());
     176        try {
     177            HttpClient.Response response = client.connect();
     178            assertEquals(200, response.getResponseCode());
     179        } finally {
     180            client.disconnect();
     181        }
     182        assertNotNull(consumer.get());
     183        assertEquals(OAuthVersion.OAuth20, consumer.get().getOAuthType());
     184        OAuth20Token token = (OAuth20Token) consumer.get();
     185        assertEquals("test_access_token", token.getBearerToken());
     186    }
     187
     188    @Test
     189    void testAuthorizeBadState(WireMockRuntimeInfo wireMockRuntimeInfo) throws IOException {
     190        oauthServer.stateToReturn = "Bad_State";
     191        final OAuth20Authorization authorization = new OAuth20Authorization();
     192        final AtomicReference<IOAuthToken> consumer = new AtomicReference<>();
     193        OAuth20Parameters parameters = (OAuth20Parameters) OAuthParameters.createDefault(OsmApi.getOsmApi().getBaseUrl(), OAuthVersion.OAuth20);
     194        RemoteControl.start();
     195        authorization.authorize(new OAuth20Parameters(parameters.getClientId(), parameters.getClientSecret(),
     196                wireMockRuntimeInfo.getHttpBaseUrl() + "/oauth2", wireMockRuntimeInfo.getHttpBaseUrl() + "/api",
     197                parameters.getRedirectUri()), consumer::set, OsmScopes.read_gpx);
     198        assertEquals(1, OpenBrowserMocker.getCalledURIs().size());
     199        HttpClient client = HttpClient.create(OpenBrowserMocker.getCalledURIs().get(0).toURL());
     200        try {
     201            HttpClient.Response response = client.connect();
     202            assertEquals(400, response.getResponseCode());
     203            String content = response.fetchContent();
     204            assertTrue(content.contains("Unknown state for authorization"));
     205        } finally {
     206            client.disconnect();
     207        }
     208        assertNull(consumer.get());
     209    }
     210}
  • new file test/unit/org/openstreetmap/josm/io/auth/CredentialsAgentTest.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/test/unit/org/openstreetmap/josm/io/auth/CredentialsAgentTest.java b/test/unit/org/openstreetmap/josm/io/auth/CredentialsAgentTest.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.io.auth;
     3
     4import static org.junit.jupiter.api.Assertions.assertArrayEquals;
     5import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
     6import static org.junit.jupiter.api.Assertions.assertEquals;
     7import static org.junit.jupiter.api.Assertions.assertNotNull;
     8import static org.junit.jupiter.api.Assertions.assertNull;
     9
     10import java.net.Authenticator;
     11import java.net.PasswordAuthentication;
     12import java.util.Arrays;
     13import java.util.List;
     14
     15import org.junit.jupiter.api.Test;
     16import org.junit.jupiter.params.ParameterizedTest;
     17import org.junit.jupiter.params.provider.MethodSource;
     18import org.openstreetmap.josm.data.oauth.OAuth20Exception;
     19import org.openstreetmap.josm.data.oauth.OAuth20Parameters;
     20import org.openstreetmap.josm.data.oauth.OAuth20Token;
     21import org.openstreetmap.josm.data.oauth.OAuthToken;
     22import org.openstreetmap.josm.io.OsmApi;
     23import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
     24
     25/**
     26 * Test interface for {@link CredentialsAgent} implementations.
     27 */
     28@BasicPreferences
     29public interface CredentialsAgentTest<T extends CredentialsAgent> {
     30    /**
     31     * Create the agent to test
     32     * @return The agent to test
     33     */
     34    T createAgent();
     35
     36    static List<String> getHosts() {
     37        return Arrays.asList("https://somewhere.random", OsmApi.getOsmApi().getHost());
     38    }
     39
     40    @ParameterizedTest
     41    @MethodSource("getHosts")
     42    @BasicPreferences // We need to reset preferences between runs
     43    default void testLookUpAndStorePasswordAuthentication(final String host) throws CredentialsAgentException {
     44        final T agent = createAgent();
     45        for (Authenticator.RequestorType type : Authenticator.RequestorType.values()) {
     46            PasswordAuthentication passwordAuthentication = agent.lookup(type, host);
     47            assertNull(passwordAuthentication, "Password authentication should not be set up yet");
     48            PasswordAuthentication toStore = new PasswordAuthentication("hunter", "password".toCharArray());
     49            agent.store(type, host, toStore);
     50            passwordAuthentication = agent.lookup(type, host);
     51            assertNotNull(passwordAuthentication);
     52            // We can't just use equals, since PasswordAuthentication does not override the default equals method
     53            assertEquals(toStore.getUserName(), passwordAuthentication.getUserName());
     54            assertArrayEquals(toStore.getPassword(), passwordAuthentication.getPassword());
     55            // This is what sets the Config values. Note that PasswordAuthentication cannot take a null password.
     56            agent.store(type, host, new PasswordAuthentication("hunter", new char[0]));
     57            // Now we need to purge the cache
     58            agent.purgeCredentialsCache(type);
     59            passwordAuthentication = agent.lookup(type, host);
     60            assertEquals(toStore.getUserName(), passwordAuthentication.getUserName());
     61            assertArrayEquals(new char[0], passwordAuthentication.getPassword());
     62            // We don't currently have a way to fully remove credentials, but that ought to be tested here.
     63        }
     64    }
     65
     66    @Test
     67    default void testLookUpAndStorePasswordAuthenticationNull() throws CredentialsAgentException {
     68        final T agent = createAgent();
     69        assertDoesNotThrow(() -> agent.store(null, "https://somewhere.random", new PasswordAuthentication("random", new char[0])));
     70        assertNull(agent.lookup(null, "https://somewhere.random"));
     71        assertDoesNotThrow(() -> agent.store(Authenticator.RequestorType.SERVER, null, new PasswordAuthentication("random", new char[0])));
     72        for (Authenticator.RequestorType type : Authenticator.RequestorType.values()) {
     73            assertNull(agent.lookup(type, null));
     74        }
     75        assertNull(agent.lookup(null, null));
     76    }
     77
     78    @Test
     79    default void testLookUpAndStoreOAuth10() throws CredentialsAgentException {
     80        final T agent = createAgent();
     81        assertNull(agent.lookupOAuthAccessToken());
     82        final OAuthToken token = new OAuthToken("foo", "bar");
     83        agent.storeOAuthAccessToken(token);
     84        final OAuthToken actual = agent.lookupOAuthAccessToken();
     85        assertEquals(token, actual);
     86        agent.storeOAuthAccessToken(null);
     87        assertNull(agent.lookupOAuthAccessToken());
     88    }
     89
     90    @ParameterizedTest
     91    @MethodSource("getHosts")
     92    default void testLookupAndStoreOAuthTokens(final String host) throws CredentialsAgentException, OAuth20Exception {
     93        final T agent = createAgent();
     94        assertNull(agent.lookupOAuthAccessToken(host));
     95        agent.storeOAuthAccessToken(host, new OAuth20Token(new OAuth20Parameters("clientId", "clientSecret",
     96                "tokenUrl", "authorizeUrl", "apiUrl", "redirectUrl"),
     97                "{\"access_token\": \"test_token\", \"token_type\": \"bearer\"}"));
     98        OAuth20Token token = (OAuth20Token) agent.lookupOAuthAccessToken(host);
     99        assertNotNull(token);
     100        assertEquals("test_token", token.getBearerToken());
     101        agent.storeOAuthAccessToken(host, null);
     102        assertNull(agent.lookupOAuthAccessToken(host));
     103    }
     104}
  • new file test/unit/org/openstreetmap/josm/io/auth/CredentialsManagerTest.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/test/unit/org/openstreetmap/josm/io/auth/CredentialsManagerTest.java b/test/unit/org/openstreetmap/josm/io/auth/CredentialsManagerTest.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.io.auth;
     3
     4import org.openstreetmap.josm.testutils.annotations.HTTP;
     5
     6/**
     7 * Test class for {@link CredentialsManager}
     8 */
     9@HTTP
     10class CredentialsManagerTest implements CredentialsAgentTest<CredentialsManager> {
     11    @Override
     12    public CredentialsManager createAgent() {
     13        return new CredentialsManager(new JosmPreferencesCredentialAgent());
     14    }
     15}
  • new file test/unit/org/openstreetmap/josm/io/auth/JosmPreferencesCredentialAgentTest.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/test/unit/org/openstreetmap/josm/io/auth/JosmPreferencesCredentialAgentTest.java b/test/unit/org/openstreetmap/josm/io/auth/JosmPreferencesCredentialAgentTest.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.io.auth;
     3
     4/**
     5 * Test {@link JosmPreferencesCredentialAgent}
     6 */
     7class JosmPreferencesCredentialAgentTest implements CredentialsAgentTest<JosmPreferencesCredentialAgent> {
     8
     9    @Override
     10    public JosmPreferencesCredentialAgent createAgent() {
     11        return new JosmPreferencesCredentialAgent();
     12    }
     13}
  • new file test/unit/org/openstreetmap/josm/io/remotecontrol/handler/AuthorizationHandlerTest.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/test/unit/org/openstreetmap/josm/io/remotecontrol/handler/AuthorizationHandlerTest.java b/test/unit/org/openstreetmap/josm/io/remotecontrol/handler/AuthorizationHandlerTest.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.io.remotecontrol.handler;
     3
     4import static org.junit.jupiter.api.Assertions.assertAll;
     5import static org.junit.jupiter.api.Assertions.assertArrayEquals;
     6import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
     7import static org.junit.jupiter.api.Assertions.assertEquals;
     8import static org.junit.jupiter.api.Assertions.assertFalse;
     9import static org.junit.jupiter.api.Assertions.assertNull;
     10import static org.junit.jupiter.api.Assertions.assertThrows;
     11import static org.junit.jupiter.api.Assertions.assertTrue;
     12
     13import java.util.Map;
     14
     15import org.junit.jupiter.api.Test;
     16import org.openstreetmap.josm.data.preferences.BooleanProperty;
     17
     18/**
     19 * Test class for {@link AuthorizationHandler}
     20 */
     21class AuthorizationHandlerTest {
     22    private static class TestAuthorizationConsumer implements AuthorizationHandler.AuthorizationConsumer {
     23        boolean validated;
     24        boolean handled;
     25        @Override
     26        public void validateRequest(String sender, String request, Map<String, String> args)
     27                throws RequestHandler.RequestHandlerBadRequestException {
     28            this.validated = true;
     29        }
     30
     31        @Override
     32        public AuthorizationHandler.ResponseRecord handleRequest(String sender, String request, Map<String, String> args)
     33                throws RequestHandler.RequestHandlerErrorException, RequestHandler.RequestHandlerBadRequestException {
     34            this.handled = true;
     35            return null;
     36        }
     37    }
     38
     39    @Test
     40    void testValidateAndHandleRequest() {
     41        final AuthorizationHandler handler = new AuthorizationHandler();
     42        handler.request = "test_state";
     43        TestAuthorizationConsumer consumer = new TestAuthorizationConsumer();
     44        AuthorizationHandler.addAuthorizationConsumer("test_state", consumer);
     45        assertAll(() -> assertDoesNotThrow(handler::validateRequest),
     46                () -> assertDoesNotThrow(handler::handleRequest),
     47                () -> assertTrue(consumer.validated),
     48                () -> assertTrue(consumer.handled));
     49        // The consumer should only ever be called once
     50        consumer.validated = false;
     51        consumer.handled = false;
     52        assertAll(() -> assertThrows(RequestHandler.RequestHandlerBadRequestException.class, handler::validateRequest),
     53                () -> assertThrows(NullPointerException.class, handler::handleRequest),
     54                () -> assertFalse(consumer.validated),
     55                () -> assertFalse(consumer.handled));
     56        // Check to make certain that a bad state doesn't work
     57        AuthorizationHandler.addAuthorizationConsumer("testState", consumer);
     58        AuthorizationHandler.addAuthorizationConsumer("test_state", consumer);
     59        assertThrows(IllegalArgumentException.class, () -> AuthorizationHandler.addAuthorizationConsumer("test_state", consumer));
     60        handler.request = "testState test_state";
     61        assertAll(() -> assertThrows(RequestHandler.RequestHandlerBadRequestException.class, handler::validateRequest),
     62                () -> assertThrows(NullPointerException.class, handler::handleRequest),
     63                () -> assertFalse(consumer.validated),
     64                () -> assertFalse(consumer.handled));
     65    }
     66
     67    @Test
     68    void testGetPermissionMessage() {
     69        assertEquals("Allow OAuth remote control to set credentials", new AuthorizationHandler().getPermissionMessage());
     70    }
     71
     72    @Test
     73    void testGetPermissionPref() {
     74        assertNull(new AuthorizationHandler().getPermissionPref());
     75    }
     76
     77    @Test
     78    void testGetPermissionPreference() {
     79        final BooleanProperty property = new AuthorizationHandler().getPermissionPreference();
     80        assertEquals("remotecontrol.permission.authorization", property.getKey());
     81        assertFalse(property.getDefaultValue());
     82    }
     83
     84    @Test
     85    void testGetMandatoryParams() {
     86        assertArrayEquals(new String[0], new AuthorizationHandler().getMandatoryParams());
     87    }
     88}