Index: core/src/org/openstreetmap/josm/data/oauth/IOAuthAuthorization.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/data/oauth/IOAuthAuthorization.java b/core/src/org/openstreetmap/josm/data/oauth/IOAuthAuthorization.java
new file mode 100644
--- /dev/null	(date 1667852632052)
+++ b/core/src/org/openstreetmap/josm/data/oauth/IOAuthAuthorization.java	(date 1667852632052)
@@ -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 xxx
+ */
+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: core/src/org/openstreetmap/josm/data/oauth/IOAuthParameters.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/data/oauth/IOAuthParameters.java b/core/src/org/openstreetmap/josm/data/oauth/IOAuthParameters.java
new file mode 100644
--- /dev/null	(date 1667852116276)
+++ b/core/src/org/openstreetmap/josm/data/oauth/IOAuthParameters.java	(date 1667852116276)
@@ -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 xxx
+ */
+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: core/src/org/openstreetmap/josm/data/oauth/IOAuthToken.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/data/oauth/IOAuthToken.java b/core/src/org/openstreetmap/josm/data/oauth/IOAuthToken.java
new file mode 100644
--- /dev/null	(date 1667306111983)
+++ b/core/src/org/openstreetmap/josm/data/oauth/IOAuthToken.java	(date 1667306111983)
@@ -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 xxx
+ */
+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: core/src/org/openstreetmap/josm/data/oauth/OAuth20Authorization.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/data/oauth/OAuth20Authorization.java b/core/src/org/openstreetmap/josm/data/oauth/OAuth20Authorization.java
new file mode 100644
--- /dev/null	(date 1667853012119)
+++ b/core/src/org/openstreetmap/josm/data/oauth/OAuth20Authorization.java	(date 1667853012119)
@@ -0,0 +1,111 @@
+// 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
+ */
+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: core/src/org/openstreetmap/josm/data/oauth/OAuth20Exception.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/data/oauth/OAuth20Exception.java b/core/src/org/openstreetmap/josm/data/oauth/OAuth20Exception.java
new file mode 100644
--- /dev/null	(date 1667853187420)
+++ b/core/src/org/openstreetmap/josm/data/oauth/OAuth20Exception.java	(date 1667853187420)
@@ -0,0 +1,67 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.oauth;
+
+import javax.json.JsonObject;
+
+/**
+ * A generic OAuth 2.0 exception
+ */
+public class OAuth20Exception extends OAuthException {
+    /**
+     * Invalid request types
+     */
+    public enum Type {
+        invalid_request,
+        invalid_client,
+        invalid_grant,
+        unauthorized_client,
+        unsupported_grant_type,
+        invalid_scope,
+        unknown
+    }
+    private final Type type;
+
+    public OAuth20Exception(Exception cause) {
+        super(cause);
+        this.type = Type.unknown;
+    }
+
+    public OAuth20Exception(String message) {
+        super(message);
+        this.type = Type.unknown;
+    }
+
+    OAuth20Exception(JsonObject serverMessage) {
+        super(serverMessage.getString("error_description", serverMessage.getString("error", "Unknown error")));
+        if (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: core/src/org/openstreetmap/josm/data/oauth/OAuth20Parameters.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/data/oauth/OAuth20Parameters.java b/core/src/org/openstreetmap/josm/data/oauth/OAuth20Parameters.java
new file mode 100644
--- /dev/null	(date 1667853214659)
+++ b/core/src/org/openstreetmap/josm/data/oauth/OAuth20Parameters.java	(date 1667853214659)
@@ -0,0 +1,149 @@
+// 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 xxx
+ */
+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");
+        // Alternatively, we could try using rfc8414 ( /.well-known/oauth-authorization-server ), but OSM 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: core/src/org/openstreetmap/josm/data/oauth/OAuth20Token.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/data/oauth/OAuth20Token.java b/core/src/org/openstreetmap/josm/data/oauth/OAuth20Token.java
new file mode 100644
--- /dev/null	(date 1667487380484)
+++ b/core/src/org/openstreetmap/josm/data/oauth/OAuth20Token.java	(date 1667487380484)
@@ -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 xxx
+ */
+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: core/src/org/openstreetmap/josm/data/oauth/OAuthAccessTokenHolder.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/data/oauth/OAuthAccessTokenHolder.java b/core/src/org/openstreetmap/josm/data/oauth/OAuthAccessTokenHolder.java
--- a/core/src/org/openstreetmap/josm/data/oauth/OAuthAccessTokenHolder.java	(revision 18597)
+++ b/core/src/org/openstreetmap/josm/data/oauth/OAuthAccessTokenHolder.java	(date 1667486994561)
@@ -3,6 +3,11 @@
 
 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 org.openstreetmap.josm.io.auth.CredentialsAgent;
 import org.openstreetmap.josm.io.auth.CredentialsAgentException;
 import org.openstreetmap.josm.spi.preferences.Config;
@@ -31,6 +36,8 @@
     private String accessTokenKey;
     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.
      *
@@ -102,6 +109,21 @@
         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}
+     */
+    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);
+        }
+        return null;
+    }
+
     /**
      * Sets the access token hold by this holder.
      *
@@ -128,6 +150,21 @@
         }
     }
 
+    /**
+     * 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.
+     */
+    public void setAccessToken(String api, IOAuthToken token) {
+        api = URI.create(api).getHost();
+        if (token == null) {
+            this.tokenMap.remove(api);
+        } else {
+            this.tokenMap.computeIfAbsent(api, key -> new EnumMap<>(OAuthVersion.class)).put(token.getOAuthType(), token);
+        }
+    }
+
     /**
      * Replies true if this holder contains an complete access token, consisting of an
      * Access Token Key and an Access Token Secret.
@@ -175,8 +212,20 @@
         try {
             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()) {
+                    for (OAuthVersion version : OAuthVersion.values()) {
+                        if (entry.getValue().containsKey(version)) {
+                            cm.storeOAuthAccessToken(entry.getKey(), entry.getValue().get(version));
+                        }
+                    }
+                }
             }
         } catch (CredentialsAgentException e) {
             Logging.error(e);
Index: core/src/org/openstreetmap/josm/data/oauth/OAuthException.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/data/oauth/OAuthException.java b/core/src/org/openstreetmap/josm/data/oauth/OAuthException.java
new file mode 100644
--- /dev/null	(date 1667420941405)
+++ b/core/src/org/openstreetmap/josm/data/oauth/OAuthException.java	(date 1667420941405)
@@ -0,0 +1,19 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.oauth;
+
+/**
+ * Base OAuth exception
+ * @author Taylor Smock
+ * @since xxx
+ */
+public abstract class OAuthException extends Exception {
+    OAuthException(Exception cause) {
+        super(cause);
+    }
+
+    OAuthException(String message) {
+        super(message);
+    }
+
+    abstract OAuthVersion[] getOAuthVersions();
+}
Index: core/src/org/openstreetmap/josm/data/oauth/OAuthParameters.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/data/oauth/OAuthParameters.java b/core/src/org/openstreetmap/josm/data/oauth/OAuthParameters.java
--- a/core/src/org/openstreetmap/josm/data/oauth/OAuthParameters.java	(revision 18597)
+++ b/core/src/org/openstreetmap/josm/data/oauth/OAuthParameters.java	(date 1667853236273)
@@ -1,11 +1,24 @@
 // License: GPL. For details, see LICENSE file.
 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.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;
 
 import oauth.signpost.OAuthConsumer;
@@ -15,7 +28,7 @@
  * This class manages an immutable set of OAuth parameters.
  * @since 2747
  */
-public class OAuthParameters {
+public class OAuthParameters implements IOAuthParameters {
 
     /**
      * The default JOSM OAuth consumer key (created by user josmeditor).
@@ -46,14 +59,107 @@
      * @since 5422
      */
     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 xxx
+     */
+    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 = "jo32l-hRbwWni_lnwQxffvSEfW-5agfuzoT8A33IFkY";
+            clientSecret = "OGH6-yYZHYAbw2J_LFS1yu1QiVFrCONhYWisftr6LRs";
+            baseUrl = "https://www.openstreetmap.org/oauth2";
+            redirectUri = "http://127.0.0.1:8111/oauth_authorization";
+        }
+        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)) {
             consumerKey = ""; // a custom consumer key is required
             consumerSecret = ""; // a custom consumer secret is requireds
@@ -81,20 +187,49 @@
      * @return the parameters
      */
     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 xxx
+     */
+    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.getParameters();
+                } catch (CredentialsAgentException e) {
+                    Logging.trace(e);
+                }
+                return oAuth20Parameters;
+            default:
+                throw new IllegalArgumentException("Unknown OAuth version: " + oAuthVersion);
+        }
     }
 
     /**
      * Remembers the current values in the preferences.
      */
+    @Override
     public void rememberPreferences() {
         Config.getPref().put("oauth.settings.consumer-key", getConsumerKey());
         Config.getPref().put("oauth.settings.consumer-secret", getConsumerSecret());
@@ -182,16 +317,37 @@
      * Gets the access token URL.
      * @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.
      * @return The authorise URL
      */
     public String getAuthoriseUrl() {
-        return authoriseUrl;
+        return this.getAuthorizationUrl();
     }
 
     /**
Index: core/src/org/openstreetmap/josm/data/oauth/OAuthVersion.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/data/oauth/OAuthVersion.java b/core/src/org/openstreetmap/josm/data/oauth/OAuthVersion.java
new file mode 100644
--- /dev/null	(date 1667309299387)
+++ b/core/src/org/openstreetmap/josm/data/oauth/OAuthVersion.java	(date 1667309299387)
@@ -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 xxx
+ */
+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: core/src/org/openstreetmap/josm/data/oauth/osm/OsmScopes.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/data/oauth/osm/OsmScopes.java b/core/src/org/openstreetmap/josm/data/oauth/osm/OsmScopes.java
new file mode 100644
--- /dev/null	(date 1667243824209)
+++ b/core/src/org/openstreetmap/josm/data/oauth/osm/OsmScopes.java	(date 1667243824209)
@@ -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 xxx
+ */
+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: core/src/org/openstreetmap/josm/data/oauth/osm/package-info.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/data/oauth/osm/package-info.java b/core/src/org/openstreetmap/josm/data/oauth/osm/package-info.java
new file mode 100644
--- /dev/null	(date 1667243761153)
+++ b/core/src/org/openstreetmap/josm/data/oauth/osm/package-info.java	(date 1667243761153)
@@ -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: core/src/org/openstreetmap/josm/gui/oauth/AbstractAuthorizationUI.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/gui/oauth/AbstractAuthorizationUI.java b/core/src/org/openstreetmap/josm/gui/oauth/AbstractAuthorizationUI.java
--- a/core/src/org/openstreetmap/josm/gui/oauth/AbstractAuthorizationUI.java	(revision 18597)
+++ b/core/src/org/openstreetmap/josm/gui/oauth/AbstractAuthorizationUI.java	(date 1667495781999)
@@ -3,8 +3,10 @@
 
 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;
 
@@ -20,7 +22,7 @@
     public static final String ACCESS_TOKEN_PROP = AbstractAuthorizationUI.class.getName() + ".accessToken";
 
     private String apiUrl;
-    private final AdvancedOAuthPropertiesPanel pnlAdvancedProperties = new AdvancedOAuthPropertiesPanel();
+    private final AdvancedOAuthPropertiesPanel pnlAdvancedProperties = new AdvancedOAuthPropertiesPanel(OAuthVersion.OAuth10a);
     private transient OAuthToken accessToken;
 
     /**
@@ -79,7 +81,7 @@
      *
      * @return the current set of advanced OAuth parameters in this UI
      */
-    public OAuthParameters getOAuthParameters() {
+    public IOAuthParameters getOAuthParameters() {
         return pnlAdvancedProperties.getAdvancedParameters();
     }
 
Index: core/src/org/openstreetmap/josm/gui/oauth/AdvancedOAuthPropertiesPanel.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/gui/oauth/AdvancedOAuthPropertiesPanel.java b/core/src/org/openstreetmap/josm/gui/oauth/AdvancedOAuthPropertiesPanel.java
--- a/core/src/org/openstreetmap/josm/gui/oauth/AdvancedOAuthPropertiesPanel.java	(revision 18597)
+++ b/core/src/org/openstreetmap/josm/gui/oauth/AdvancedOAuthPropertiesPanel.java	(date 1667852565013)
@@ -15,7 +15,10 @@
 import javax.swing.JLabel;
 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;
 import org.openstreetmap.josm.gui.help.HelpUtil;
@@ -51,13 +54,16 @@
     private final JosmTextField tfAuthoriseURL = new JosmTextField();
     private final JosmTextField tfOsmLoginURL = new JosmTextField();
     private final JosmTextField tfOsmLogoutURL = new JosmTextField();
+    private final OAuthVersion oauthVersion;
     private transient UseDefaultItemListener ilUseDefault;
     private String apiUrl;
 
     /**
      * Constructs a new {@code AdvancedOAuthPropertiesPanel}.
+     * @param oauthVersion The OAuth version to make the panel for
      */
-    public AdvancedOAuthPropertiesPanel() {
+    public AdvancedOAuthPropertiesPanel(OAuthVersion oauthVersion) {
+        this.oauthVersion = oauthVersion;
         build();
     }
 
@@ -74,10 +80,14 @@
         add(cbUseDefaults, gc);
 
         // -- consumer key
-        gc.gridy = 1;
+        gc.gridy++;
         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;
         gc.weightx = 1.0;
@@ -85,10 +95,14 @@
         SelectAllOnFocusGainedDecorator.decorate(tfConsumerKey);
 
         // -- 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;
         gc.weightx = 1.0;
@@ -96,10 +110,14 @@
         SelectAllOnFocusGainedDecorator.decorate(tfConsumerSecret);
 
         // -- 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;
         gc.weightx = 1.0;
@@ -107,7 +125,7 @@
         SelectAllOnFocusGainedDecorator.decorate(tfRequestTokenURL);
 
         // -- access token URL
-        gc.gridy = 4;
+        gc.gridy++;
         gc.gridx = 0;
         gc.weightx = 0.0;
         add(new JLabel(tr("Access Token URL:")), gc);
@@ -118,7 +136,7 @@
         SelectAllOnFocusGainedDecorator.decorate(tfAccessTokenURL);
 
         // -- authorise URL
-        gc.gridy = 5;
+        gc.gridy++;
         gc.gridx = 0;
         gc.weightx = 0.0;
         add(new JLabel(tr("Authorize URL:")), gc);
@@ -128,27 +146,29 @@
         add(tfAuthoriseURL, gc);
         SelectAllOnFocusGainedDecorator.decorate(tfAuthoriseURL);
 
-        // -- OSM login URL
-        gc.gridy = 6;
-        gc.gridx = 0;
-        gc.weightx = 0.0;
-        add(new JLabel(tr("OSM login URL:")), gc);
+        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);
+            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);
+            // -- 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);
+            gc.gridx = 1;
+            gc.weightx = 1.0;
+            add(tfOsmLogoutURL, gc);
+            SelectAllOnFocusGainedDecorator.decorate(tfOsmLogoutURL);
+        }
 
         ilUseDefault = new UseDefaultItemListener();
         cbUseDefaults.addItemListener(ilUseDefault);
@@ -191,14 +211,27 @@
 
     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);
     }
@@ -216,17 +249,26 @@
      *
      * @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()
+                );
     }
 
     /**
@@ -235,21 +277,31 @@
      * @param parameters the advanced parameters. Must not be null.
      * @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);
         } else {
             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: core/src/org/openstreetmap/josm/gui/oauth/FullyAutomaticAuthorizationUI.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/gui/oauth/FullyAutomaticAuthorizationUI.java b/core/src/org/openstreetmap/josm/gui/oauth/FullyAutomaticAuthorizationUI.java
--- a/core/src/org/openstreetmap/josm/gui/oauth/FullyAutomaticAuthorizationUI.java	(revision 18597)
+++ b/core/src/org/openstreetmap/josm/gui/oauth/FullyAutomaticAuthorizationUI.java	(date 1667496147832)
@@ -28,6 +28,7 @@
 import javax.swing.text.JTextComponent;
 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;
 import org.openstreetmap.josm.gui.PleaseWaitRunnable;
@@ -384,7 +385,7 @@
             executor.execute(new TestAccessTokenTask(
                     FullyAutomaticAuthorizationUI.this,
                     getApiUrl(),
-                    getAdvancedPropertiesPanel().getAdvancedParameters(),
+                    (OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters(),
                     getAccessToken()
             ));
         }
@@ -437,7 +438,7 @@
                             + "a valid login URL from the OAuth Authorize Endpoint URL ''{0}''.<br><br>"
                             + "Please check your advanced setting and try again."
                             + "</html>",
-                            getAdvancedPropertiesPanel().getAdvancedParameters().getAuthoriseUrl()),
+                            ((OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters()).getAuthoriseUrl()),
                     tr("OAuth authorization failed"),
                     JOptionPane.ERROR_MESSAGE,
                     HelpUtil.ht("/Dialog/OAuthAuthorisationWizard#FullyAutomaticProcessFailed")
@@ -445,7 +446,7 @@
         }
 
         protected void alertLoginFailed() {
-            final String loginUrl = getAdvancedPropertiesPanel().getAdvancedParameters().getOsmLoginUrl();
+            final String loginUrl = ((OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters()).getOsmLoginUrl();
             HelpAwareOptionPane.showOptionDialog(
                     FullyAutomaticAuthorizationUI.this,
                     tr("<html>"
@@ -479,7 +480,7 @@
             try {
                 getProgressMonitor().setTicksCount(3);
                 OsmOAuthAuthorizationClient authClient = new OsmOAuthAuthorizationClient(
-                        getAdvancedPropertiesPanel().getAdvancedParameters()
+                        (OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters()
                 );
                 OAuthToken requestToken = authClient.getRequestToken(
                         getProgressMonitor().createSubTaskMonitor(1, false)
Index: core/src/org/openstreetmap/josm/gui/oauth/ManualAuthorizationUI.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/gui/oauth/ManualAuthorizationUI.java b/core/src/org/openstreetmap/josm/gui/oauth/ManualAuthorizationUI.java
--- a/core/src/org/openstreetmap/josm/gui/oauth/ManualAuthorizationUI.java	(revision 18597)
+++ b/core/src/org/openstreetmap/josm/gui/oauth/ManualAuthorizationUI.java	(date 1667496260821)
@@ -25,6 +25,7 @@
 import javax.swing.text.JTextComponent;
 
 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;
 import org.openstreetmap.josm.gui.widgets.HtmlPanel;
@@ -232,7 +233,7 @@
             TestAccessTokenTask task = new TestAccessTokenTask(
                     ManualAuthorizationUI.this,
                     getApiUrl(),
-                    getAdvancedPropertiesPanel().getAdvancedParameters(),
+                    (OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters(),
                     getAccessToken()
             );
             executor.execute(task);
Index: core/src/org/openstreetmap/josm/gui/oauth/OAuthAuthorizationWizard.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/gui/oauth/OAuthAuthorizationWizard.java b/core/src/org/openstreetmap/josm/gui/oauth/OAuthAuthorizationWizard.java
--- a/core/src/org/openstreetmap/josm/gui/oauth/OAuthAuthorizationWizard.java	(revision 18597)
+++ b/core/src/org/openstreetmap/josm/gui/oauth/OAuthAuthorizationWizard.java	(date 1667507429997)
@@ -255,7 +255,7 @@
      * @return the current OAuth parameters.
      */
     public OAuthParameters getOAuthParameters() {
-        return getCurrentAuthorisationUI().getOAuthParameters();
+        return (OAuthParameters) getCurrentAuthorisationUI().getOAuthParameters();
     }
 
     /**
Index: core/src/org/openstreetmap/josm/gui/oauth/SemiAutomaticAuthorizationUI.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/gui/oauth/SemiAutomaticAuthorizationUI.java b/core/src/org/openstreetmap/josm/gui/oauth/SemiAutomaticAuthorizationUI.java
--- a/core/src/org/openstreetmap/josm/gui/oauth/SemiAutomaticAuthorizationUI.java	(revision 18597)
+++ b/core/src/org/openstreetmap/josm/gui/oauth/SemiAutomaticAuthorizationUI.java	(date 1667496272815)
@@ -22,6 +22,7 @@
 import javax.swing.JPanel;
 
 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;
 import org.openstreetmap.josm.gui.widgets.HtmlPanel;
@@ -78,7 +79,7 @@
 
     protected void transitionToRetrieveAccessToken() {
         OsmOAuthAuthorizationClient client = new OsmOAuthAuthorizationClient(
-                getAdvancedPropertiesPanel().getAdvancedParameters()
+                (OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters()
         );
         String authoriseUrl = client.getAuthoriseUrl(requestToken);
         OpenBrowser.displayUrl(authoriseUrl);
@@ -183,7 +184,7 @@
                     + "Please click on <strong>{0}</strong> to retrieve an OAuth Request Token from "
                     + "''{1}''.</html>",
                     tr("Retrieve Request Token"),
-                    getAdvancedPropertiesPanel().getAdvancedParameters().getRequestTokenUrl()
+                    ((OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters()).getRequestTokenUrl()
             ));
             pnl.add(h, gc);
 
@@ -390,7 +391,7 @@
         public void actionPerformed(ActionEvent evt) {
             final RetrieveRequestTokenTask task = new RetrieveRequestTokenTask(
                     SemiAutomaticAuthorizationUI.this,
-                    getAdvancedPropertiesPanel().getAdvancedParameters()
+                    (OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters()
             );
             executor.execute(task);
             Runnable r = () -> {
@@ -418,7 +419,7 @@
         public void actionPerformed(ActionEvent evt) {
             final RetrieveAccessTokenTask task = new RetrieveAccessTokenTask(
                     SemiAutomaticAuthorizationUI.this,
-                    getAdvancedPropertiesPanel().getAdvancedParameters(),
+                    (OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters(),
                     requestToken
             );
             executor.execute(task);
@@ -450,7 +451,7 @@
             TestAccessTokenTask task = new TestAccessTokenTask(
                     SemiAutomaticAuthorizationUI.this,
                     getApiUrl(),
-                    getAdvancedPropertiesPanel().getAdvancedParameters(),
+                    (OAuthParameters) getAdvancedPropertiesPanel().getAdvancedParameters(),
                     getAccessToken()
             );
             executor.execute(task);
Index: core/src/org/openstreetmap/josm/gui/preferences/server/AuthenticationPreferencesPanel.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/gui/preferences/server/AuthenticationPreferencesPanel.java b/core/src/org/openstreetmap/josm/gui/preferences/server/AuthenticationPreferencesPanel.java
--- a/core/src/org/openstreetmap/josm/gui/preferences/server/AuthenticationPreferencesPanel.java	(revision 18597)
+++ b/core/src/org/openstreetmap/josm/gui/preferences/server/AuthenticationPreferencesPanel.java	(date 1667424496163)
@@ -4,6 +4,7 @@
 import static org.openstreetmap.josm.tools.I18n.tr;
 
 import java.awt.BorderLayout;
+import java.awt.FlowLayout;
 import java.awt.GridBagConstraints;
 import java.awt.GridBagLayout;
 import java.awt.Insets;
@@ -17,11 +18,13 @@
 import javax.swing.JRadioButton;
 
 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;
 import org.openstreetmap.josm.io.OsmApi;
 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;
 
 /**
@@ -32,14 +35,18 @@
 
     /** 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;
 
     /**
      * Constructs a new {@code AuthenticationPreferencesPanel}.
@@ -55,35 +62,39 @@
      */
     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;
         gc.gridwidth = 2;
@@ -94,7 +105,8 @@
 
         //-- 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);
         pnlAuthenticationParameters.add(pnlBasicAuthPreferences, BorderLayout.CENTER);
@@ -109,6 +121,8 @@
             rbBasicAuthentication.setSelected(true);
         } 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''.",
                     "osm-server.auth-method", authMethod));
@@ -116,6 +130,7 @@
         }
         pnlBasicAuthPreferences.initFromPreferences();
         pnlOAuthPreferences.initFromPreferences();
+        pnlOAuth20Preferences.initFromPreferences();
     }
 
     /**
@@ -126,8 +141,12 @@
         String authMethod;
         if (rbBasicAuthentication.isSelected()) {
             authMethod = "basic";
-        } else {
+        } else if (rbOAuth.isSelected()) {
             authMethod = "oauth";
+        } else if (rbOAuth20.isSelected()) {
+            authMethod = "oauth20";
+        } else {
+            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);
         if ("basic".equals(authMethod)) {
@@ -140,6 +159,11 @@
             pnlBasicAuthPreferences.clearPassword();
             pnlBasicAuthPreferences.saveToPreferences();
             pnlOAuthPreferences.saveToPreferences();
+        } else { // oauth20
+            // clear the password in the preferences
+            pnlBasicAuthPreferences.clearPassword();
+            pnlBasicAuthPreferences.saveToPreferences();
+            pnlOAuth20Preferences.saveToPreferences();
         }
     }
 
@@ -149,14 +173,16 @@
     class AuthenticationMethodChangeListener implements ItemListener {
         @Override
         public void itemStateChanged(ItemEvent e) {
-            if (rbBasicAuthentication.isSelected()) {
-                pnlAuthenticationParameters.removeAll();
+            pnlAuthenticationParameters.removeAll();
+            if (rbBasicAuthentication.isSelected()) {
                 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: core/src/org/openstreetmap/josm/gui/preferences/server/OAuthAuthenticationPreferencesPanel.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/gui/preferences/server/OAuthAuthenticationPreferencesPanel.java b/core/src/org/openstreetmap/josm/gui/preferences/server/OAuthAuthenticationPreferencesPanel.java
--- a/core/src/org/openstreetmap/josm/gui/preferences/server/OAuthAuthenticationPreferencesPanel.java	(revision 18597)
+++ b/core/src/org/openstreetmap/josm/gui/preferences/server/OAuthAuthenticationPreferencesPanel.java	(date 1667852534870)
@@ -23,28 +23,35 @@
 import javax.swing.JPanel;
 
 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;
 import org.openstreetmap.josm.gui.oauth.AuthorizationProcedure;
 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;
 import org.openstreetmap.josm.tools.Logging;
 import org.openstreetmap.josm.tools.UserCancelException;
 
 /**
- * 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
  */
@@ -53,17 +60,30 @@
     private final JCheckBox cbShowAdvancedParameters = new JCheckBox(tr("Display Advanced OAuth Parameters"));
     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();
     }
 
     /**
@@ -117,7 +137,9 @@
 
     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();
             pnlAlreadyAuthorised.revalidate();
@@ -180,11 +202,15 @@
             lbl.setFont(lbl.getFont().deriveFont(Font.PLAIN));
 
             // 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
             add(new JPanel(), GBC.std().fill(GBC.BOTH));
@@ -253,8 +279,12 @@
 
             // -- 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;
             gc.gridwidth = 2;
@@ -277,10 +307,24 @@
         }
 
         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());
         }
     }
@@ -295,25 +339,67 @@
             this.procedure = procedure;
             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);
             }
         }
 
         @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());
+                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: core/src/org/openstreetmap/josm/io/auth/CredentialsAgent.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/io/auth/CredentialsAgent.java b/core/src/org/openstreetmap/josm/io/auth/CredentialsAgent.java
--- a/core/src/org/openstreetmap/josm/io/auth/CredentialsAgent.java	(revision 18597)
+++ b/core/src/org/openstreetmap/josm/io/auth/CredentialsAgent.java	(date 1667248475819)
@@ -5,6 +5,7 @@
 import java.net.Authenticator.RequestorType;
 import java.net.PasswordAuthentication;
 
+import org.openstreetmap.josm.data.oauth.IOAuthToken;
 import org.openstreetmap.josm.data.oauth.OAuthToken;
 
 /**
@@ -64,6 +65,16 @@
      */
     OAuthToken lookupOAuthAccessToken() throws CredentialsAgentException;
 
+    /**
+     * Lookup the current OAuth Access Token to access the specifiedserver. 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
+     */
+    IOAuthToken lookupOAuthAccessToken(String host) throws CredentialsAgentException;
+
     /**
      * Stores the OAuth Access Token <code>accessToken</code>.
      *
@@ -72,6 +83,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.
+     * @throws CredentialsAgentException if something goes wrong
+     */
+    void storeOAuthAccessToken(String host, IOAuthToken accessToken) throws CredentialsAgentException;
+
     /**
      * Purges the internal credentials cache for the given requestor type.
      * @param requestorType the type of service.
Index: core/src/org/openstreetmap/josm/io/auth/CredentialsManager.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/io/auth/CredentialsManager.java b/core/src/org/openstreetmap/josm/io/auth/CredentialsManager.java
--- a/core/src/org/openstreetmap/josm/io/auth/CredentialsManager.java	(revision 18597)
+++ b/core/src/org/openstreetmap/josm/io/auth/CredentialsManager.java	(date 1667308562334)
@@ -7,6 +7,7 @@
 import java.util.Objects;
 
 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;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
@@ -160,11 +161,21 @@
         return delegate.lookupOAuthAccessToken();
     }
 
+    @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);
+    }
+
     @Override
     public Component getPreferencesDecorationPanel() {
         return delegate.getPreferencesDecorationPanel();
Index: core/src/org/openstreetmap/josm/io/auth/JosmPreferencesCredentialAgent.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/io/auth/JosmPreferencesCredentialAgent.java b/core/src/org/openstreetmap/josm/io/auth/JosmPreferencesCredentialAgent.java
--- a/core/src/org/openstreetmap/josm/io/auth/JosmPreferencesCredentialAgent.java	(revision 18597)
+++ b/core/src/org/openstreetmap/josm/io/auth/JosmPreferencesCredentialAgent.java	(date 1667853309008)
@@ -8,13 +8,20 @@
 import java.net.PasswordAuthentication;
 import java.util.Objects;
 
+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;
 
 /**
  * This is the default credentials agent in JOSM. It keeps username and password for both
@@ -109,6 +116,24 @@
         return new OAuthToken(accessTokenKey, accessTokenSecret);
     }
 
+    @Override
+    public IOAuthToken lookupOAuthAccessToken(String host) throws CredentialsAgentException {
+        for (OAuthVersion oauthType : OAuthVersion.values()) {
+            String token = Config.getPref().get("oauth.access-token.object." + oauthType + "." + host, null);
+            String parameters = Config.getPref().get("oauth.access-token.parameters." + oauthType + "." + host, 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);
+                }
+            }
+        }
+
+        throw new CredentialsAgentException("No OAuth token found for " + host);
+    }
+
     /**
      * Stores the OAuth Access Token <code>accessToken</code>.
      *
@@ -126,6 +151,23 @@
         }
     }
 
+    @Override
+    public void storeOAuthAccessToken(String host, IOAuthToken accessToken) throws CredentialsAgentException {
+        Objects.requireNonNull(host, "host");
+        if (accessToken == null) {
+            // Assume we want to remove all access tokens
+            for (OAuthVersion oauthType : OAuthVersion.values()) {
+                Config.getPref().put("oauth.access-token.object." + oauthType + "." + host, null);
+                Config.getPref().put("oauth.access-token.parameters." + oauthType + "." + host, null);
+            }
+        } else {
+            Config.getPref().put("oauth.access-token.object." + accessToken.getOAuthType() + "." + host,
+                    accessToken.toPreferencesString());
+            Config.getPref().put("oauth.access-token.parameters." + accessToken.getOAuthType() + "." + host,
+                    accessToken.getParameters().toPreferencesString());
+        }
+    }
+
     @Override
     public Component getPreferencesDecorationPanel() {
         HtmlPanel pnlMessage = new HtmlPanel();
Index: core/src/org/openstreetmap/josm/io/remotecontrol/handler/AuthorizationHandler.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/io/remotecontrol/handler/AuthorizationHandler.java b/core/src/org/openstreetmap/josm/io/remotecontrol/handler/AuthorizationHandler.java
new file mode 100644
--- /dev/null	(date 1667853339764)
+++ b/core/src/org/openstreetmap/josm/io/remotecontrol/handler/AuthorizationHandler.java	(date 1667853339764)
@@ -0,0 +1,151 @@
+// 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.Optional;
+
+import org.openstreetmap.josm.data.preferences.BooleanProperty;
+import org.openstreetmap.josm.io.remotecontrol.PermissionPrefWithDefault;
+
+/**
+ * Handle authorization requests (mostly OAuth)
+ */
+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 (this.request.contains(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();
+            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);
+        }
+    }
+
+    @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[0];
+    }
+}
Index: core/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java b/core/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java
--- a/core/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java	(revision 18597)
+++ b/core/src/org/openstreetmap/josm/io/remotecontrol/RequestProcessor.java	(date 1667243685512)
@@ -30,6 +30,7 @@
 import org.openstreetmap.josm.gui.help.HelpUtil;
 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;
 import org.openstreetmap.josm.io.remotecontrol.handler.ImportHandler;
@@ -171,6 +172,7 @@
             addRequestHandlerClass(VersionHandler.command, VersionHandler.class, true);
             addRequestHandlerClass(FeaturesHandler.command, FeaturesHandler.class, true);
             addRequestHandlerClass(OpenApiHandler.command, OpenApiHandler.class, true);
+            addRequestHandlerClass(AuthorizationHandler.command, AuthorizationHandler.class, true);
         }
     }
 
Index: core/src/org/openstreetmap/josm/io/MessageNotifier.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/io/MessageNotifier.java b/core/src/org/openstreetmap/josm/io/MessageNotifier.java
--- a/core/src/org/openstreetmap/josm/io/MessageNotifier.java	(revision 18597)
+++ b/core/src/org/openstreetmap/josm/io/MessageNotifier.java	(date 1667499592391)
@@ -144,7 +144,8 @@
             try {
                 if (JosmPreferencesCredentialAgent.class.equals(credManager.getCredentialsAgentClass())) {
                     if (OsmApi.isUsingOAuth()) {
-                        return credManager.lookupOAuthAccessToken() != null;
+                        return credManager.lookupOAuthAccessToken() != null
+                        || credManager.lookupOAuthAccessToken(OsmApi.getOsmApi().getHost()) != null;
                     } else {
                         String username = Config.getPref().get("osm-server.username", null);
                         String password = Config.getPref().get("osm-server.password", null);
Index: core/src/org/openstreetmap/josm/io/OsmApi.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/io/OsmApi.java b/core/src/org/openstreetmap/josm/io/OsmApi.java
--- a/core/src/org/openstreetmap/josm/io/OsmApi.java	(revision 18597)
+++ b/core/src/org/openstreetmap/josm/io/OsmApi.java	(date 1667500452497)
@@ -29,6 +29,7 @@
 
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.notes.Note;
+import org.openstreetmap.josm.data.oauth.OAuthVersion;
 import org.openstreetmap.josm.data.osm.Changeset;
 import org.openstreetmap.josm.data.osm.IPrimitive;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
@@ -645,7 +646,24 @@
      * @since 6349
      */
     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 xxx
+     */
+    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;
     }
 
     /**
@@ -653,7 +671,7 @@
      * @return the authentication method
      */
     public static String getAuthMethod() {
-        return Config.getPref().get("osm-server.auth-method", "oauth");
+        return Config.getPref().get("osm-server.auth-method", "oauth20"); // fixme switch to oauth20 by default
     }
 
     protected final String sendPostRequest(String urlSuffix, String requestBody, ProgressMonitor monitor) throws OsmTransferException {
Index: core/src/org/openstreetmap/josm/io/OsmConnection.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/io/OsmConnection.java b/core/src/org/openstreetmap/josm/io/OsmConnection.java
--- a/core/src/org/openstreetmap/josm/io/OsmConnection.java	(revision 18597)
+++ b/core/src/org/openstreetmap/josm/io/OsmConnection.java	(date 1667853291980)
@@ -10,12 +10,26 @@
 import java.nio.charset.StandardCharsets;
 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;
 import org.openstreetmap.josm.tools.Logging;
@@ -36,6 +50,7 @@
     protected boolean cancel;
     protected HttpClient activeConnection;
     protected OAuthParameters oauthParameters;
+    protected IOAuthParameters oAuth20Parameters;
 
     /**
      * Retrieves OAuth access token.
@@ -171,21 +186,112 @@
             fetcher.obtainAccessToken(apiUrl);
             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(OsmApi.getOsmApi().getServerUrl(), 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);
+        }
+    }
 
     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: core/src/org/openstreetmap/josm/io/OsmServerReader.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/org/openstreetmap/josm/io/OsmServerReader.java b/core/src/org/openstreetmap/josm/io/OsmServerReader.java
--- a/core/src/org/openstreetmap/josm/io/OsmServerReader.java	(revision 18597)
+++ b/core/src/org/openstreetmap/josm/io/OsmServerReader.java	(date 1667502292491)
@@ -49,7 +49,8 @@
     protected OsmServerReader() {
         try {
             doAuthenticate = OsmApi.isUsingOAuth()
-                    && CredentialsManager.getInstance().lookupOAuthAccessToken() != null
+                    && (CredentialsManager.getInstance().lookupOAuthAccessToken() != null
+                        || CredentialsManager.getInstance().lookupOAuthAccessToken(this.api.getHost()) != null)
                     && OsmApi.USE_OAUTH_FOR_ALL_REQUESTS.get();
         } catch (CredentialsAgentException e) {
             Logging.warn(e);
Index: plugins/native-password-manager/src/org/openstreetmap/josm/plugins/npm/NPMCredentialsAgent.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/plugins/native-password-manager/src/org/openstreetmap/josm/plugins/npm/NPMCredentialsAgent.java b/plugins/native-password-manager/src/org/openstreetmap/josm/plugins/npm/NPMCredentialsAgent.java
--- a/plugins/native-password-manager/src/org/openstreetmap/josm/plugins/npm/NPMCredentialsAgent.java	(revision 36035)
+++ b/plugins/native-password-manager/src/org/openstreetmap/josm/plugins/npm/NPMCredentialsAgent.java	(date 1667310213497)
@@ -6,9 +6,10 @@
 import java.awt.Component;
 import java.net.Authenticator.RequestorType;
 import java.net.PasswordAuthentication;
+import java.net.URI;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.EnumMap;
 import java.util.List;
 import java.util.Map;
 import java.util.zip.CRC32;
@@ -17,7 +18,12 @@
 
 import org.netbeans.spi.keyring.KeyringProvider;
 import org.openstreetmap.josm.data.Preferences;
+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;
@@ -25,23 +31,30 @@
 import org.openstreetmap.josm.io.auth.CredentialsAgentException;
 import org.openstreetmap.josm.spi.preferences.Config;
 
+/**
+ * The native password manager credentials agent
+ */
 public class NPMCredentialsAgent extends AbstractCredentialsAgent {
 
     private KeyringProvider provider;
-    private NPMType type;
+    private final NPMType type;
     
     /**
-     * Cache the results since there might be pop ups and password prompts from
+     * Cache the results since there might be pop-ups and password prompts from
      * the native manager. This can get annoying, if it shows too often.
-     * 
+     * <br>
      * Yes, there is another cache in AbstractCredentialsAgent. It is used
      * to avoid prompting the user for login multiple times in one session,
      * when they decide not to save the credentials.
      * In contrast, this cache avoids read request the backend in general.
      */
-    private Map<RequestorType, PasswordAuthentication> credentialsCache = new HashMap<>();
+    private final Map<RequestorType, PasswordAuthentication> credentialsCache = new EnumMap<>(RequestorType.class);
     private OAuthToken oauthCache;
-    
+
+    /**
+     * Create a new {@link NPMCredentialsAgent}
+     * @param type The backend storage type
+     */
     public NPMCredentialsAgent(NPMType type) {
         this.type = type;
     }
@@ -95,7 +108,7 @@
     }
     
     @Override
-    public PasswordAuthentication lookup(RequestorType rt, String host) throws CredentialsAgentException {
+    public PasswordAuthentication lookup(RequestorType rt, String host) {
         PasswordAuthentication cache = credentialsCache.get(rt);
         if (cache != null) 
             return cache;
@@ -125,7 +138,7 @@
     }
 
     @Override
-    public void store(RequestorType rt, String host, PasswordAuthentication credentials) throws CredentialsAgentException {
+    public void store(RequestorType rt, String host, PasswordAuthentication credentials) {
         char[] username, password;
         if (credentials == null) {
             username = null;
@@ -169,12 +182,12 @@
             } else {
                 getProvider().save(prefix+".password", password, passwordDescription);
             }
-            credentialsCache.put(rt, new PasswordAuthentication(stringNotNull(username), password));
+            credentialsCache.put(rt, new PasswordAuthentication(stringNotNull(username), password != null ? password : new char[0]));
         }
     }
 
     @Override
-    public OAuthToken lookupOAuthAccessToken() throws CredentialsAgentException {
+    public OAuthToken lookupOAuthAccessToken() {
         if (oauthCache != null)
             return oauthCache;
         String prolog = getOAuthDescriptor();
@@ -184,13 +197,35 @@
     }
 
     @Override
-    public void storeOAuthAccessToken(OAuthToken oat) throws CredentialsAgentException {
+    public IOAuthToken lookupOAuthAccessToken(String host) throws CredentialsAgentException {
+        String prolog = getOAuthDescriptor();
+        OAuthVersion[] versions = OAuthVersion.values();
+        // Prefer newer OAuth protocols
+        for (int i = versions.length - 1; i >= 0; i--) {
+            OAuthVersion version = versions[i];
+            char[] tokenObject = getProvider().read(prolog + ".object." + version + "." + host);
+            char[] parametersObject = getProvider().read(prolog + ".parameters." + version + "." + host);
+            if (version == OAuthVersion.OAuth20 // There is currently only an OAuth 2.0 path
+                    && tokenObject != null && tokenObject.length > 0
+                    && parametersObject != null && parametersObject.length > 0) {
+                OAuth20Parameters oAuth20Parameters = new OAuth20Parameters(stringNotNull(parametersObject));
+                try {
+                    return new OAuth20Token(oAuth20Parameters, stringNotNull(tokenObject));
+                } catch (OAuth20Exception e) {
+                    throw new CredentialsAgentException(e);
+                }
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void storeOAuthAccessToken(OAuthToken oat) {
         String key, secret;
         if (oat == null) {
             key = null;
             secret = null;
-        }
-        else {
+        } else {
             key = oat.getKey();
             secret = oat.getSecret();
         }
@@ -206,6 +241,25 @@
         }
     }
 
+    @Override
+    public void storeOAuthAccessToken(String host, IOAuthToken accessToken) {
+        String prolog = getOAuthDescriptor();
+        if (accessToken == null) {
+            for (OAuthVersion version : OAuthVersion.values()) {
+                getProvider().delete(prolog + ".object." + version + "." + host);
+                getProvider().delete(prolog + ".parameters." + version + "." + host);
+            }
+        } else {
+            OAuthVersion oauthType = accessToken.getOAuthType();
+            getProvider().save(prolog + ".object." + oauthType + "." + host,
+                    accessToken.toPreferencesString().toCharArray(),
+                    tr("JOSM/OAuth/{0}/Token", URI.create(host).getHost()));
+            getProvider().save(prolog + ".parameters." + oauthType + "." + host,
+                    accessToken.getParameters().toPreferencesString().toCharArray(),
+                    tr("JOSM/OAuth/{0}/Parameters", URI.create(host).getHost()));
+        }
+    }
+
     private static String stringNotNull(char[] charData) {
         if (charData == null)
             return "";
@@ -216,13 +270,13 @@
     public Component getPreferencesDecorationPanel() {
         HtmlPanel pnlMessage = new HtmlPanel();
         HTMLEditorKit kit = (HTMLEditorKit)pnlMessage.getEditorPane().getEditorKit();
-        kit.getStyleSheet().addRule(".warning-body {background-color:rgb(253,255,221);padding: 10pt; border-color:rgb(128,128,128);border-style: solid;border-width: 1px;}");
+        kit.getStyleSheet().addRule(".warning-body {background-color:rgb(253,255,221);padding: 10pt;" +
+                "border-color:rgb(128,128,128);border-style: solid;border-width: 1px;}");
         StringBuilder text = new StringBuilder();
-        text.append("<html><body>"
-                    + "<p class=\"warning-body\">"
-                    + "<strong>"+tr("Native Password Manager Plugin")+"</strong><br>"
-                    + tr("The username and password is protected by {0}.", type.getName())
-        );
+        text.append("<html><body><p class=\"warning-body\"><strong>")
+                .append(tr("Native Password Manager Plugin"))
+                .append("</strong><br>")
+                .append(tr("The username and password is protected by {0}.", type.getName()));
         List<String> sensitive = new ArrayList<>();
         if (Config.getPref().get("osm-server.username", null) != null) {
             sensitive.add(tr("username"));
@@ -243,7 +297,8 @@
             sensitive.add(tr("oauth secret"));
         }
         if (!sensitive.isEmpty()) {
-            text.append(tr("<br><strong>Warning:</strong> There may be sensitive data left in your preference file. ({0})", String.join(", ", sensitive)));
+            text.append(tr("<br><strong>Warning:</strong> There may be sensitive data left in your preference file. ({0})",
+                    String.join(", ", sensitive)));
         }
         pnlMessage.setText(text.toString());
         return pnlMessage;
