Index: trunk/src/org/openstreetmap/josm/data/UserIdentityManager.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/UserIdentityManager.java	(revision 18649)
+++ trunk/src/org/openstreetmap/josm/data/UserIdentityManager.java	(revision 18650)
@@ -6,5 +6,4 @@
 import java.text.MessageFormat;
 
-import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
 import org.openstreetmap.josm.data.osm.User;
 import org.openstreetmap.josm.data.osm.UserInfo;
@@ -69,6 +68,5 @@
         if (instance == null) {
             instance = new UserIdentityManager();
-            if (OsmApi.isUsingOAuth() && OAuthAccessTokenHolder.getInstance().containsAccessToken() &&
-                    !NetworkManager.isOffline(OnlineResource.OSM_API)) {
+            if (OsmApi.isUsingOAuthAndOAuthSetUp(OsmApi.getOsmApi()) && !NetworkManager.isOffline(OnlineResource.OSM_API)) {
                 try {
                     instance.initFromOAuth();
@@ -306,5 +304,10 @@
             break;
         default: // Do nothing
-        }
+            if (evt.getKey() != null && evt.getKey().equals("oauth.access-token.parameters.OAuth20." + OsmApi.getOsmApi().getHost())) {
+                accessTokenKeyChanged = true;
+                accessTokenSecretChanged = true;
+            }
+        }
+        // oauth.access-token.parameters.OAuth20.api.openstreetmap.org
 
         if (accessTokenKeyChanged && accessTokenSecretChanged) {
Index: trunk/src/org/openstreetmap/josm/data/oauth/IOAuthAuthorization.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/oauth/IOAuthAuthorization.java	(revision 18650)
+++ trunk/src/org/openstreetmap/josm/data/oauth/IOAuthAuthorization.java	(revision 18650)
@@ -0,0 +1,19 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.oauth;
+
+import java.util.function.Consumer;
+
+/**
+ * Interface for OAuth authorization classes
+ * @author Taylor Smock
+ * @since 18650
+ */
+public interface IOAuthAuthorization {
+    /**
+     * Perform the authorization dance
+     * @param parameters The OAuth parameters
+     * @param consumer The callback for the generated token
+     * @param scopes The scopes to ask for
+     */
+    void authorize(IOAuthParameters parameters, Consumer<IOAuthToken> consumer, Enum<?>... scopes);
+}
Index: trunk/src/org/openstreetmap/josm/data/oauth/IOAuthParameters.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/oauth/IOAuthParameters.java	(revision 18650)
+++ trunk/src/org/openstreetmap/josm/data/oauth/IOAuthParameters.java	(revision 18650)
@@ -0,0 +1,94 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.oauth;
+
+import java.util.stream.Stream;
+
+/**
+ * A generic interface for OAuth Parameters
+ * @author Taylor Smock
+ * @since 18650
+ */
+public interface IOAuthParameters {
+    /**
+     * Get the access token URL
+     * @return The URL to use to switch the code to a token
+     */
+    String getAccessTokenUrl();
+
+    /**
+     * Get the base authorization URL to open in a browser
+     * @return The base URL to send to the browser
+     */
+    String getAuthorizationUrl();
+
+    /**
+     * Get the authorization URL to open in a browser
+     * @param state The state to prevent attackers from providing their own token
+     * @param scopes The scopes to request
+     * @return The URL to send to the browser
+     */
+    default String getAuthorizationUrl(String state, Enum<?>... scopes) {
+        return this.getAuthorizationUrl(state, Stream.of(scopes).map(Enum::toString).toArray(String[]::new));
+    }
+
+    /**
+     * Get the authorization URL to open in a browser
+     * @param state The state to prevent attackers from providing their own token
+     * @param scopes The scopes to request
+     * @return The URL to send to the browser
+     */
+    default String getAuthorizationUrl(String state, String... scopes) {
+        // response_type = code | token, but token is deprecated in the draft oauth 2.1 spec
+        // 2.1 is adding code_challenge, code_challenge_method
+        // code_challenge requires a code_verifier
+        return this.getAuthorizationUrl() + "?response_type=code&client_id=" + this.getClientId()
+                + "&redirect_uri=" + this.getRedirectUri()
+                + "&scope=" + String.join(" ", scopes)
+                // State is used to detect/prevent cross-site request forgery
+                + "&state=" + state;
+    }
+
+    /**
+     * Get the OAuth version that the API expects
+     * @return The oauth version
+     */
+    OAuthVersion getOAuthVersion();
+
+    /**
+     * Get the client id
+     * @return The client id
+     */
+    String getClientId();
+
+    /**
+     * Get the client secret
+     * @return The client secret
+     */
+    String getClientSecret();
+
+    /**
+     * Get the redirect URI
+     * @return The redirect URI
+     */
+    default String getRedirectUri() {
+        return null;
+    }
+
+    /**
+     * Convert to a preference string
+     * @return the preference string
+     */
+    default String toPreferencesString() {
+        return null;
+    }
+
+    /**
+     * Get the actual API URL
+     * @return The API URl
+     */
+    default String getApiUrl() {
+        return null;
+    }
+
+    void rememberPreferences();
+}
Index: trunk/src/org/openstreetmap/josm/data/oauth/IOAuthToken.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/oauth/IOAuthToken.java	(revision 18650)
+++ trunk/src/org/openstreetmap/josm/data/oauth/IOAuthToken.java	(revision 18650)
@@ -0,0 +1,37 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.oauth;
+
+import org.openstreetmap.josm.tools.HttpClient;
+
+/**
+ * An interface for oauth tokens
+ * @author Taylor Smock
+ * @since 18650
+ */
+public interface IOAuthToken {
+    /**
+     * Sign a client
+     * @param client The client to sign
+     */
+    void sign(HttpClient client) throws OAuthException;
+
+    /**
+     * Get the preferences string of this auth token.
+     * This should match the expected return body from the authentication server.
+     * For OAuth, this is typically JSON.
+     * @return The preferences string
+     */
+    String toPreferencesString();
+
+    /**
+     * Get the auth type of this token
+     * @return The auth type
+     */
+    OAuthVersion getOAuthType();
+
+    /**
+     * Get the OAuth parameters
+     * @return The OAuth parameters
+     */
+    IOAuthParameters getParameters();
+}
Index: trunk/src/org/openstreetmap/josm/data/oauth/OAuth20Authorization.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/oauth/OAuth20Authorization.java	(revision 18650)
+++ trunk/src/org/openstreetmap/josm/data/oauth/OAuth20Authorization.java	(revision 18650)
@@ -0,0 +1,112 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.oauth;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.function.Consumer;
+
+import org.openstreetmap.josm.io.remotecontrol.handler.AuthorizationHandler;
+import org.openstreetmap.josm.io.remotecontrol.handler.RequestHandler;
+import org.openstreetmap.josm.tools.HttpClient;
+import org.openstreetmap.josm.tools.JosmRuntimeException;
+import org.openstreetmap.josm.tools.OpenBrowser;
+
+/**
+ * Authorize the application
+ * @since 18650
+ */
+public class OAuth20Authorization implements IOAuthAuthorization {
+    /**
+     * See <a href="https://www.rfc-editor.org/rfc/rfc7636">RFC7636</a>: PKCE
+     * @param cryptographicallyRandomString A cryptographically secure string
+     * @return The S256 bytes
+     */
+    private static String getPKCES256CodeChallenge(String cryptographicallyRandomString) {
+        // S256: code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
+        try {
+            byte[] encodedBytes = cryptographicallyRandomString.getBytes(StandardCharsets.US_ASCII);
+            MessageDigest digest = MessageDigest.getInstance("SHA-256");
+            return new String(Base64.getUrlEncoder().encode(digest.digest(encodedBytes)), StandardCharsets.US_ASCII)
+                    .replace("=", "").replace("+", "-").replace("/", "_");
+        } catch (NoSuchAlgorithmException e) {
+            throw new JosmRuntimeException(e);
+        }
+    }
+
+    @Override
+    public void authorize(IOAuthParameters parameters, Consumer<IOAuthToken> consumer, Enum<?>... scopes) {
+        final String state = UUID.randomUUID().toString();
+        final String codeVerifier = UUID.randomUUID().toString(); // Cryptographically random string (ASCII)
+        final String s256CodeChallenge = getPKCES256CodeChallenge(codeVerifier);
+
+        // Enable authorization remote control
+        new AuthorizationHandler().getPermissionPreference().put(true);
+        String url = parameters.getAuthorizationUrl(state, scopes)
+                + "&code_challenge_method=S256&code_challenge=" + s256CodeChallenge;
+        AuthorizationHandler.addAuthorizationConsumer(state, new OAuth20AuthorizationHandler(state, codeVerifier, parameters, consumer));
+        OpenBrowser.displayUrl(url);
+    }
+
+    private static class OAuth20AuthorizationHandler implements AuthorizationHandler.AuthorizationConsumer {
+
+        private final String state;
+        private final IOAuthParameters parameters;
+        private final Consumer<IOAuthToken> consumer;
+        private final String codeVerifier;
+
+        OAuth20AuthorizationHandler(String state, String codeVerifier, IOAuthParameters parameters, Consumer<IOAuthToken> consumer) {
+            this.state = state;
+            this.parameters = parameters;
+            this.consumer = consumer;
+            this.codeVerifier = codeVerifier;
+        }
+
+        @Override
+        public void validateRequest(String sender, String request, Map<String, String> args)
+                throws RequestHandler.RequestHandlerBadRequestException {
+            String argState = args.get("state");
+            if (!Objects.equals(this.state, argState)) {
+                throw new RequestHandler.RequestHandlerBadRequestException(
+                        tr("Mismatched state: Expected {0} but got {1}", this.state, argState));
+            }
+        }
+
+        @Override
+        public AuthorizationHandler.ResponseRecord handleRequest(String sender, String request, Map<String, String> args)
+                throws RequestHandler.RequestHandlerErrorException, RequestHandler.RequestHandlerBadRequestException {
+            String code = args.get("code");
+            try {
+                HttpClient tradeCodeForToken = HttpClient.create(new URL(parameters.getAccessTokenUrl()), "POST");
+                tradeCodeForToken.setRequestBody(("grant_type=authorization_code&client_id=" + parameters.getClientId()
+                        + "&redirect_uri=" + parameters.getRedirectUri()
+                        + "&code=" + code
+                        + (this.codeVerifier != null ? "&code_verifier=" + this.codeVerifier : "")
+                ).getBytes(StandardCharsets.UTF_8));
+                try {
+                    tradeCodeForToken.connect();
+                    HttpClient.Response response = tradeCodeForToken.getResponse();
+                    OAuth20Token oAuth20Token = new OAuth20Token(parameters, response.getContentReader());
+                    consumer.accept(oAuth20Token);
+                } catch (IOException | OAuth20Exception e) {
+                    consumer.accept(null);
+                    throw new JosmRuntimeException(e);
+                } finally {
+                    tradeCodeForToken.disconnect();
+                }
+            } catch (MalformedURLException e) {
+                throw new JosmRuntimeException(e);
+            }
+            return null;
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/data/oauth/OAuth20Exception.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/oauth/OAuth20Exception.java	(revision 18650)
+++ trunk/src/org/openstreetmap/josm/data/oauth/OAuth20Exception.java	(revision 18650)
@@ -0,0 +1,87 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.oauth;
+
+import javax.json.JsonObject;
+
+/**
+ * A generic OAuth 2.0 exception
+ * @since 18650
+ */
+public final class OAuth20Exception extends OAuthException {
+    private static final long serialVersionUID = -203910656089454886L;
+
+    /**
+     * Invalid request types
+     */
+    public enum Type {
+        invalid_request,
+        invalid_client,
+        invalid_grant,
+        unauthorized_client,
+        unsupported_grant_type,
+        invalid_scope,
+        unknown
+    }
+
+    private final Type type;
+
+    /**
+     * Create a new exception with a specified cause
+     * @param cause The cause leading to this exception
+     */
+    public OAuth20Exception(Exception cause) {
+        super(cause);
+        this.type = Type.unknown;
+    }
+
+    /**
+     * Create a new exception with a given message
+     * @param message The message to use
+     */
+    public OAuth20Exception(String message) {
+        super(message);
+        this.type = Type.unknown;
+    }
+
+    /**
+     * Create an exception from a server message
+     * @param serverMessage The server message. Should conform to
+     *                      <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2">RFC 6747 Section 4.2.2</a>, but in JSON
+     *                      format.
+     */
+    OAuth20Exception(JsonObject serverMessage) {
+        super(serverMessage != null
+                ? serverMessage.getString("error_description", serverMessage.getString("error", "Unknown error"))
+                : "Unknown error");
+        if (serverMessage != null && serverMessage.containsKey("error")) {
+            switch(serverMessage.getString("error")) {
+                case "invalid_request":
+                case "invalid_client":
+                case "invalid_grant":
+                case "unauthorized_client":
+                case "unsupported_grant_type":
+                case "invalid_scope":
+                    this.type = Type.valueOf(serverMessage.getString("error"));
+                    break;
+                default:
+                    this.type = Type.unknown;
+            }
+        } else {
+            this.type = Type.unknown;
+        }
+    }
+
+    @Override
+    OAuthVersion[] getOAuthVersions() {
+        return new OAuthVersion[] {OAuthVersion.OAuth20};
+    }
+
+    @Override
+    public String getMessage() {
+        String message = super.getMessage();
+        if (message == null) {
+            return "OAuth error " + this.type;
+        }
+        return "OAuth error " + this.type + ": " + message;
+    }
+}
Index: trunk/src/org/openstreetmap/josm/data/oauth/OAuth20Parameters.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/oauth/OAuth20Parameters.java	(revision 18650)
+++ trunk/src/org/openstreetmap/josm/data/oauth/OAuth20Parameters.java	(revision 18650)
@@ -0,0 +1,150 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.oauth;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonReader;
+import javax.json.JsonStructure;
+import javax.json.JsonValue;
+
+import org.openstreetmap.josm.spi.preferences.Config;
+
+/**
+ * Parameters for OAuth 2.0
+ * @author Taylor Smock
+ * @since 18650
+ */
+public final class OAuth20Parameters implements IOAuthParameters {
+    private static final String REDIRECT_URI = "redirect_uri";
+    private static final String CLIENT_ID = "client_id";
+    private static final String CLIENT_SECRET = "client_secret";
+    private static final String TOKEN_URL = "token_url";
+    private static final String AUTHORIZE_URL = "authorize_url";
+    private static final String API_URL = "api_url";
+    private final String redirectUri;
+    private final String clientSecret;
+    private final String clientId;
+    private final String tokenUrl;
+    private final String authorizeUrl;
+    private final String apiUrl;
+
+    /**
+     * Recreate a parameter object from a JSON string
+     * @param jsonString The JSON string with the required data
+     */
+    public OAuth20Parameters(String jsonString) {
+        try (ByteArrayInputStream bais = new ByteArrayInputStream(jsonString.getBytes(StandardCharsets.UTF_8));
+             JsonReader reader = Json.createReader(bais)) {
+            JsonStructure structure = reader.read();
+            if (structure.getValueType() != JsonValue.ValueType.OBJECT) {
+                throw new IllegalArgumentException("Invalid JSON object: " + jsonString);
+            }
+            JsonObject jsonObject = structure.asJsonObject();
+            this.redirectUri = jsonObject.getString(REDIRECT_URI);
+            this.clientId = jsonObject.getString(CLIENT_ID);
+            this.clientSecret = jsonObject.getString(CLIENT_SECRET, null);
+            this.tokenUrl = jsonObject.getString(TOKEN_URL);
+            this.authorizeUrl = jsonObject.getString(AUTHORIZE_URL);
+            this.apiUrl = jsonObject.getString(API_URL);
+        } catch (IOException e) {
+            // This should literally never happen -- ByteArrayInputStream does not do *anything* in the close method.
+            throw new UncheckedIOException(e);
+        }
+    }
+
+    /**
+     * Create a new OAuth parameter object
+     * @param clientId The client id. May not be {@code null}.
+     * @param clientSecret The client secret. May be {@code null}. Not currently used.
+     * @param baseUrl The base url. This assumes that the endpoints are {@code /token} and {@code /authorize}.
+     * @param apiUrl The API url
+     * @param redirectUri The redirect URI for the client.
+     */
+    public OAuth20Parameters(String clientId, String clientSecret, String baseUrl, String apiUrl, String redirectUri) {
+        this(clientId, clientSecret, baseUrl + "/token", baseUrl + "/authorize", apiUrl, redirectUri);
+    }
+
+    /**
+     * Create a new OAuth parameter object
+     * @param clientId The client id.
+     * @param clientSecret The client secret. May be {@code null}. Not currently used.
+     * @param tokenUrl The token request URL (RFC6749 4.4.2)
+     * @param authorizeUrl The authorization request URL (RFC6749 4.1.1)
+     * @param apiUrl The API url
+     * @param redirectUri The redirect URI for the client.
+     */
+    public OAuth20Parameters(String clientId, String clientSecret, String tokenUrl, String authorizeUrl, String apiUrl, String redirectUri) {
+        Objects.requireNonNull(authorizeUrl, "authorizeUrl");
+        Objects.requireNonNull(clientId, "clientId");
+        Objects.requireNonNull(redirectUri, "redirectUri");
+        Objects.requireNonNull(tokenUrl, "tokenUrl");
+        Objects.requireNonNull(apiUrl, "apiUrl");
+        // Alternatively, we could try using rfc8414 ( /.well-known/oauth-authorization-server ), but OSM (doorkeeper) doesn't support it.
+        this.redirectUri = redirectUri;
+        this.clientId = clientId;
+        this.clientSecret = clientSecret;
+        this.tokenUrl = tokenUrl;
+        this.authorizeUrl = authorizeUrl;
+        this.apiUrl = apiUrl;
+    }
+
+    @Override
+    public String getAccessTokenUrl() {
+        return this.tokenUrl;
+    }
+
+    @Override
+    public String getAuthorizationUrl() {
+        return this.authorizeUrl;
+    }
+
+    @Override
+    public OAuthVersion getOAuthVersion() {
+        return OAuthVersion.OAuth20;
+    }
+
+    @Override
+    public String getClientId() {
+        return this.clientId;
+    }
+
+    @Override
+    public String getClientSecret() {
+        return this.clientSecret;
+    }
+
+    @Override
+    public String getRedirectUri() {
+        return this.redirectUri;
+    }
+
+    @Override
+    public String getApiUrl() {
+        return this.apiUrl;
+    }
+
+    @Override
+    public void rememberPreferences() {
+        Config.getPref().put("oauth.access-token.parameters." + OAuthVersion.OAuth20 + "." + this.apiUrl,
+                this.toPreferencesString());
+    }
+
+    @Override
+    public String toPreferencesString() {
+        JsonObjectBuilder builder = Json.createObjectBuilder();
+        builder.add(CLIENT_ID, this.clientId);
+        builder.add(REDIRECT_URI, this.redirectUri);
+        if (this.apiUrl != null) builder.add(API_URL, this.apiUrl);
+        if (this.authorizeUrl != null) builder.add(AUTHORIZE_URL, this.authorizeUrl);
+        if (this.clientSecret != null) builder.add(CLIENT_SECRET, this.clientSecret);
+        if (this.tokenUrl != null) builder.add(TOKEN_URL, this.tokenUrl);
+        return builder.build().toString();
+    }
+}
Index: trunk/src/org/openstreetmap/josm/data/oauth/OAuth20Token.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/oauth/OAuth20Token.java	(revision 18650)
+++ trunk/src/org/openstreetmap/josm/data/oauth/OAuth20Token.java	(revision 18650)
@@ -0,0 +1,182 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.oauth;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.URI;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonReader;
+import javax.json.JsonStructure;
+import javax.json.JsonValue;
+
+import org.openstreetmap.josm.tools.HttpClient;
+import org.openstreetmap.josm.tools.JosmRuntimeException;
+
+/**
+ * Token holder for OAuth 2.0
+ * @author Taylor Smock
+ * @since 18650
+ */
+public final class OAuth20Token implements IOAuthToken {
+    private static final String ACCESS_TOKEN = "access_token";
+    private static final String CREATED_AT = "created_at";
+    private static final String EXPIRES_IN = "expires_in";
+    private static final String REFRESH_TOKEN = "refresh_token";
+    private static final String SCOPE = "scope";
+    private static final String TOKEN_TYPE = "token_type";
+    private final String accessToken;
+    private final String tokenType;
+    private final int expiresIn;
+    private final String refreshToken;
+    private final String[] scopes;
+    private final Instant createdAt;
+    private final IOAuthParameters oauthParameters;
+
+    /**
+     * Create a new OAuth token
+     * @param oauthParameters The parameters for the OAuth token
+     * @param json The stored JSON for the token
+     * @throws OAuth20Exception If the JSON creates an invalid token
+     */
+    public OAuth20Token(IOAuthParameters oauthParameters, String json) throws OAuth20Exception {
+        this(oauthParameters, new InputStreamReader(new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8));
+    }
+
+    OAuth20Token(IOAuthParameters oauthParameters, Reader bufferedReader) throws OAuth20Exception {
+        this.oauthParameters = oauthParameters;
+        try (JsonReader reader = Json.createReader(bufferedReader)) {
+            JsonStructure structure = reader.read();
+            if (structure.getValueType() != JsonValue.ValueType.OBJECT
+            || !structure.asJsonObject().containsKey(ACCESS_TOKEN)
+            || !structure.asJsonObject().containsKey(TOKEN_TYPE)) {
+                if (structure.getValueType() == JsonValue.ValueType.OBJECT
+                && structure.asJsonObject().containsKey("error")) {
+                    throw new OAuth20Exception(structure.asJsonObject());
+                } else {
+                    throw new OAuth20Exception("Either " + ACCESS_TOKEN + " or " + TOKEN_TYPE + " is not present: " + structure);
+                }
+            }
+            JsonObject object = structure.asJsonObject();
+            this.accessToken = object.getString(ACCESS_TOKEN);
+            this.tokenType = object.getString(TOKEN_TYPE);
+            this.expiresIn = object.getInt(EXPIRES_IN, Integer.MAX_VALUE);
+            this.refreshToken = object.getString(REFRESH_TOKEN, null);
+            this.scopes = object.getString(SCOPE, "").split(" ");
+            if (object.containsKey(CREATED_AT)) {
+                this.createdAt = Instant.ofEpochSecond(object.getJsonNumber(CREATED_AT).longValue());
+            } else {
+                this.createdAt = Instant.now();
+            }
+        }
+    }
+
+    @Override
+    public void sign(HttpClient client) throws OAuthException {
+        if (!this.oauthParameters.getApiUrl().contains(client.getURL().getHost())) {
+            String host = URI.create(this.oauthParameters.getAccessTokenUrl()).getHost();
+            throw new IllegalArgumentException("Cannot sign URL with token for different host: Expected " + host
+                + " but got " + client.getURL().getHost());
+        }
+        if (this.getBearerToken() != null) {
+            client.setHeader("Authorization", "Bearer " + this.getBearerToken());
+            return;
+        }
+        throw new OAuth20Exception("Unknown token type: " + this.tokenType);
+    }
+
+    /**
+     * Get the OAuth 2.0 bearer token
+     * @return The bearer token. May return {@code null} if the token type is not a bearer type.
+     */
+    public String getBearerToken() {
+        if ("bearer".equalsIgnoreCase(this.tokenType)) {
+            return this.accessToken;
+        }
+        return null;
+    }
+
+    @Override
+    public String toPreferencesString() {
+        final OAuth20Token tokenToSave;
+        if (shouldRefresh()) {
+            tokenToSave = refresh();
+        } else {
+            tokenToSave = this;
+        }
+        JsonObjectBuilder jsonObjectBuilder = Json.createObjectBuilder();
+        jsonObjectBuilder.add(ACCESS_TOKEN, tokenToSave.accessToken);
+        jsonObjectBuilder.add(TOKEN_TYPE, tokenToSave.tokenType);
+        if (tokenToSave.createdAt != null) {
+            jsonObjectBuilder.add(CREATED_AT, tokenToSave.createdAt.getEpochSecond());
+        }
+        if (tokenToSave.expiresIn != Integer.MAX_VALUE) {
+            jsonObjectBuilder.add(EXPIRES_IN, tokenToSave.expiresIn);
+        }
+        if (tokenToSave.refreshToken != null) {
+            jsonObjectBuilder.add(REFRESH_TOKEN, tokenToSave.refreshToken);
+        }
+        if (tokenToSave.scopes.length > 0) {
+            jsonObjectBuilder.add(SCOPE, String.join(" ", tokenToSave.scopes));
+        }
+        return jsonObjectBuilder.build().toString();
+    }
+
+    @Override
+    public OAuthVersion getOAuthType() {
+        return OAuthVersion.OAuth20;
+    }
+
+    @Override
+    public IOAuthParameters getParameters() {
+        return this.oauthParameters;
+    }
+
+    /**
+     * Check if the token should be refreshed
+     * @return {@code true} if the token should be refreshed
+     */
+    boolean shouldRefresh() {
+        return this.refreshToken != null && this.expiresIn != Integer.MAX_VALUE
+                // We should refresh the token when 10% of its lifespan has been spent.
+                // We aren't an application that will be used every day by every user.
+                && this.createdAt.getEpochSecond() + this.expiresIn < Instant.now().getEpochSecond() - this.expiresIn * 9L / 10;
+    }
+
+    /**
+     * Refresh the OAuth 2.0 token
+     * @return The new token to use
+     */
+    OAuth20Token refresh() {
+        // This bit isn't necessarily OAuth 2.1 compliant. Spec isn't finished yet, but
+        // refresh tokens will either be sender constrained or some kind of rotation or both.
+        // This refresh code handles rotation, mostly by creating a new OAuth20Token. :)
+        // For sender constrained, it will likely allow self-signed certificates (RFC8705).
+        // Note: OSM doesn't have age limits on their tokens, at time of writing.
+        String refresh = "grant_type=refresh_token&refresh_token=" + this.refreshToken;
+        if (this.scopes.length > 0) {
+            refresh += "&scope=" + String.join(" ", this.scopes);
+        }
+        HttpClient client = null;
+        try {
+            client = HttpClient.create(new URL(this.oauthParameters.getAccessTokenUrl()), "POST");
+            client.setRequestBody(refresh.getBytes(StandardCharsets.UTF_8));
+            client.connect();
+            HttpClient.Response response = client.getResponse();
+            return new OAuth20Token(this.oauthParameters, response.getContentReader());
+        } catch (IOException | OAuth20Exception e) {
+            throw new JosmRuntimeException(e);
+        } finally {
+            if (client != null) {
+                client.disconnect();
+            }
+        }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/data/oauth/OAuthAccessTokenHolder.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/oauth/OAuthAccessTokenHolder.java	(revision 18649)
+++ trunk/src/org/openstreetmap/josm/data/oauth/OAuthAccessTokenHolder.java	(revision 18650)
@@ -4,6 +4,14 @@
 import static org.openstreetmap.josm.tools.I18n.tr;
 
+import java.net.URI;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
 import org.openstreetmap.josm.io.auth.CredentialsAgent;
 import org.openstreetmap.josm.io.auth.CredentialsAgentException;
+import org.openstreetmap.josm.io.auth.CredentialsManager;
 import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
@@ -32,4 +40,6 @@
     private String accessTokenSecret;
 
+    private final Map<String, Map<OAuthVersion, IOAuthToken>> tokenMap = new HashMap<>();
+
     /**
      * Replies true if current access token should be saved to the preferences file.
@@ -101,4 +111,28 @@
             return null;
         return new OAuthToken(accessTokenKey, accessTokenSecret);
+    }
+
+    /**
+     * Replies the access token.
+     * @param api The api the token is for
+     * @param version The OAuth version the token is for
+     * @return the access token, can be {@code null}
+     * @since 18650
+     */
+    public IOAuthToken getAccessToken(String api, OAuthVersion version) {
+        api = URI.create(api).getHost();
+        if (this.tokenMap.containsKey(api)) {
+            Map<OAuthVersion, IOAuthToken> map = this.tokenMap.get(api);
+            return map.get(version);
+        }
+        try {
+            IOAuthToken token = CredentialsManager.getInstance().lookupOAuthAccessToken(api);
+            // We *do* want to set the API token to null, if it doesn't exist. Just to avoid unnecessary lookups.
+            this.setAccessToken(api, token);
+            return token;
+        } catch (CredentialsAgentException exception) {
+            Logging.trace(exception);
+        }
+        return null;
     }
 
@@ -126,4 +160,24 @@
             this.accessTokenKey = token.getKey();
             this.accessTokenSecret = token.getSecret();
+        }
+    }
+
+    /**
+     * Sets the access token hold by this holder.
+     *
+     * @param api The api the token is for
+     * @param token the access token. Can be null to clear the content in this holder.
+     * @since 18650
+     */
+    public void setAccessToken(String api, IOAuthToken token) {
+        Objects.requireNonNull(api, "api url");
+        // Sometimes the api might be sent as the host
+        api = Optional.ofNullable(URI.create(api).getHost()).orElse(api);
+        if (token == null) {
+            if (this.tokenMap.containsKey(api)) {
+                this.tokenMap.get(api).clear();
+            }
+        } else {
+            this.tokenMap.computeIfAbsent(api, key -> new EnumMap<>(OAuthVersion.class)).put(token.getOAuthType(), token);
         }
     }
@@ -176,6 +230,22 @@
             if (!saveToPreferences) {
                 cm.storeOAuthAccessToken(null);
+                for (String host : this.tokenMap.keySet()) {
+                    cm.storeOAuthAccessToken(host, null);
+                }
             } else {
-                cm.storeOAuthAccessToken(new OAuthToken(accessTokenKey, accessTokenSecret));
+                if (this.accessTokenKey != null && this.accessTokenSecret != null) {
+                    cm.storeOAuthAccessToken(new OAuthToken(accessTokenKey, accessTokenSecret));
+                }
+                for (Map.Entry<String, Map<OAuthVersion, IOAuthToken>> entry : this.tokenMap.entrySet()) {
+                    if (entry.getValue().isEmpty()) {
+                        cm.storeOAuthAccessToken(entry.getKey(), null);
+                        continue;
+                    }
+                    for (OAuthVersion version : OAuthVersion.values()) {
+                        if (entry.getValue().containsKey(version)) {
+                            cm.storeOAuthAccessToken(entry.getKey(), entry.getValue().get(version));
+                        }
+                    }
+                }
             }
         } catch (CredentialsAgentException e) {
Index: trunk/src/org/openstreetmap/josm/data/oauth/OAuthException.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/oauth/OAuthException.java	(revision 18650)
+++ trunk/src/org/openstreetmap/josm/data/oauth/OAuthException.java	(revision 18650)
@@ -0,0 +1,19 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.oauth;
+
+/**
+ * Base OAuth exception
+ * @author Taylor Smock
+ * @since 18650
+ */
+public abstract class OAuthException extends Exception {
+    OAuthException(Exception cause) {
+        super(cause);
+    }
+
+    OAuthException(String message) {
+        super(message);
+    }
+
+    abstract OAuthVersion[] getOAuthVersions();
+}
Index: trunk/src/org/openstreetmap/josm/data/oauth/OAuthParameters.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/oauth/OAuthParameters.java	(revision 18649)
+++ trunk/src/org/openstreetmap/josm/data/oauth/OAuthParameters.java	(revision 18650)
@@ -2,9 +2,23 @@
 package org.openstreetmap.josm.data.oauth;
 
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.net.URL;
 import java.util.Objects;
 
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.JsonReader;
+import javax.json.JsonStructure;
+import javax.json.JsonValue;
+
+import org.openstreetmap.josm.io.OsmApi;
+import org.openstreetmap.josm.io.auth.CredentialsAgentException;
+import org.openstreetmap.josm.io.auth.CredentialsManager;
 import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.spi.preferences.IUrls;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
+import org.openstreetmap.josm.tools.HttpClient;
+import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.Utils;
 
@@ -16,5 +30,5 @@
  * @since 2747
  */
-public class OAuthParameters {
+public class OAuthParameters implements IOAuthParameters {
 
     /**
@@ -47,11 +61,106 @@
      */
     public static OAuthParameters createDefault(String apiUrl) {
+        return (OAuthParameters) createDefault(apiUrl, OAuthVersion.OAuth10a);
+    }
+
+    /**
+     * Replies a set of default parameters for a consumer accessing an OSM server
+     * at the given API url. URL parameters are only set if the URL equals {@link IUrls#getDefaultOsmApiUrl}
+     * or references the domain "dev.openstreetmap.org", otherwise they may be <code>null</code>.
+     *
+     * @param apiUrl The API URL for which the OAuth default parameters are created. If null or empty, the default OSM API url is used.
+     * @param oAuthVersion The OAuth version to create default parameters for
+     * @return a set of default parameters for the given {@code apiUrl}
+     * @since 18650
+     */
+    public static IOAuthParameters createDefault(String apiUrl, OAuthVersion oAuthVersion) {
+        if (!Utils.isValidUrl(apiUrl)) {
+            apiUrl = null;
+        }
+
+        switch (oAuthVersion) {
+            case OAuth10a:
+                return getDefaultOAuth10Parameters(apiUrl);
+            case OAuth20:
+            case OAuth21: // For now, OAuth 2.1 (draft) is just OAuth 2.0 with mandatory extensions, which we implement.
+                return getDefaultOAuth20Parameters(apiUrl);
+            default:
+                throw new IllegalArgumentException("Unknown OAuth version: " + oAuthVersion);
+        }
+    }
+
+    /**
+     * Get the default OAuth 2.0 parameters
+     * @param apiUrl The API url
+     * @return The default parameters
+     */
+    private static OAuth20Parameters getDefaultOAuth20Parameters(String apiUrl) {
+        final String clientId;
+        final String clientSecret;
+        final String redirectUri;
+        final String baseUrl;
+        if (apiUrl != null && !Config.getUrls().getDefaultOsmApiUrl().equals(apiUrl)) {
+            clientId = "";
+            clientSecret = "";
+            baseUrl = apiUrl;
+            HttpClient client = null;
+            redirectUri = "";
+            // Check if the server is RFC 8414 compliant
+            try {
+                client = HttpClient.create(new URL(apiUrl + (apiUrl.endsWith("/") ? "" : "/") + ".well-known/oauth-authorization-server"));
+                HttpClient.Response response = client.connect();
+                if (response.getResponseCode() == 200) {
+                    try (BufferedReader reader = response.getContentReader();
+                         JsonReader jsonReader = Json.createReader(reader)) {
+                        JsonStructure structure = jsonReader.read();
+                        if (structure.getValueType() == JsonValue.ValueType.OBJECT) {
+                            return parseAuthorizationServerMetadataResponse(clientId, clientSecret, apiUrl,
+                                    redirectUri, structure.asJsonObject());
+                        }
+                    }
+                }
+            } catch (IOException | OAuthException e) {
+                Logging.trace(e);
+            } finally {
+                if (client != null) client.disconnect();
+            }
+        } else {
+            clientId = "edPII614Lm0_0zEpc_QzEltA9BUll93-Y-ugRQUoHMI";
+            // We don't actually use the client secret in our authorization flow.
+            clientSecret = null;
+            baseUrl = "https://www.openstreetmap.org/oauth2";
+            redirectUri = "http://127.0.0.1:8111/oauth_authorization";
+            apiUrl = OsmApi.getOsmApi().getBaseUrl();
+        }
+        return new OAuth20Parameters(clientId, clientSecret, baseUrl, apiUrl, redirectUri);
+    }
+
+    /**
+     * Parse the response from <a href="https://www.rfc-editor.org/rfc/rfc8414.html">RFC 8414</a>
+     * (OAuth 2.0 Authorization Server Metadata)
+     * @return The parameters for the server metadata
+     */
+    private static OAuth20Parameters parseAuthorizationServerMetadataResponse(String clientId, String clientSecret,
+                                                                              String apiUrl, String redirectUri,
+                                                                              JsonObject serverMetadata)
+            throws OAuthException {
+        final String authorizationEndpoint = serverMetadata.getString("authorization_endpoint", null);
+        final String tokenEndpoint = serverMetadata.getString("token_endpoint", null);
+        // This may also have additional documentation like what the endpoints allow (e.g. scopes, algorithms, etc.)
+        if (authorizationEndpoint == null || tokenEndpoint == null) {
+            throw new OAuth20Exception("Either token endpoint or authorization endpoints are missing");
+        }
+        return new OAuth20Parameters(clientId, clientSecret, tokenEndpoint, authorizationEndpoint, apiUrl, redirectUri);
+    }
+
+    /**
+     * Get the default OAuth 1.0a parameters
+     * @param apiUrl The api url
+     * @return The default parameters
+     */
+    private static OAuthParameters getDefaultOAuth10Parameters(String apiUrl) {
         final String consumerKey;
         final String consumerSecret;
         final String serverUrl;
-
-        if (!Utils.isValidUrl(apiUrl)) {
-            apiUrl = null;
-        }
 
         if (apiUrl != null && !Config.getUrls().getDefaultOsmApiUrl().equals(apiUrl)) {
@@ -82,13 +191,41 @@
      */
     public static OAuthParameters createFromApiUrl(String apiUrl) {
-        OAuthParameters parameters = createDefault(apiUrl);
-        return new OAuthParameters(
-                Config.getPref().get("oauth.settings.consumer-key", parameters.getConsumerKey()),
-                Config.getPref().get("oauth.settings.consumer-secret", parameters.getConsumerSecret()),
-                Config.getPref().get("oauth.settings.request-token-url", parameters.getRequestTokenUrl()),
-                Config.getPref().get("oauth.settings.access-token-url", parameters.getAccessTokenUrl()),
-                Config.getPref().get("oauth.settings.authorise-url", parameters.getAuthoriseUrl()),
-                Config.getPref().get("oauth.settings.osm-login-url", parameters.getOsmLoginUrl()),
-                Config.getPref().get("oauth.settings.osm-logout-url", parameters.getOsmLogoutUrl()));
+        return (OAuthParameters) createFromApiUrl(apiUrl, OAuthVersion.OAuth10a);
+    }
+
+    /**
+     * Replies a set of parameters as defined in the preferences.
+     *
+     * @param oAuthVersion The OAuth version to use.
+     * @param apiUrl the API URL. Must not be {@code null}.
+     * @return the parameters
+     * @since 18650
+     */
+    public static IOAuthParameters createFromApiUrl(String apiUrl, OAuthVersion oAuthVersion) {
+        IOAuthParameters parameters = createDefault(apiUrl, oAuthVersion);
+        switch (oAuthVersion) {
+            case OAuth10a:
+                OAuthParameters oauth10aParameters = (OAuthParameters) parameters;
+                return new OAuthParameters(
+                    Config.getPref().get("oauth.settings.consumer-key", oauth10aParameters.getConsumerKey()),
+                    Config.getPref().get("oauth.settings.consumer-secret", oauth10aParameters.getConsumerSecret()),
+                    Config.getPref().get("oauth.settings.request-token-url", oauth10aParameters.getRequestTokenUrl()),
+                    Config.getPref().get("oauth.settings.access-token-url", oauth10aParameters.getAccessTokenUrl()),
+                    Config.getPref().get("oauth.settings.authorise-url", oauth10aParameters.getAuthoriseUrl()),
+                    Config.getPref().get("oauth.settings.osm-login-url", oauth10aParameters.getOsmLoginUrl()),
+                    Config.getPref().get("oauth.settings.osm-logout-url", oauth10aParameters.getOsmLogoutUrl()));
+            case OAuth20:
+            case OAuth21: // Right now, OAuth 2.1 will work with our OAuth 2.0 implementation
+                OAuth20Parameters oAuth20Parameters = (OAuth20Parameters) parameters;
+                try {
+                    IOAuthToken storedToken = CredentialsManager.getInstance().lookupOAuthAccessToken(apiUrl);
+                    return storedToken != null ? storedToken.getParameters() : oAuth20Parameters;
+                } catch (CredentialsAgentException e) {
+                    Logging.trace(e);
+                }
+                return oAuth20Parameters;
+            default:
+                throw new IllegalArgumentException("Unknown OAuth version: " + oAuthVersion);
+        }
     }
 
@@ -96,4 +233,5 @@
      * Remembers the current values in the preferences.
      */
+    @Override
     public void rememberPreferences() {
         Config.getPref().put("oauth.settings.consumer-key", getConsumerKey());
@@ -183,8 +321,29 @@
      * @return The access token URL
      */
+    @Override
     public String getAccessTokenUrl() {
         return accessTokenUrl;
     }
 
+    @Override
+    public String getAuthorizationUrl() {
+        return this.authoriseUrl;
+    }
+
+    @Override
+    public OAuthVersion getOAuthVersion() {
+        return OAuthVersion.OAuth10a;
+    }
+
+    @Override
+    public String getClientId() {
+        return this.consumerKey;
+    }
+
+    @Override
+    public String getClientSecret() {
+        return this.consumerSecret;
+    }
+
     /**
      * Gets the authorise URL.
@@ -192,5 +351,5 @@
      */
     public String getAuthoriseUrl() {
-        return authoriseUrl;
+        return this.getAuthorizationUrl();
     }
 
Index: trunk/src/org/openstreetmap/josm/data/oauth/OAuthVersion.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/oauth/OAuthVersion.java	(revision 18650)
+++ trunk/src/org/openstreetmap/josm/data/oauth/OAuthVersion.java	(revision 18650)
@@ -0,0 +1,16 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.oauth;
+
+/**
+ * The OAuth versions ordered oldest to newest
+ * @author Taylor Smock
+ * @since 18650
+ */
+public enum OAuthVersion {
+    /** <a href="https://oauth.net/core/1.0a/">OAuth 1.0a</a> */
+    OAuth10a,
+    /** <a href="https://datatracker.ietf.org/doc/html/rfc6749">OAuth 2.0</a> */
+    OAuth20,
+    /** <a href="https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-06">OAuth 2.1 (draft)</a> */
+    OAuth21
+}
Index: trunk/src/org/openstreetmap/josm/data/oauth/osm/OsmScopes.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/oauth/osm/OsmScopes.java	(revision 18650)
+++ trunk/src/org/openstreetmap/josm/data/oauth/osm/OsmScopes.java	(revision 18650)
@@ -0,0 +1,24 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.oauth.osm;
+
+/**
+ * The possible scopes for OSM
+ * @author Taylor Smock
+ * @since 18650
+ */
+public enum OsmScopes {
+    /** Read user preferences */
+    read_prefs,
+    /** Modify user preferences */
+    write_prefs,
+    /** Write diary posts */
+    write_diary,
+    /** Modify the map */
+    write_api,
+    /** Read private GPS traces */
+    read_gpx,
+    /** Upload GPS traces */
+    write_gpx,
+    /** Modify notes */
+    write_notes
+}
Index: trunk/src/org/openstreetmap/josm/data/oauth/osm/package-info.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/oauth/osm/package-info.java	(revision 18650)
+++ trunk/src/org/openstreetmap/josm/data/oauth/osm/package-info.java	(revision 18650)
@@ -0,0 +1,6 @@
+// License: GPL. For details, see LICENSE file.
+
+/**
+ * Provides the classes for OAuth authentication to OSM.
+ */
+package org.openstreetmap.josm.data.oauth.osm;
Index: trunk/src/org/openstreetmap/josm/gui/oauth/AbstractAuthorizationUI.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/oauth/AbstractAuthorizationUI.java	(revision 18649)
+++ trunk/src/org/openstreetmap/josm/gui/oauth/AbstractAuthorizationUI.java	(revision 18650)
@@ -4,6 +4,8 @@
 import java.util.Objects;
 
+import org.openstreetmap.josm.data.oauth.IOAuthParameters;
 import org.openstreetmap.josm.data.oauth.OAuthParameters;
 import org.openstreetmap.josm.data.oauth.OAuthToken;
+import org.openstreetmap.josm.data.oauth.OAuthVersion;
 import org.openstreetmap.josm.gui.widgets.VerticallyScrollablePanel;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
@@ -21,5 +23,5 @@
 
     private String apiUrl;
-    private final AdvancedOAuthPropertiesPanel pnlAdvancedProperties = new AdvancedOAuthPropertiesPanel();
+    private final AdvancedOAuthPropertiesPanel pnlAdvancedProperties = new AdvancedOAuthPropertiesPanel(OAuthVersion.OAuth10a);
     private transient OAuthToken accessToken;
 
@@ -80,5 +82,5 @@
      * @return the current set of advanced OAuth parameters in this UI
      */
-    public OAuthParameters getOAuthParameters() {
+    public IOAuthParameters getOAuthParameters() {
         return pnlAdvancedProperties.getAdvancedParameters();
     }
Index: trunk/src/org/openstreetmap/josm/gui/oauth/AdvancedOAuthPropertiesPanel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/oauth/AdvancedOAuthPropertiesPanel.java	(revision 18649)
+++ trunk/src/org/openstreetmap/josm/gui/oauth/AdvancedOAuthPropertiesPanel.java	(revision 18650)
@@ -16,5 +16,8 @@
 import javax.swing.JOptionPane;
 
+import org.openstreetmap.josm.data.oauth.IOAuthParameters;
+import org.openstreetmap.josm.data.oauth.OAuth20Parameters;
 import org.openstreetmap.josm.data.oauth.OAuthParameters;
+import org.openstreetmap.josm.data.oauth.OAuthVersion;
 import org.openstreetmap.josm.gui.HelpAwareOptionPane;
 import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
@@ -52,4 +55,5 @@
     private final JosmTextField tfOsmLoginURL = new JosmTextField();
     private final JosmTextField tfOsmLogoutURL = new JosmTextField();
+    private final OAuthVersion oauthVersion;
     private transient UseDefaultItemListener ilUseDefault;
     private String apiUrl;
@@ -57,6 +61,8 @@
     /**
      * Constructs a new {@code AdvancedOAuthPropertiesPanel}.
-     */
-    public AdvancedOAuthPropertiesPanel() {
+     * @param oauthVersion The OAuth version to make the panel for
+     */
+    public AdvancedOAuthPropertiesPanel(OAuthVersion oauthVersion) {
+        this.oauthVersion = oauthVersion;
         build();
     }
@@ -71,5 +77,5 @@
         gc.weightx = 1.0;
         gc.insets = new Insets(0, 0, 3, 3);
-        gc.gridwidth = 2;
+        gc.gridwidth = 3;
         add(cbUseDefaults, gc);
 
@@ -78,5 +84,9 @@
         gc.weightx = 0.0;
         gc.gridwidth = 1;
-        add(new JLabel(tr("Consumer Key:")), gc);
+        if (this.oauthVersion == OAuthVersion.OAuth10a) {
+            add(new JLabel(tr("Consumer Key:")), gc);
+        } else {
+            add(new JLabel(tr("Client ID:")), gc);
+        }
 
         gc.gridx = 1;
@@ -86,8 +96,12 @@
 
         // -- consumer secret
-        gc.gridy = 2;
+        gc.gridy++;
         gc.gridx = 0;
         gc.weightx = 0.0;
-        add(new JLabel(tr("Consumer Secret:")), gc);
+        if (this.oauthVersion == OAuthVersion.OAuth10a) {
+            add(new JLabel(tr("Consumer Secret:")), gc);
+        } else {
+            add(new JLabel(tr("Client Secret:")), gc);
+        }
 
         gc.gridx = 1;
@@ -97,8 +111,12 @@
 
         // -- request token URL
-        gc.gridy = 3;
+        gc.gridy++;
         gc.gridx = 0;
         gc.weightx = 0.0;
-        add(new JLabel(tr("Request Token URL:")), gc);
+        if (this.oauthVersion == OAuthVersion.OAuth10a) {
+            add(new JLabel(tr("Request Token URL:")), gc);
+        } else {
+            add(new JLabel(tr("Redirect URL:")), gc);
+        }
 
         gc.gridx = 1;
@@ -108,5 +126,5 @@
 
         // -- access token URL
-        gc.gridy = 4;
+        gc.gridy++;
         gc.gridx = 0;
         gc.weightx = 0.0;
@@ -119,5 +137,5 @@
 
         // -- authorise URL
-        gc.gridy = 5;
+        gc.gridy++;
         gc.gridx = 0;
         gc.weightx = 0.0;
@@ -129,25 +147,27 @@
         SelectAllOnFocusGainedDecorator.decorate(tfAuthoriseURL);
 
-        // -- OSM login URL
-        gc.gridy = 6;
-        gc.gridx = 0;
-        gc.weightx = 0.0;
-        add(new JLabel(tr("OSM login URL:")), gc);
-
-        gc.gridx = 1;
-        gc.weightx = 1.0;
-        add(tfOsmLoginURL, gc);
-        SelectAllOnFocusGainedDecorator.decorate(tfOsmLoginURL);
-
-        // -- OSM logout URL
-        gc.gridy = 7;
-        gc.gridx = 0;
-        gc.weightx = 0.0;
-        add(new JLabel(tr("OSM logout URL:")), gc);
-
-        gc.gridx = 1;
-        gc.weightx = 1.0;
-        add(tfOsmLogoutURL, gc);
-        SelectAllOnFocusGainedDecorator.decorate(tfOsmLogoutURL);
+        if (this.oauthVersion == OAuthVersion.OAuth10a) {
+            // -- OSM login URL
+            gc.gridy++;
+            gc.gridx = 0;
+            gc.weightx = 0.0;
+            add(new JLabel(tr("OSM login URL:")), gc);
+
+            gc.gridx = 1;
+            gc.weightx = 1.0;
+            add(tfOsmLoginURL, gc);
+            SelectAllOnFocusGainedDecorator.decorate(tfOsmLoginURL);
+
+            // -- OSM logout URL
+            gc.gridy++;
+            gc.gridx = 0;
+            gc.weightx = 0.0;
+            add(new JLabel(tr("OSM logout URL:")), gc);
+
+            gc.gridx = 1;
+            gc.weightx = 1.0;
+            add(tfOsmLogoutURL, gc);
+            SelectAllOnFocusGainedDecorator.decorate(tfOsmLogoutURL);
+        }
 
         ilUseDefault = new UseDefaultItemListener();
@@ -192,12 +212,25 @@
     protected void resetToDefaultSettings() {
         cbUseDefaults.setSelected(true);
-        OAuthParameters params = OAuthParameters.createDefault(apiUrl);
-        tfConsumerKey.setText(params.getConsumerKey());
-        tfConsumerSecret.setText(params.getConsumerSecret());
-        tfRequestTokenURL.setText(params.getRequestTokenUrl());
-        tfAccessTokenURL.setText(params.getAccessTokenUrl());
-        tfAuthoriseURL.setText(params.getAuthoriseUrl());
-        tfOsmLoginURL.setText(params.getOsmLoginUrl());
-        tfOsmLogoutURL.setText(params.getOsmLogoutUrl());
+        IOAuthParameters iParams = OAuthParameters.createDefault(apiUrl, this.oauthVersion);
+        switch (this.oauthVersion) {
+            case OAuth10a:
+                OAuthParameters params = (OAuthParameters) iParams;
+                tfConsumerKey.setText(params.getConsumerKey());
+                tfConsumerSecret.setText(params.getConsumerSecret());
+                tfRequestTokenURL.setText(params.getRequestTokenUrl());
+                tfAccessTokenURL.setText(params.getAccessTokenUrl());
+                tfAuthoriseURL.setText(params.getAuthoriseUrl());
+                tfOsmLoginURL.setText(params.getOsmLoginUrl());
+                tfOsmLogoutURL.setText(params.getOsmLogoutUrl());
+                break;
+            case OAuth20:
+            case OAuth21:
+                OAuth20Parameters params20 = (OAuth20Parameters) iParams;
+                tfConsumerKey.setText(params20.getClientId());
+                tfConsumerSecret.setText(params20.getClientSecret());
+                tfAccessTokenURL.setText(params20.getAccessTokenUrl());
+                tfAuthoriseURL.setText(params20.getAuthorizationUrl());
+                tfRequestTokenURL.setText(params20.getRedirectUri());
+        }
 
         setChildComponentsEnabled(false);
@@ -217,15 +250,24 @@
      * @return the OAuth parameters
      */
-    public OAuthParameters getAdvancedParameters() {
+    public IOAuthParameters getAdvancedParameters() {
         if (cbUseDefaults.isSelected())
-            return OAuthParameters.createDefault(apiUrl);
-        return new OAuthParameters(
-            tfConsumerKey.getText(),
-            tfConsumerSecret.getText(),
-            tfRequestTokenURL.getText(),
-            tfAccessTokenURL.getText(),
-            tfAuthoriseURL.getText(),
-            tfOsmLoginURL.getText(),
-            tfOsmLogoutURL.getText());
+            return OAuthParameters.createDefault(apiUrl, this.oauthVersion);
+        if (this.oauthVersion == OAuthVersion.OAuth10a) {
+            return new OAuthParameters(
+                    tfConsumerKey.getText(),
+                    tfConsumerSecret.getText(),
+                    tfRequestTokenURL.getText(),
+                    tfAccessTokenURL.getText(),
+                    tfAuthoriseURL.getText(),
+                    tfOsmLoginURL.getText(),
+                    tfOsmLogoutURL.getText());
+        }
+        return new OAuth20Parameters(
+                tfConsumerKey.getText(),
+                tfConsumerSecret.getText(),
+                tfAuthoriseURL.getText(),
+                tfAccessTokenURL.getText(),
+                tfRequestTokenURL.getText()
+                );
     }
 
@@ -236,7 +278,7 @@
      * @throws IllegalArgumentException if parameters is null.
      */
-    public void setAdvancedParameters(OAuthParameters parameters) {
+    public void setAdvancedParameters(IOAuthParameters parameters) {
         CheckParameterUtil.ensureParameterNotNull(parameters, "parameters");
-        if (parameters.equals(OAuthParameters.createDefault(apiUrl))) {
+        if (parameters.equals(OAuthParameters.createDefault(apiUrl, parameters.getOAuthVersion()))) {
             cbUseDefaults.setSelected(true);
             setChildComponentsEnabled(false);
@@ -244,11 +286,21 @@
             cbUseDefaults.setSelected(false);
             setChildComponentsEnabled(true);
-            tfConsumerKey.setText(parameters.getConsumerKey() == null ? "" : parameters.getConsumerKey());
-            tfConsumerSecret.setText(parameters.getConsumerSecret() == null ? "" : parameters.getConsumerSecret());
-            tfRequestTokenURL.setText(parameters.getRequestTokenUrl() == null ? "" : parameters.getRequestTokenUrl());
-            tfAccessTokenURL.setText(parameters.getAccessTokenUrl() == null ? "" : parameters.getAccessTokenUrl());
-            tfAuthoriseURL.setText(parameters.getAuthoriseUrl() == null ? "" : parameters.getAuthoriseUrl());
-            tfOsmLoginURL.setText(parameters.getOsmLoginUrl() == null ? "" : parameters.getOsmLoginUrl());
-            tfOsmLogoutURL.setText(parameters.getOsmLogoutUrl() == null ? "" : parameters.getOsmLogoutUrl());
+            if (parameters instanceof OAuthParameters) {
+                OAuthParameters parameters10 = (OAuthParameters) parameters;
+                tfConsumerKey.setText(parameters10.getConsumerKey() == null ? "" : parameters10.getConsumerKey());
+                tfConsumerSecret.setText(parameters10.getConsumerSecret() == null ? "" : parameters10.getConsumerSecret());
+                tfRequestTokenURL.setText(parameters10.getRequestTokenUrl() == null ? "" : parameters10.getRequestTokenUrl());
+                tfAccessTokenURL.setText(parameters10.getAccessTokenUrl() == null ? "" : parameters10.getAccessTokenUrl());
+                tfAuthoriseURL.setText(parameters10.getAuthoriseUrl() == null ? "" : parameters10.getAuthoriseUrl());
+                tfOsmLoginURL.setText(parameters10.getOsmLoginUrl() == null ? "" : parameters10.getOsmLoginUrl());
+                tfOsmLogoutURL.setText(parameters10.getOsmLogoutUrl() == null ? "" : parameters10.getOsmLogoutUrl());
+            } else if (parameters instanceof OAuth20Parameters) {
+                OAuth20Parameters parameters20 = (OAuth20Parameters) parameters;
+                tfConsumerKey.setText(parameters20.getClientId());
+                tfConsumerSecret.setText(parameters20.getClientSecret());
+                tfAccessTokenURL.setText(parameters20.getAccessTokenUrl());
+                tfAuthoriseURL.setText(parameters20.getAuthorizationUrl());
+                tfRequestTokenURL.setText(parameters20.getRedirectUri());
+            }
         }
     }
Index: trunk/src/org/openstreetmap/josm/gui/oauth/FullyAutomaticAuthorizationUI.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/oauth/FullyAutomaticAuthorizationUI.java	(revision 18649)
+++ trunk/src/org/openstreetmap/josm/gui/oauth/FullyAutomaticAuthorizationUI.java	(revision 18650)
@@ -29,4 +29,5 @@
 import javax.swing.text.html.HTMLEditorKit;
 
+import org.openstreetmap.josm.data.oauth.OAuthParameters;
 import org.openstreetmap.josm.data.oauth.OAuthToken;
 import org.openstreetmap.josm.gui.HelpAwareOptionPane;
@@ -385,5 +386,5 @@
                     FullyAutomaticAuthorizationUI.this,
                     getApiUrl(),
-                    getAdvancedPropertiesPanel().getAdvancedParameters(),
+                    (OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters(),
                     getAccessToken()
             ));
@@ -438,5 +439,5 @@
                             + "Please check your advanced setting and try again."
                             + "</html>",
-                            getAdvancedPropertiesPanel().getAdvancedParameters().getAuthoriseUrl()),
+                            ((OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters()).getAuthoriseUrl()),
                     tr("OAuth authorization failed"),
                     JOptionPane.ERROR_MESSAGE,
@@ -446,5 +447,5 @@
 
         protected void alertLoginFailed() {
-            final String loginUrl = getAdvancedPropertiesPanel().getAdvancedParameters().getOsmLoginUrl();
+            final String loginUrl = ((OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters()).getOsmLoginUrl();
             HelpAwareOptionPane.showOptionDialog(
                     FullyAutomaticAuthorizationUI.this,
@@ -480,5 +481,5 @@
                 getProgressMonitor().setTicksCount(3);
                 OsmOAuthAuthorizationClient authClient = new OsmOAuthAuthorizationClient(
-                        getAdvancedPropertiesPanel().getAdvancedParameters()
+                        (OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters()
                 );
                 OAuthToken requestToken = authClient.getRequestToken(
Index: trunk/src/org/openstreetmap/josm/gui/oauth/ManualAuthorizationUI.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/oauth/ManualAuthorizationUI.java	(revision 18649)
+++ trunk/src/org/openstreetmap/josm/gui/oauth/ManualAuthorizationUI.java	(revision 18650)
@@ -26,4 +26,5 @@
 
 import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
+import org.openstreetmap.josm.data.oauth.OAuthParameters;
 import org.openstreetmap.josm.data.oauth.OAuthToken;
 import org.openstreetmap.josm.gui.widgets.DefaultTextComponentValidator;
@@ -233,5 +234,5 @@
                     ManualAuthorizationUI.this,
                     getApiUrl(),
-                    getAdvancedPropertiesPanel().getAdvancedParameters(),
+                    (OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters(),
                     getAccessToken()
             );
Index: trunk/src/org/openstreetmap/josm/gui/oauth/OAuthAuthorizationWizard.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/oauth/OAuthAuthorizationWizard.java	(revision 18649)
+++ trunk/src/org/openstreetmap/josm/gui/oauth/OAuthAuthorizationWizard.java	(revision 18650)
@@ -256,5 +256,5 @@
      */
     public OAuthParameters getOAuthParameters() {
-        return getCurrentAuthorisationUI().getOAuthParameters();
+        return (OAuthParameters) getCurrentAuthorisationUI().getOAuthParameters();
     }
 
Index: trunk/src/org/openstreetmap/josm/gui/oauth/SemiAutomaticAuthorizationUI.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/oauth/SemiAutomaticAuthorizationUI.java	(revision 18649)
+++ trunk/src/org/openstreetmap/josm/gui/oauth/SemiAutomaticAuthorizationUI.java	(revision 18650)
@@ -23,4 +23,5 @@
 
 import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
+import org.openstreetmap.josm.data.oauth.OAuthParameters;
 import org.openstreetmap.josm.data.oauth.OAuthToken;
 import org.openstreetmap.josm.gui.util.GuiHelper;
@@ -79,5 +80,5 @@
     protected void transitionToRetrieveAccessToken() {
         OsmOAuthAuthorizationClient client = new OsmOAuthAuthorizationClient(
-                getAdvancedPropertiesPanel().getAdvancedParameters()
+                (OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters()
         );
         String authoriseUrl = client.getAuthoriseUrl(requestToken);
@@ -184,5 +185,5 @@
                     + "''{1}''.</html>",
                     tr("Retrieve Request Token"),
-                    getAdvancedPropertiesPanel().getAdvancedParameters().getRequestTokenUrl()
+                    ((OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters()).getRequestTokenUrl()
             ));
             pnl.add(h, gc);
@@ -391,5 +392,5 @@
             final RetrieveRequestTokenTask task = new RetrieveRequestTokenTask(
                     SemiAutomaticAuthorizationUI.this,
-                    getAdvancedPropertiesPanel().getAdvancedParameters()
+                    (OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters()
             );
             executor.execute(task);
@@ -419,5 +420,5 @@
             final RetrieveAccessTokenTask task = new RetrieveAccessTokenTask(
                     SemiAutomaticAuthorizationUI.this,
-                    getAdvancedPropertiesPanel().getAdvancedParameters(),
+                    (OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters(),
                     requestToken
             );
@@ -451,5 +452,5 @@
                     SemiAutomaticAuthorizationUI.this,
                     getApiUrl(),
-                    getAdvancedPropertiesPanel().getAdvancedParameters(),
+                    (OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters(),
                     getAccessToken()
             );
Index: trunk/src/org/openstreetmap/josm/gui/preferences/advanced/AdvancedPreference.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/preferences/advanced/AdvancedPreference.java	(revision 18649)
+++ trunk/src/org/openstreetmap/josm/gui/preferences/advanced/AdvancedPreference.java	(revision 18650)
@@ -236,6 +236,6 @@
         Map<String, Setting<?>> orig = Preferences.main().getAllSettings();
         Map<String, Setting<?>> defaults = tmpPrefs.getAllDefaults();
-        orig.remove("osm-server.password");
-        defaults.remove("osm-server.password");
+        Preferences.main().getSensitive().forEach(orig::remove);
+        tmpPrefs.getSensitive().forEach(defaults::remove);
         if (tmpPrefs != Preferences.main()) {
             loaded = tmpPrefs.getAllSettings();
Index: trunk/src/org/openstreetmap/josm/gui/preferences/server/AuthenticationPreferencesPanel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/preferences/server/AuthenticationPreferencesPanel.java	(revision 18649)
+++ trunk/src/org/openstreetmap/josm/gui/preferences/server/AuthenticationPreferencesPanel.java	(revision 18650)
@@ -5,4 +5,5 @@
 
 import java.awt.BorderLayout;
+import java.awt.FlowLayout;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
@@ -18,4 +19,5 @@
 
 import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
+import org.openstreetmap.josm.data.oauth.OAuthVersion;
 import org.openstreetmap.josm.gui.help.HelpUtil;
 import org.openstreetmap.josm.gui.widgets.VerticallyScrollablePanel;
@@ -23,4 +25,5 @@
 import org.openstreetmap.josm.io.auth.CredentialsManager;
 import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.tools.GBC;
 import org.openstreetmap.josm.tools.Logging;
 
@@ -33,12 +36,16 @@
     /** indicates whether we use basic authentication */
     private final JRadioButton rbBasicAuthentication = new JRadioButton();
-    /** indicates whether we use OAuth as authentication scheme */
+    /** indicates whether we use OAuth 1.0a as authentication scheme */
     private final JRadioButton rbOAuth = new JRadioButton();
+    /** indicates whether we use OAuth 2.0 as authentication scheme */
+    private final JRadioButton rbOAuth20 = new JRadioButton();
     /** the panel which contains the authentication parameters for the respective authentication scheme */
     private final JPanel pnlAuthenticationParameters = new JPanel(new BorderLayout());
     /** the panel for the basic authentication parameters */
     private BasicAuthenticationPreferencesPanel pnlBasicAuthPreferences;
-    /** the panel for the OAuth authentication parameters */
+    /** the panel for the OAuth 1.0a authentication parameters */
     private OAuthAuthenticationPreferencesPanel pnlOAuthPreferences;
+    /** the panel for the OAuth 2.0 authentication parameters */
+    private OAuthAuthenticationPreferencesPanel pnlOAuth20Preferences;
 
     /**
@@ -56,33 +63,37 @@
     protected final void build() {
         setLayout(new GridBagLayout());
-        GridBagConstraints gc = new GridBagConstraints();
 
         AuthenticationMethodChangeListener authChangeListener = new AuthenticationMethodChangeListener();
 
+        JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEADING));
         // -- radio button for basic authentication
-        gc.anchor = GridBagConstraints.NORTHWEST;
-        gc.fill = GridBagConstraints.HORIZONTAL;
-        gc.gridx = 1;
-        gc.weightx = 1.0;
-        gc.insets = new Insets(0, 0, 0, 3);
-        add(rbBasicAuthentication, gc);
+        buttonPanel.add(rbBasicAuthentication);
         rbBasicAuthentication.setText(tr("Use Basic Authentication"));
         rbBasicAuthentication.setToolTipText(tr("Select to use HTTP basic authentication with your OSM username and password"));
         rbBasicAuthentication.addItemListener(authChangeListener);
 
-        //-- radio button for OAuth
-        gc.gridx = 0;
-        gc.weightx = 0.0;
-        add(rbOAuth, gc);
-        rbOAuth.setText(tr("Use OAuth"));
-        rbOAuth.setToolTipText(tr("Select to use OAuth as authentication mechanism"));
+        //-- radio button for OAuth 1.0a
+        buttonPanel.add(rbOAuth);
+        rbOAuth.setText(tr("Use OAuth {0}", "1.0a"));
+        rbOAuth.setToolTipText(tr("Select to use OAuth {0} as authentication mechanism", "1.0a"));
         rbOAuth.addItemListener(authChangeListener);
 
+        //-- radio button for OAuth 2.0
+        buttonPanel.add(rbOAuth20);
+        rbOAuth20.setText(tr("Use OAuth {0}", "2.0"));
+        rbOAuth20.setToolTipText(tr("Select to use OAuth {0} as authentication mechanism", "2.0"));
+        rbOAuth20.addItemListener(authChangeListener);
+
+        add(buttonPanel, GBC.eol());
         //-- radio button for OAuth
         ButtonGroup bg = new ButtonGroup();
         bg.add(rbBasicAuthentication);
         bg.add(rbOAuth);
+        bg.add(rbOAuth20);
 
         //-- add the panel which will hold the authentication parameters
+        GridBagConstraints gc = new GridBagConstraints();
+        gc.anchor = GridBagConstraints.NORTHWEST;
+        gc.insets = new Insets(0, 0, 0, 3);
         gc.gridx = 0;
         gc.gridy = 1;
@@ -95,5 +106,6 @@
         //-- the two panels for authentication parameters
         pnlBasicAuthPreferences = new BasicAuthenticationPreferencesPanel();
-        pnlOAuthPreferences = new OAuthAuthenticationPreferencesPanel();
+        pnlOAuthPreferences = new OAuthAuthenticationPreferencesPanel(OAuthVersion.OAuth10a);
+        pnlOAuth20Preferences = new OAuthAuthenticationPreferencesPanel(OAuthVersion.OAuth20);
 
         rbBasicAuthentication.setSelected(true);
@@ -110,4 +122,6 @@
         } else if ("oauth".equals(authMethod)) {
             rbOAuth.setSelected(true);
+        } else if ("oauth20".equals(authMethod)) {
+            rbOAuth20.setSelected(true);
         } else {
             Logging.warn(tr("Unsupported value in preference ''{0}'', got ''{1}''. Using authentication method ''Basic Authentication''.",
@@ -117,4 +131,5 @@
         pnlBasicAuthPreferences.initFromPreferences();
         pnlOAuthPreferences.initFromPreferences();
+        pnlOAuth20Preferences.initFromPreferences();
     }
 
@@ -127,6 +142,10 @@
         if (rbBasicAuthentication.isSelected()) {
             authMethod = "basic";
+        } else if (rbOAuth.isSelected()) {
+            authMethod = "oauth";
+        } else if (rbOAuth20.isSelected()) {
+            authMethod = "oauth20";
         } else {
-            authMethod = "oauth";
+            throw new IllegalStateException("One of OAuth 2.0, OAuth 1.0a, or Basic authentication must be checked");
         }
         Config.getPref().put("osm-server.auth-method", authMethod);
@@ -141,4 +160,9 @@
             pnlBasicAuthPreferences.saveToPreferences();
             pnlOAuthPreferences.saveToPreferences();
+        } else { // oauth20
+            // clear the password in the preferences
+            pnlBasicAuthPreferences.clearPassword();
+            pnlBasicAuthPreferences.saveToPreferences();
+            pnlOAuth20Preferences.saveToPreferences();
         }
     }
@@ -150,12 +174,14 @@
         @Override
         public void itemStateChanged(ItemEvent e) {
+            pnlAuthenticationParameters.removeAll();
             if (rbBasicAuthentication.isSelected()) {
-                pnlAuthenticationParameters.removeAll();
                 pnlAuthenticationParameters.add(pnlBasicAuthPreferences, BorderLayout.CENTER);
                 pnlBasicAuthPreferences.revalidate();
-            } else {
-                pnlAuthenticationParameters.removeAll();
+            } else if (rbOAuth.isSelected()) {
                 pnlAuthenticationParameters.add(pnlOAuthPreferences, BorderLayout.CENTER);
                 pnlOAuthPreferences.revalidate();
+            } else if (rbOAuth20.isSelected()) {
+                pnlAuthenticationParameters.add(pnlOAuth20Preferences, BorderLayout.CENTER);
+                pnlOAuth20Preferences.revalidate();
             }
             repaint();
Index: trunk/src/org/openstreetmap/josm/gui/preferences/server/OAuthAuthenticationPreferencesPanel.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/preferences/server/OAuthAuthenticationPreferencesPanel.java	(revision 18649)
+++ trunk/src/org/openstreetmap/josm/gui/preferences/server/OAuthAuthenticationPreferencesPanel.java	(revision 18650)
@@ -24,7 +24,12 @@
 
 import org.openstreetmap.josm.actions.ExpertToggleAction;
+import org.openstreetmap.josm.data.oauth.IOAuthToken;
+import org.openstreetmap.josm.data.oauth.OAuth20Authorization;
+import org.openstreetmap.josm.data.oauth.OAuth20Token;
 import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
 import org.openstreetmap.josm.data.oauth.OAuthParameters;
 import org.openstreetmap.josm.data.oauth.OAuthToken;
+import org.openstreetmap.josm.data.oauth.OAuthVersion;
+import org.openstreetmap.josm.data.oauth.osm.OsmScopes;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.oauth.AdvancedOAuthPropertiesPanel;
@@ -32,8 +37,10 @@
 import org.openstreetmap.josm.gui.oauth.OAuthAuthorizationWizard;
 import org.openstreetmap.josm.gui.oauth.TestAccessTokenTask;
+import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
 import org.openstreetmap.josm.gui.widgets.JosmTextField;
 import org.openstreetmap.josm.io.OsmApi;
 import org.openstreetmap.josm.io.auth.CredentialsManager;
+import org.openstreetmap.josm.io.remotecontrol.RemoteControl;
 import org.openstreetmap.josm.tools.GBC;
 import org.openstreetmap.josm.tools.ImageProvider;
@@ -42,8 +49,8 @@
 
 /**
- * The preferences panel for the OAuth preferences. This just a summary panel
+ * The preferences panel for the OAuth 1.0a preferences. This just a summary panel
  * showing the current Access Token Key and Access Token Secret, if the
  * user already has an Access Token.
- *
+ * <br>
  * For initial authorisation see {@link OAuthAuthorizationWizard}.
  * @since 2745
@@ -54,15 +61,28 @@
     private final JCheckBox cbSaveToPreferences = new JCheckBox(tr("Save to preferences"));
     private final JPanel pnlAuthorisationMessage = new JPanel(new BorderLayout());
-    private final NotYetAuthorisedPanel pnlNotYetAuthorised = new NotYetAuthorisedPanel();
-    private final AdvancedOAuthPropertiesPanel pnlAdvancedProperties = new AdvancedOAuthPropertiesPanel();
-    private final AlreadyAuthorisedPanel pnlAlreadyAuthorised = new AlreadyAuthorisedPanel();
+    private final NotYetAuthorisedPanel pnlNotYetAuthorised;
+    private final AdvancedOAuthPropertiesPanel pnlAdvancedProperties;
+    private final AlreadyAuthorisedPanel pnlAlreadyAuthorised;
+    private final OAuthVersion oAuthVersion;
     private String apiUrl;
 
     /**
-     * Create the panel
+     * Create the panel. Uses {@link OAuthVersion#OAuth10a}.
      */
     public OAuthAuthenticationPreferencesPanel() {
+        this(OAuthVersion.OAuth10a);
+    }
+
+    /**
+     * Create the panel.
+     * @param oAuthVersion The OAuth version to use
+     */
+    public OAuthAuthenticationPreferencesPanel(OAuthVersion oAuthVersion) {
+        this.oAuthVersion = oAuthVersion;
+        // These must come after we set the oauth version
+        this.pnlNotYetAuthorised = new NotYetAuthorisedPanel();
+        this.pnlAdvancedProperties = new AdvancedOAuthPropertiesPanel(this.oAuthVersion);
+        this.pnlAlreadyAuthorised = new AlreadyAuthorisedPanel();
         build();
-        refreshView();
     }
 
@@ -118,5 +138,7 @@
     protected void refreshView() {
         pnlAuthorisationMessage.removeAll();
-        if (OAuthAccessTokenHolder.getInstance().containsAccessToken()) {
+        if ((this.oAuthVersion == OAuthVersion.OAuth10a &&
+                OAuthAccessTokenHolder.getInstance().containsAccessToken())
+        || OAuthAccessTokenHolder.getInstance().getAccessToken(this.apiUrl, this.oAuthVersion) != null) {
             pnlAuthorisationMessage.add(pnlAlreadyAuthorised, BorderLayout.CENTER);
             pnlAlreadyAuthorised.refreshView();
@@ -181,9 +203,13 @@
 
             // Action for authorising now
-            add(new JButton(new AuthoriseNowAction(AuthorizationProcedure.FULLY_AUTOMATIC)), GBC.eol());
+            if (oAuthVersion == OAuthVersion.OAuth10a) {
+                add(new JButton(new AuthoriseNowAction(AuthorizationProcedure.FULLY_AUTOMATIC)), GBC.eol());
+            }
             add(new JButton(new AuthoriseNowAction(AuthorizationProcedure.SEMI_AUTOMATIC)), GBC.eol());
-            JButton authManually = new JButton(new AuthoriseNowAction(AuthorizationProcedure.MANUALLY));
-            add(authManually, GBC.eol());
-            ExpertToggleAction.addVisibilitySwitcher(authManually);
+            if (oAuthVersion == OAuthVersion.OAuth10a) {
+                JButton authManually = new JButton(new AuthoriseNowAction(AuthorizationProcedure.MANUALLY));
+                add(authManually, GBC.eol());
+                ExpertToggleAction.addVisibilitySwitcher(authManually);
+            }
 
             // filler - grab remaining space
@@ -254,6 +280,10 @@
             // -- action buttons
             JPanel btns = new JPanel(new FlowLayout(FlowLayout.LEFT));
-            btns.add(new JButton(new RenewAuthorisationAction(AuthorizationProcedure.FULLY_AUTOMATIC)));
-            btns.add(new JButton(new TestAuthorisationAction()));
+            if (oAuthVersion == OAuthVersion.OAuth10a) {
+                // these want the OAuth 1.0 token information
+                btns.add(new JButton(new RenewAuthorisationAction(AuthorizationProcedure.FULLY_AUTOMATIC)));
+                btns.add(new JButton(new TestAuthorisationAction()));
+            }
+            btns.add(new JButton(new RemoveAuthorisationAction()));
             gc.gridy = 4;
             gc.gridx = 0;
@@ -278,8 +308,22 @@
 
         protected final void refreshView() {
-            String v = OAuthAccessTokenHolder.getInstance().getAccessTokenKey();
-            tfAccessTokenKey.setText(v == null ? "" : v);
-            v = OAuthAccessTokenHolder.getInstance().getAccessTokenSecret();
-            tfAccessTokenSecret.setText(v == null ? "" : v);
+            switch (oAuthVersion) {
+                case OAuth10a:
+                    String v = OAuthAccessTokenHolder.getInstance().getAccessTokenKey();
+                    tfAccessTokenKey.setText(v == null ? "" : v);
+                    v = OAuthAccessTokenHolder.getInstance().getAccessTokenSecret();
+                    tfAccessTokenSecret.setText(v == null ? "" : v);
+                    tfAccessTokenSecret.setVisible(true);
+                    break;
+                case OAuth20:
+                case OAuth21:
+                    String token = "";
+                    if (apiUrl != null) {
+                        OAuth20Token bearerToken = (OAuth20Token) OAuthAccessTokenHolder.getInstance().getAccessToken(apiUrl, oAuthVersion);
+                        token = bearerToken == null ? "" : bearerToken.getBearerToken();
+                    }
+                    tfAccessTokenKey.setText(token == null ? "" : token);
+                    tfAccessTokenSecret.setVisible(false);
+            }
             cbSaveToPreferences.setSelected(OAuthAccessTokenHolder.getInstance().isSaveToPreferences());
         }
@@ -296,5 +340,6 @@
             putValue(NAME, tr("{0} ({1})", tr("Authorize now"), procedure.getText()));
             putValue(SHORT_DESCRIPTION, procedure.getDescription());
-            if (procedure == AuthorizationProcedure.FULLY_AUTOMATIC) {
+            if (procedure == AuthorizationProcedure.FULLY_AUTOMATIC
+            || OAuthAuthenticationPreferencesPanel.this.oAuthVersion != OAuthVersion.OAuth10a) {
                 new ImageProvider("oauth", "oauth-small").getResource().attachImageIcon(this);
             }
@@ -303,16 +348,58 @@
         @Override
         public void actionPerformed(ActionEvent arg0) {
-            OAuthAuthorizationWizard wizard = new OAuthAuthorizationWizard(
-                    OAuthAuthenticationPreferencesPanel.this,
-                    procedure,
-                    apiUrl,
-                    MainApplication.worker);
-            try {
-                wizard.showDialog();
-            } catch (UserCancelException ignore) {
-                Logging.trace(ignore);
-                return;
-            }
-            pnlAdvancedProperties.setAdvancedParameters(wizard.getOAuthParameters());
+            if (OAuthAuthenticationPreferencesPanel.this.oAuthVersion == OAuthVersion.OAuth10a) {
+                OAuthAuthorizationWizard wizard = new OAuthAuthorizationWizard(
+                        OAuthAuthenticationPreferencesPanel.this,
+                        procedure,
+                        apiUrl,
+                        MainApplication.worker);
+                try {
+                    wizard.showDialog();
+                } catch (UserCancelException ignore) {
+                    Logging.trace(ignore);
+                    return;
+                }
+                pnlAdvancedProperties.setAdvancedParameters(wizard.getOAuthParameters());
+                refreshView();
+            } else {
+                final boolean remoteControlIsRunning = Boolean.TRUE.equals(RemoteControl.PROP_REMOTECONTROL_ENABLED.get());
+                // TODO: Ask user if they want to start remote control?
+                if (!remoteControlIsRunning) {
+                    RemoteControl.start();
+                }
+                new OAuth20Authorization().authorize(OAuthParameters.createDefault(OsmApi.getOsmApi().getServerUrl(), oAuthVersion), token -> {
+                    if (!remoteControlIsRunning) {
+                        RemoteControl.stop();
+                    }
+                    // Clean up old token/password
+                    OAuthAccessTokenHolder.getInstance().setAccessToken(null);
+                    OAuthAccessTokenHolder.getInstance().setAccessToken(OsmApi.getOsmApi().getServerUrl(), token);
+                    OAuthAccessTokenHolder.getInstance().save(CredentialsManager.getInstance());
+                    GuiHelper.runInEDT(OAuthAuthenticationPreferencesPanel.this::refreshView);
+                }, OsmScopes.read_gpx, OsmScopes.write_gpx,
+                        OsmScopes.read_prefs, OsmScopes.write_prefs,
+                        OsmScopes.write_api, OsmScopes.write_notes);
+            }
+        }
+    }
+
+    /**
+     * Remove the OAuth authorization token
+     */
+    private class RemoveAuthorisationAction extends AbstractAction {
+        RemoveAuthorisationAction() {
+            putValue(NAME, tr("Remove token"));
+            putValue(SHORT_DESCRIPTION, tr("Remove token from JOSM. This does not revoke the token."));
+            new ImageProvider("cancel").getResource().attachImageIcon(this);
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            if (oAuthVersion == OAuthVersion.OAuth10a) {
+                OAuthAccessTokenHolder.getInstance().setAccessToken(null);
+            } else {
+                OAuthAccessTokenHolder.getInstance().setAccessToken(apiUrl, (IOAuthToken) null);
+            }
+            OAuthAccessTokenHolder.getInstance().save(CredentialsManager.getInstance());
             refreshView();
         }
Index: trunk/src/org/openstreetmap/josm/io/MessageNotifier.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/MessageNotifier.java	(revision 18649)
+++ trunk/src/org/openstreetmap/josm/io/MessageNotifier.java	(revision 18650)
@@ -11,4 +11,5 @@
 
 import org.openstreetmap.josm.data.UserIdentityManager;
+import org.openstreetmap.josm.data.oauth.OAuthVersion;
 import org.openstreetmap.josm.data.osm.UserInfo;
 import org.openstreetmap.josm.data.preferences.BooleanProperty;
@@ -144,6 +145,11 @@
             try {
                 if (JosmPreferencesCredentialAgent.class.equals(credManager.getCredentialsAgentClass())) {
-                    if (OsmApi.isUsingOAuth()) {
+                    if (OsmApi.isUsingOAuth(OAuthVersion.OAuth10a)) {
                         return credManager.lookupOAuthAccessToken() != null;
+                    } else if (OsmApi.isUsingOAuth(OAuthVersion.OAuth20) || OsmApi.isUsingOAuth(OAuthVersion.OAuth21)) {
+                        return credManager.lookupOAuthAccessToken(OsmApi.getOsmApi().getHost()) != null;
+                    } else if (OsmApi.isUsingOAuth()) {
+                        // Ensure we do not forget to update this section
+                        throw new IllegalStateException("Unknown oauth version: " + OsmApi.getAuthMethod());
                     } else {
                         String username = Config.getPref().get("osm-server.username", null);
Index: trunk/src/org/openstreetmap/josm/io/OsmApi.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/OsmApi.java	(revision 18649)
+++ trunk/src/org/openstreetmap/josm/io/OsmApi.java	(revision 18650)
@@ -30,4 +30,6 @@
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.notes.Note;
+import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
+import org.openstreetmap.josm.data.oauth.OAuthVersion;
 import org.openstreetmap.josm.data.osm.Changeset;
 import org.openstreetmap.josm.data.osm.IPrimitive;
@@ -83,4 +85,6 @@
 
     private static final ListenerList<OsmApiInitializationListener> listeners = ListenerList.create();
+    /** This is used to make certain we have set osm-server.auth-method to the "right" default */
+    private static boolean oauthCompatibilitySwitch;
 
     private URL url;
@@ -646,5 +650,42 @@
      */
     public static boolean isUsingOAuth() {
-        return "oauth".equals(getAuthMethod());
+        return isUsingOAuth(OAuthVersion.OAuth10a)
+                || isUsingOAuth(OAuthVersion.OAuth20)
+                || isUsingOAuth(OAuthVersion.OAuth21);
+    }
+
+    /**
+     * Determines if JOSM is configured to access OSM API via OAuth
+     * @param version The OAuth version
+     * @return {@code true} if JOSM is configured to access OSM API via OAuth, {@code false} otherwise
+     * @since 18650
+     */
+    public static boolean isUsingOAuth(OAuthVersion version) {
+        if (version == OAuthVersion.OAuth10a) {
+            return "oauth".equalsIgnoreCase(getAuthMethod());
+        } else if (version == OAuthVersion.OAuth20 || version == OAuthVersion.OAuth21) {
+            return "oauth20".equalsIgnoreCase(getAuthMethod());
+        }
+        return false;
+    }
+
+    /**
+     * Ensure that OAuth is set up
+     * @param api The api for which we need OAuth keys
+     * @return {@code true} if we are using OAuth and there are keys for the specified API
+     */
+    public static boolean isUsingOAuthAndOAuthSetUp(OsmApi api) {
+        if (OsmApi.isUsingOAuth()) {
+            if (OsmApi.isUsingOAuth(OAuthVersion.OAuth10a)) {
+                return OAuthAccessTokenHolder.getInstance().containsAccessToken();
+            }
+            if (OsmApi.isUsingOAuth(OAuthVersion.OAuth20)) {
+                return OAuthAccessTokenHolder.getInstance().getAccessToken(api.getBaseUrl(), OAuthVersion.OAuth20) != null;
+            }
+            if (OsmApi.isUsingOAuth(OAuthVersion.OAuth21)) {
+                return OAuthAccessTokenHolder.getInstance().getAccessToken(api.getBaseUrl(), OAuthVersion.OAuth21) != null;
+            }
+        }
+        return false;
     }
 
@@ -654,5 +695,23 @@
      */
     public static String getAuthMethod() {
-        return Config.getPref().get("osm-server.auth-method", "oauth");
+        setCurrentAuthMethod();
+        return Config.getPref().get("osm-server.auth-method", "oauth20");
+    }
+
+    /**
+     * This is a compatibility method for users who currently use OAuth 1.0 -- we are changing the default from oauth to oauth20,
+     * but since oauth was the default, pre-existing users will suddenly be switched to oauth20.
+     * This should be removed whenever {@link OAuthVersion#OAuth10a} support is removed.
+     */
+    private static void setCurrentAuthMethod() {
+        if (!oauthCompatibilitySwitch) {
+            oauthCompatibilitySwitch = true;
+            final String prefKey = "osm-server.auth-method";
+            if ("oauth20".equals(Config.getPref().get(prefKey, "oauth20"))
+                && !isUsingOAuthAndOAuthSetUp(OsmApi.getOsmApi())
+                && OAuthAccessTokenHolder.getInstance().containsAccessToken()) {
+                Config.getPref().put(prefKey, "oauth");
+            }
+        }
     }
 
Index: trunk/src/org/openstreetmap/josm/io/OsmConnection.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/OsmConnection.java	(revision 18649)
+++ trunk/src/org/openstreetmap/josm/io/OsmConnection.java	(revision 18650)
@@ -11,10 +11,24 @@
 import java.util.Base64;
 import java.util.Objects;
-
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+
+import javax.swing.JOptionPane;
+
+import org.openstreetmap.josm.data.oauth.IOAuthParameters;
+import org.openstreetmap.josm.data.oauth.IOAuthToken;
+import org.openstreetmap.josm.data.oauth.OAuth20Authorization;
 import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
 import org.openstreetmap.josm.data.oauth.OAuthParameters;
+import org.openstreetmap.josm.data.oauth.OAuthVersion;
+import org.openstreetmap.josm.data.oauth.osm.OsmScopes;
+import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.util.GuiHelper;
 import org.openstreetmap.josm.io.auth.CredentialsAgentException;
 import org.openstreetmap.josm.io.auth.CredentialsAgentResponse;
 import org.openstreetmap.josm.io.auth.CredentialsManager;
+import org.openstreetmap.josm.io.remotecontrol.RemoteControl;
 import org.openstreetmap.josm.tools.HttpClient;
 import org.openstreetmap.josm.tools.JosmRuntimeException;
@@ -37,4 +51,5 @@
     protected HttpClient activeConnection;
     protected OAuthParameters oauthParameters;
+    protected IOAuthParameters oAuth20Parameters;
 
     /**
@@ -172,6 +187,91 @@
             OAuthAccessTokenHolder.getInstance().setSaveToPreferences(true);
             OAuthAccessTokenHolder.getInstance().save(CredentialsManager.getInstance());
-        } catch (MalformedURLException | InterruptedException | InvocationTargetException e) {
+        } catch (MalformedURLException | InvocationTargetException e) {
             throw new MissingOAuthAccessTokenException(e);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new MissingOAuthAccessTokenException(e);
+        }
+    }
+
+    /**
+     * Obtains an OAuth access token for the connection.
+     * Afterwards, the token is accessible via {@link OAuthAccessTokenHolder} / {@link CredentialsManager}.
+     * @throws MissingOAuthAccessTokenException if the process cannot be completed successfully
+     */
+    private void obtainOAuth20Token() throws MissingOAuthAccessTokenException {
+        if (!Boolean.TRUE.equals(GuiHelper.runInEDTAndWaitAndReturn(() ->
+                ConditionalOptionPaneUtil.showConfirmationDialog("oauth.oauth20.obtain.automatically",
+                    MainApplication.getMainFrame(),
+                    tr("Obtain OAuth 2.0 token for authentication?"),
+                    tr("Obtain authentication to OSM servers"),
+                    JOptionPane.YES_NO_CANCEL_OPTION,
+                    JOptionPane.QUESTION_MESSAGE, JOptionPane.YES_OPTION)))) {
+            return; // User doesn't want to perform auth
+        }
+        final boolean remoteControlIsRunning = Boolean.TRUE.equals(RemoteControl.PROP_REMOTECONTROL_ENABLED.get());
+        if (!remoteControlIsRunning) {
+            RemoteControl.start();
+        }
+        AtomicBoolean done = new AtomicBoolean();
+        Consumer<IOAuthToken> consumer = authToken -> {
+                    if (!remoteControlIsRunning) {
+                        RemoteControl.stop();
+                    }
+                    // Clean up old token/password
+                    OAuthAccessTokenHolder.getInstance().setAccessToken(null);
+                    OAuthAccessTokenHolder.getInstance().setAccessToken(OsmApi.getOsmApi().getServerUrl(), authToken);
+                    OAuthAccessTokenHolder.getInstance().save(CredentialsManager.getInstance());
+                    synchronized (done) {
+                        done.set(true);
+                        done.notifyAll();
+                    }
+                };
+        new OAuth20Authorization().authorize(oAuth20Parameters,
+                consumer, OsmScopes.read_gpx, OsmScopes.write_gpx,
+                OsmScopes.read_prefs, OsmScopes.write_prefs,
+                OsmScopes.write_api, OsmScopes.write_notes);
+        synchronized (done) {
+            // Only wait at most 5 minutes
+            int counter = 0;
+            while (!done.get() && counter < 5) {
+                try {
+                    done.wait(TimeUnit.MINUTES.toMillis(1));
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                    Logging.trace(e);
+                    consumer.accept(null);
+                    throw new MissingOAuthAccessTokenException(e);
+                }
+                counter++;
+            }
+        }
+    }
+
+    /**
+     * Signs the connection with an OAuth authentication header
+     *
+     * @param connection the connection
+     *
+     * @throws MissingOAuthAccessTokenException if there is currently no OAuth Access Token configured
+     * @throws OsmTransferException if signing fails
+     */
+    protected void addOAuth20AuthorizationHeader(HttpClient connection) throws OsmTransferException {
+        if (this.oAuth20Parameters == null) {
+            this.oAuth20Parameters = OAuthParameters.createFromApiUrl(connection.getURL().getHost(), OAuthVersion.OAuth20);
+        }
+        OAuthAccessTokenHolder holder = OAuthAccessTokenHolder.getInstance();
+        IOAuthToken token = holder.getAccessToken(connection.getURL().toExternalForm(), OAuthVersion.OAuth20);
+        if (token == null) {
+            obtainOAuth20Token();
+            token = holder.getAccessToken(connection.getURL().toExternalForm(), OAuthVersion.OAuth20);
+        }
+        if (token == null) { // check if wizard completed
+            throw new MissingOAuthAccessTokenException();
+        }
+        try {
+            token.sign(connection);
+        } catch (org.openstreetmap.josm.data.oauth.OAuthException e) {
+            throw new OsmTransferException(tr("Failed to sign a HTTP connection with an OAuth Authentication header"), e);
         }
     }
@@ -179,12 +279,18 @@
     protected void addAuth(HttpClient connection) throws OsmTransferException {
         final String authMethod = OsmApi.getAuthMethod();
-        if ("basic".equals(authMethod)) {
-            addBasicAuthorizationHeader(connection);
-        } else if ("oauth".equals(authMethod)) {
-            addOAuthAuthorizationHeader(connection);
-        } else {
-            String msg = tr("Unexpected value for preference ''{0}''. Got ''{1}''.", "osm-server.auth-method", authMethod);
-            Logging.warn(msg);
-            throw new OsmTransferException(msg);
+        switch (authMethod) {
+            case "basic":
+                addBasicAuthorizationHeader(connection);
+                return;
+            case "oauth":
+                addOAuthAuthorizationHeader(connection);
+                return;
+            case "oauth20":
+                addOAuth20AuthorizationHeader(connection);
+                return;
+            default:
+                String msg = tr("Unexpected value for preference ''{0}''. Got ''{1}''.", "osm-server.auth-method", authMethod);
+                Logging.warn(msg);
+                throw new OsmTransferException(msg);
         }
     }
Index: trunk/src/org/openstreetmap/josm/io/OsmServerReader.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/OsmServerReader.java	(revision 18649)
+++ trunk/src/org/openstreetmap/josm/io/OsmServerReader.java	(revision 18650)
@@ -11,4 +11,5 @@
 import java.net.URL;
 import java.util.List;
+import java.util.Objects;
 
 import javax.xml.parsers.ParserConfigurationException;
@@ -18,5 +19,4 @@
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.gui.progress.ProgressMonitor;
-import org.openstreetmap.josm.io.auth.CredentialsAgentException;
 import org.openstreetmap.josm.io.auth.CredentialsManager;
 import org.openstreetmap.josm.tools.HttpClient;
@@ -39,5 +39,5 @@
  */
 public abstract class OsmServerReader extends OsmConnection {
-    private final OsmApi api = OsmApi.getOsmApi();
+    private final OsmApi api;
     private boolean doAuthenticate;
     protected boolean gpxParsedProperly;
@@ -48,11 +48,15 @@
      */
     protected OsmServerReader() {
-        try {
-            doAuthenticate = OsmApi.isUsingOAuth()
-                    && CredentialsManager.getInstance().lookupOAuthAccessToken() != null
-                    && OsmApi.USE_OAUTH_FOR_ALL_REQUESTS.get();
-        } catch (CredentialsAgentException e) {
-            Logging.warn(e);
-        }
+        this(OsmApi.getOsmApi());
+    }
+
+    /**
+     * Constructs a new {@code OsmServerReader}.
+     * @param osmApi The API to use for this call
+     * @since 18650
+     */
+    protected OsmServerReader(OsmApi osmApi) {
+        this.api = osmApi;
+        this.doAuthenticate = OsmApi.isUsingOAuthAndOAuthSetUp(osmApi) && OsmApi.USE_OAUTH_FOR_ALL_REQUESTS.get();
     }
 
@@ -185,5 +189,5 @@
             activeConnection = client;
             adaptRequest(client);
-            if (doAuthenticate) {
+            if (doAuthenticate && Objects.equals(this.api.getHost(), client.getURL().getHost())) {
                 addAuth(client);
             }
Index: trunk/src/org/openstreetmap/josm/io/auth/CredentialsAgent.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/auth/CredentialsAgent.java	(revision 18649)
+++ trunk/src/org/openstreetmap/josm/io/auth/CredentialsAgent.java	(revision 18650)
@@ -6,4 +6,7 @@
 import java.net.PasswordAuthentication;
 
+import javax.annotation.Nullable;
+
+import org.openstreetmap.josm.data.oauth.IOAuthToken;
 import org.openstreetmap.josm.data.oauth.OAuthToken;
 
@@ -66,4 +69,16 @@
 
     /**
+     * Lookup the current OAuth Access Token to access the specified server. Replies null, if no
+     * Access Token is currently managed by this CredentialAgent.
+     *
+     * @param host The host to get OAuth credentials for
+     * @return the current OAuth Access Token to access the specified server.
+     * @throws CredentialsAgentException if something goes wrong
+     * @since 18650
+     */
+    @Nullable
+    IOAuthToken lookupOAuthAccessToken(String host) throws CredentialsAgentException;
+
+    /**
      * Stores the OAuth Access Token <code>accessToken</code>.
      *
@@ -72,4 +87,15 @@
      */
     void storeOAuthAccessToken(OAuthToken accessToken) throws CredentialsAgentException;
+
+    /**
+     * Stores the OAuth Access Token <code>accessToken</code>.
+     *
+     * @param host The host the access token is for
+     * @param accessToken the access Token. null, to remove the Access Token. This will remove all IOAuthTokens <i>not</i> managed by
+     *                    {@link #storeOAuthAccessToken(OAuthToken)}.
+     * @throws CredentialsAgentException if something goes wrong
+     * @since 18650
+     */
+    void storeOAuthAccessToken(String host, IOAuthToken accessToken) throws CredentialsAgentException;
 
     /**
Index: trunk/src/org/openstreetmap/josm/io/auth/CredentialsManager.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/auth/CredentialsManager.java	(revision 18649)
+++ trunk/src/org/openstreetmap/josm/io/auth/CredentialsManager.java	(revision 18650)
@@ -8,4 +8,5 @@
 
 import org.openstreetmap.josm.data.UserIdentityManager;
+import org.openstreetmap.josm.data.oauth.IOAuthToken;
 import org.openstreetmap.josm.data.oauth.OAuthToken;
 import org.openstreetmap.josm.io.OsmApi;
@@ -162,6 +163,16 @@
 
     @Override
+    public IOAuthToken lookupOAuthAccessToken(String host) throws CredentialsAgentException {
+        return delegate.lookupOAuthAccessToken(host);
+    }
+
+    @Override
     public void storeOAuthAccessToken(OAuthToken accessToken) throws CredentialsAgentException {
         delegate.storeOAuthAccessToken(accessToken);
+    }
+
+    @Override
+    public void storeOAuthAccessToken(String host, IOAuthToken accessToken) throws CredentialsAgentException {
+        delegate.storeOAuthAccessToken(host, accessToken);
     }
 
Index: trunk/src/org/openstreetmap/josm/io/auth/JosmPreferencesCredentialAgent.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/auth/JosmPreferencesCredentialAgent.java	(revision 18649)
+++ trunk/src/org/openstreetmap/josm/io/auth/JosmPreferencesCredentialAgent.java	(revision 18650)
@@ -7,13 +7,22 @@
 import java.net.Authenticator.RequestorType;
 import java.net.PasswordAuthentication;
+import java.util.HashSet;
 import java.util.Objects;
-
+import java.util.Set;
+
+import javax.json.JsonException;
 import javax.swing.text.html.HTMLEditorKit;
 
+import org.openstreetmap.josm.data.oauth.IOAuthToken;
+import org.openstreetmap.josm.data.oauth.OAuth20Exception;
+import org.openstreetmap.josm.data.oauth.OAuth20Parameters;
+import org.openstreetmap.josm.data.oauth.OAuth20Token;
 import org.openstreetmap.josm.data.oauth.OAuthToken;
+import org.openstreetmap.josm.data.oauth.OAuthVersion;
 import org.openstreetmap.josm.gui.widgets.HtmlPanel;
 import org.openstreetmap.josm.io.DefaultProxySelector;
 import org.openstreetmap.josm.io.OsmApi;
 import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.tools.Utils;
 
 /**
@@ -69,5 +78,5 @@
             if (Objects.equals(OsmApi.getOsmApi().getHost(), host)) {
                 Config.getPref().put("osm-server.username", credentials.getUserName());
-                if (credentials.getPassword() == null) {
+                if (credentials.getPassword().length == 0) { // PasswordAuthentication#getPassword cannot be null
                     Config.getPref().put("osm-server.password", null);
                 } else {
@@ -76,5 +85,5 @@
             } else if (host != null) {
                 Config.getPref().put("server.username."+host, credentials.getUserName());
-                if (credentials.getPassword() == null) {
+                if (credentials.getPassword().length == 0) {
                     Config.getPref().put("server.password."+host, null);
                 } else {
@@ -85,5 +94,5 @@
         case PROXY:
             Config.getPref().put(DefaultProxySelector.PROXY_USER, credentials.getUserName());
-            if (credentials.getPassword() == null) {
+            if (credentials.getPassword().length == 0) {
                 Config.getPref().put(DefaultProxySelector.PROXY_PASS, null);
             } else {
@@ -108,4 +117,28 @@
             return null;
         return new OAuthToken(accessTokenKey, accessTokenSecret);
+    }
+
+    @Override
+    public IOAuthToken lookupOAuthAccessToken(String host) throws CredentialsAgentException {
+        Set<String> keySet = new HashSet<>(Config.getPref().getKeySet());
+        keySet.addAll(Config.getPref().getSensitive()); // Just in case we decide to not return sensitive keys in getKeySet
+        for (OAuthVersion oauthType : OAuthVersion.values()) {
+            final String hostKey = "oauth.access-token.object." + oauthType + "." + host;
+            final String parametersKey = "oauth.access-token.parameters." + oauthType + "." + host;
+            if (!keySet.contains(hostKey) || !keySet.contains(parametersKey)) {
+                continue; // Avoid adding empty temporary entries to preferences
+            }
+            String token = Config.getPref().get(hostKey, null);
+            String parameters = Config.getPref().get(parametersKey, null);
+            if (!Utils.isBlank(token) && !Utils.isBlank(parameters) && OAuthVersion.OAuth20 == oauthType) {
+                try {
+                    OAuth20Parameters oAuth20Parameters = new OAuth20Parameters(parameters);
+                    return new OAuth20Token(oAuth20Parameters, token);
+                } catch (OAuth20Exception | JsonException e) {
+                    throw new CredentialsAgentException(e);
+                }
+            }
+        }
+        return null;
     }
 
@@ -124,4 +157,31 @@
             Config.getPref().put("oauth.access-token.key", accessToken.getKey());
             Config.getPref().put("oauth.access-token.secret", accessToken.getSecret());
+        }
+    }
+
+    @Override
+    public void storeOAuthAccessToken(String host, IOAuthToken accessToken) throws CredentialsAgentException {
+        Objects.requireNonNull(host, "host");
+        if (accessToken == null) {
+            Set<String> keySet = new HashSet<>(Config.getPref().getKeySet());
+            keySet.addAll(Config.getPref().getSensitive()); // Just in case we decide to not return sensitive keys in getKeySet
+            // Assume we want to remove all access tokens
+            for (OAuthVersion oauthType : OAuthVersion.values()) {
+                final String hostKey = "oauth.access-token.parameters." + oauthType + "." + host;
+                final String parametersKey = "oauth.access-token.parameters." + oauthType + "." + host;
+                if (keySet.contains(hostKey)) {
+                    Config.getPref().removeSensitive(hostKey);
+                }
+                if (keySet.contains(parametersKey)) {
+                    Config.getPref().removeSensitive(parametersKey);
+                }
+            }
+        } else {
+            final String hostKey = "oauth.access-token.object." + accessToken.getOAuthType() + "." + host;
+            final String parametersKey = "oauth.access-token.parameters." + accessToken.getOAuthType() + "." + host;
+            Config.getPref().put(hostKey, accessToken.toPreferencesString());
+            Config.getPref().put(parametersKey, accessToken.getParameters().toPreferencesString());
+            Config.getPref().addSensitive(this, hostKey);
+            Config.getPref().addSensitive(this, parametersKey);
         }
     }
Index: trunk/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java	(revision 18649)
+++ trunk/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java	(revision 18650)
@@ -31,4 +31,5 @@
 import org.openstreetmap.josm.io.remotecontrol.handler.AddNodeHandler;
 import org.openstreetmap.josm.io.remotecontrol.handler.AddWayHandler;
+import org.openstreetmap.josm.io.remotecontrol.handler.AuthorizationHandler;
 import org.openstreetmap.josm.io.remotecontrol.handler.FeaturesHandler;
 import org.openstreetmap.josm.io.remotecontrol.handler.ImageryHandler;
@@ -172,4 +173,5 @@
             addRequestHandlerClass(FeaturesHandler.command, FeaturesHandler.class, true);
             addRequestHandlerClass(OpenApiHandler.command, OpenApiHandler.class, true);
+            addRequestHandlerClass(AuthorizationHandler.command, AuthorizationHandler.class, true);
         }
     }
Index: trunk/src/org/openstreetmap/josm/io/remotecontrol/handler/AuthorizationHandler.java
===================================================================
--- trunk/src/org/openstreetmap/josm/io/remotecontrol/handler/AuthorizationHandler.java	(revision 18650)
+++ trunk/src/org/openstreetmap/josm/io/remotecontrol/handler/AuthorizationHandler.java	(revision 18650)
@@ -0,0 +1,159 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.io.remotecontrol.handler;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.openstreetmap.josm.data.preferences.BooleanProperty;
+import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
+
+/**
+ * Handle authorization requests (mostly OAuth)
+ * @since 18650
+ */
+public class AuthorizationHandler extends RequestHandler {
+
+    /**
+     * The actual authorization consumer/handler
+     */
+    public interface AuthorizationConsumer {
+        /**
+         * Validate the request
+         * @param args The GET request arguments
+         * @param request The request URL without "GET".
+         * @param sender who sent the request? the host from referer header or IP of request sender
+         * @throws RequestHandlerBadRequestException if the request is invalid
+         * @see RequestHandler#validateRequest()
+         */
+        void validateRequest(String sender, String request, Map<String, String> args)
+                throws RequestHandlerBadRequestException;
+
+        /**
+         * Handle the request. Any time-consuming operation must be performed asynchronously to avoid delaying the HTTP response.
+         * @param args The GET request arguments
+         * @param request The request URL without "GET".
+         * @param sender who sent the request? the host from referer header or IP of request sender
+         * @return The response to show the user. May be {@code null}.
+         * @throws RequestHandlerErrorException if an error occurs while processing the request
+         * @throws RequestHandlerBadRequestException if the request is invalid
+         * @see RequestHandler#handleRequest()
+         */
+        ResponseRecord handleRequest(String sender, String request, Map<String, String> args)
+                throws RequestHandlerErrorException, RequestHandlerBadRequestException;
+    }
+
+    /**
+     * A basic record for changing responses
+     */
+    public static final class ResponseRecord {
+        private final String content;
+        private final String type;
+
+        /**
+         * Create a new record
+         * @param content The content to show the user
+         * @param type The content mime type
+         */
+        public ResponseRecord(String content, String type) {
+            this.content = content;
+            this.type = type;
+        }
+
+        /**
+         * Get the content for the response
+         * @return The content as a string
+         */
+        public String content() {
+            return this.content;
+        }
+
+        /**
+         * Get the type for the response
+         * @return The response mime type
+         */
+        public String type() {
+            return this.type;
+        }
+    }
+
+    /**
+     * The remote control command
+     */
+    public static final String command = "oauth_authorization";
+
+    private static final BooleanProperty PROPERTY = new BooleanProperty("remotecontrol.permission.authorization", false);
+    private static final Map<String, AuthorizationConsumer> AUTHORIZATION_CONSUMERS = new HashMap<>();
+
+    private AuthorizationConsumer consumer;
+    /**
+     * Add an authorization consumer.
+     * @param state The unique state for each request (for OAuth, this would be the {@code state} parameter)
+     * @param consumer The consumer of the response
+     */
+    public static synchronized void addAuthorizationConsumer(String state, AuthorizationConsumer consumer) {
+        if (AUTHORIZATION_CONSUMERS.containsKey(state)) {
+            throw new IllegalArgumentException("Cannot add multiple consumers for one authorization state");
+        }
+        AUTHORIZATION_CONSUMERS.put(state, consumer);
+    }
+
+    @Override
+    protected void validateRequest() throws RequestHandlerBadRequestException {
+        boolean clearAll = false;
+        for (Map.Entry<String, AuthorizationConsumer> entry : AUTHORIZATION_CONSUMERS.entrySet()) {
+            if (Objects.equals(this.args.get("state"), entry.getKey())) {
+                if (this.consumer == null) {
+                    this.consumer = entry.getValue();
+                } else {
+                    // Remove all authorization consumers. Someone might be playing games.
+                    clearAll = true;
+                }
+            }
+        }
+        if (clearAll) {
+            AUTHORIZATION_CONSUMERS.clear();
+            this.consumer = null;
+            throw new RequestHandlerBadRequestException("Multiple states for authorization");
+        }
+
+        if (this.consumer == null) {
+            throw new RequestHandlerBadRequestException("Unknown state for authorization");
+        }
+        this.consumer.validateRequest(this.sender, this.request, this.args);
+    }
+
+    @Override
+    protected void handleRequest() throws RequestHandlerErrorException, RequestHandlerBadRequestException {
+        ResponseRecord response = this.consumer.handleRequest(this.sender, this.request, this.args);
+        if (response != null) {
+            this.content = Optional.ofNullable(response.content()).orElse(this.content);
+            this.contentType = Optional.ofNullable(response.type()).orElse(this.contentType);
+        }
+        // Only ever allow a consumer to be used once
+        AUTHORIZATION_CONSUMERS.entrySet().stream().filter(entry -> Objects.equals(this.consumer, entry.getValue()))
+                .map(Map.Entry::getKey).collect(Collectors.toList()).forEach(AUTHORIZATION_CONSUMERS::remove);
+        this.consumer = null;
+    }
+
+    @Override
+    public String getPermissionMessage() {
+        return "Allow OAuth remote control to set credentials";
+    }
+
+    @Override
+    public PermissionPrefWithDefault getPermissionPref() {
+        return null;
+    }
+
+    public BooleanProperty getPermissionPreference() {
+        return PROPERTY;
+    }
+
+    @Override
+    public String[] getMandatoryParams() {
+        return new String[] {"code", "state"};
+    }
+}
Index: trunk/src/org/openstreetmap/josm/spi/preferences/AbstractPreferences.java
===================================================================
--- trunk/src/org/openstreetmap/josm/spi/preferences/AbstractPreferences.java	(revision 18649)
+++ trunk/src/org/openstreetmap/josm/spi/preferences/AbstractPreferences.java	(revision 18650)
@@ -2,11 +2,19 @@
 package org.openstreetmap.josm.spi.preferences;
 
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Set;
 import java.util.TreeMap;
 import java.util.stream.Collectors;
 
+import org.openstreetmap.josm.io.DefaultProxySelector;
+import org.openstreetmap.josm.io.auth.CredentialsAgent;
+import org.openstreetmap.josm.io.auth.CredentialsManager;
 import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.Utils;
@@ -17,4 +25,9 @@
  */
 public abstract class AbstractPreferences implements IPreferences {
+    /** The preference key for sensitive keys */
+    private static final String KEY_SENSITIVE_KEYS = "sensitive.keys";
+
+    /** A set of sensitive keys that should not be seen/distributed outside of specific callers (like a {@link CredentialsAgent}) */
+    private static final Set<String> SENSITIVE_KEYS = new HashSet<>();
 
     @Override
@@ -176,3 +189,42 @@
                 .collect(Collectors.toCollection(LinkedList::new));
     }
+
+    @Override
+    public void addSensitive(CredentialsAgent caller, String key) {
+        if (SENSITIVE_KEYS.isEmpty()) {
+            populateSensitiveKeys();
+        }
+        if (CredentialsManager.getInstance().getCredentialsAgentClass().equals(caller.getClass())) {
+            SENSITIVE_KEYS.add(key);
+            putList("sensitive.keys", SENSITIVE_KEYS.stream().sorted().collect(Collectors.toList()));
+        }
+    }
+
+    @Override
+    public Collection<String> getSensitive() {
+        if (SENSITIVE_KEYS.isEmpty()) {
+            populateSensitiveKeys();
+        }
+        return Collections.unmodifiableSet(SENSITIVE_KEYS);
+    }
+
+    @Override
+    public void removeSensitive(String key) {
+        if (KEY_SENSITIVE_KEYS.equals(key)) {
+            throw new IllegalArgumentException(KEY_SENSITIVE_KEYS + " cannot be removed from the sensitive key list.");
+        }
+        // Reset the key first -- avoid race conditions where a sensitive value might be visible if we start restricting access in the future.
+        put(key, null);
+        SENSITIVE_KEYS.remove(key);
+        putList(KEY_SENSITIVE_KEYS, SENSITIVE_KEYS.stream().sorted().collect(Collectors.toList()));
+    }
+
+    /**
+     * Populate the sensitive key set from preferences
+     */
+    private void populateSensitiveKeys() {
+        SENSITIVE_KEYS.addAll(getList(KEY_SENSITIVE_KEYS, Arrays.asList("sensitive.keys", "osm-server.username", "osm-server.password",
+                DefaultProxySelector.PROXY_USER, DefaultProxySelector.PROXY_PASS,
+                "oauth.access-token.key", "oauth.access-token.secret")));
+    }
 }
Index: trunk/src/org/openstreetmap/josm/spi/preferences/IPreferences.java
===================================================================
--- trunk/src/org/openstreetmap/josm/spi/preferences/IPreferences.java	(revision 18649)
+++ trunk/src/org/openstreetmap/josm/spi/preferences/IPreferences.java	(revision 18650)
@@ -2,8 +2,11 @@
 package org.openstreetmap.josm.spi.preferences;
 
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+
+import org.openstreetmap.josm.io.auth.CredentialsAgent;
 
 /**
@@ -240,3 +243,26 @@
      */
     Set<String> getKeySet();
+
+    /**
+     * Add sensitive keys
+     * @param caller The calling agent
+     * @param key The key that may contain sensitive information
+     * @since 18650
+     */
+    void addSensitive(CredentialsAgent caller, String key);
+
+    /**
+     * Get sensitive keys
+     * @return The sensitive keys
+     * @since 18650
+     */
+    Collection<String> getSensitive();
+
+    /**
+     * Remove sensitive keys. This removes the key from the sensitive list <i>and</i>
+     * removes the stored preference value.
+     * @param key The key to remove
+     * @since 18650
+     */
+    void removeSensitive(String key);
 }
