oauth2: new backend using libsoup/libcurl

New backend implements identity provider for obtaining OAuth2 access
token for systems without HMI support.
Access token is obtained by making direct HTTP request to OAuth2 server
and using refresh token obtained by user in some other way.
New provider automatically updates stored refresh token when OAuth2
server is issuing new one.
This commit is contained in:
Mateusz Polrola 2014-08-29 14:54:47 +02:00 committed by Patrick Ohly
parent 6298afabcb
commit 00ff3abbc7
6 changed files with 420 additions and 0 deletions

View File

@ -0,0 +1,54 @@
Google CalDAV/CardDAV via OAuth2
================================
Setup without HMI
----------------------------
In case of system without HMI OAuth2 autohirzation can be made using
refresh token obtained by user in other way (e.g. using gSSO on
another system with HMI support, described below).
OAuth2 authentication using refresh token is enabled by setting OAuth2
refresh token as password and setting username to
"username=refresh_token:{'TokenHost': <'https://accounts.google.com'>, 'TokenPath': <'/o/oauth2/token'>, 'Scope': <'https://www.googleapis.com/auth/carddav'>, 'ClientID': <'678546420751-r51m6emeabphdtdkr3cf5p71slalvsxap0.apps.googleusercontent.com'>, 'ClientSecret': <'qJyR2nz8s3kXzmBwjHH8il28'>}"
Values of ClientId and ClientSecret need to be substituted with
correct values.
Obtaining OAuth2 refresh token
==============================
Obtaining refresh token value using gSSO
----------------------------------------
gSSO identity need to be created using:
gsso-example --create-identity=google-for-syncevolution --identity-method=oauth --identity-realms=google.com
Identity stored with id 7
After that Google access token can be queried using:
gsso-example --get-google-token=7 --client-id=94523794261470.apps.googleusercontent.com --client-secret=SlVBAcxamM0TBPlvX2c1zbaEY
Got response: {'Scope': <'https://www.google.com/m8/feeds https://www.googleapis.com/auth/carddav'>, 'AccessToken': <'ya29.dgBz-K-p5jYehyEAAAC5-vhCTOK183D8YxCnv8JJlGKTeV42B6IICDS3pTc1aVl7lTSLHv3OUXfSWEN3ZTA'>, 'TokenParameters': <@a{sv} {}>, 'TokenType': <'Bearer'>, 'RefreshToken': <'1/qXyWpORF_J30KNhZs7kVU8UnHLtt7o-hlud2yViII0A'>, 'Duration': <int64 3600>, 'Timestamp': <int64 1409818710>}
Values of client-id and client-secret need to be substituted with correct values.
Refresh token is returned in response.
Please note that gsso-example currently does not provide a way to
define requested scope of token as parameter, in example above its
source code was modified to use
"https://www.google.com/m8/feeds https://www.googleapis.com/auth/carddav"
scope as default one.
Obtaining refresh token value using GNOME Online Accounts (GOA)
---------------------------------------------------------------
Default values of Client Id and Client Secret for GOA are:
923794261470.apps.googleusercontent.com and
SlVBAcxamM0TBPlvX2c1zbEY
After setting up account its refresh token value can be obtained from
GNOME Keyring, using the seahorse UI. The keyring should contain a
password for "GOA google credentials for identity account_<account
id>", which is a JSON string containing a couple of values, including
refresh token.

View File

