Ticket #20768: 21607.patch

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

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/data/oauth/IOAuthAuthorization.java b/core/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 core/src/org/openstreetmap/josm/data/oauth/IOAuthParameters.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/data/oauth/IOAuthParameters.java b/core/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 core/src/org/openstreetmap/josm/data/oauth/IOAuthToken.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/data/oauth/IOAuthToken.java b/core/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 core/src/org/openstreetmap/josm/data/oauth/OAuth20Authorization.java

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

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/data/oauth/OAuth20Exception.java b/core/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 */
     9public class OAuth20Exception extends OAuthException {
     10    /**
     11     * Invalid request types
     12     */
     13    public enum Type {
     14        invalid_request,
     15        invalid_client,
     16        invalid_grant,
     17        unauthorized_client,
     18        unsupported_grant_type,
     19        invalid_scope,
     20        unknown
     21    }
     22    private final Type type;
     23
     24    public OAuth20Exception(Exception cause) {
     25        super(cause);
     26        this.type = Type.unknown;
     27    }
     28
     29    public OAuth20Exception(String message) {
     30        super(message);
     31        this.type = Type.unknown;
     32    }
     33
     34    OAuth20Exception(JsonObject serverMessage) {
     35        super(serverMessage.getString("error_description", serverMessage.getString("error", "Unknown error")));
     36        if (serverMessage.containsKey("error")) {
     37            switch(serverMessage.getString("error")) {
     38                case "invalid_request":
     39                case "invalid_client":
     40                case "invalid_grant":
     41                case "unauthorized_client":
     42                case "unsupported_grant_type":
     43                case "invalid_scope":
     44                    this.type = Type.valueOf(serverMessage.getString("error"));
     45                    break;
     46                default:
     47                    this.type = Type.unknown;
     48            }
     49        } else {
     50            this.type = Type.unknown;
     51        }
     52    }
     53
     54    @Override
     55    OAuthVersion[] getOAuthVersions() {
     56        return new OAuthVersion[] {OAuthVersion.OAuth20};
     57    }
     58
     59    @Override
     60    public String getMessage() {
     61        String message = super.getMessage();
     62        if (message == null) {
     63            return "OAuth error " + this.type;
     64        }
     65        return "OAuth error " + this.type + ": " + message;
     66    }
     67}
  • new file core/src/org/openstreetmap/josm/data/oauth/OAuth20Parameters.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/data/oauth/OAuth20Parameters.java b/core/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        // Alternatively, we could try using rfc8414 ( /.well-known/oauth-authorization-server ), but OSM doesn't support it.
     89        this.redirectUri = redirectUri;
     90        this.clientId = clientId;
     91        this.clientSecret = clientSecret;
     92        this.tokenUrl = tokenUrl;
     93        this.authorizeUrl = authorizeUrl;
     94        this.apiUrl = apiUrl;
     95    }
     96
     97    @Override
     98    public String getAccessTokenUrl() {
     99        return this.tokenUrl;
     100    }
     101
     102    @Override
     103    public String getAuthorizationUrl() {
     104        return this.authorizeUrl;
     105    }
     106
     107    @Override
     108    public OAuthVersion getOAuthVersion() {
     109        return OAuthVersion.OAuth20;
     110    }
     111
     112    @Override
     113    public String getClientId() {
     114        return this.clientId;
     115    }
     116
     117    @Override
     118    public String getClientSecret() {
     119        return this.clientSecret;
     120    }
     121
     122    @Override
     123    public String getRedirectUri() {
     124        return this.redirectUri;
     125    }
     126
     127    @Override
     128    public String getApiUrl() {
     129        return this.apiUrl;
     130    }
     131
     132    @Override
     133    public void rememberPreferences() {
     134        Config.getPref().put("oauth.access-token.parameters." + OAuthVersion.OAuth20 + "." + this.apiUrl,
     135                this.toPreferencesString());
     136    }
     137
     138    @Override
     139    public String toPreferencesString() {
     140        JsonObjectBuilder builder = Json.createObjectBuilder();
     141        builder.add(CLIENT_ID, this.clientId);
     142        builder.add(REDIRECT_URI, this.redirectUri);
     143        if (this.apiUrl != null) builder.add(API_URL, this.apiUrl);
     144        if (this.authorizeUrl != null) builder.add(AUTHORIZE_URL, this.authorizeUrl);
     145        if (this.clientSecret != null) builder.add(CLIENT_SECRET, this.clientSecret);
     146        if (this.tokenUrl != null) builder.add(TOKEN_URL, this.tokenUrl);
     147        return builder.build().toString();
     148    }
     149}
  • new file core/src/org/openstreetmap/josm/data/oauth/OAuth20Token.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/data/oauth/OAuth20Token.java b/core/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}
  • core/src/org/openstreetmap/josm/data/oauth/OAuthAccessTokenHolder.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/data/oauth/OAuthAccessTokenHolder.java b/core/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;
     10
    611import org.openstreetmap.josm.io.auth.CredentialsAgent;
    712import org.openstreetmap.josm.io.auth.CredentialsAgentException;
    813import org.openstreetmap.josm.spi.preferences.Config;
     
    3136    private String accessTokenKey;
    3237    private String accessTokenSecret;
    3338
     39    private final Map<String, Map<OAuthVersion, IOAuthToken>> tokenMap = new HashMap<>();
     40
    3441    /**
    3542     * Replies true if current access token should be saved to the preferences file.
    3643     *
     
    102109        return new OAuthToken(accessTokenKey, accessTokenSecret);
    103110    }
    104111
     112    /**
     113     * Replies the access token.
     114     * @param api The api the token is for
     115     * @param version The OAuth version the token is for
     116     * @return the access token, can be {@code null}
     117     */
     118    public IOAuthToken getAccessToken(String api, OAuthVersion version) {
     119        api = URI.create(api).getHost();
     120        if (this.tokenMap.containsKey(api)) {
     121            Map<OAuthVersion, IOAuthToken> map = this.tokenMap.get(api);
     122            return map.get(version);
     123        }
     124        return null;
     125    }
     126
    105127    /**
    106128     * Sets the access token hold by this holder.
    107129     *
     
    128150        }
    129151    }
    130152
     153    /**
     154     * Sets the access token hold by this holder.
     155     *
     156     * @param api The api the token is for
     157     * @param token the access token. Can be null to clear the content in this holder.
     158     */
     159    public void setAccessToken(String api, IOAuthToken token) {
     160        api = URI.create(api).getHost();
     161        if (token == null) {
     162            this.tokenMap.remove(api);
     163        } else {
     164            this.tokenMap.computeIfAbsent(api, key -> new EnumMap<>(OAuthVersion.class)).put(token.getOAuthType(), token);
     165        }
     166    }
     167
    131168    /**
    132169     * Replies true if this holder contains an complete access token, consisting of an
    133170     * Access Token Key and an Access Token Secret.
     
    175212        try {
    176213            if (!saveToPreferences) {
    177214                cm.storeOAuthAccessToken(null);
     215                for (String host : this.tokenMap.keySet()) {
     216                    cm.storeOAuthAccessToken(host, null);
     217                }
    178218            } else {
    179                 cm.storeOAuthAccessToken(new OAuthToken(accessTokenKey, accessTokenSecret));
     219                if (this.accessTokenKey != null && this.accessTokenSecret != null) {
     220                    cm.storeOAuthAccessToken(new OAuthToken(accessTokenKey, accessTokenSecret));
     221                }
     222                for (Map.Entry<String, Map<OAuthVersion, IOAuthToken>> entry : this.tokenMap.entrySet()) {
     223                    for (OAuthVersion version : OAuthVersion.values()) {
     224                        if (entry.getValue().containsKey(version)) {
     225                            cm.storeOAuthAccessToken(entry.getKey(), entry.getValue().get(version));
     226                        }
     227                    }
     228                }
    180229            }
    181230        } catch (CredentialsAgentException e) {
    182231            Logging.error(e);
  • new file core/src/org/openstreetmap/josm/data/oauth/OAuthException.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/data/oauth/OAuthException.java b/core/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}
  • core/src/org/openstreetmap/josm/data/oauth/OAuthParameters.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/data/oauth/OAuthParameters.java b/core/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.auth.CredentialsAgentException;
     16import org.openstreetmap.josm.io.auth.CredentialsManager;
    617import org.openstreetmap.josm.spi.preferences.Config;
    718import org.openstreetmap.josm.spi.preferences.IUrls;
    819import org.openstreetmap.josm.tools.CheckParameterUtil;
     20import org.openstreetmap.josm.tools.HttpClient;
     21import org.openstreetmap.josm.tools.Logging;
    922import org.openstreetmap.josm.tools.Utils;
    1023
    1124import oauth.signpost.OAuthConsumer;
     
    1528 * This class manages an immutable set of OAuth parameters.
    1629 * @since 2747
    1730 */
    18 public class OAuthParameters {
     31public class OAuthParameters implements IOAuthParameters {
    1932
    2033    /**
    2134     * The default JOSM OAuth consumer key (created by user josmeditor).
     
    4659     * @since 5422
    4760     */
    4861    public static OAuthParameters createDefault(String apiUrl) {
     62        return (OAuthParameters) createDefault(apiUrl, OAuthVersion.OAuth10a);
     63    }
     64
     65    /**
     66     * Replies a set of default parameters for a consumer accessing an OSM server
     67     * at the given API url. URL parameters are only set if the URL equals {@link IUrls#getDefaultOsmApiUrl}
     68     * or references the domain "dev.openstreetmap.org", otherwise they may be <code>null</code>.
     69     *
     70     * @param apiUrl The API URL for which the OAuth default parameters are created. If null or empty, the default OSM API url is used.
     71     * @param oAuthVersion The OAuth version to create default parameters for
     72     * @return a set of default parameters for the given {@code apiUrl}
     73     * @since xxx
     74     */
     75    public static IOAuthParameters createDefault(String apiUrl, OAuthVersion oAuthVersion) {
     76        if (!Utils.isValidUrl(apiUrl)) {
     77            apiUrl = null;
     78        }
     79
     80        switch (oAuthVersion) {
     81            case OAuth10a:
     82                return getDefaultOAuth10Parameters(apiUrl);
     83            case OAuth20:
     84            case OAuth21: // For now, OAuth 2.1 (draft) is just OAuth 2.0 with mandatory extensions, which we implement.
     85                return getDefaultOAuth20Parameters(apiUrl);
     86            default:
     87                throw new IllegalArgumentException("Unknown OAuth version: " + oAuthVersion);
     88        }
     89    }
     90
     91    /**
     92     * Get the default OAuth 2.0 parameters
     93     * @param apiUrl The API url
     94     * @return The default parameters
     95     */
     96    private static OAuth20Parameters getDefaultOAuth20Parameters(String apiUrl) {
     97        final String clientId;
     98        final String clientSecret;
     99        final String redirectUri;
     100        final String baseUrl;
     101        if (apiUrl != null && !Config.getUrls().getDefaultOsmApiUrl().equals(apiUrl)) {
     102            clientId = "";
     103            clientSecret = "";
     104            baseUrl = apiUrl;
     105            HttpClient client = null;
     106            redirectUri = "";
     107            // Check if the server is RFC 8414 compliant
     108            try {
     109                client = HttpClient.create(new URL(apiUrl + (apiUrl.endsWith("/") ? "" : "/") + ".well-known/oauth-authorization-server"));
     110                HttpClient.Response response = client.connect();
     111                if (response.getResponseCode() == 200) {
     112                    try (BufferedReader reader = response.getContentReader();
     113                         JsonReader jsonReader = Json.createReader(reader)) {
     114                        JsonStructure structure = jsonReader.read();
     115                        if (structure.getValueType() == JsonValue.ValueType.OBJECT) {
     116                            return parseAuthorizationServerMetadataResponse(clientId, clientSecret, apiUrl,
     117                                    redirectUri, structure.asJsonObject());
     118                        }
     119                    }
     120                }
     121            } catch (IOException | OAuthException e) {
     122                Logging.trace(e);
     123            } finally {
     124                if (client != null) client.disconnect();
     125            }
     126        } else {
     127            clientId = "jo32l-hRbwWni_lnwQxffvSEfW-5agfuzoT8A33IFkY";
     128            clientSecret = "OGH6-yYZHYAbw2J_LFS1yu1QiVFrCONhYWisftr6LRs";
     129            baseUrl = "https://www.openstreetmap.org/oauth2";
     130            redirectUri = "http://127.0.0.1:8111/oauth_authorization";
     131        }
     132        return new OAuth20Parameters(clientId, clientSecret, baseUrl, apiUrl, redirectUri);
     133    }
     134
     135    /**
     136     * Parse the response from <a href="https://www.rfc-editor.org/rfc/rfc8414.html">RFC 8414</a>
     137     * (OAuth 2.0 Authorization Server Metadata)
     138     * @return The parameters for the server metadata
     139     */
     140    private static OAuth20Parameters parseAuthorizationServerMetadataResponse(String clientId, String clientSecret,
     141                                                                              String apiUrl, String redirectUri,
     142                                                                              JsonObject serverMetadata)
     143            throws OAuthException {
     144        final String authorizationEndpoint = serverMetadata.getString("authorization_endpoint", null);
     145        final String tokenEndpoint = serverMetadata.getString("token_endpoint", null);
     146        // This may also have additional documentation like what the endpoints allow (e.g. scopes, algorithms, etc.)
     147        if (authorizationEndpoint == null || tokenEndpoint == null) {
     148            throw new OAuth20Exception("Either token endpoint or authorization endpoints are missing");
     149        }
     150        return new OAuth20Parameters(clientId, clientSecret, tokenEndpoint, authorizationEndpoint, apiUrl, redirectUri);
     151    }
     152
     153    /**
     154     * Get the default OAuth 1.0a parameters
     155     * @param apiUrl The api url
     156     * @return The default parameters
     157     */
     158    private static OAuthParameters getDefaultOAuth10Parameters(String apiUrl) {
    49159        final String consumerKey;
    50160        final String consumerSecret;
    51161        final String serverUrl;
    52162
    53         if (!Utils.isValidUrl(apiUrl)) {
    54             apiUrl = null;
    55         }
    56 
    57163        if (apiUrl != null && !Config.getUrls().getDefaultOsmApiUrl().equals(apiUrl)) {
    58164            consumerKey = ""; // a custom consumer key is required
    59165            consumerSecret = ""; // a custom consumer secret is requireds
     
    81187     * @return the parameters
    82188     */
    83189    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()));
     190        return (OAuthParameters) createFromApiUrl(apiUrl, OAuthVersion.OAuth10a);
     191    }
     192
     193    /**
     194     * Replies a set of parameters as defined in the preferences.
     195     *
     196     * @param oAuthVersion The OAuth version to use.
     197     * @param apiUrl the API URL. Must not be {@code null}.
     198     * @return the parameters
     199     * @since xxx
     200     */
     201    public static IOAuthParameters createFromApiUrl(String apiUrl, OAuthVersion oAuthVersion) {
     202        IOAuthParameters parameters = createDefault(apiUrl, oAuthVersion);
     203        switch (oAuthVersion) {
     204            case OAuth10a:
     205                OAuthParameters oauth10aParameters = (OAuthParameters) parameters;
     206                return new OAuthParameters(
     207                    Config.getPref().get("oauth.settings.consumer-key", oauth10aParameters.getConsumerKey()),
     208                    Config.getPref().get("oauth.settings.consumer-secret", oauth10aParameters.getConsumerSecret()),
     209                    Config.getPref().get("oauth.settings.request-token-url", oauth10aParameters.getRequestTokenUrl()),
     210                    Config.getPref().get("oauth.settings.access-token-url", oauth10aParameters.getAccessTokenUrl()),
     211                    Config.getPref().get("oauth.settings.authorise-url", oauth10aParameters.getAuthoriseUrl()),
     212                    Config.getPref().get("oauth.settings.osm-login-url", oauth10aParameters.getOsmLoginUrl()),
     213                    Config.getPref().get("oauth.settings.osm-logout-url", oauth10aParameters.getOsmLogoutUrl()));
     214            case OAuth20:
     215            case OAuth21: // Right now, OAuth 2.1 will work with our OAuth 2.0 implementation
     216                OAuth20Parameters oAuth20Parameters = (OAuth20Parameters) parameters;
     217                try {
     218                    IOAuthToken storedToken = CredentialsManager.getInstance().lookupOAuthAccessToken(apiUrl);
     219                    return storedToken.getParameters();
     220                } catch (CredentialsAgentException e) {
     221                    Logging.trace(e);
     222                }
     223                return oAuth20Parameters;
     224            default:
     225                throw new IllegalArgumentException("Unknown OAuth version: " + oAuthVersion);
     226        }
    93227    }
    94228
    95229    /**
    96230     * Remembers the current values in the preferences.
    97231     */
     232    @Override
    98233    public void rememberPreferences() {
    99234        Config.getPref().put("oauth.settings.consumer-key", getConsumerKey());
    100235        Config.getPref().put("oauth.settings.consumer-secret", getConsumerSecret());
     
    182317     * Gets the access token URL.
    183318     * @return The access token URL
    184319     */
     320    @Override
    185321    public String getAccessTokenUrl() {
    186322        return accessTokenUrl;
    187323    }
    188324
     325    @Override
     326    public String getAuthorizationUrl() {
     327        return this.authoriseUrl;
     328    }
     329
     330    @Override
     331    public OAuthVersion getOAuthVersion() {
     332        return OAuthVersion.OAuth10a;
     333    }
     334
     335    @Override
     336    public String getClientId() {
     337        return this.consumerKey;
     338    }
     339
     340    @Override
     341    public String getClientSecret() {
     342        return this.consumerSecret;
     343    }
     344
    189345    /**
    190346     * Gets the authorise URL.
    191347     * @return The authorise URL
    192348     */
    193349    public String getAuthoriseUrl() {
    194         return authoriseUrl;
     350        return this.getAuthorizationUrl();
    195351    }
    196352
    197353    /**
  • new file core/src/org/openstreetmap/josm/data/oauth/OAuthVersion.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/data/oauth/OAuthVersion.java b/core/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 core/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/core/src/org/openstreetmap/josm/data/oauth/osm/OsmScopes.java b/core/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 core/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/core/src/org/openstreetmap/josm/data/oauth/osm/package-info.java b/core/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;
  • core/src/org/openstreetmap/josm/gui/oauth/AbstractAuthorizationUI.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/gui/oauth/AbstractAuthorizationUI.java b/core/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
  • core/src/org/openstreetmap/josm/gui/oauth/AdvancedOAuthPropertiesPanel.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/gui/oauth/AdvancedOAuthPropertiesPanel.java b/core/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
     
    7480        add(cbUseDefaults, gc);
    7581
    7682        // -- consumer key
    77         gc.gridy = 1;
     83        gc.gridy++;
    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
  • core/src/org/openstreetmap/josm/gui/oauth/FullyAutomaticAuthorizationUI.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/gui/oauth/FullyAutomaticAuthorizationUI.java b/core/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)
  • core/src/org/openstreetmap/josm/gui/oauth/ManualAuthorizationUI.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/gui/oauth/ManualAuthorizationUI.java b/core/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);
  • core/src/org/openstreetmap/josm/gui/oauth/OAuthAuthorizationWizard.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/gui/oauth/OAuthAuthorizationWizard.java b/core/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    /**
  • core/src/org/openstreetmap/josm/gui/oauth/SemiAutomaticAuthorizationUI.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/gui/oauth/SemiAutomaticAuthorizationUI.java b/core/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);
  • core/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/core/src/org/openstreetmap/josm/gui/preferences/server/AuthenticationPreferencesPanel.java b/core/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        }
  • core/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/core/src/org/openstreetmap/josm/gui/preferences/server/OAuthAuthenticationPreferencesPanel.java b/core/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                if (!remoteControlIsRunning) {
     367                    RemoteControl.start();
     368                }
     369                new OAuth20Authorization().authorize(OAuthParameters.createDefault(OsmApi.getOsmApi().getServerUrl(), oAuthVersion), token -> {
     370                    if (!remoteControlIsRunning) {
     371                        RemoteControl.stop();
     372                    }
     373                    // Clean up old token/password
     374                    OAuthAccessTokenHolder.getInstance().setAccessToken(null);
     375                    OAuthAccessTokenHolder.getInstance().setAccessToken(OsmApi.getOsmApi().getServerUrl(), token);
     376                    OAuthAccessTokenHolder.getInstance().save(CredentialsManager.getInstance());
     377                    GuiHelper.runInEDT(OAuthAuthenticationPreferencesPanel.this::refreshView);
     378                }, OsmScopes.read_gpx, OsmScopes.write_gpx,
     379                        OsmScopes.read_prefs, OsmScopes.write_prefs,
     380                        OsmScopes.write_api, OsmScopes.write_notes);
     381            }
     382        }
     383    }
     384
     385    /**
     386     * Remove the OAuth authorization token
     387     */
     388    private class RemoveAuthorisationAction extends AbstractAction {
     389        RemoveAuthorisationAction() {
     390            putValue(NAME, tr("Remove token"));
     391            putValue(SHORT_DESCRIPTION, tr("Remove token from JOSM. This does not revoke the token."));
     392            new ImageProvider("cancel").getResource().attachImageIcon(this);
     393        }
     394
     395        @Override
     396        public void actionPerformed(ActionEvent e) {
     397            if (oAuthVersion == OAuthVersion.OAuth10a) {
     398                OAuthAccessTokenHolder.getInstance().setAccessToken(null);
     399            } else {
     400                OAuthAccessTokenHolder.getInstance().setAccessToken(apiUrl, (IOAuthToken) null);
     401            }
     402            OAuthAccessTokenHolder.getInstance().save(CredentialsManager.getInstance());
    317403            refreshView();
    318404        }
    319405    }
  • core/src/org/openstreetmap/josm/io/auth/CredentialsAgent.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/io/auth/CredentialsAgent.java b/core/src/org/openstreetmap/josm/io/auth/CredentialsAgent.java
    a b  
    55import java.net.Authenticator.RequestorType;
    66import java.net.PasswordAuthentication;
    77
     8import org.openstreetmap.josm.data.oauth.IOAuthToken;
    89import org.openstreetmap.josm.data.oauth.OAuthToken;
    910
    1011/**
     
    6465     */
    6566    OAuthToken lookupOAuthAccessToken() throws CredentialsAgentException;
    6667
     68    /**
     69     * Lookup the current OAuth Access Token to access the specifiedserver. Replies null, if no
     70     * Access Token is currently managed by this CredentialAgent.
     71     *
     72     * @param host The host to get OAuth credentials for
     73     * @return the current OAuth Access Token to access the specified server.
     74     * @throws CredentialsAgentException if something goes wrong
     75     */
     76    IOAuthToken lookupOAuthAccessToken(String host) throws CredentialsAgentException;
     77
    6778    /**
    6879     * Stores the OAuth Access Token <code>accessToken</code>.
    6980     *
     
    7283     */
    7384    void storeOAuthAccessToken(OAuthToken accessToken) throws CredentialsAgentException;
    7485
     86    /**
     87     * Stores the OAuth Access Token <code>accessToken</code>.
     88     *
     89     * @param host The host the access token is for
     90     * @param accessToken the access Token. null, to remove the Access Token.
     91     * @throws CredentialsAgentException if something goes wrong
     92     */
     93    void storeOAuthAccessToken(String host, IOAuthToken accessToken) throws CredentialsAgentException;
     94
    7595    /**
    7696     * Purges the internal credentials cache for the given requestor type.
    7797     * @param requestorType the type of service.
  • core/src/org/openstreetmap/josm/io/auth/CredentialsManager.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/io/auth/CredentialsManager.java b/core/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();
  • core/src/org/openstreetmap/josm/io/auth/JosmPreferencesCredentialAgent.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/io/auth/JosmPreferencesCredentialAgent.java b/core/src/org/openstreetmap/josm/io/auth/JosmPreferencesCredentialAgent.java
    a b  
    88import java.net.PasswordAuthentication;
    99import java.util.Objects;
    1010
     11import javax.json.JsonException;
    1112import javax.swing.text.html.HTMLEditorKit;
    1213
     14import org.openstreetmap.josm.data.oauth.IOAuthToken;
     15import org.openstreetmap.josm.data.oauth.OAuth20Exception;
     16import org.openstreetmap.josm.data.oauth.OAuth20Parameters;
     17import org.openstreetmap.josm.data.oauth.OAuth20Token;
    1318import org.openstreetmap.josm.data.oauth.OAuthToken;
     19import org.openstreetmap.josm.data.oauth.OAuthVersion;
    1420import org.openstreetmap.josm.gui.widgets.HtmlPanel;
    1521import org.openstreetmap.josm.io.DefaultProxySelector;
    1622import org.openstreetmap.josm.io.OsmApi;
    1723import org.openstreetmap.josm.spi.preferences.Config;
     24import org.openstreetmap.josm.tools.Utils;
    1825
    1926/**
    2027 * This is the default credentials agent in JOSM. It keeps username and password for both
     
    109116        return new OAuthToken(accessTokenKey, accessTokenSecret);
    110117    }
    111118
     119    @Override
     120    public IOAuthToken lookupOAuthAccessToken(String host) throws CredentialsAgentException {
     121        for (OAuthVersion oauthType : OAuthVersion.values()) {
     122            String token = Config.getPref().get("oauth.access-token.object." + oauthType + "." + host, null);
     123            String parameters = Config.getPref().get("oauth.access-token.parameters." + oauthType + "." + host, null);
     124            if (!Utils.isBlank(token) && !Utils.isBlank(parameters) && OAuthVersion.OAuth20 == oauthType) {
     125                try {
     126                    OAuth20Parameters oAuth20Parameters = new OAuth20Parameters(parameters);
     127                    return new OAuth20Token(oAuth20Parameters, token);
     128                } catch (OAuth20Exception | JsonException e) {
     129                    throw new CredentialsAgentException(e);
     130                }
     131            }
     132        }
     133
     134        throw new CredentialsAgentException("No OAuth token found for " + host);
     135    }
     136
    112137    /**
    113138     * Stores the OAuth Access Token <code>accessToken</code>.
    114139     *
     
    126151        }
    127152    }
    128153
     154    @Override
     155    public void storeOAuthAccessToken(String host, IOAuthToken accessToken) throws CredentialsAgentException {
     156        Objects.requireNonNull(host, "host");
     157        if (accessToken == null) {
     158            // Assume we want to remove all access tokens
     159            for (OAuthVersion oauthType : OAuthVersion.values()) {
     160                Config.getPref().put("oauth.access-token.object." + oauthType + "." + host, null);
     161                Config.getPref().put("oauth.access-token.parameters." + oauthType + "." + host, null);
     162            }
     163        } else {
     164            Config.getPref().put("oauth.access-token.object." + accessToken.getOAuthType() + "." + host,
     165                    accessToken.toPreferencesString());
     166            Config.getPref().put("oauth.access-token.parameters." + accessToken.getOAuthType() + "." + host,
     167                    accessToken.getParameters().toPreferencesString());
     168        }
     169    }
     170
    129171    @Override
    130172    public Component getPreferencesDecorationPanel() {
    131173        HtmlPanel pnlMessage = new HtmlPanel();
  • new file core/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/core/src/org/openstreetmap/josm/io/remotecontrol/handler/AuthorizationHandler.java b/core/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.Optional;
     7
     8import org.openstreetmap.josm.data.preferences.BooleanProperty;
     9import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
     10
     11/**
     12 * Handle authorization requests (mostly OAuth)
     13 */
     14public class AuthorizationHandler extends RequestHandler {
     15
     16    /**
     17     * The actual authorization consumer/handler
     18     */
     19    public interface AuthorizationConsumer {
     20        /**
     21         * Validate the request
     22         * @param args The GET request arguments
     23         * @param request The request URL without "GET".
     24         * @param sender who sent the request? the host from referer header or IP of request sender
     25         * @throws RequestHandlerBadRequestException if the request is invalid
     26         * @see RequestHandler#validateRequest()
     27         */
     28        void validateRequest(String sender, String request, Map<String, String> args)
     29                throws RequestHandlerBadRequestException;
     30
     31        /**
     32         * Handle the request. Any time-consuming operation must be performed asynchronously to avoid delaying the HTTP response.
     33         * @param args The GET request arguments
     34         * @param request The request URL without "GET".
     35         * @param sender who sent the request? the host from referer header or IP of request sender
     36         * @return The response to show the user. May be {@code null}.
     37         * @throws RequestHandlerErrorException if an error occurs while processing the request
     38         * @throws RequestHandlerBadRequestException if the request is invalid
     39         * @see RequestHandler#handleRequest()
     40         */
     41        ResponseRecord handleRequest(String sender, String request, Map<String, String> args)
     42                throws RequestHandlerErrorException, RequestHandlerBadRequestException;
     43    }
     44
     45    /**
     46     * A basic record for changing responses
     47     */
     48    public static final class ResponseRecord {
     49        private final String content;
     50        private final String type;
     51
     52        /**
     53         * Create a new record
     54         * @param content The content to show the user
     55         * @param type The content mime type
     56         */
     57        public ResponseRecord(String content, String type) {
     58            this.content = content;
     59            this.type = type;
     60        }
     61
     62        /**
     63         * Get the content for the response
     64         * @return The content as a string
     65         */
     66        public String content() {
     67            return this.content;
     68        }
     69
     70        /**
     71         * Get the type for the response
     72         * @return The response mime type
     73         */
     74        public String type() {
     75            return this.type;
     76        }
     77    }
     78
     79    /**
     80     * The remote control command
     81     */
     82    public static final String command = "oauth_authorization";
     83
     84    private static final BooleanProperty PROPERTY = new BooleanProperty("remotecontrol.permission.authorization", false);
     85    private static final Map<String, AuthorizationConsumer> AUTHORIZATION_CONSUMERS = new HashMap<>();
     86
     87    private AuthorizationConsumer consumer;
     88    /**
     89     * Add an authorization consumer.
     90     * @param state The unique state for each request (for OAuth, this would be the {@code state} parameter)
     91     * @param consumer The consumer of the response
     92     */
     93    public static synchronized void addAuthorizationConsumer(String state, AuthorizationConsumer consumer) {
     94        if (AUTHORIZATION_CONSUMERS.containsKey(state)) {
     95            throw new IllegalArgumentException("Cannot add multiple consumers for one authorization state");
     96        }
     97        AUTHORIZATION_CONSUMERS.put(state, consumer);
     98    }
     99
     100    @Override
     101    protected void validateRequest() throws RequestHandlerBadRequestException {
     102        boolean clearAll = false;
     103        for (Map.Entry<String, AuthorizationConsumer> entry : AUTHORIZATION_CONSUMERS.entrySet()) {
     104            if (this.request.contains(entry.getKey())) {
     105                if (this.consumer == null) {
     106                    this.consumer = entry.getValue();
     107                } else {
     108                    // Remove all authorization consumers. Someone might be playing games.
     109                    clearAll = true;
     110                }
     111            }
     112        }
     113        if (clearAll) {
     114            AUTHORIZATION_CONSUMERS.clear();
     115            throw new RequestHandlerBadRequestException("Multiple states for authorization");
     116        }
     117
     118        if (this.consumer == null) {
     119            throw new RequestHandlerBadRequestException("Unknown state for authorization");
     120        }
     121        this.consumer.validateRequest(this.sender, this.request, this.args);
     122    }
     123
     124    @Override
     125    protected void handleRequest() throws RequestHandlerErrorException, RequestHandlerBadRequestException {
     126        ResponseRecord response = this.consumer.handleRequest(this.sender, this.request, this.args);
     127        if (response != null) {
     128            this.content = Optional.ofNullable(response.content()).orElse(this.content);
     129            this.contentType = Optional.ofNullable(response.type()).orElse(this.contentType);
     130        }
     131    }
     132
     133    @Override
     134    public String getPermissionMessage() {
     135        return "Allow OAuth remote control to set credentials";
     136    }
     137
     138    @Override
     139    public PermissionPrefWithDefault getPermissionPref() {
     140        return null;
     141    }
     142
     143    public BooleanProperty getPermissionPreference() {
     144        return PROPERTY;
     145    }
     146
     147    @Override
     148    public String[] getMandatoryParams() {
     149        return new String[0];
     150    }
     151}
  • core/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java b/core/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
  • core/src/org/openstreetmap/josm/io/MessageNotifier.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/io/MessageNotifier.java b/core/src/org/openstreetmap/josm/io/MessageNotifier.java
    a b  
    144144            try {
    145145                if (JosmPreferencesCredentialAgent.class.equals(credManager.getCredentialsAgentClass())) {
    146146                    if (OsmApi.isUsingOAuth()) {
    147                         return credManager.lookupOAuthAccessToken() != null;
     147                        return credManager.lookupOAuthAccessToken() != null
     148                        || credManager.lookupOAuthAccessToken(OsmApi.getOsmApi().getHost()) != null;
    148149                    } else {
    149150                        String username = Config.getPref().get("osm-server.username", null);
    150151                        String password = Config.getPref().get("osm-server.password", null);
  • core/src/org/openstreetmap/josm/io/OsmApi.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/io/OsmApi.java b/core/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.OAuthVersion;
    3233import org.openstreetmap.josm.data.osm.Changeset;
    3334import org.openstreetmap.josm.data.osm.IPrimitive;
    3435import org.openstreetmap.josm.data.osm.OsmPrimitive;
     
    645646     * @since 6349
    646647     */
    647648    public static boolean isUsingOAuth() {
    648         return "oauth".equals(getAuthMethod());
     649        return isUsingOAuth(OAuthVersion.OAuth10a)
     650                || isUsingOAuth(OAuthVersion.OAuth20)
     651                || isUsingOAuth(OAuthVersion.OAuth21);
     652    }
     653
     654    /**
     655     * Determines if JOSM is configured to access OSM API via OAuth
     656     * @param version The OAuth version
     657     * @return {@code true} if JOSM is configured to access OSM API via OAuth, {@code false} otherwise
     658     * @since xxx
     659     */
     660    public static boolean isUsingOAuth(OAuthVersion version) {
     661        if (version == OAuthVersion.OAuth10a) {
     662            return "oauth".equalsIgnoreCase(getAuthMethod());
     663        } else if (version == OAuthVersion.OAuth20 || version == OAuthVersion.OAuth21) {
     664            return "oauth20".equalsIgnoreCase(getAuthMethod());
     665        }
     666        return false;
    649667    }
    650668
    651669    /**
     
    653671     * @return the authentication method
    654672     */
    655673    public static String getAuthMethod() {
    656         return Config.getPref().get("osm-server.auth-method", "oauth");
     674        return Config.getPref().get("osm-server.auth-method", "oauth20"); // fixme switch to oauth20 by default
    657675    }
    658676
    659677    protected final String sendPostRequest(String urlSuffix, String requestBody, ProgressMonitor monitor) throws OsmTransferException {
  • core/src/org/openstreetmap/josm/io/OsmConnection.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/io/OsmConnection.java b/core/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(OsmApi.getOsmApi().getServerUrl(), 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
  • core/src/org/openstreetmap/josm/io/OsmServerReader.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/core/src/org/openstreetmap/josm/io/OsmServerReader.java b/core/src/org/openstreetmap/josm/io/OsmServerReader.java
    a b  
    4949    protected OsmServerReader() {
    5050        try {
    5151            doAuthenticate = OsmApi.isUsingOAuth()
    52                     && CredentialsManager.getInstance().lookupOAuthAccessToken() != null
     52                    && (CredentialsManager.getInstance().lookupOAuthAccessToken() != null
     53                        || CredentialsManager.getInstance().lookupOAuthAccessToken(this.api.getHost()) != null)
    5354                    && OsmApi.USE_OAUTH_FOR_ALL_REQUESTS.get();
    5455        } catch (CredentialsAgentException e) {
    5556            Logging.warn(e);
  • plugins/native-password-manager/src/org/openstreetmap/josm/plugins/npm/NPMCredentialsAgent.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/plugins/native-password-manager/src/org/openstreetmap/josm/plugins/npm/NPMCredentialsAgent.java b/plugins/native-password-manager/src/org/openstreetmap/josm/plugins/npm/NPMCredentialsAgent.java
    a b  
    66import java.awt.Component;
    77import java.net.Authenticator.RequestorType;
    88import java.net.PasswordAuthentication;
     9import java.net.URI;
    910import java.nio.charset.StandardCharsets;
    1011import java.util.ArrayList;
    11 import java.util.HashMap;
     12import java.util.EnumMap;
    1213import java.util.List;
    1314import java.util.Map;
    1415import java.util.zip.CRC32;
     
    1718
    1819import org.netbeans.spi.keyring.KeyringProvider;
    1920import org.openstreetmap.josm.data.Preferences;
     21import org.openstreetmap.josm.data.oauth.IOAuthToken;
     22import org.openstreetmap.josm.data.oauth.OAuth20Exception;
     23import org.openstreetmap.josm.data.oauth.OAuth20Parameters;
     24import org.openstreetmap.josm.data.oauth.OAuth20Token;
    2025import org.openstreetmap.josm.data.oauth.OAuthToken;
     26import org.openstreetmap.josm.data.oauth.OAuthVersion;
    2127import org.openstreetmap.josm.gui.widgets.HtmlPanel;
    2228import org.openstreetmap.josm.io.DefaultProxySelector;
    2329import org.openstreetmap.josm.io.OsmApi;
     
    2531import org.openstreetmap.josm.io.auth.CredentialsAgentException;
    2632import org.openstreetmap.josm.spi.preferences.Config;
    2733
     34/**
     35 * The native password manager credentials agent
     36 */
    2837public class NPMCredentialsAgent extends AbstractCredentialsAgent {
    2938
    3039    private KeyringProvider provider;
    31     private NPMType type;
     40    private final NPMType type;
    3241   
    3342    /**
    34      * Cache the results since there might be pop ups and password prompts from
     43     * Cache the results since there might be pop-ups and password prompts from
    3544     * the native manager. This can get annoying, if it shows too often.
    36      *
     45     * <br>
    3746     * Yes, there is another cache in AbstractCredentialsAgent. It is used
    3847     * to avoid prompting the user for login multiple times in one session,
    3948     * when they decide not to save the credentials.
    4049     * In contrast, this cache avoids read request the backend in general.
    4150     */
    42     private Map<RequestorType, PasswordAuthentication> credentialsCache = new HashMap<>();
     51    private final Map<RequestorType, PasswordAuthentication> credentialsCache = new EnumMap<>(RequestorType.class);
    4352    private OAuthToken oauthCache;
    44    
     53
     54    /**
     55     * Create a new {@link NPMCredentialsAgent}
     56     * @param type The backend storage type
     57     */
    4558    public NPMCredentialsAgent(NPMType type) {
    4659        this.type = type;
    4760    }
     
    95108    }
    96109   
    97110    @Override
    98     public PasswordAuthentication lookup(RequestorType rt, String host) throws CredentialsAgentException {
     111    public PasswordAuthentication lookup(RequestorType rt, String host) {
    99112        PasswordAuthentication cache = credentialsCache.get(rt);
    100113        if (cache != null)
    101114            return cache;
     
    125138    }
    126139
    127140    @Override
    128     public void store(RequestorType rt, String host, PasswordAuthentication credentials) throws CredentialsAgentException {
     141    public void store(RequestorType rt, String host, PasswordAuthentication credentials) {
    129142        char[] username, password;
    130143        if (credentials == null) {
    131144            username = null;
     
    169182            } else {
    170183                getProvider().save(prefix+".password", password, passwordDescription);
    171184            }
    172             credentialsCache.put(rt, new PasswordAuthentication(stringNotNull(username), password));
     185            credentialsCache.put(rt, new PasswordAuthentication(stringNotNull(username), password != null ? password : new char[0]));
    173186        }
    174187    }
    175188
    176189    @Override
    177     public OAuthToken lookupOAuthAccessToken() throws CredentialsAgentException {
     190    public OAuthToken lookupOAuthAccessToken() {
    178191        if (oauthCache != null)
    179192            return oauthCache;
    180193        String prolog = getOAuthDescriptor();
     
    184197    }
    185198
    186199    @Override
    187     public void storeOAuthAccessToken(OAuthToken oat) throws CredentialsAgentException {
     200    public IOAuthToken lookupOAuthAccessToken(String host) throws CredentialsAgentException {
     201        String prolog = getOAuthDescriptor();
     202        OAuthVersion[] versions = OAuthVersion.values();
     203        // Prefer newer OAuth protocols
     204        for (int i = versions.length - 1; i >= 0; i--) {
     205            OAuthVersion version = versions[i];
     206            char[] tokenObject = getProvider().read(prolog + ".object." + version + "." + host);
     207            char[] parametersObject = getProvider().read(prolog + ".parameters." + version + "." + host);
     208            if (version == OAuthVersion.OAuth20 // There is currently only an OAuth 2.0 path
     209                    && tokenObject != null && tokenObject.length > 0
     210                    && parametersObject != null && parametersObject.length > 0) {
     211                OAuth20Parameters oAuth20Parameters = new OAuth20Parameters(stringNotNull(parametersObject));
     212                try {
     213                    return new OAuth20Token(oAuth20Parameters, stringNotNull(tokenObject));
     214                } catch (OAuth20Exception e) {
     215                    throw new CredentialsAgentException(e);
     216                }
     217            }
     218        }
     219        return null;
     220    }
     221
     222    @Override
     223    public void storeOAuthAccessToken(OAuthToken oat) {
    188224        String key, secret;
    189225        if (oat == null) {
    190226            key = null;
    191227            secret = null;
    192         }
    193         else {
     228        } else {
    194229            key = oat.getKey();
    195230            secret = oat.getSecret();
    196231        }
     
    206241        }
    207242    }
    208243
     244    @Override
     245    public void storeOAuthAccessToken(String host, IOAuthToken accessToken) {
     246        String prolog = getOAuthDescriptor();
     247        if (accessToken == null) {
     248            for (OAuthVersion version : OAuthVersion.values()) {
     249                getProvider().delete(prolog + ".object." + version + "." + host);
     250                getProvider().delete(prolog + ".parameters." + version + "." + host);
     251            }
     252        } else {
     253            OAuthVersion oauthType = accessToken.getOAuthType();
     254            getProvider().save(prolog + ".object." + oauthType + "." + host,
     255                    accessToken.toPreferencesString().toCharArray(),
     256                    tr("JOSM/OAuth/{0}/Token", URI.create(host).getHost()));
     257            getProvider().save(prolog + ".parameters." + oauthType + "." + host,
     258                    accessToken.getParameters().toPreferencesString().toCharArray(),
     259                    tr("JOSM/OAuth/{0}/Parameters", URI.create(host).getHost()));
     260        }
     261    }
     262
    209263    private static String stringNotNull(char[] charData) {
    210264        if (charData == null)
    211265            return "";
     
    216270    public Component getPreferencesDecorationPanel() {
    217271        HtmlPanel pnlMessage = new HtmlPanel();
    218272        HTMLEditorKit kit = (HTMLEditorKit)pnlMessage.getEditorPane().getEditorKit();
    219         kit.getStyleSheet().addRule(".warning-body {background-color:rgb(253,255,221);padding: 10pt; border-color:rgb(128,128,128);border-style: solid;border-width: 1px;}");
     273        kit.getStyleSheet().addRule(".warning-body {background-color:rgb(253,255,221);padding: 10pt;" +
     274                "border-color:rgb(128,128,128);border-style: solid;border-width: 1px;}");
    220275        StringBuilder text = new StringBuilder();
    221         text.append("<html><body>"
    222                     + "<p class=\"warning-body\">"
    223                     + "<strong>"+tr("Native Password Manager Plugin")+"</strong><br>"
    224                     + tr("The username and password is protected by {0}.", type.getName())
    225         );
     276        text.append("<html><body><p class=\"warning-body\"><strong>")
     277                .append(tr("Native Password Manager Plugin"))
     278                .append("</strong><br>")
     279                .append(tr("The username and password is protected by {0}.", type.getName()));
    226280        List<String> sensitive = new ArrayList<>();
    227281        if (Config.getPref().get("osm-server.username", null) != null) {
    228282            sensitive.add(tr("username"));
     
    243297            sensitive.add(tr("oauth secret"));
    244298        }
    245299        if (!sensitive.isEmpty()) {
    246             text.append(tr("<br><strong>Warning:</strong> There may be sensitive data left in your preference file. ({0})", String.join(", ", sensitive)));
     300            text.append(tr("<br><strong>Warning:</strong> There may be sensitive data left in your preference file. ({0})",
     301                    String.join(", ", sensitive)));
    247302        }
    248303        pnlMessage.setText(text.toString());
    249304        return pnlMessage;