// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.oauth; import static org.openstreetmap.josm.tools.I18n.tr; import java.io.DataOutputStream; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.lang.reflect.Field; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.logging.Logger; import oauth.signpost.OAuth; import oauth.signpost.OAuthConsumer; import oauth.signpost.OAuthProvider; import oauth.signpost.basic.DefaultOAuthProvider; import oauth.signpost.exception.OAuthCommunicationException; import oauth.signpost.exception.OAuthException; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.Version; import org.openstreetmap.josm.data.oauth.OAuthParameters; import org.openstreetmap.josm.data.oauth.OAuthToken; import org.openstreetmap.josm.data.oauth.OsmPrivileges; import org.openstreetmap.josm.gui.progress.NullProgressMonitor; import org.openstreetmap.josm.gui.progress.ProgressMonitor; import org.openstreetmap.josm.io.OsmTransferCancelledException; import org.openstreetmap.josm.tools.CheckParameterUtil; public class OsmOAuthAuthorizationClient { @SuppressWarnings("unused") static private final Logger logger = Logger.getLogger(OsmOAuthAuthorizationClient.class.getName()); private OAuthParameters oauthProviderParameters; private OAuthConsumer consumer; private OAuthProvider provider; private boolean canceled; private HttpURLConnection connection; /** * Creates a new authorisation client with default OAuth parameters * */ public OsmOAuthAuthorizationClient() { oauthProviderParameters = OAuthParameters.createDefault(); consumer = oauthProviderParameters.buildConsumer(); provider = oauthProviderParameters.buildProvider(consumer); } /** * Creates a new authorisation client with the parameters <code>parameters</code>. * * @param parameters the OAuth parameters. Must not be null. * @throws IllegalArgumentException thrown if parameters is null */ public OsmOAuthAuthorizationClient(OAuthParameters parameters) throws IllegalArgumentException { CheckParameterUtil.ensureParameterNotNull(parameters, "parameters"); oauthProviderParameters = new OAuthParameters(parameters); consumer = oauthProviderParameters.buildConsumer(); provider = oauthProviderParameters.buildProvider(consumer); } /** * Creates a new authorisation client with the parameters <code>parameters</code> * and an already known Request Token. * * @param parameters the OAuth parameters. Must not be null. * @param requestToken the request token. Must not be null. * @throws IllegalArgumentException thrown if parameters is null * @throws IllegalArgumentException thrown if requestToken is null */ public OsmOAuthAuthorizationClient(OAuthParameters parameters, OAuthToken requestToken) throws IllegalArgumentException { CheckParameterUtil.ensureParameterNotNull(parameters, "parameters"); oauthProviderParameters = new OAuthParameters(parameters); consumer = oauthProviderParameters.buildConsumer(); provider = oauthProviderParameters.buildProvider(consumer); consumer.setTokenWithSecret(requestToken.getKey(), requestToken.getSecret()); } public void cancel() { DefaultOAuthProvider p = (DefaultOAuthProvider)provider; canceled = true; if (p != null) { try { Field f = p.getClass().getDeclaredField("connection"); f.setAccessible(true); HttpURLConnection con = (HttpURLConnection)f.get(p); if (con != null) { con.disconnect(); } } catch(NoSuchFieldException e) { e.printStackTrace(); System.err.println(tr("Warning: failed to cancel running OAuth operation")); } catch(SecurityException e) { e.printStackTrace(); System.err.println(tr("Warning: failed to cancel running OAuth operation")); } catch(IllegalAccessException e) { e.printStackTrace(); System.err.println(tr("Warning: failed to cancel running OAuth operation")); } } synchronized(this) { if (connection != null) { connection.disconnect(); } } } /** * Submits a request for a Request Token to the Request Token Endpoint Url of the OAuth Service * Provider and replies the request token. * * @param monitor a progress monitor. Defaults to {@see NullProgressMonitor#INSTANCE} if null * @return the OAuth Request Token * @throws OsmOAuthAuthorizationException thrown if something goes wrong when retrieving the request token */ public OAuthToken getRequestToken(ProgressMonitor monitor) throws OsmOAuthAuthorizationException, OsmTransferCancelledException { if (monitor == null) { monitor = NullProgressMonitor.INSTANCE; } try { monitor.beginTask(""); monitor.indeterminateSubTask(tr("Retrieving OAuth Request Token from ''{0}''", oauthProviderParameters.getRequestTokenUrl())); provider.retrieveRequestToken(null); return OAuthToken.createToken(consumer); } catch(OAuthCommunicationException e){ if (canceled) throw new OsmTransferCancelledException(); throw new OsmOAuthAuthorizationException(e); } catch(OAuthException e){ if (canceled) throw new OsmTransferCancelledException(); throw new OsmOAuthAuthorizationException(e); } finally { monitor.finishTask(); } } /** * Submits a request for an Access Token to the Access Token Endpoint Url of the OAuth Service * Provider and replies the request token. * * You must have requested a Request Token using {@see #getRequestToken(ProgressMonitor)} first. * * @param monitor a progress monitor. Defaults to {@see NullProgressMonitor#INSTANCE} if null * @return the OAuth Access Token * @throws OsmOAuthAuthorizationException thrown if something goes wrong when retrieving the request token * @see #getRequestToken(ProgressMonitor) */ public OAuthToken getAccessToken(ProgressMonitor monitor) throws OsmOAuthAuthorizationException, OsmTransferCancelledException { if (monitor == null) { monitor = NullProgressMonitor.INSTANCE; } try { monitor.beginTask(""); monitor.indeterminateSubTask(tr("Retrieving OAuth Access Token from ''{0}''", oauthProviderParameters.getAccessTokenUrl())); provider.retrieveAccessToken(null); return OAuthToken.createToken(consumer); } catch(OAuthCommunicationException e){ if (canceled) throw new OsmTransferCancelledException(); throw new OsmOAuthAuthorizationException(e); } catch(OAuthException e){ if (canceled) throw new OsmTransferCancelledException(); throw new OsmOAuthAuthorizationException(e); } finally { monitor.finishTask(); } } /** * Builds the authorise URL for a given Request Token. Users can be redirected to this URL. * There they can login to OSM and authorise the request. * * @param requestToken the request token * @return the authorise URL for this request */ public String getAuthoriseUrl(OAuthToken requestToken) { StringBuilder sb = new StringBuilder(); // OSM is an OAuth 1.0 provider and JOSM isn't a web app. We just add the oauth request token to // the authorisation request, no callback parameter. // sb.append(oauthProviderParameters.getAuthoriseUrl()).append("?") .append(OAuth.OAUTH_TOKEN).append("=").append(requestToken.getKey()); return sb.toString(); } protected String extractOsmSession(HttpURLConnection connection) { List<String> setCookies = connection.getHeaderFields().get("Set-Cookie"); if (setCookies == null) // no cookies set return null; for (String setCookie: setCookies) { String[] kvPairs = setCookie.split(";"); if (kvPairs == null || kvPairs.length == 0) { continue; } for (String kvPair : kvPairs) { kvPair = kvPair.trim(); String [] kv = kvPair.split("="); if (kv == null || kv.length != 2) { continue; } if (kv[0].equals("_osm_session")) // osm session cookie found return kv[1]; } } return null; } protected String buildPostRequest(Map<String,String> parameters) throws OsmOAuthAuthorizationException { try { StringBuilder sb = new StringBuilder(); for(Iterator<Entry<String,String>> it = parameters.entrySet().iterator(); it.hasNext();) { Entry<String,String> entry = it.next(); String value = entry.getValue(); value = (value == null) ? "" : value; sb.append(entry.getKey()).append("=").append(URLEncoder.encode(value, "UTF-8")); if (it.hasNext()) { sb.append("&"); } } return sb.toString(); } catch(UnsupportedEncodingException e) { throw new OsmOAuthAuthorizationException(e); } } /** * Derives the OSM login URL from the OAuth Authorization Website URL * * @return the OSM login URL * @throws OsmOAuthAuthorizationException thrown if something went wrong, in particular if the * URLs are malformed */ public String buildOsmLoginUrl() throws OsmOAuthAuthorizationException{ try { URL autUrl = new URL(oauthProviderParameters.getAuthoriseUrl()); URL url = new URL(Main.pref.get("oauth.protocol", "https"), autUrl.getHost(), autUrl.getPort(), "/login"); return url.toString(); } catch(MalformedURLException e) { throw new OsmOAuthAuthorizationException(e); } } /** * Derives the OSM logout URL from the OAuth Authorization Website URL * * @return the OSM logout URL * @throws OsmOAuthAuthorizationException thrown if something went wrong, in particular if the * URLs are malformed */ protected String buildOsmLogoutUrl() throws OsmOAuthAuthorizationException{ try { URL autUrl = new URL(oauthProviderParameters.getAuthoriseUrl()); URL url = new URL("http", autUrl.getHost(), autUrl.getPort(), "/logout"); return url.toString(); } catch(MalformedURLException e) { throw new OsmOAuthAuthorizationException(e); } } /** * Submits a request to the OSM website for a login form. The OSM website replies a session ID in * a cookie. * * @return the session ID * @throws OsmOAuthAuthorizationException thrown if something went wrong */ protected String fetchOsmWebsiteSessionId() throws OsmOAuthAuthorizationException { try { StringBuilder sb = new StringBuilder(); sb.append(buildOsmLoginUrl()).append("?cookie_test=true"); URL url = new URL(sb.toString()); synchronized(this) { connection = (HttpURLConnection)url.openConnection(); } connection.setRequestMethod("GET"); connection.setDoInput(true); connection.setDoOutput(false); setHttpRequestParameters(connection); connection.connect(); String sessionId = extractOsmSession(connection); if (sessionId == null) throw new OsmOAuthAuthorizationException(tr("OSM website did not return a session cookie in response to ''{0}'',", url.toString())); return sessionId; } catch(IOException e) { throw new OsmOAuthAuthorizationException(e); } finally { synchronized(this) { connection = null; } } } protected void authenticateOsmSession(String sessionId, String userName, String password) throws OsmLoginFailedException { DataOutputStream dout = null; try { URL url = new URL(buildOsmLoginUrl()); synchronized(this) { connection = (HttpURLConnection)url.openConnection(); } connection.setRequestMethod("POST"); connection.setDoInput(true); connection.setDoOutput(true); connection.setUseCaches(false); Map<String,String> parameters = new HashMap<String, String>(); parameters.put("user[email]", userName); parameters.put("user[password]", password); parameters.put("referer", "/"); parameters.put("commit", "Login"); String request = buildPostRequest(parameters); connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); connection.setRequestProperty("Content-Length", Integer.toString(request.length())); connection.setRequestProperty("Cookie", "_osm_session=" + sessionId); // make sure we can catch 302 Moved Temporarily below connection.setInstanceFollowRedirects(false); setHttpRequestParameters(connection); connection.connect(); dout = new DataOutputStream(connection.getOutputStream()); dout.writeBytes(request); dout.flush(); dout.close(); // after a successful login the OSM website sends a redirect to a follow up page. Everything // else, including a 200 OK, is a failed login. A 200 OK is replied if the login form with // an error page is sent to back to the user. // int retCode = connection.getResponseCode(); if (retCode != HttpURLConnection.HTTP_MOVED_TEMP) throw new OsmOAuthAuthorizationException(tr("Failed to authenticate user ''{0}'' with password ''***'' as OAuth user", userName)); } catch(OsmOAuthAuthorizationException e) { throw new OsmLoginFailedException(e.getCause()); } catch(IOException e) { throw new OsmLoginFailedException(e); } finally { if (dout != null) { try { dout.close(); } catch(IOException e) { /* ignore */ } } synchronized(this) { connection = null; } } } protected void logoutOsmSession(String sessionId) throws OsmOAuthAuthorizationException { try { URL url = new URL(buildOsmLogoutUrl()); synchronized(this) { connection = (HttpURLConnection)url.openConnection(); } connection.setRequestMethod("GET"); connection.setDoInput(true); connection.setDoOutput(false); setHttpRequestParameters(connection); connection.connect(); }catch(MalformedURLException e) { throw new OsmOAuthAuthorizationException(e); } catch(IOException e) { throw new OsmOAuthAuthorizationException(e); } finally { synchronized(this) { connection = null; } } } protected void sendAuthorisationRequest(String sessionId, OAuthToken requestToken, OsmPrivileges privileges) throws OsmOAuthAuthorizationException { Map<String, String> parameters = new HashMap<String, String>(); parameters.put("oauth_token", requestToken.getKey()); parameters.put("oauth_callback", ""); if (privileges.isAllowWriteApi()) { parameters.put("allow_write_api", "yes"); } if (privileges.isAllowWriteGpx()) { parameters.put("allow_write_gpx", "yes"); } if (privileges.isAllowReadGpx()) { parameters.put("allow_read_gpx", "yes"); } if (privileges.isAllowWritePrefs()) { parameters.put("allow_write_prefs", "yes"); } if (privileges.isAllowReadPrefs()) { parameters.put("allow_read_prefs", "yes"); } parameters.put("commit", "Save changes"); String request = buildPostRequest(parameters); DataOutputStream dout = null; try { URL url = new URL(oauthProviderParameters.getAuthoriseUrl()); synchronized(this) { connection = (HttpURLConnection)url.openConnection(); } connection.setRequestMethod("POST"); connection.setDoInput(true); connection.setDoOutput(true); connection.setUseCaches(false); connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); connection.setRequestProperty("Content-Length", Integer.toString(request.length())); connection.setRequestProperty("Cookie", "_osm_session=" + sessionId); connection.setInstanceFollowRedirects(false); setHttpRequestParameters(connection); connection.connect(); dout = new DataOutputStream(connection.getOutputStream()); dout.writeBytes(request); dout.flush(); dout.close(); int retCode = connection.getResponseCode(); if (retCode != HttpURLConnection.HTTP_MOVED_TEMP) throw new OsmOAuthAuthorizationException(tr("Failed to authorize OAuth request ''{0}''", requestToken.getKey())); } catch(MalformedURLException e) { throw new OsmOAuthAuthorizationException(e); } catch(IOException e) { throw new OsmOAuthAuthorizationException(e); } finally { if (dout != null) { try { dout.close(); } catch(IOException e) { /* ignore */ } } synchronized(this) { connection = null; } } } protected void setHttpRequestParameters(HttpURLConnection connection) { connection.setRequestProperty("User-Agent", Version.getInstance().getAgentString()); connection.setRequestProperty("Host", connection.getURL().getHost()); } /** * Automatically authorises a request token for a set of privileges. * * @param requestToken the request token. Must not be null. * @param osmUserName the OSM user name. Must not be null. * @param osmPassword the OSM password. Must not be null. * @param privileges the set of privileges. Must not be null. * @param monitor a progress monitor. Defaults to {@see NullProgressMonitor#INSTANCE} if null * @throws IllegalArgumentException thrown if requestToken is null * @throws IllegalArgumentException thrown if osmUserName is null * @throws IllegalArgumentException thrown if osmPassword is null * @throws IllegalArgumentException thrown if privileges is null * @throws OsmOAuthAuthorizationException thrown if the authorisation fails * @throws OsmTransferCancelledException thrown if the task is cancelled by the user */ public void authorise(OAuthToken requestToken, String osmUserName, String osmPassword, OsmPrivileges privileges, ProgressMonitor monitor) throws IllegalArgumentException, OsmOAuthAuthorizationException, OsmTransferCancelledException{ CheckParameterUtil.ensureParameterNotNull(requestToken, "requestToken"); CheckParameterUtil.ensureParameterNotNull(osmUserName, "osmUserName"); CheckParameterUtil.ensureParameterNotNull(osmPassword, "osmPassword"); CheckParameterUtil.ensureParameterNotNull(privileges, "privileges"); if (monitor == null) { monitor = NullProgressMonitor.INSTANCE; } try { monitor.beginTask(tr("Authorizing OAuth Request token ''{0}'' at the OSM website ...", requestToken.getKey())); monitor.setTicksCount(4); monitor.indeterminateSubTask(tr("Initializing a session at the OSM website...")); String sessionId = fetchOsmWebsiteSessionId(); if (canceled) throw new OsmTransferCancelledException(); monitor.worked(1); monitor.indeterminateSubTask(tr("Authenticating the session for user ''{0}''...", osmUserName)); authenticateOsmSession(sessionId, osmUserName, osmPassword); if (canceled) throw new OsmTransferCancelledException(); monitor.worked(1); monitor.indeterminateSubTask(tr("Authorizing request token ''{0}''...", requestToken.getKey())); sendAuthorisationRequest(sessionId, requestToken, privileges); if (canceled) throw new OsmTransferCancelledException(); monitor.worked(1); monitor.indeterminateSubTask(tr("Logging out session ''{0}''...", sessionId)); logoutOsmSession(sessionId); if (canceled) throw new OsmTransferCancelledException(); monitor.worked(1); } catch(OsmOAuthAuthorizationException e) { if (canceled) throw new OsmTransferCancelledException(); throw e; } finally { monitor.finishTask(); } } }