From 8f3f6130ab6893b9ba11a82f9830b4163c9f9c09 Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Fri, 13 Sep 2013 14:56:28 +0200 Subject: [PATCH] GOA: get OAuth2 tokens out of GNOME Online Accounts "username = goa:..." selects an account in GOA and retrieves the OAuth2 token from that. The implementation uses the GOA D-Bus API directly, because our C++ D-Bus bindings are easier to use and this avoids an additional library dependency. --- src/backends/goa/GOARegister.cpp | 51 +++++++ src/backends/goa/README | 112 ++++++++++++++ src/backends/goa/configure-sub.in | 15 ++ src/backends/goa/goa.am | 26 ++++ src/backends/goa/goa.cpp | 235 ++++++++++++++++++++++++++++++ src/backends/goa/goa.h | 34 +++++ 6 files changed, 473 insertions(+) create mode 100644 src/backends/goa/GOARegister.cpp create mode 100644 src/backends/goa/README create mode 100644 src/backends/goa/configure-sub.in create mode 100644 src/backends/goa/goa.am create mode 100644 src/backends/goa/goa.cpp create mode 100644 src/backends/goa/goa.h diff --git a/src/backends/goa/GOARegister.cpp b/src/backends/goa/GOARegister.cpp new file mode 100644 index 00000000..4b4e48f6 --- /dev/null +++ b/src/backends/goa/GOARegister.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2013 Intel Corporation + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) version 3. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +#include + +#include "goa.h" + +#include + +#include +SE_BEGIN_CXX + +static class GOAProvider : public IdentityProvider +{ +public: + GOAProvider() : + IdentityProvider("goa", + "goa:\n" + " Authentication using GNOME Online Accounts,\n" + " using an account created and managed with GNOME Control Center.") + {} + + virtual boost::shared_ptr create(const InitStateString &username, + const InitStateString &password) + { + // Returning NULL if not enabled... + boost::shared_ptr provider; +#ifdef USE_GOA + provider = createGOAAuthProvider(username, password); +#endif + return provider; + } +} gsso; + +SE_END_CXX diff --git a/src/backends/goa/README b/src/backends/goa/README new file mode 100644 index 00000000..49ce70a3 --- /dev/null +++ b/src/backends/goa/README @@ -0,0 +1,112 @@ +Google CalDAV/CardDAV via OAuth2 with GNOME Online Accounts (GOA) +================================================================= + +Setup +----- + +SyncEvolution depends on a GNOME Online Accounts with CalDAV *and* +CardDAV enabled for Google. This is hard-coded in the source code, so +recompiling is the only (sane) way to change that. CalDAV has been +enabled for a while, CardDAV is recent (>= 3.10). + +It is possible to patch 3.8 without recompiling (see below). Versions +older than 3.8 do not work because they lack OAuth2 support. + +SyncEvolution needs an active account for Google in the GNOME Control +Center, under "online accounts". Enable the different data categories +if and only if you want to access the data with the core GNOME +apps. SyncEvolution ignores these settings. + + +Usage +----- + +OAuth2 authentication with GNOME Online Accounts is enabled by setting +username or databaseUser to a string of the format + goa: + +Typically there is only one account using a Google email address, so +that can be used to select the account. SyncEvolution checks if it is +really unique and if not, provides a list of all accounts with their +account ID. Then the unique account ID should be used instead. + +The base URL for each service currently needs to be given via syncURL: + + syncevolution --print-databases \ + backend=carddav \ + username=goa:john.doe@gmail.com \ + syncURL=https://www.googleapis.com/.well-known/carddav + + src/syncevolution --print-databases \ + backend=caldav \ + username=goa:john.doe@gmail.com \ + syncURL=https://apidata.googleusercontent.com/caldav/v2 + +Once that works, follow the "CalDAV and CardDAV" instructions from the +README with the different username and syncURL. + + +Debugging +--------- + +Add --daemon=no to the command line to prevent shifting the actual +command executing into syncevo-dbus-server and (from there) +syncevo-dbus-helper. + +Set SYNCEVOLUTION_DEBUG=1 to see all debug messages and increase the +loglevel to see HTTP requests: + + SYNCEVOLUTION_DEBUG=1 syncevolution --daemon=no \ + loglevel=4 \ + --print-databases \ + ... + +Known Problems +-------------- + +When accessing CardDAV: + +status-line] < HTTP/1.1 401 Unauthorized +[hdr] WWW-Authenticate: AuthSub realm="https://www.google.com/accounts/AuthSubRequest" allowed-scopes="https://www.googleapis.com/auth/carddav" +... + + + + GData + authError + Authorization + Invalid Credentials + + +... +[INFO] operation temporarily (?) failed, going to retry in 5.0s before giving up in 295.8s: PROPFIND: Neon error code 3 = NE_AUTH, HTTP status 401: Could not authenticate to server: ignored AuthSub challenge +... + +This happens when using a GNOME Online Accounts which does (or did) +not request CardDAV access when logging into Google. Install GNOME +Online Accounts >= 3.10 or patch it (see below), "killall goa-daemon", +then re-create the account in the GNOME Control Center. + +Patching GOA 3.8 +---------------- + +It is possible to add CardDAV support to 3.8 without recompiling GNOME +Online Accounts. However, the downside is that this approach has to +disable access to some other kind of data and breaks when updating or +reinstalling GOA. + +1. Locate libgoa-backend-1.0.so.0.0.0: typically it is in /usr/lib or /usr/lib64. +2. Open it in a text editor which can handle binary data (like emacs). +3. Switch to "overwrite mode". +4. Find the string starting with https://www.googleapis.com/auth/userinfo.email +6. Overwrite the part which you don't need with https://www.googleapis.com/auth/carddav + and spaces. + +For example, if Google Docs access is not needed, replace +"https://docs.google.com/feeds/ https://docs.googleusercontent.com/ https://spreadsheets.google.com/feeds/ " +with +"https://www.googleapis.com/auth/carddav " + +Here's a perl command which replaces Google Docs with CardDAV: + +perl -pi -e 's;https://docs.google.com/feeds/ https://docs.googleusercontent.com/ https://spreadsheets.google.com/feeds/ ;https://www.googleapis.com/auth/carddav ;' /usr/lib*/libgoa-backend-1.0.so.0.0.0 diff --git a/src/backends/goa/configure-sub.in b/src/backends/goa/configure-sub.in new file mode 100644 index 00000000..a0327449 --- /dev/null +++ b/src/backends/goa/configure-sub.in @@ -0,0 +1,15 @@ +AC_ARG_ENABLE(goa, + AS_HELP_STRING([--disable-goa], + [enables or disables support for the GNOME Online Account single-sign-on system; default is on]), + [enable_goa="$enableval" + test "$enable_goa" = "yes" || test "$enable_goa" = "no" || AC_MSG_ERROR([invalid value for --enable-goa: $enable_goa]) + ], + enable_goa="yes") +if test $enable_goa = "yes"; then + AC_DEFINE(USE_GOA, 1, [use GNOME Online Accounts]) + # link into static executables, similar to a SyncSource + SYNCSOURCES="$SYNCSOURCES src/backends/goa/providergoa.la" +fi + +# conditional compilation in make +AM_CONDITIONAL([USE_GOA], [test "$use_goa" = "yes"]) diff --git a/src/backends/goa/goa.am b/src/backends/goa/goa.am new file mode 100644 index 00000000..d4db7ec7 --- /dev/null +++ b/src/backends/goa/goa.am @@ -0,0 +1,26 @@ +dist_noinst_DATA += src/backends/goa/configure-sub.in \ + src/backends/goa/README \ + $(NONE) + +src_backends_goa_lib = src/backends/goa/providergoa.la +MOSTLYCLEANFILES += $(src_backends_goa_lib) + +src_backends_goa_providergoa_la_SOURCES = \ + src/backends/goa/goa.h \ + src/backends/goa/goa.cpp \ + $(NONE) + +if ENABLE_MODULES +src_backends_goa_backenddir = $(BACKENDS_DIRECTORY) +src_backends_goa_backend_LTLIBRARIES = $(src_backends_goa_lib) +src_backends_goa_providergoa_la_SOURCES += \ + src/backends/goa/GOARegister.cpp +else +noinst_LTLIBRARIES += $(src_backends_goa_lib) +endif + +src_backends_goa_providergoa_la_LIBADD = $(SYNCEVOLUTION_LIBS) $(gdbus_build_dir)/libgdbussyncevo.la $(DBUS_LIBS) +src_backends_goa_providergoa_la_LDFLAGS = -module -avoid-version +src_backends_goa_providergoa_la_CXXFLAGS = $(SYNCEVOLUTION_CFLAGS) $(SYNCEVO_WFLAGS) $(DBUS_CFLAGS) +src_backends_goa_providergoa_la_CPPFLAGS = -I$(gdbus_dir) -I$(top_srcdir)/test $(BACKEND_CPPFLAGS) +src_backends_goa_providergoa_la_DEPENDENCIES = src/syncevo/libsyncevolution.la diff --git a/src/backends/goa/goa.cpp b/src/backends/goa/goa.cpp new file mode 100644 index 00000000..24c938af --- /dev/null +++ b/src/backends/goa/goa.cpp @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2013 Intel Corporation + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) version 3. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + */ + +#include + +#ifdef USE_GOA + +#include "goa.h" +#include +#include + +#include +#include + +#include + +#include +SE_BEGIN_CXX + +/* + * We call the GOA D-Bus API directly. This is easier than using + * libgoa because our own D-Bus wrapper gives us data in C++ data + * structures. It also avoids another library dependency. + */ + +static const char GOA_BUS_NAME[] = "org.gnome.OnlineAccounts"; +static const char GOA_PATH[] = "/org/gnome/OnlineAccounts"; + +static const char OBJECT_MANAGER_INTERFACE[] = "org.freedesktop.DBus.ObjectManager"; +static const char OBJECT_MANAGER_GET_MANAGED_OBJECTS[] = "GetManagedObjects"; + +static const char GOA_ACCOUNT_INTERFACE[] = "org.gnome.OnlineAccounts.Account"; +static const char GOA_ACCOUNT_ENSURE_CREDENTIALS[] = "EnsureCredentials"; +static const char GOA_ACCOUNT_PRESENTATION_IDENTITY[] = "PresentationIdentity"; +static const char GOA_ACCOUNT_ID[] = "Id"; +static const char GOA_ACCOUNT_PROVIDER_NAME[] = "ProviderName"; + +static const char GOA_OAUTH2_INTERFACE[] = "org.gnome.OnlineAccounts.OAuth2Based"; +static const char GOA_OAUTH2_GET_ACCESS_TOKEN[] = "GetAccessToken"; + +class GOAAccount; + +class GOAManager : private GDBusCXX::DBusRemoteObject +{ + typedef std::map // property value - we only care about strings + > Properties; + typedef std::map Interfaces; + typedef std::map ManagedObjects; + GDBusCXX::DBusClientCall1 m_getManagedObjects; + + public: + GOAManager(const GDBusCXX::DBusConnectionPtr &conn); + + /** + * Find a particular account, identified by its representation ID + * (the unique user visible string). The account must support OAuth2, + * otherwise an error is thrown. + */ + boost::shared_ptr lookupAccount(const std::string &representationID); +}; + +class GOAAccount +{ + GDBusCXX::DBusRemoteObject m_account; + GDBusCXX::DBusRemoteObject m_oauth2; + + +public: + GOAAccount(const GDBusCXX::DBusConnectionPtr &conn, + const std::string &path); + + GDBusCXX::DBusClientCall1 m_ensureCredentials; + GDBusCXX::DBusClientCall1 m_getAccessToken; +}; + +GOAManager::GOAManager(const GDBusCXX::DBusConnectionPtr &conn) : + GDBusCXX::DBusRemoteObject(conn, GOA_PATH, OBJECT_MANAGER_INTERFACE, GOA_BUS_NAME), + m_getManagedObjects(*this, OBJECT_MANAGER_GET_MANAGED_OBJECTS) +{ +} + +boost::shared_ptr GOAManager::lookupAccount(const std::string &username) +{ + SE_LOG_DEBUG(NULL, "Looking up all accounts in GNOME Online Accounts, searching for '%s'.", username.c_str()); + ManagedObjects objects = m_getManagedObjects(); + + GDBusCXX::DBusObject_t accountPath; + bool unique = true; + bool hasOAuth2 = false; + std::vector accounts; + BOOST_FOREACH (const ManagedObjects::value_type &object, objects) { + const GDBusCXX::DBusObject_t &path = object.first; + const Interfaces &interfaces = object.second; + // boost::adaptors::keys() would be nicer, but is not available on Ubuntu Lucid. + std::list interfaceKeys; + BOOST_FOREACH (const Interfaces::value_type &entry, interfaces) { + interfaceKeys.push_back(entry.first); + } + SE_LOG_DEBUG(NULL, "GOA object %s implements %s", path.c_str(), + boost::join(interfaceKeys, ", ").c_str()); + Interfaces::const_iterator it = interfaces.find(GOA_ACCOUNT_INTERFACE); + if (it != interfaces.end()) { + const Properties &properties = it->second; + Properties::const_iterator id = properties.find(GOA_ACCOUNT_ID); + Properties::const_iterator presentationID = properties.find(GOA_ACCOUNT_PRESENTATION_IDENTITY); + if (id != properties.end() && + presentationID != properties.end()) { + const std::string &idStr = boost::get(id->second); + const std::string &presentationIDStr = boost::get(presentationID->second); + Properties::const_iterator provider = properties.find(GOA_ACCOUNT_PROVIDER_NAME); + std::string description = StringPrintf("%s, %s = %s", + provider == properties.end() ? "???" : boost::get(provider->second).c_str(), + presentationIDStr.c_str(), + idStr.c_str()); + SE_LOG_DEBUG(NULL, "GOA account %s", description.c_str()); + accounts.push_back(description); + // The assumption here is that ID and presentation + // identifier are so different that there can be + // no overlap. Otherwise we would have to know + // whether the user gave us an ID or presentation + // identifier. + if (idStr == username || + presentationIDStr == username) { + if (accountPath.empty()) { + accountPath = path; + hasOAuth2 = interfaces.find(GOA_OAUTH2_INTERFACE) != interfaces.end(); + SE_LOG_DEBUG(NULL, "found matching GNOME Online Account for '%s': %s", username.c_str(), description.c_str()); + } else { + unique = false; + } + } + } else { + SE_LOG_DEBUG(NULL, "ignoring %s, lacks expected properties", + path.c_str()); + } + } + } + + std::sort(accounts.begin(), accounts.end()); + if (accountPath.empty()) { + if (accounts.empty()) { + SE_THROW(StringPrintf("GNOME Online Account '%s' not found. You must set up the account in GNOME Control Center/Online Accounts first.", username.c_str())); + } else { + SE_THROW(StringPrintf("GNOME Online Account '%s' not found. Choose one of the following:\n%s", + username.c_str(), + boost::join(accounts, "\n").c_str())); + } + } else if (!unique) { + SE_THROW(StringPrintf("GNOME Online Account '%s' is not unique. Choose one of the following, using the unique ID instead of the more ambiguous representation name:\n%s", + username.c_str(), + boost::join(accounts, "\n").c_str())); + } else if (!hasOAuth2) { + SE_THROW(StringPrintf("Found GNOME Online Account '%s', but it does not support OAuth2. Are you sure that you picked the right account and that you are using GNOME Online Accounts >= 3.8?", + username.c_str())); + } + + boost::shared_ptr account(new GOAAccount(getConnection(), accountPath)); + return account; +} + +GOAAccount::GOAAccount(const GDBusCXX::DBusConnectionPtr &conn, + const std::string &path) : + m_account(conn, path, GOA_ACCOUNT_INTERFACE, GOA_BUS_NAME), + m_oauth2(conn, path, GOA_OAUTH2_INTERFACE, GOA_BUS_NAME), + m_ensureCredentials(m_account, GOA_ACCOUNT_ENSURE_CREDENTIALS), + m_getAccessToken(m_oauth2, GOA_OAUTH2_GET_ACCESS_TOKEN) +{ +} + +class GOAAuthProvider : public AuthProvider +{ + boost::shared_ptr m_account; + +public: + GOAAuthProvider(const boost::shared_ptr &account) : + m_account(account) + {} + + virtual bool methodIsSupported(AuthMethod method) const { return method == AUTH_METHOD_OAUTH2; } + + virtual Credentials getCredentials() const { SE_THROW("only OAuth2 is supported"); } + + virtual std::string getOAuth2Bearer(int failedTokens) const + { + m_account->m_ensureCredentials(); + std::string token = m_account->m_getAccessToken(); + return token; + } + + virtual std::string getUsername() const { return ""; } +}; + +boost::shared_ptr createGOAAuthProvider(const InitStateString &username, + const InitStateString &password) +{ + // Because we share the connection, hopefully this won't be too expensive. + GDBusCXX::DBusErrorCXX err; + GDBusCXX::DBusConnectionPtr conn = dbus_get_bus_connection("SESSION", + "", + false, + &err); + if (!conn) { + err.throwFailure("connecting to session bus"); + } + + GOAManager manager(conn); + boost::shared_ptr account = manager.lookupAccount(username); + boost::shared_ptr provider(new GOAAuthProvider(account)); + return provider; +} + +SE_END_CXX + +#endif // USE_GOA + + diff --git a/src/backends/goa/goa.h b/src/backends/goa/goa.h new file mode 100644 index 00000000..338964f9 --- /dev/null +++ b/src/backends/goa/goa.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2013 Intel Corporation + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) version 3. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301 USA + */ +#ifndef INCL_SYNC_EVOLUTION_GOA_AUTH_PROVIDER +# define INCL_SYNC_EVOLUTION_GOA_AUTH_PROVIDER + +#include + +#include + +#include +SE_BEGIN_CXX + +class AuthProvider; +boost::shared_ptr createGOAAuthProvider(const InitStateString &username, + const InitStateString &password); + +SE_END_CXX +#endif