@ -0,0 +1,25 @@
PKG_CHECK_MODULES(JSON, [json], HAVE_JSON=yes, HAVE_JSON=no)
def_refresh_token="no"
if test "$ENABLE_LIBSOUP" = "yes" && test "$HAVE_JSON" = "yes"; then
def_refresh_token="yes"
fi
AC_ARG_ENABLE(refresh-token,
AS_HELP_STRING([--enable-refresh-token],
[enables or disables support for refresh token single-sign-on system without HMI; default is on if development files are available]),
[enable_refresh_token="$enableval"
test "$enable_refresh_token" = "yes" || test "$enable_refresh_token" = "no" || AC_MSG_ERROR([invalid value for --enable-refresh-token: $enable_refresh_token])
test "$enable_refresh_token" = "no" || test "$HAVE_JSON" = "yes" || test "$ENABLE_LIBSOUP" = "yes"|| AC_MSG_ERROR([required pkg(s) not found that are needed for --enable-refresh-token])],
enable_refresh_token="$def_refresh_token")
if test "$enable_refresh_token" = "yes"; then
# link into static executables, similar to a SyncSource
SYNCSOURCES="$SYNCSOURCES src/backends/oauth2/providerrefreshtoken.la"
if test "$enable_static" = "yes"; then
AC_DEFINE(STATIC_REFRESH_TOKEN, 1, [activate gsso])
fi
fi
# conditional compilation in make
AM_CONDITIONAL([USE_REFRESH_TOKEN], [test "$enable_refresh_token" = "yes"])

View File

@ -0,0 +1,30 @@
dist_noinst_DATA += src/backends/oauth2/configure-sub.in \
src/backends/oauth2/README \
$(NONE)
src_backends_oauth2_libs =
if USE_REFRESH_TOKEN
src_backends_oauth2_libs += src/backends/oauth2/providerrefreshtoken.la
endif
MOSTLYCLEANFILES += $(src_backends_oauth2_libs)
src_backends_oauth2_sources = \
src/backends/oauth2/oauth2.h \
src/backends/oauth2/oauth2.cpp \
$(NONE)
if ENABLE_MODULES
src_backends_oauth2_backenddir = $(BACKENDS_DIRECTORY)
src_backends_oauth2_backend_LTLIBRARIES = $(src_backends_oauth2_libs)
src_backends_oauth2_sources += \
src/backends/oauth2/oauth2Register.cpp
else
noinst_LTLIBRARIES += $(src_backends_oauth2_libs)
endif
src_backends_oauth2_providerrefreshtoken_la_SOURCES = $(src_backends_oauth2_sources)
src_backends_oauth2_providerrefreshtoken_la_LIBADD = $(JSON_LIBS) $(GLIB_LIBS) $(SYNCEVOLUTION_LIBS)
src_backends_oauth2_providerrefreshtoken_la_LDFLAGS = -module -avoid-version
src_backends_oauth2_providerrefreshtoken_la_CXXFLAGS = $(JSON_CFLAGS) $(GLIB_CFLAGS) $(SYNCEVO_WFLAGS) $(SYNCEVOLUTION_CFLAGS)
src_backends_oauth2_providerrefreshtoken_la_CPPFLAGS = -DUSE_REFRESH_TOKEN -I$(top_srcdir)/test $(BACKEND_CPPFLAGS)
src_backends_oauth2_providerrefreshtoken_la_DEPENDENCIES = src/syncevo/libsyncevolution.la

View File

@ -0,0 +1,222 @@
/*
* Copyright (C) 2014 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 <syncevo/IdentityProvider.h>
#include <syncevo/GLibSupport.h>
#include <syncevo/GVariantSupport.h>
#include <syncevo/SoupTransportAgent.h>
#include <json/json.h>
#include <syncevo/declarations.h>
SE_BEGIN_CXX
class RefreshTokenAuthProvider : public AuthProvider
{
boost::shared_ptr<HTTPTransportAgent> m_agent;
std::string m_tokenHost;
std::string m_tokenPath;
std::string m_scope;
std::string m_clientID;
std::string m_clientSecret;
std::string m_refreshToken;
mutable std::string m_accessToken;
public:
RefreshTokenAuthProvider(const char* tokenHost,
const char* tokenPath,
const char* scope,
const char* clientID,
const char* clientSecret,
const char* refreshToken) :
m_tokenHost(tokenHost),
m_tokenPath(tokenPath),
m_scope(scope),
m_clientID(clientID),
m_clientSecret(clientSecret),
m_refreshToken(refreshToken)
{
#ifdef ENABLE_LIBSOUP
boost::shared_ptr<SoupTransportAgent> agent(new SoupTransportAgent(static_cast<GMainLoop *>(NULL)));
m_agent = agent;
#elif defined(ENABLE_LIBCURL)
m_agent = new CurlTransportAgent();
#endif
}
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 PasswordUpdateCallback &passwordUpdateCallback) const
{
SE_LOG_DEBUG(NULL, "retrieving OAuth2 token, attempt %d", failedTokens);
//in case of retry do not use cached access token, request it again
if (1 >= failedTokens) {
m_accessToken.clear();
}
if (m_accessToken.empty()) {
const char *reply;
size_t replyLen;
std::string contentType;
m_agent->setURL(m_tokenHost + m_tokenPath);
m_agent->setContentType("application/x-www-form-urlencoded");
std::ostringstream oss;
oss<<"grant_type=refresh_token&client_id="<<m_clientID;
oss<<"&client_secret="<<m_clientSecret<<"&scope="<<m_scope;
oss<<"&refresh_token="<<m_refreshToken;
std::string requestBody = oss.str();
m_agent->send(requestBody.c_str(), requestBody.length());
switch (m_agent->wait()) {
case TransportAgent::ACTIVE:
SE_LOG_DEBUG(NULL, "retrieving OAuth2 token - agent active");
break;
case TransportAgent::GOT_REPLY:
{
SE_LOG_DEBUG(NULL, "retrieving OAuth2 token - agent got reply");
m_agent->getReply(reply, replyLen, contentType);
json_object *jobj = json_tokener_parse(reply);
if (jobj) {
json_object_object_foreach(jobj, key, val) {
if (strcmp("access_token", key) == 0) {
m_accessToken = json_object_get_string(val);
}
if (strcmp("refresh_token", key) == 0) {
std::string newRefreshToken = json_object_get_string(val);
SE_LOG_INFO(NULL, "refresh token invalidated - updating refresh token to %s", newRefreshToken.c_str());
if (passwordUpdateCallback) {
passwordUpdateCallback(newRefreshToken);
}
}
}
json_object_put(jobj);
}
else {
SE_THROW("OAuth2 misformatted response");
}
}
break;
case TransportAgent::TIME_OUT:
SE_LOG_DEBUG(NULL, "retrieving OAuth2 token - agent time out");
SE_THROW("OAuth2 request timed out");
break;
case TransportAgent::INACTIVE:
case TransportAgent::CLOSED:
case TransportAgent::FAILED: {
std::string errorString;
m_agent->getReply(reply, replyLen, contentType);
json_object *jobj = json_tokener_parse(reply);
if (jobj) {
json_object_object_foreach(jobj, key, val) {
if (strcmp ("error", key) == 0) {
errorString = json_object_get_string(val);
}
}
json_object_put(jobj);
}
SE_THROW("OAuth2 request failed with error: " + errorString);
break;
}
case TransportAgent::CANCELED:
SE_LOG_DEBUG(NULL, "retrieving OAuth2 token - agent cancelled");
SE_THROW("OAuth2 request cancelled");
break;
}
}
return m_accessToken;
}
virtual std::string getUsername() const { return ""; }
};
boost::shared_ptr<AuthProvider> createOAuth2AuthProvider(const InitStateString &username,
const InitStateString &password)
{
// Expected content of parameter GVariant.
boost::shared_ptr<GVariantType> hashtype(g_variant_type_new("a{sv}"), g_variant_type_free);
// 'username' is the part after refresh_token: which we can parse directly.
GErrorCXX gerror;
GVariantStealCXX parametersVar(g_variant_parse(hashtype.get(), username.c_str(), NULL, NULL, gerror));
if (!parametersVar) {
gerror.throwError(SE_HERE, "parsing 'refresh_token:' username");
}
GHashTableCXX parameters(Variant2HashTable(parametersVar));
// Extract the values that we expect in the parameters hash.
const char *tokenHost;
const char *tokenPath;
const char *scope;
const char *clientID;
const char *clientSecret;
GVariant *value;
value = (GVariant *)g_hash_table_lookup(parameters, "TokenHost");
if (!value ||
!g_variant_type_equal(G_VARIANT_TYPE_STRING, g_variant_get_type(value))) {
SE_THROW("need 'TokenHost: <string>' in 'refresh_token:' parameters");
}
tokenHost = g_variant_get_string(value, NULL);
value = (GVariant *)g_hash_table_lookup(parameters, "TokenPath");
if (!value ||
!g_variant_type_equal(G_VARIANT_TYPE_STRING, g_variant_get_type(value))) {
SE_THROW("need 'TokenPath: <string>' in 'refresh_token:' parameters");
}
tokenPath = g_variant_get_string(value, NULL);
value = (GVariant *)g_hash_table_lookup(parameters, "Scope");
if (!value ||
!g_variant_type_equal(G_VARIANT_TYPE_STRING, g_variant_get_type(value))) {
SE_THROW("need 'Scope: <string>' in 'refresh_token:' parameters");
}
scope = g_variant_get_string(value, NULL);
value = (GVariant *)g_hash_table_lookup(parameters, "ClientID");
if (!value ||
!g_variant_type_equal(G_VARIANT_TYPE_STRING, g_variant_get_type(value))) {
SE_THROW("need 'ClientID: <string>' in 'refresh_token:' parameters");
}
clientID = g_variant_get_string(value, NULL);
value = (GVariant *)g_hash_table_lookup(parameters, "ClientSecret");
if (!value ||
!g_variant_type_equal(G_VARIANT_TYPE_STRING, g_variant_get_type(value))) {
SE_THROW("need 'ClientSecret: <string>' in 'refresh_token:' parameters");
}
clientSecret = g_variant_get_string(value, NULL);
if (password.empty()) {
SE_THROW("need refresh token provided as password");
}
boost::shared_ptr<AuthProvider> provider(new RefreshTokenAuthProvider(tokenHost, tokenPath, scope, clientID, clientSecret, password.c_str()));
return provider;
}
SE_END_CXX

View File

@ -0,0 +1,34 @@
/*
* Copyright (C) 2014 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_OAUTH2_AUTH_PROVIDER
#define INCL_SYNC_EVOLUTION_OAUTH2_AUTH_PROVIDER
#include <syncevo/util.h>
#include <boost/shared_ptr.hpp>
#include <syncevo/declarations.h>
SE_BEGIN_CXX
class AuthProvider;
boost::shared_ptr<AuthProvider> createOAuth2AuthProvider(const InitStateString &username,
const InitStateString &password);
SE_END_CXX
#endif

View File

@ -0,0 +1,55 @@
/*
* Copyright (C) 2014 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>
#if defined(USE_REFRESH_TOKEN) || defined(STATIC_REFRESH_TOKEN)
#include "oauth2.h"
#include <syncevo/IdentityProvider.h>
#include <syncevo/declarations.h>
SE_BEGIN_CXX
static class OAuth2Provider : public IdentityProvider
{
public:
OAuth2Provider() :
IdentityProvider("refresh_token",
"refresh_token:<parameters>\n"
" Authentication using refresh token.\n"
" GVariant text dump suitable for g_variant_parse() (see\n"
" https://developer.gnome.org/glib/stable/gvariant-text.html).\n"
" It must contain a hash with keys 'TokenHost', 'TokenPath', \n"
" 'Scope', 'ClientID', 'ClientSecret'\n")
{}
virtual boost::shared_ptr<AuthProvider> create(const InitStateString &username,
const InitStateString &password)
{
boost::shared_ptr<AuthProvider> provider;
provider = createOAuth2AuthProvider(username, password);
return provider;
}
} gsso;
SE_END_CXX
#endif // is enabled