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.
This commit is contained in:
Patrick Ohly 2013-09-13 14:56:28 +02:00
parent 582025171d
commit 8f3f6130ab
6 changed files with 473 additions and 0 deletions

View file

@ -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 <config.h>
#include "goa.h"
#include <syncevo/IdentityProvider.h>
#include <syncevo/declarations.h>
SE_BEGIN_CXX
static class GOAProvider : public IdentityProvider
{
public:
GOAProvider() :
IdentityProvider("goa",
"goa:<GOA account presentation ID = email address>\n"
" Authentication using GNOME Online Accounts,\n"
" using an account created and managed with GNOME Control Center.")
{}
virtual boost::shared_ptr<AuthProvider> create(const InitStateString &username,
const InitStateString &password)
{
// Returning NULL if not enabled...
boost::shared_ptr<AuthProvider> provider;
#ifdef USE_GOA
provider = createGOAAuthProvider(username, password);
#endif
return provider;
}
} gsso;
SE_END_CXX

112
src/backends/goa/README Normal file
View file

@ -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:<GOA account presentation ID = email address | account ID>
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"
...
<?xml version="1.0" encoding="UTF-8"?>
<errors xmlns="http://schemas.google.com/g/2005">
<error>
<domain>GData</domain>
<code>authError</code>
<location type="header">Authorization</location>
<internalReason>Invalid Credentials</internalReason>
</error>
</errors>
...
[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

View file

@ -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"])

26
src/backends/goa/goa.am Normal file
View file

@ -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

235
src/backends/goa/goa.cpp Normal file
View file

@ -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 <config.h>
#ifdef USE_GOA
#include "goa.h"
#include <syncevo/IdentityProvider.h>
#include <gdbus-cxx-bridge.h>
#include <boost/algorithm/string/join.hpp>
#include <boost/foreach.hpp>
#include <algorithm>
#include <syncevo/declarations.h>
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<std::string, // property name
boost::variant<std::string> // property value - we only care about strings
> Properties;
typedef std::map<std::string, // interface name
Properties
> Interfaces;
typedef std::map<GDBusCXX::DBusObject_t, Interfaces> ManagedObjects;
GDBusCXX::DBusClientCall1<ManagedObjects> 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<GOAAccount> 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<int32_t> m_ensureCredentials;
GDBusCXX::DBusClientCall1<std::string> 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<GOAAccount> 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<std::string> 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<std::string> 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<std::string>(id->second);
const std::string &presentationIDStr = boost::get<std::string>(presentationID->second);
Properties::const_iterator provider = properties.find(GOA_ACCOUNT_PROVIDER_NAME);
std::string description = StringPrintf("%s, %s = %s",
provider == properties.end() ? "???" : boost::get<std::string>(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<GOAAccount> 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<GOAAccount> m_account;
public:
GOAAuthProvider(const boost::shared_ptr<GOAAccount> &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<AuthProvider> 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<GOAAccount> account = manager.lookupAccount(username);
boost::shared_ptr<AuthProvider> provider(new GOAAuthProvider(account));
return provider;
}
SE_END_CXX
#endif // USE_GOA

34
src/backends/goa/goa.h Normal file
View file

@ -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 <syncevo/util.h>
#include <boost/shared_ptr.hpp>
#include <syncevo/declarations.h>
SE_BEGIN_CXX
class AuthProvider;
boost::shared_ptr<AuthProvider> createGOAAuthProvider(const InitStateString &username,
const InitStateString &password);
SE_END_CXX
#endif