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:
parent
6298afabcb
commit
00ff3abbc7
|
@ -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.
|
|
@ -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"])
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue