source: josm/trunk/src/org/openstreetmap/josm/io/OsmConnection.java @ 12992

Last change on this file since 12992 was 12992, checked in by Don-vip, 12 months ago

fix #15435 - do not cache incorrect login credentials when using basic auth

  • Property svn:eol-style set to native
File size: 7.9 KB
RevLine 
[6380]1// License: GPL. For details, see LICENSE file.
[626]2package org.openstreetmap.josm.io;
3
[2748]4import static org.openstreetmap.josm.tools.I18n.tr;
5
[9352]6import java.lang.reflect.InvocationTargetException;
[6248]7import java.net.Authenticator.RequestorType;
[9352]8import java.net.MalformedURLException;
9import java.net.URL;
[7082]10import java.nio.charset.StandardCharsets;
[10618]11import java.util.Base64;
[9352]12import java.util.Objects;
[626]13
[12686]14import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
[2748]15import org.openstreetmap.josm.data.oauth.OAuthParameters;
[4245]16import org.openstreetmap.josm.io.auth.CredentialsAgentException;
[6248]17import org.openstreetmap.josm.io.auth.CredentialsAgentResponse;
[4245]18import org.openstreetmap.josm.io.auth.CredentialsManager;
[9172]19import org.openstreetmap.josm.tools.HttpClient;
[12803]20import org.openstreetmap.josm.tools.JosmRuntimeException;
[12620]21import org.openstreetmap.josm.tools.Logging;
[626]22
[8840]23import oauth.signpost.OAuthConsumer;
24import oauth.signpost.exception.OAuthException;
25
[626]26/**
27 * Base class that handles common things like authentication for the reader and writer
28 * to the osm server.
29 *
30 * @author imi
31 */
32public class OsmConnection {
[12992]33
34    private static final String BASIC_AUTH = "Basic ";
35
[8840]36    protected boolean cancel;
[9309]37    protected HttpClient activeConnection;
[2748]38    protected OAuthParameters oauthParameters;
[626]39
[1169]40    /**
[12803]41     * Retrieves OAuth access token.
42     * @since 12803
43     */
44    public interface OAuthAccessTokenFetcher {
45        /**
46         * Obtains an OAuth access token for the connection. Afterwards, the token is accessible via {@link OAuthAccessTokenHolder}.
47         * @param serverUrl the URL to OSM server
48         * @throws InterruptedException if we're interrupted while waiting for the event dispatching thread to finish OAuth authorization task
49         * @throws InvocationTargetException if an exception is thrown while running OAuth authorization task
50         */
51        void obtainAccessToken(URL serverUrl) throws InvocationTargetException, InterruptedException;
52    }
53
[12869]54    static volatile OAuthAccessTokenFetcher fetcher = u -> {
[12803]55        throw new JosmRuntimeException("OsmConnection.setOAuthAccessTokenFetcher() has not been called");
56    };
57
58    /**
59     * Sets the OAuth access token fetcher.
60     * @param tokenFetcher new OAuth access token fetcher. Cannot be null
61     * @since 12803
62     */
63    public static void setOAuthAccessTokenFetcher(OAuthAccessTokenFetcher tokenFetcher) {
64        fetcher = Objects.requireNonNull(tokenFetcher, "tokenFetcher");
65    }
66
67    /**
[6643]68     * Cancels the connection.
69     */
[1169]70    public void cancel() {
71        cancel = true;
[2322]72        synchronized (this) {
73            if (activeConnection != null) {
74                activeConnection.disconnect();
75            }
76        }
[1169]77    }
[626]78
[2748]79    /**
[12992]80     * Retrieves login from basic authentication header, if set.
81     *
82     * @param con the connection
83     * @return login from basic authentication header, or {@code null}
84     * @throws OsmTransferException if something went wrong. Check for nested exceptions
85     * @since 12992
86     */
87    protected String retrieveBasicAuthorizationLogin(HttpClient con) throws OsmTransferException {
88        String auth = con.getRequestHeader("Authorization");
89        if (auth != null && auth.startsWith(BASIC_AUTH)) {
90            try {
91                String[] token = new String(Base64.getDecoder().decode(auth.substring(BASIC_AUTH.length())),
92                        StandardCharsets.UTF_8).split(":");
93                if (token.length == 2) {
94                    return token[0];
95                }
96            } catch (IllegalArgumentException e) {
97                Logging.error(e);
98            }
99        }
100        return null;
101    }
102
103    /**
[2748]104     * Adds an authentication header for basic authentication
[2801]105     *
[2748]106     * @param con the connection
[8291]107     * @throws OsmTransferException if something went wrong. Check for nested exceptions
[2748]108     */
[9172]109    protected void addBasicAuthorizationHeader(HttpClient con) throws OsmTransferException {
[4245]110        CredentialsAgentResponse response;
[1955]111        try {
[4245]112            synchronized (CredentialsManager.getInstance()) {
[4690]113                response = CredentialsManager.getInstance().getCredentials(RequestorType.SERVER,
114                con.getURL().getHost(), false /* don't know yet whether the credentials will succeed */);
[1955]115            }
[4245]116        } catch (CredentialsAgentException e) {
[2641]117            throw new OsmTransferException(e);
[1955]118        }
[11544]119        if (response != null) {
120            if (response.isCanceled()) {
121                cancel = true;
122                return;
123            } else {
124                String username = response.getUsername() == null ? "" : response.getUsername();
125                String password = response.getPassword() == null ? "" : String.valueOf(response.getPassword());
126                String token = username + ':' + password;
[12992]127                con.setHeader("Authorization", BASIC_AUTH + Base64.getEncoder().encodeToString(token.getBytes(StandardCharsets.UTF_8)));
[11544]128            }
[2641]129        }
[1169]130    }
[1881]131
132    /**
[2748]133     * Signs the connection with an OAuth authentication header
[2801]134     *
[2748]135     * @param connection the connection
[2801]136     *
[12470]137     * @throws MissingOAuthAccessTokenException if there is currently no OAuth Access Token configured
[8291]138     * @throws OsmTransferException if signing fails
[2748]139     */
[9172]140    protected void addOAuthAuthorizationHeader(HttpClient connection) throws OsmTransferException {
[2748]141        if (oauthParameters == null) {
[12928]142            oauthParameters = OAuthParameters.createFromApiUrl(OsmApi.getOsmApi().getServerUrl());
[2748]143        }
144        OAuthConsumer consumer = oauthParameters.buildConsumer();
145        OAuthAccessTokenHolder holder = OAuthAccessTokenHolder.getInstance();
[9352]146        if (!holder.containsAccessToken()) {
147            obtainAccessToken(connection);
148        }
149        if (!holder.containsAccessToken()) { // check if wizard completed
[2862]150            throw new MissingOAuthAccessTokenException();
[9352]151        }
[2748]152        consumer.setTokenWithSecret(holder.getAccessTokenKey(), holder.getAccessTokenSecret());
153        try {
154            consumer.sign(connection);
[8510]155        } catch (OAuthException e) {
[2748]156            throw new OsmTransferException(tr("Failed to sign a HTTP connection with an OAuth Authentication header"), e);
157        }
158    }
159
[9352]160    /**
[12803]161     * Obtains an OAuth access token for the connection.
162     * Afterwards, the token is accessible via {@link OAuthAccessTokenHolder} / {@link CredentialsManager}.
[9352]163     * @param connection connection for which the access token should be obtained
[12803]164     * @throws MissingOAuthAccessTokenException if the process cannot be completed successfully
[9352]165     */
166    protected void obtainAccessToken(final HttpClient connection) throws MissingOAuthAccessTokenException {
167        try {
[9353]168            final URL apiUrl = new URL(OsmApi.getOsmApi().getServerUrl());
[9352]169            if (!Objects.equals(apiUrl.getHost(), connection.getURL().getHost())) {
170                throw new MissingOAuthAccessTokenException();
171            }
[12803]172            fetcher.obtainAccessToken(apiUrl);
173            OAuthAccessTokenHolder.getInstance().setSaveToPreferences(true);
[12928]174            OAuthAccessTokenHolder.getInstance().save(CredentialsManager.getInstance());
[9352]175        } catch (MalformedURLException | InterruptedException | InvocationTargetException e) {
[10237]176            throw new MissingOAuthAccessTokenException(e);
[9352]177        }
178    }
179
[9172]180    protected void addAuth(HttpClient connection) throws OsmTransferException {
[9352]181        final String authMethod = OsmApi.getAuthMethod();
[7012]182        if ("basic".equals(authMethod)) {
[2748]183            addBasicAuthorizationHeader(connection);
[7012]184        } else if ("oauth".equals(authMethod)) {
[2748]185            addOAuthAuthorizationHeader(connection);
186        } else {
[6248]187            String msg = tr("Unexpected value for preference ''{0}''. Got ''{1}''.", "osm-server.auth-method", authMethod);
[12620]188            Logging.warn(msg);
[2748]189            throw new OsmTransferException(msg);
190        }
191    }
192
193    /**
[1881]194     * Replies true if this connection is canceled
[2512]195     *
[1881]196     * @return true if this connection is canceled
197     */
198    public boolean isCanceled() {
199        return cancel;
200    }
[626]201}
Note: See TracBrowser for help on using the repository browser.