Merge lp:~mardy/ubuntuone-credentials/signon-plugin into lp:ubuntuone-credentials

Proposed by Alberto Mardegan
Status: Work in progress
Proposed branch: lp:~mardy/ubuntuone-credentials/signon-plugin
Merge into: lp:ubuntuone-credentials
Diff against target: 3370 lines (+2886/-118)
23 files modified
data/ubuntuone.provider (+2/-2)
debian/control (+3/-2)
debian/libubuntuoneauth-2.0-0.symbols (+4/-0)
debian/signon-plugin-ubuntuone.install (+1/-1)
libubuntuoneauth/CMakeLists.txt (+13/-3)
libubuntuoneauth/accountmanager.cpp (+44/-0)
libubuntuoneauth/accountmanager.h (+43/-0)
libubuntuoneauth/authenticator.cpp (+176/-0)
libubuntuoneauth/authenticator.h (+71/-0)
libubuntuoneauth/common.h (+30/-0)
libubuntuoneauth/keyring.cpp (+34/-60)
libubuntuoneauth/libubuntuoneauth.symbols (+2/-1)
libubuntuoneauth/ssoservice.cpp (+44/-13)
libubuntuoneauth/token.cpp (+54/-5)
libubuntuoneauth/token.h (+4/-0)
signon-plugin/CMakeLists.txt (+9/-2)
signon-plugin/i18n.cpp (+37/-0)
signon-plugin/i18n.h (+31/-0)
signon-plugin/tests/CMakeLists.txt (+52/-0)
signon-plugin/tests/tst_plugin.cpp (+1840/-0)
signon-plugin/ubuntuone-plugin.cpp (+327/-19)
signon-plugin/ubuntuone-plugin.h (+45/-7)
signon-plugin/ubuntuonedata.h (+20/-3)
To merge this branch: bzr merge lp:~mardy/ubuntuone-credentials/signon-plugin
Reviewer Review Type Date Requested Status
dobey Pending
Review via email: mp+287132@code.launchpad.net

Commit message

WIP Implement SignOn authentication plugin, use it libubuntuoneauth.

Description of the change

WIP Implement SignOn authentication plugin, use it libubuntuoneauth.

To post a comment you must log in.
237. By Alberto Mardegan

Force export of symbol

238. By Alberto Mardegan

Allow authentication with no account

239. By Alberto Mardegan

No undefined symbols

240. By Alberto Mardegan

Test plugin loading

241. By Alberto Mardegan

Properly export symbols

242. By Alberto Mardegan

debugging

243. By Alberto Mardegan

Return specific error codes

244. By Alberto Mardegan

Tweak on application name

245. By Alberto Mardegan

missing include

246. By Alberto Mardegan

Explain hack

247. By Alberto Mardegan

Never remove accounts

248. By Alberto Mardegan

Plugin: don't delete the reply while using it

249. By Alberto Mardegan

Handle invalid email error

250. By Alberto Mardegan

Allow UI interactions when retrieving token

251. By Alberto Mardegan

Fix dialog title and handling of 2fa reply

252. By Alberto Mardegan

Avoid double free

253. By Alberto Mardegan

Update method name in .provider file

254. By Alberto Mardegan

Merge trunk

[ CI Train Bot ]
* Resync trunk.
[ Michael Zanetti ]
* update to uitk 1.3 (LP: #1560621)
[ Rodney Dawes ]
* Disable tests on powerpc as something in xenial is causing a crash.

255. By Alberto Mardegan

Use Internal namespace

Unmerged revisions

255. By Alberto Mardegan

Use Internal namespace

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'data/ubuntuone.provider'
--- data/ubuntuone.provider 2013-10-16 08:21:08 +0000
+++ data/ubuntuone.provider 2016-04-22 09:51:32 +0000
@@ -8,8 +8,8 @@
88
9 <template>9 <template>
10 <group name="auth">10 <group name="auth">
11 <setting name="method">password</setting>11 <setting name="method">ubuntuone</setting>
12 <setting name="mechanism">password</setting>12 <setting name="mechanism">ubuntuone</setting>
13 </group>13 </group>
14 </template>14 </template>
15</provider>15</provider>
1616
=== modified file 'debian/control'
--- debian/control 2015-11-16 20:17:01 +0000
+++ debian/control 2016-04-22 09:51:32 +0000
@@ -14,7 +14,7 @@
14 qtdeclarative5-dev-tools,14 qtdeclarative5-dev-tools,
15 qtdeclarative5-ubuntu-ui-toolkit-plugin,15 qtdeclarative5-ubuntu-ui-toolkit-plugin,
16 python3-all:native,16 python3-all:native,
17 signon-plugins-dev,17 signon-plugins-dev (>= 8.58),
18 ubuntu-ui-toolkit-autopilot:native,18 ubuntu-ui-toolkit-autopilot:native,
19 xvfb,19 xvfb,
20Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>20Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
@@ -135,7 +135,7 @@
135135
136Package: signon-plugin-ubuntuone136Package: signon-plugin-ubuntuone
137Architecture: any137Architecture: any
138Multi-Arch: foreign138Multi-Arch: same
139Pre-Depends:139Pre-Depends:
140 multiarch-support,140 multiarch-support,
141 ${misc:Pre-Depends},141 ${misc:Pre-Depends},
@@ -159,6 +159,7 @@
159Depends:159Depends:
160 libubuntuoneauth-2.0-0 (= ${binary:Version}),160 libubuntuoneauth-2.0-0 (= ${binary:Version}),
161 qml-module-ubuntuone (= ${binary:Version}),161 qml-module-ubuntuone (= ${binary:Version}),
162 signon-plugin-ubuntuone,
162 ubuntu-system-settings-online-accounts,163 ubuntu-system-settings-online-accounts,
163 ${misc:Depends},164 ${misc:Depends},
164 ${shlibs:Depends},165 ${shlibs:Depends},
165166
=== modified file 'debian/libubuntuoneauth-2.0-0.symbols'
--- debian/libubuntuoneauth-2.0-0.symbols 2015-12-07 21:38:01 +0000
+++ debian/libubuntuoneauth-2.0-0.symbols 2016-04-22 09:51:32 +0000
@@ -111,7 +111,11 @@
111 (c++)"UbuntuOne::Token::Token(QString, QString, QString, QString)@Base" 13.08111 (c++)"UbuntuOne::Token::Token(QString, QString, QString, QString)@Base" 13.08
112 (c++)"UbuntuOne::Token::Token(QString, QString, QString, QString, QString, QString)@Base" 14.04+14.10.20140908112 (c++)"UbuntuOne::Token::Token(QString, QString, QString, QString, QString, QString)@Base" 14.04+14.10.20140908
113 (c++)"UbuntuOne::Token::~Token()@Base" 13.08113 (c++)"UbuntuOne::Token::~Token()@Base" 13.08
114 (c++)"UbuntuOne::Token::name() const@Base" 15.11+16.04.20151207.1
114 (c++)"UbuntuOne::Token::consumerKey() const@Base" 15.10+15.10.20150617115 (c++)"UbuntuOne::Token::consumerKey() const@Base" 15.10+15.10.20150617
116 (c++)"UbuntuOne::Token::consumerSecret() const@Base" 15.11+16.04.20151207.1
117 (c++)"UbuntuOne::Token::tokenKey() const@Base" 15.11+16.04.20151207.1
118 (c++)"UbuntuOne::Token::tokenSecret() const@Base" 15.11+16.04.20151207.1
115 (c++)"UbuntuOne::Token::created() const@Base" 14.04+14.10.20140818119 (c++)"UbuntuOne::Token::created() const@Base" 14.04+14.10.20140818
116 (c++)"UbuntuOne::Token::updated() const@Base" 14.04+14.10.20140818120 (c++)"UbuntuOne::Token::updated() const@Base" 14.04+14.10.20140818
117 (c++)"UbuntuOne::Token::getServerTimestamp() const@Base" 15.11+16.04.20151207.1121 (c++)"UbuntuOne::Token::getServerTimestamp() const@Base" 15.11+16.04.20151207.1
118122
=== modified file 'debian/signon-plugin-ubuntuone.install'
--- debian/signon-plugin-ubuntuone.install 2013-11-27 21:19:44 +0000
+++ debian/signon-plugin-ubuntuone.install 2016-04-22 09:51:32 +0000
@@ -1,1 +1,1 @@
1usr/lib/signon/*.so1usr/lib/*/signon/*.so
22
=== modified file 'libubuntuoneauth/CMakeLists.txt'
--- libubuntuoneauth/CMakeLists.txt 2013-11-22 19:17:04 +0000
+++ libubuntuoneauth/CMakeLists.txt 2016-04-22 09:51:32 +0000
@@ -16,7 +16,17 @@
16# The sources for building the library16# The sources for building the library
17FILE (GLOB SOURCES *.cpp)17FILE (GLOB SOURCES *.cpp)
18# HEADERS only includes the public headers for installation.18# HEADERS only includes the public headers for installation.
19FILE (GLOB HEADERS *.h)19FILE (GLOB HEADERS
20 errormessages.h
21 identityprovider.h
22 keyring.h
23 logging.h
24 network.h
25 requests.h
26 responses.h
27 ssoservice.h
28 token.h
29)
2030
21pkg_check_modules(OAUTH REQUIRED oauth)31pkg_check_modules(OAUTH REQUIRED oauth)
22add_definitions(${OAUTH_CFLAGS} ${OAUTH_CFLAGS_OTHER})32add_definitions(${OAUTH_CFLAGS} ${OAUTH_CFLAGS_OTHER})
@@ -29,12 +39,12 @@
29target_link_libraries (${AUTH_LIB_NAME}39target_link_libraries (${AUTH_LIB_NAME}
30 ${LIBSIGNON_LDFLAGS}40 ${LIBSIGNON_LDFLAGS}
31 ${OAUTH_LDFLAGS}41 ${OAUTH_LDFLAGS}
32 -Wl,--version-script -Wl,${CMAKE_CURRENT_SOURCE_DIR}/libubuntuoneauth.symbols
33)42)
3443
35SET_TARGET_PROPERTIES(${AUTH_LIB_NAME} PROPERTIES44SET_TARGET_PROPERTIES(${AUTH_LIB_NAME} PROPERTIES
36 VERSION ${AUTH_LIB_VERSION}45 VERSION ${AUTH_LIB_VERSION}
37 SOVERSION ${AUTH_LIB_SOVERSION}46 SOVERSION ${AUTH_LIB_SOVERSION}
47 LINK_FLAGS "-Wl,--version-script -Wl,\"${CMAKE_CURRENT_SOURCE_DIR}/libubuntuoneauth.symbols\""
38)48)
3949
40INSTALL (50INSTALL (
@@ -64,4 +74,4 @@
64)74)
6575
66add_subdirectory(tests)76add_subdirectory(tests)
67add_subdirectory(examples)
68\ No newline at end of file77\ No newline at end of file
78add_subdirectory(examples)
6979
=== added file 'libubuntuoneauth/accountmanager.cpp'
--- libubuntuoneauth/accountmanager.cpp 1970-01-01 00:00:00 +0000
+++ libubuntuoneauth/accountmanager.cpp 2016-04-22 09:51:32 +0000
@@ -0,0 +1,44 @@
1/*
2 * Copyright 2016 Canonical Ltd.
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of version 3 of the GNU Lesser General Public
6 * License as published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 * General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public
14 * License along with this library; if not, write to the
15 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
16 * Boston, MA 02110-1301, USA.
17 */
18
19#include "accountmanager.h"
20
21namespace Internal {
22
23AccountManager *AccountManager::m_instance = 0;
24
25AccountManager::AccountManager(QObject *parent):
26 Accounts::Manager(parent)
27{
28}
29
30AccountManager::~AccountManager()
31{
32 m_instance = 0;
33}
34
35AccountManager *AccountManager::instance()
36{
37 if (!m_instance) {
38 m_instance = new AccountManager;
39 }
40
41 return m_instance;
42}
43
44} // namespace
045
=== added file 'libubuntuoneauth/accountmanager.h'
--- libubuntuoneauth/accountmanager.h 1970-01-01 00:00:00 +0000
+++ libubuntuoneauth/accountmanager.h 2016-04-22 09:51:32 +0000
@@ -0,0 +1,43 @@
1/*
2 * Copyright 2016 Canonical Ltd.
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of version 3 of the GNU Lesser General Public
6 * License as published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 * General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public
14 * License along with this library; if not, write to the
15 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
16 * Boston, MA 02110-1301, USA.
17 */
18#ifndef _U1_ACCOUNTMANAGER_H_
19#define _U1_ACCOUNTMANAGER_H_
20
21#include <Accounts/Manager>
22#include <QObject>
23
24namespace Internal {
25
26class AccountManager : public Accounts::Manager
27{
28 Q_OBJECT
29
30public:
31 static AccountManager *instance();
32 ~AccountManager();
33
34protected:
35 explicit AccountManager(QObject *parent = 0);
36
37private:
38 static AccountManager *m_instance;
39};
40
41} /* namespace */
42
43#endif /* _U1_ACCOUNTMANAGER_H_ */
044
=== added file 'libubuntuoneauth/authenticator.cpp'
--- libubuntuoneauth/authenticator.cpp 1970-01-01 00:00:00 +0000
+++ libubuntuoneauth/authenticator.cpp 2016-04-22 09:51:32 +0000
@@ -0,0 +1,176 @@
1/*
2 * Copyright 2016 Canonical Ltd.
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of version 3 of the GNU Lesser General Public
6 * License as published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 * General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public
14 * License along with this library; if not, write to the
15 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
16 * Boston, MA 02110-1301, USA.
17 */
18
19#include <Accounts/Account>
20#include <Accounts/Service>
21#include <SignOn/AuthSession>
22#include <SignOn/Identity>
23#include <SignOn/IdentityInfo>
24
25#include <QDebug>
26
27#include "accountmanager.h"
28#include "authenticator.h"
29#include "../signon-plugin/ubuntuonedata.h"
30
31using namespace Internal;
32using namespace UbuntuOne;
33
34Authenticator::Authenticator(QObject *parent):
35 QObject(parent),
36 m_manager(AccountManager::instance()),
37 m_invalidate(false),
38 m_uiAllowed(true)
39{
40}
41
42void Authenticator::handleError(const SignOn::Error &e)
43{
44 qCritical() << "Authentication error:" << e.message();
45 Q_EMIT error(AuthenticationError);
46}
47
48void Authenticator::handleSessionData(const SignOn::SessionData &data)
49{
50 PluginData reply = data.data<PluginData>();
51
52 auto errorCode = PluginData::ErrorCode(reply.U1ErrorCode());
53 if (errorCode != PluginData::NoError) {
54 switch (errorCode) {
55 case PluginData::OneTimePasswordRequired:
56 qDebug() << "Error: OTP required";
57 Q_EMIT error(OneTimePasswordRequired);
58 break;
59 case PluginData::InvalidPassword:
60 qDebug() << "Error: invalid password";
61 Q_EMIT error(InvalidPassword);
62 break;
63 default:
64 qWarning() << "Unknown error code" << errorCode;
65 Q_EMIT error(AuthenticationError);
66 }
67 return;
68 }
69
70 Token token(reply.TokenKey(), reply.TokenSecret(),
71 reply.ConsumerKey(), reply.ConsumerSecret());
72 if (token.isValid()) {
73 Q_EMIT authenticated(token);
74 } else {
75 QString message("Failed to convert result to Token object.");
76 qCritical() << message;
77 Q_EMIT error(AuthenticationError);
78 }
79}
80
81quint32 Authenticator::credentialsId()
82{
83 QString providerId("ubuntuone");
84 Accounts::AccountIdList accountIds = m_manager->accountList(providerId);
85
86 if (accountIds.isEmpty()) {
87 qDebug() << "authenticate(): No UbuntuOne accounts found";
88 Q_EMIT error(AccountNotFound);
89 return 0;
90 }
91
92 if (accountIds.count() > 1) {
93 qWarning() << "authenticate(): Found '" << accountIds.count() <<
94 "' accounts. Using first.";
95 }
96
97 qDebug() << "authenticate(): Using account '" << accountIds[0] << "'.";
98
99 auto account = m_manager->account(accountIds[0]);
100 if (Q_UNLIKELY(!account)) {
101 qDebug() << "Couldn't load account";
102 /* This could either happen because the account was deleted right while
103 * we were loading it, or because the accounts DB was locked by another
104 * app. Let's just return an authentication error here, so the client
105 * can retry.
106 */
107 Q_EMIT error(AuthenticationError);
108 return 0;
109 }
110
111 /* Here we should check that the account service is enabled; but since the
112 * old code was not doing this check, and that from the API there is no way
113 * of knowing which service we are interested in, let's leave it as a TODO.
114 */
115
116 return account->credentialsId();
117}
118
119void Authenticator::authenticate(const QString &tokenName,
120 const QString &userName,
121 const QString &password,
122 const QString &otp)
123{
124 SignOn::Identity *identity;
125 if (userName.isEmpty()) {
126 // Use existing account
127 quint32 id = credentialsId();
128 if (Q_UNLIKELY(!id)) return;
129
130 identity = SignOn::Identity::existingIdentity(id, this);
131 if (Q_UNLIKELY(!identity)) {
132 qCritical() << "authenticate(): unable to load credentials" << id;
133 Q_EMIT error(AccountNotFound);
134 return;
135 }
136 } else {
137 identity = SignOn::Identity::newIdentity(SignOn::IdentityInfo(), this);
138 }
139
140 auto session = identity->createSession(QStringLiteral("ubuntuone"));
141 if (Q_UNLIKELY(!session)) {
142 qCritical() << "Unable to create AuthSession.";
143 Q_EMIT error(AuthenticationError);
144 return;
145 }
146
147 connect(session, SIGNAL(response(const SignOn::SessionData&)),
148 this, SLOT(handleSessionData(const SignOn::SessionData&)));
149 connect(session, SIGNAL(error(const SignOn::Error&)),
150 this, SLOT(handleError(const SignOn::Error&)));
151
152 PluginData data;
153 data.setTokenName(tokenName);
154 data.setUserName(userName);
155 data.setSecret(password);
156 data.setOneTimePassword(otp);
157 int uiPolicy = m_uiAllowed ?
158 SignOn::DefaultPolicy : SignOn::NoUserInteractionPolicy;
159 if (m_invalidate) {
160 uiPolicy |= SignOn::RequestPasswordPolicy;
161 m_invalidate = false;
162 }
163 data.setUiPolicy(uiPolicy);
164
165 session->process(data, QStringLiteral("ubuntuone"));
166}
167
168void Authenticator::invalidateCredentials()
169{
170 m_invalidate = true;
171}
172
173void Authenticator::setUiAllowed(bool allowed)
174{
175 m_uiAllowed = allowed;
176}
0177
=== added file 'libubuntuoneauth/authenticator.h'
--- libubuntuoneauth/authenticator.h 1970-01-01 00:00:00 +0000
+++ libubuntuoneauth/authenticator.h 2016-04-22 09:51:32 +0000
@@ -0,0 +1,71 @@
1/*
2 * Copyright 2016 Canonical Ltd.
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of version 3 of the GNU Lesser General Public
6 * License as published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 * General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public
14 * License along with this library; if not, write to the
15 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
16 * Boston, MA 02110-1301, USA.
17 */
18#ifndef _U1_AUTHENTICATOR_H_
19#define _U1_AUTHENTICATOR_H_
20
21#include <Accounts/Manager>
22#include <SignOn/Identity>
23
24#include <QObject>
25
26#include "token.h"
27
28namespace Internal {
29
30class Authenticator : public QObject
31{
32 Q_OBJECT
33
34public:
35 enum ErrorCode {
36 NoError = 0,
37 AccountNotFound,
38 OneTimePasswordRequired,
39 InvalidPassword,
40 AuthenticationError, // will create more specific codes if needed
41 };
42
43 explicit Authenticator(QObject *parent = 0);
44
45 void authenticate(const QString &tokenName,
46 const QString &userName = QString(),
47 const QString &password = QString(),
48 const QString &otp = QString());
49 void invalidateCredentials();
50 void setUiAllowed(bool allowed);
51
52Q_SIGNALS:
53 void authenticated(const UbuntuOne::Token& token);
54 void error(Internal::Authenticator::ErrorCode code);
55
56private:
57 quint32 credentialsId();
58
59private Q_SLOTS:
60 void handleError(const SignOn::Error &error);
61 void handleSessionData(const SignOn::SessionData &data);
62
63private:
64 Accounts::Manager *m_manager;
65 bool m_invalidate;
66 bool m_uiAllowed;
67};
68
69} /* namespace */
70
71#endif /* _U1_AUTHENTICATOR_H_ */
072
=== added file 'libubuntuoneauth/common.h'
--- libubuntuoneauth/common.h 1970-01-01 00:00:00 +0000
+++ libubuntuoneauth/common.h 2016-04-22 09:51:32 +0000
@@ -0,0 +1,30 @@
1/*
2 * Copyright 2016 Canonical Ltd.
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of version 3 of the GNU Lesser General Public
6 * License as published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 * General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public
14 * License along with this library; if not, write to the
15 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
16 * Boston, MA 02110-1301, USA.
17 */
18
19#ifndef U1_COMMON_H
20#define U1_COMMON_H
21
22#include <QtGlobal>
23
24#if defined(BUILDING_LIBU1AUTH)
25# define U1_EXPORT Q_DECL_EXPORT
26#else
27# define U1_EXPORT Q_DECL_IMPORT
28#endif
29
30#endif // U1_COMMON_H
031
=== modified file 'libubuntuoneauth/keyring.cpp'
--- libubuntuoneauth/keyring.cpp 2015-03-12 13:13:46 +0000
+++ libubuntuoneauth/keyring.cpp 2016-04-22 09:51:32 +0000
@@ -23,7 +23,9 @@
2323
24#include <QDebug>24#include <QDebug>
2525
26#include "authenticator.h"
26#include "keyring.h"27#include "keyring.h"
28#include "../signon-plugin/ubuntuonedata.h"
2729
28using namespace Accounts;30using namespace Accounts;
29using namespace SignOn;31using namespace SignOn;
@@ -46,58 +48,41 @@
4648
47 void Keyring::handleSessionData(const SignOn::SessionData &data)49 void Keyring::handleSessionData(const SignOn::SessionData &data)
48 {50 {
49 QString secret = data.Secret();51 PluginData reply = data.data<PluginData>();
5052
51 if (secret.length() == 0) {53 Token token(reply.TokenKey(), reply.TokenSecret(),
52 QString msg("Could not read credentials secret value.");54 reply.ConsumerKey(), reply.ConsumerSecret());
53 qCritical() << msg;55 if (token.isValid()) {
54 emit keyringError(msg);56 emit tokenFound(token);
55 return;
56 }
57
58 Token *token = Token::fromQuery(secret);
59 if (token->isValid()) {
60 emit tokenFound(*token);
61 } else {57 } else {
62 QString message("Failed to convert result to Token object.");58 QString message("Failed to convert result to Token object.");
63 qCritical() << message;59 qCritical() << message;
64 emit keyringError(message);60 emit keyringError(message);
65 }61 }
66 delete token;
67 }62 }
6863
69 void Keyring::findToken()64 void Keyring::findToken()
70 {65 {
71 QString _acctName("ubuntuone");66 using namespace Internal;
72 AccountIdList _ids = _manager.accountList(_acctName);67
73 Identity *identity;68 auto authenticator = new Authenticator;
74 Account *account;69 authenticator->setUiAllowed(true);
7570
76 if (_ids.length() > 0) {71 connect(authenticator, &Authenticator::authenticated,
77 if (_ids.length() > 1) {72 [=](const Token &token) {
78 qDebug() << "findToken(): Found '" << _ids.length() << "' accounts. Using first.";73 Q_EMIT tokenFound(token);
79 }74 authenticator->deleteLater();
80 account = _manager.account(_ids[0]);75 });
81 qDebug() << "findToken(): Using Ubuntu One account '" << _ids[0] << "'.";76 connect(authenticator, &Authenticator::error,
82 identity = Identity::existingIdentity(account->credentialsId());77 [=](Authenticator::ErrorCode code) {
83 if (identity == NULL) {78 if (code == Authenticator::AccountNotFound) {
84 qCritical() << "findToken(): disabled account " << _acctName << _ids[0];79 Q_EMIT tokenNotFound();
85 emit tokenNotFound();80 } else {
86 return;81 Q_EMIT keyringError("Authentication failed");
87 }82 }
88 AuthSession *session = identity->createSession(QStringLiteral("password"));83 authenticator->deleteLater();
89 if (session != NULL) {84 });
90 connect(session, SIGNAL(response(const SignOn::SessionData&)),85 authenticator->authenticate(Token::buildTokenName());
91 this, SLOT(handleSessionData(const SignOn::SessionData&)));
92 connect(session, SIGNAL(error(const SignOn::Error&)),
93 this, SLOT(handleError(const SignOn::Error&)));
94 session->process(SessionData(), QStringLiteral("password"));
95 return;
96 }
97 qCritical() << "Unable to create AuthSession.";
98 }
99 qDebug() << "findToken(): No accounts found matching " << _acctName;
100 emit tokenNotFound();
101 }86 }
10287
103 void Keyring::handleCredentialsStored(const quint32 id)88 void Keyring::handleCredentialsStored(const quint32 id)
@@ -167,11 +152,13 @@
167152
168 void Keyring::handleAccountRemoved()153 void Keyring::handleAccountRemoved()
169 {154 {
155 /* DEPRECATED, UNUSED */
170 emit tokenDeleted();156 emit tokenDeleted();
171 }157 }
172158
173 void Keyring::handleDeleteError(const SignOn::Error &error)159 void Keyring::handleDeleteError(const SignOn::Error &error)
174 {160 {
161 /* DEPRECATED, UNUSED */
175 // Just log the error here, as we don't want to infinite loop.162 // Just log the error here, as we don't want to infinite loop.
176 qWarning() << "Error deleting token:" << error.message();163 qWarning() << "Error deleting token:" << error.message();
177 }164 }
@@ -180,24 +167,11 @@
180 {167 {
181 QString _acctName("ubuntuone");168 QString _acctName("ubuntuone");
182 AccountIdList _ids = _manager.accountList(_acctName);169 AccountIdList _ids = _manager.accountList(_acctName);
183 if (_ids.length() > 0) {170 if (_ids.isEmpty()) {
184 if (_ids.length() > 1) {171 emit tokenNotFound();
185 qDebug() << "deleteToken(): Found '" << _ids.length() << "' accounts. Using first.";
186 }
187 Account *account = _manager.account(_ids[0]);
188 qDebug() << "deleteToken(): Using Ubuntu One account '" << _ids[0] << "'.";
189 Identity *identity = Identity::existingIdentity(account->credentialsId());
190 connect(account, SIGNAL(removed()),
191 this, SLOT(handleAccountRemoved()));
192 connect(identity, SIGNAL(error(const SignOn::Error&)),
193 this, SLOT(handleDeleteError(const SignOn::Error&)));
194
195 identity->remove();
196 account->remove();
197 account->sync();
198 return;
199 }172 }
200 emit tokenNotFound();173
174 /* We don't remove accounts anymore */
201 }175 }
202176
203} // namespace UbuntuOne177} // namespace UbuntuOne
204178
=== modified file 'libubuntuoneauth/libubuntuoneauth.symbols'
--- libubuntuoneauth/libubuntuoneauth.symbols 2013-07-22 15:54:02 +0000
+++ libubuntuoneauth/libubuntuoneauth.symbols 2016-04-22 09:51:32 +0000
@@ -1,7 +1,8 @@
1{1{
2 global:2 global:
3 extern "C++" {3 extern "C++" {
4 *UbuntuOne::*;4 UbuntuOne::*;
5 *for?UbuntuOne::*;
5 };6 };
6 qt_*;7 qt_*;
7 local:8 local:
89
=== modified file 'libubuntuoneauth/ssoservice.cpp'
--- libubuntuoneauth/ssoservice.cpp 2015-01-15 21:12:35 +0000
+++ libubuntuoneauth/ssoservice.cpp 2016-04-22 09:51:32 +0000
@@ -17,11 +17,13 @@
17 */17 */
18#include <sys/utsname.h>18#include <sys/utsname.h>
1919
20#include <QCoreApplication>
20#include <QDebug>21#include <QDebug>
21#include <QtGlobal>22#include <QtGlobal>
22#include <QNetworkRequest>23#include <QNetworkRequest>
23#include <QUrlQuery>24#include <QUrlQuery>
2425
26#include "authenticator.h"
25#include "logging.h"27#include "logging.h"
26#include "ssoservice.h"28#include "ssoservice.h"
27#include "requests.h"29#include "requests.h"
@@ -64,9 +66,6 @@
64 this, SLOT(accountPinged(QNetworkReply*)));66 this, SLOT(accountPinged(QNetworkReply*)));
6567
66 connect(&(_provider),68 connect(&(_provider),
67 SIGNAL(OAuthTokenGranted(const OAuthTokenResponse&)),
68 this, SLOT(tokenReceived(const OAuthTokenResponse&)));
69 connect(&(_provider),
70 SIGNAL(AccountGranted(const AccountResponse&)),69 SIGNAL(AccountGranted(const AccountResponse&)),
71 this, SLOT(accountRegistered(const AccountResponse&)));70 this, SLOT(accountRegistered(const AccountResponse&)));
72 connect(&(_provider),71 connect(&(_provider),
@@ -116,12 +115,40 @@
116115
117 void SSOService::login(QString email, QString password, QString twoFactorCode)116 void SSOService::login(QString email, QString password, QString twoFactorCode)
118 {117 {
119 OAuthTokenRequest request(getAuthBaseUrl(),118 using namespace Internal;
120 email, password,119
121 Token::buildTokenName(), twoFactorCode);120 auto authenticator = new Authenticator;
122 _tempEmail = email;121 /* This is a hack: there should be a public API to decide whether UI
123122 * interactions are allowed.
124 _provider.GetOAuthToken(request);123 * For the time being, allow them everywhere except from the account
124 * plugin (which has its own UI to request all the needed info).
125 */
126 if (QCoreApplication::applicationName() == "online-accounts-ui") {
127 qDebug() << "In account plugin: disabling UI interactions";
128 authenticator->setUiAllowed(false);
129 } else {
130 authenticator->setUiAllowed(true);
131 }
132
133 connect(authenticator, &Authenticator::authenticated,
134 [=](const Token &token) {
135 _keyring->storeToken(token, email);
136 authenticator->deleteLater();
137 });
138 connect(authenticator, &Authenticator::error,
139 [=](Authenticator::ErrorCode code) {
140 if (code == Authenticator::AccountNotFound) {
141 Q_EMIT credentialsNotFound();
142 } else if (code == Authenticator::OneTimePasswordRequired) {
143 Q_EMIT twoFactorAuthRequired();
144 } else {
145 /* TODO: deliver a proper error response. */
146 Q_EMIT requestFailed(ErrorResponse());
147 }
148 authenticator->deleteLater();
149 });
150 authenticator->authenticate(Token::buildTokenName(),
151 email, password, twoFactorCode);
125 }152 }
126153
127 void SSOService::handleTwoFactorAuthRequired()154 void SSOService::handleTwoFactorAuthRequired()
@@ -147,10 +174,14 @@
147174
148 void SSOService::tokenReceived(const OAuthTokenResponse& token)175 void SSOService::tokenReceived(const OAuthTokenResponse& token)
149 {176 {
150 Token realToken = Token(token.token_key(), token.token_secret(),177 // Not used anymore
151 token.consumer_key(), token.consumer_secret(),178
152 token.date_created(), token.date_updated());179 /* The following two lines are needed to ensure that the
153 _keyring->storeToken(realToken, _tempEmail);180 * OAuthTokenRequest::~OAuthTokenRequest() symbol is exported by the
181 * library.
182 */
183 OAuthTokenRequest request;
184 qDebug() << request.serialize();
154 }185 }
155186
156 void SSOService::accountPinged(QNetworkReply*)187 void SSOService::accountPinged(QNetworkReply*)
157188
=== modified file 'libubuntuoneauth/token.cpp'
--- libubuntuoneauth/token.cpp 2015-12-07 21:17:00 +0000
+++ libubuntuoneauth/token.cpp 2016-04-22 09:51:32 +0000
@@ -46,10 +46,18 @@
46 QString consumer_key, QString consumer_secret)46 QString consumer_key, QString consumer_secret)
47 {47 {
48 _tokenHash[TOKEN_NAME_KEY] = buildTokenName();48 _tokenHash[TOKEN_NAME_KEY] = buildTokenName();
49 _tokenHash[TOKEN_TOKEN_KEY] = token_key;49 if (!token_key.isEmpty()) {
50 _tokenHash[TOKEN_TOKEN_SEC_KEY] = token_secret;50 _tokenHash[TOKEN_TOKEN_KEY] = token_key;
51 _tokenHash[TOKEN_CONSUMER_KEY] = consumer_key;51 }
52 _tokenHash[TOKEN_CONSUMER_SEC_KEY] = consumer_secret;52 if (!token_secret.isEmpty()) {
53 _tokenHash[TOKEN_TOKEN_SEC_KEY] = token_secret;
54 }
55 if (!consumer_key.isEmpty()) {
56 _tokenHash[TOKEN_CONSUMER_KEY] = consumer_key;
57 }
58 if (!consumer_secret.isEmpty()) {
59 _tokenHash[TOKEN_CONSUMER_SEC_KEY] = consumer_secret;
60 }
53 }61 }
5462
55 Token::Token(QString token_key, QString token_secret,63 Token::Token(QString token_key, QString token_secret,
@@ -93,9 +101,19 @@
93 }101 }
94102
95 /**103 /**
104 * \fn QString Token::name()
105 *
106 * Returns the token name, or empty string if not set.
107 */
108 QString Token::name() const
109 {
110 return _tokenHash.value(TOKEN_NAME_KEY, "");
111 }
112
113 /**
96 * \fn QString Token::consumerKey()114 * \fn QString Token::consumerKey()
97 *115 *
98 * Retruns a consumer key for this token, or empty string if consumer key is not set.116 * Returns a consumer key for this token, or empty string if consumer key is not set.
99 */117 */
100 QString Token::consumerKey() const118 QString Token::consumerKey() const
101 {119 {
@@ -103,6 +121,36 @@
103 }121 }
104122
105 /**123 /**
124 * \fn QString Token::consumerSecret()
125 *
126 * Returns a consumer secret for this token, or empty string if not set.
127 */
128 QString Token::consumerSecret() const
129 {
130 return _tokenHash.value(TOKEN_CONSUMER_SEC_KEY, "");
131 }
132
133 /**
134 * \fn QString Token::tokenKey()
135 *
136 * Returns a token key for this token, or empty string if token key is not set.
137 */
138 QString Token::tokenKey() const
139 {
140 return _tokenHash.value(TOKEN_TOKEN_KEY, "");
141 }
142
143 /**
144 * \fn QString Token::tokenSecret()
145 *
146 * Returns a token secret for this token, or empty string if not set.
147 */
148 QString Token::tokenSecret() const
149 {
150 return _tokenHash.value(TOKEN_TOKEN_SEC_KEY, "");
151 }
152
153 /**
106 * \fn bool Token::isValid()154 * \fn bool Token::isValid()
107 *155 *
108 * Check that the token is valid.156 * Check that the token is valid.
@@ -291,6 +339,7 @@
291 QStringList params = query.split("&");339 QStringList params = query.split("&");
292 for (int i = 0; i < params.size(); ++i) {340 for (int i = 0; i < params.size(); ++i) {
293 QStringList pair = params.at(i).split("=");341 QStringList pair = params.at(i).split("=");
342 if (pair.count() < 2) continue;
294 if (pair.at(0) == TOKEN_NAME_KEY) {343 if (pair.at(0) == TOKEN_NAME_KEY) {
295 // TODO: Need to figure out how to actually use the344 // TODO: Need to figure out how to actually use the
296 // QUrl::fromPercentEncoding at this point in the code.345 // QUrl::fromPercentEncoding at this point in the code.
297346
=== modified file 'libubuntuoneauth/token.h'
--- libubuntuoneauth/token.h 2015-12-07 21:17:00 +0000
+++ libubuntuoneauth/token.h 2016-04-22 09:51:32 +0000
@@ -55,7 +55,11 @@
5555
56 static QString dateStringToISO(const QString date);56 static QString dateStringToISO(const QString date);
5757
58 QString name() const;
58 QString consumerKey() const;59 QString consumerKey() const;
60 QString consumerSecret() const;
61 QString tokenKey() const;
62 QString tokenSecret() const;
5963
60 private:64 private:
61 QHash<QString, QString> _tokenHash;65 QHash<QString, QString> _tokenHash;
6266
=== modified file 'signon-plugin/CMakeLists.txt'
--- signon-plugin/CMakeLists.txt 2014-07-24 13:30:19 +0000
+++ signon-plugin/CMakeLists.txt 2016-04-22 09:51:32 +0000
@@ -1,3 +1,8 @@
1project(SignonPlugin)
2
3set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions -fno-rtti")
4set(CMAKE_MODULE_LINKER_FLAGS "-Wl,--no-undefined")
5
1# Qt5 bits6# Qt5 bits
2SET (CMAKE_INCLUDE_CURRENT_DIR ON)7SET (CMAKE_INCLUDE_CURRENT_DIR ON)
3SET (CMAKE_AUTOMOC ON)8SET (CMAKE_AUTOMOC ON)
@@ -24,13 +29,15 @@
24 -L${CMAKE_BINARY_DIR}/libubuntuoneauth29 -L${CMAKE_BINARY_DIR}/libubuntuoneauth
25 ${AUTH_LIB_NAME}30 ${AUTH_LIB_NAME}
26 ${SIGNON_PLUGIN_LDFLAGS}31 ${SIGNON_PLUGIN_LDFLAGS}
32 ${SIGNON_LDFLAGS}
33 ${SIGNON_LDFLAGS_OTHER}
27)34)
2835
29SET (SIGNON_PLUGIN_INSTALL_DIR lib/signon)36SET (SIGNON_PLUGIN_INSTALL_DIR ${CMAKE_INSTALL_LIBDIR}/signon)
3037
31INSTALL (38INSTALL (
32 TARGETS ${SIGNON_PLUGIN_NAME}39 TARGETS ${SIGNON_PLUGIN_NAME}
33 LIBRARY DESTINATION ${SIGNON_PLUGIN_INSTALL_DIR}40 LIBRARY DESTINATION ${SIGNON_PLUGIN_INSTALL_DIR}
34)41)
3542
36#add_subdirectory(tests)43add_subdirectory(tests)
3744
=== added file 'signon-plugin/i18n.cpp'
--- signon-plugin/i18n.cpp 1970-01-01 00:00:00 +0000
+++ signon-plugin/i18n.cpp 2016-04-22 09:51:32 +0000
@@ -0,0 +1,37 @@
1/*
2 * Copyright 2016 Canonical Ltd.
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of version 3 of the GNU Lesser General Public
6 * License as published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 * General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public
14 * License along with this library; if not, write to the
15 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
16 * Boston, MA 02110-1301, USA.
17 */
18
19#define NO_TR_OVERRIDE
20#include "i18n.h"
21
22#include <libintl.h>
23
24namespace UbuntuOne {
25
26void initTr(const char *domain, const char *localeDir)
27{
28 bindtextdomain(domain, localeDir);
29 textdomain(domain);
30}
31
32QString _(const char *text, const char *domain)
33{
34 return QString::fromUtf8(dgettext(domain, text));
35}
36
37} // namespace
038
=== added file 'signon-plugin/i18n.h'
--- signon-plugin/i18n.h 1970-01-01 00:00:00 +0000
+++ signon-plugin/i18n.h 2016-04-22 09:51:32 +0000
@@ -0,0 +1,31 @@
1/*
2 * Copyright 2016 Canonical Ltd.
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of version 3 of the GNU Lesser General Public
6 * License as published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 * General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public
14 * License along with this library; if not, write to the
15 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
16 * Boston, MA 02110-1301, USA.
17 */
18
19#ifndef UBUNTUONE_I18N_H
20#define UBUNTUONE_I18N_H
21
22#include <QString>
23
24namespace UbuntuOne {
25
26void initTr(const char *domain, const char *localeDir);
27QString _(const char *text, const char *domain = 0);
28
29} // namespace
30
31#endif // UBUNTUONE_I18N_H
032
=== added directory 'signon-plugin/tests'
=== added file 'signon-plugin/tests/CMakeLists.txt'
--- signon-plugin/tests/CMakeLists.txt 1970-01-01 00:00:00 +0000
+++ signon-plugin/tests/CMakeLists.txt 2016-04-22 09:51:32 +0000
@@ -0,0 +1,52 @@
1# The thing we're building in here
2SET (TESTS_TARGET test-ubuntuone-plugin)
3
4# Qt5 bits
5SET (CMAKE_INCLUDE_CURRENT_DIR ON)
6SET (CMAKE_AUTOMOC ON)
7find_package(Qt5Core REQUIRED)
8
9pkg_check_modules(SIGNON REQUIRED signon-plugins)
10add_definitions(
11 ${SIGNON_CFLAGS}
12 ${SIGNON_CFLAGS_OTHER}
13 "-DPLUGIN_PATH=\"${CMAKE_CURRENT_BINARY_DIR}/../libubuntuoneplugin.so\""
14)
15
16# Workaround for cmake not adding these to automoc properly
17SET (CMAKE_AUTOMOC_MOC_OPTIONS "${SIGNON_CFLAGS} ${CMAKE_AUTOMOC_MOC_OPTIONS}")
18
19# The sources for building the tests
20SET (SOURCES
21 ${SignonPlugin_SOURCE_DIR}/i18n.cpp
22 ${SignonPlugin_SOURCE_DIR}/ubuntuone-plugin.cpp
23 tst_plugin.cpp
24)
25
26include_directories(
27 ${SignonPlugin_SOURCE_DIR}
28)
29
30add_executable (${TESTS_TARGET} ${SOURCES})
31qt5_use_modules (${TESTS_TARGET} Test Network)
32target_link_libraries (${TESTS_TARGET}
33 -Wl,-rpath,${CMAKE_BINARY_DIR}/libubuntuoneauth
34 -L${CMAKE_BINARY_DIR}/libubuntuoneauth
35 ${AUTH_LIB_NAME}
36 ${SIGNON_LDFLAGS}
37)
38
39add_custom_target(ubuntuone-plugin-tests
40 COMMAND ${CMAKE_CURRENT_BINARY_DIR}/${TESTS_TARGET}
41 DEPENDS ${TESTS_TARGET}
42)
43
44add_custom_target(ubuntuone-plugin-tests-valgrind
45 COMMAND valgrind --tool=memcheck ${CMAKE_CURRENT_BINARY_DIR}/${TESTS_TARGET}
46 DEPENDS ${TESTS_TARGET}
47)
48
49add_custom_target(ubuntuone-plugin-tests-valgrind-leaks
50 COMMAND valgrind --tool=memcheck --track-origins=yes --num-callers=40 --leak-resolution=high --leak-check=full ${CMAKE_CURRENT_BINARY_DIR}/${TESTS_TARGET}
51 DEPENDS ${TESTS_TARGET}
52)
053
=== added file 'signon-plugin/tests/tst_plugin.cpp'
--- signon-plugin/tests/tst_plugin.cpp 1970-01-01 00:00:00 +0000
+++ signon-plugin/tests/tst_plugin.cpp 2016-04-22 09:51:32 +0000
@@ -0,0 +1,1840 @@
1/*
2 * Copyright 2016 Canonical Ltd.
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of version 3 of the GNU Lesser General Public
6 * License as published by the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 * General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public
14 * License along with this library; if not, write to the
15 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
16 * Boston, MA 02110-1301, USA.
17 */
18
19#include <QJsonDocument>
20#include <QJsonObject>
21#include <QNetworkAccessManager>
22#include <QNetworkReply>
23#include <QPointer>
24#include <QRegExp>
25#include <QSignalSpy>
26#include <QTimer>
27#include <QtTest/QtTest>
28
29#include <SignOn/uisessiondata_priv.h>
30
31#include "ubuntuone-plugin.h"
32
33using namespace SignOn;
34
35namespace QTest {
36template<>
37char *toString(const QVariantMap &map)
38{
39 QJsonDocument doc(QJsonObject::fromVariantMap(map));
40 return qstrdup(doc.toJson(QJsonDocument::Compact).data());
41}
42} // QTest namespace
43
44class TestNetworkReply: public QNetworkReply
45{
46 Q_OBJECT
47
48public:
49 TestNetworkReply(QObject *parent = 0):
50 QNetworkReply(parent),
51 m_offset(0)
52 {}
53
54 void setError(NetworkError errorCode, const QString &errorString,
55 int delay = -1) {
56 QNetworkReply::setError(errorCode, errorString);
57 if (delay > 0) {
58 QTimer::singleShot(delay, this, SLOT(fail()));
59 }
60 }
61
62 void setRawHeader(const QByteArray &headerName, const QByteArray &value) {
63 QNetworkReply::setRawHeader(headerName, value);
64 }
65
66 void setContentType(const QString &contentType) {
67 setRawHeader("Content-Type", contentType.toUtf8());
68 }
69
70 void setStatusCode(int statusCode) {
71 setAttribute(QNetworkRequest::HttpStatusCodeAttribute, statusCode);
72 }
73
74 void setContent(const QByteArray &content) {
75 m_content = content;
76 m_offset = 0;
77
78 open(ReadOnly | Unbuffered);
79 setHeader(QNetworkRequest::ContentLengthHeader, QVariant(content.size()));
80 }
81
82 void start() {
83 QTimer::singleShot(0, this, SIGNAL(readyRead()));
84 QTimer::singleShot(10, this, SLOT(finish()));
85 }
86
87public Q_SLOTS:
88 void finish() { setFinished(true); Q_EMIT finished(); }
89 void fail() { Q_EMIT error(error()); }
90
91protected:
92 void abort() Q_DECL_OVERRIDE {}
93 qint64 bytesAvailable() const Q_DECL_OVERRIDE {
94 return m_content.size() - m_offset + QIODevice::bytesAvailable();
95 }
96
97 bool isSequential() const Q_DECL_OVERRIDE { return true; }
98 qint64 readData(char *data, qint64 maxSize) Q_DECL_OVERRIDE {
99 if (m_offset >= m_content.size())
100 return -1;
101 qint64 number = qMin(maxSize, m_content.size() - m_offset);
102 memcpy(data, m_content.constData() + m_offset, number);
103 m_offset += number;
104 return number;
105 }
106
107private:
108 QByteArray m_content;
109 qint64 m_offset;
110};
111
112class TestNetworkAccessManager: public QNetworkAccessManager
113{
114 Q_OBJECT
115
116public:
117 TestNetworkAccessManager(): QNetworkAccessManager() {}
118
119 void setNextReply(TestNetworkReply *reply) { m_nextReply = reply; }
120
121protected:
122 QNetworkReply *createRequest(Operation op, const QNetworkRequest &request,
123 QIODevice *outgoingData = 0) Q_DECL_OVERRIDE {
124 Q_UNUSED(op);
125 m_lastRequest = request;
126 m_lastRequestData = outgoingData->readAll();
127 m_nextReply->start();
128 return m_nextReply;
129 }
130
131public:
132 QPointer<TestNetworkReply> m_nextReply;
133 QNetworkRequest m_lastRequest;
134 QByteArray m_lastRequestData;
135};
136
137class PluginTest: public QObject
138{
139 Q_OBJECT
140
141private Q_SLOTS:
142 void initTestCase();
143 void cleanupTestCase();
144
145 void testLoading();
146 void testInitialization();
147 void testPluginType();
148 void testPluginMechanisms();
149 void testStoredToken_data();
150 void testStoredToken();
151 void testUserInteraction();
152 void testTokenCreation_data();
153 void testTokenCreation();
154
155 void init();
156 void cleanup();
157
158private:
159 UbuntuOne::SignOnPlugin *m_testPlugin;
160};
161
162void PluginTest::initTestCase()
163{
164 qRegisterMetaType<SignOn::SessionData>();
165 qRegisterMetaType<SignOn::UiSessionData>();
166 qRegisterMetaType<SignOn::Error>();
167}
168
169void PluginTest::cleanupTestCase()
170{
171}
172
173// prepare each test by creating new plugin
174void PluginTest::init()
175{
176 m_testPlugin = new UbuntuOne::SignOnPlugin();
177}
178
179// finish each test by deleting plugin
180void PluginTest::cleanup()
181{
182 delete m_testPlugin;
183 m_testPlugin = 0;
184}
185
186void PluginTest::testLoading()
187{
188 QLibrary module(PLUGIN_PATH);
189 if (!module.load()) {
190 qDebug() << "Failed to load module:" << module.errorString();
191 }
192
193 QVERIFY(module.isLoaded());
194 typedef AuthPluginInterface *(*AuthPluginInstanceF)();
195 auto instance =
196 (AuthPluginInstanceF)module.resolve("auth_plugin_instance");
197 QVERIFY(instance);
198
199 auto plugin = qobject_cast<AuthPluginInterface *>(instance());
200 QVERIFY(plugin);
201}
202
203void PluginTest::testInitialization()
204{
205 QVERIFY(m_testPlugin);
206}
207
208void PluginTest::testPluginType()
209{
210 QCOMPARE(m_testPlugin->type(), QString("ubuntuone"));
211}
212
213void PluginTest::testPluginMechanisms()
214{
215 QStringList mechs = m_testPlugin->mechanisms();
216 QCOMPARE(mechs.count(), 1);
217 QCOMPARE(mechs[0], QString("ubuntuone"));
218}
219
220void PluginTest::testStoredToken_data()
221{
222 QTest::addColumn<QVariantMap>("sessionData");
223 QTest::addColumn<int>("networkError");
224 QTest::addColumn<int>("httpStatus");
225 QTest::addColumn<QString>("replyContents");
226 QTest::addColumn<int>("expectedErrorCode");
227 QTest::addColumn<bool>("uiExpected");
228 QTest::addColumn<QVariantMap>("expectedResponse");
229 QTest::addColumn<QVariantMap>("expectedStore");
230
231 UbuntuOne::PluginData sessionData;
232 UbuntuOne::PluginData response;
233 UbuntuOne::PluginData stored;
234
235 QTest::newRow("empty") <<
236 sessionData.toMap() <<
237 -1 << -1 << QString() <<
238 int(Error::MissingData) <<
239 false << QVariantMap() << QVariantMap();
240
241 sessionData.setTokenName("helloworld");
242 sessionData.setSecret("consumer_key=aAa&consumer_secret=bBb&name=helloworld&token=cCc&token_secret=dDd");
243 response.setConsumerKey("aAa");
244 response.setConsumerSecret("bBb");
245 response.setTokenKey("cCc");
246 response.setTokenSecret("dDd");
247 QVariantMap storedData;
248 storedData[sessionData.TokenName()] = response.toMap();
249 stored.setStoredData(storedData);
250 response.setTokenName(sessionData.TokenName());
251 QTest::newRow("in secret, valid") <<
252 sessionData.toMap() <<
253 -1 <<
254 200 << QString("{\n"
255 " \"is_valid\": true,\n"
256 " \"identifier\": \"64we8bn\",\n"
257 " \"account_verified\": true\n"
258 "}") <<
259 -1 <<
260 false << response.toMap() << stored.toMap();
261 sessionData = UbuntuOne::PluginData();
262 response = UbuntuOne::PluginData();
263 stored = UbuntuOne::PluginData();
264 storedData.clear();
265
266 sessionData.setTokenName("helloworld");
267 sessionData.setSecret("consumer_key=aAa&consumer_secret=bBb&name=helloworld&token=cCc&token_secret=dDd");
268 response.setConsumerKey("aAa");
269 response.setConsumerSecret("bBb");
270 response.setTokenKey("cCc");
271 response.setTokenSecret("dDd");
272 storedData[sessionData.TokenName()] = response.toMap();
273 stored.setStoredData(storedData);
274 response = UbuntuOne::PluginData();
275 QTest::newRow("in secret, invalid") <<
276 sessionData.toMap() <<
277 -1 <<
278 200 << QString("{\n"
279 " \"is_valid\": false,\n"
280 " \"identifier\": \"64we8bn\",\n"
281 " \"account_verified\": true\n"
282 "}") <<
283 -1 <<
284 true << response.toMap() << stored.toMap();
285 sessionData = UbuntuOne::PluginData();
286 response = UbuntuOne::PluginData();
287 stored = UbuntuOne::PluginData();
288 storedData.clear();
289
290 sessionData.setTokenName("helloworld");
291 sessionData.setSecret("consumer_key=aAa&consumer_secret=bBb&name=helloworld&token=cCc&token_secret=dDd");
292 response.setConsumerKey("aAa");
293 response.setConsumerSecret("bBb");
294 response.setTokenKey("cCc");
295 response.setTokenSecret("dDd");
296 storedData[sessionData.TokenName()] = response.toMap();
297 stored.setStoredData(storedData);
298 response = UbuntuOne::PluginData();
299 QTest::newRow("in secret, network error") <<
300 sessionData.toMap() <<
301 int(QNetworkReply::SslHandshakeFailedError) <<
302 -1 << QString() <<
303 int(SignOn::Error::Ssl) <<
304 true << response.toMap() << stored.toMap();
305 sessionData = UbuntuOne::PluginData();
306 response = UbuntuOne::PluginData();
307 stored = UbuntuOne::PluginData();
308 storedData.clear();
309}
310
311void PluginTest::testStoredToken()
312{
313 QFETCH(QVariantMap, sessionData);
314 QFETCH(int, httpStatus);
315 QFETCH(int, networkError);
316 QFETCH(QString, replyContents);
317 QFETCH(int, expectedErrorCode);
318 QFETCH(bool, uiExpected);
319 QFETCH(QVariantMap, expectedResponse);
320 QFETCH(QVariantMap, expectedStore);
321
322 QSignalSpy result(m_testPlugin, SIGNAL(result(const SignOn::SessionData&)));
323 QSignalSpy error(m_testPlugin, SIGNAL(error(const SignOn::Error &)));
324 QSignalSpy userActionRequired(m_testPlugin,
325 SIGNAL(userActionRequired(const SignOn::UiSessionData&)));
326 QSignalSpy store(m_testPlugin, SIGNAL(store(const SignOn::SessionData&)));
327
328 /* Prepare network reply */
329 TestNetworkAccessManager *nam = new TestNetworkAccessManager;
330 m_testPlugin->m_networkAccessManager = nam;
331 TestNetworkReply *reply = new TestNetworkReply(this);
332 if (httpStatus > 0) {
333 reply->setStatusCode(httpStatus);
334 } else {
335 reply->setError(QNetworkReply::NetworkError(networkError),
336 "Network error");
337 }
338 reply->setContent(replyContents.toUtf8());
339 nam->setNextReply(reply);
340
341
342 m_testPlugin->process(sessionData, "ubuntuone");
343 if (expectedErrorCode < 0) {
344 QCOMPARE(error.count(), 0);
345 QTRY_COMPARE(userActionRequired.count(), uiExpected ? 1 : 0);
346 if (!expectedResponse.isEmpty()) {
347 QTRY_COMPARE(result.count(), 1);
348 QVariantMap resp = result.at(0).at(0).value<SessionData>().toMap();
349 QCOMPARE(resp, expectedResponse);
350 } else {
351 QCOMPARE(result.count(), 0);
352 }
353
354 if (!expectedStore.isEmpty()) {
355 QCOMPARE(store.count(), 1);
356 QVariantMap storedData =
357 store.at(0).at(0).value<SessionData>().toMap();
358 QCOMPARE(storedData, expectedStore);
359 } else {
360 QCOMPARE(store.count(), 0);
361 }
362 } else {
363 QTRY_COMPARE(error.count(), 1);
364 Error err = error.at(0).at(0).value<Error>();
365 QCOMPARE(err.type(), expectedErrorCode);
366 }
367}
368
369void PluginTest::testUserInteraction()
370{
371 QSignalSpy result(m_testPlugin, SIGNAL(result(const SignOn::SessionData&)));
372 QSignalSpy error(m_testPlugin, SIGNAL(error(const SignOn::Error &)));
373 QSignalSpy userActionRequired(m_testPlugin,
374 SIGNAL(userActionRequired(const SignOn::UiSessionData&)));
375 QSignalSpy store(m_testPlugin, SIGNAL(store(const SignOn::SessionData&)));
376
377 TestNetworkAccessManager *nam = new TestNetworkAccessManager;
378 m_testPlugin->m_networkAccessManager = nam;
379
380 UbuntuOne::PluginData sessionData;
381 sessionData.setTokenName("helloworld");
382 sessionData.setUserName("tom@example.com");
383 m_testPlugin->process(sessionData, "ubuntuone");
384
385 QTRY_COMPARE(userActionRequired.count(), 1);
386 QVariantMap data =
387 userActionRequired.at(0).at(0).value<UiSessionData>().toMap();
388 QVariantMap expectedUserInteraction;
389 expectedUserInteraction[SSOUI_KEY_QUERYUSERNAME] = true;
390 expectedUserInteraction[SSOUI_KEY_USERNAME] = "tom@example.com";
391 expectedUserInteraction[SSOUI_KEY_QUERYPASSWORD] = true;
392 QCOMPARE(data, expectedUserInteraction);
393 userActionRequired.clear();
394
395 /* Prepare network reply */
396 TestNetworkReply *reply = new TestNetworkReply(this);
397 reply->setStatusCode(401);
398 reply->setContent("{\n"
399 " \"code\": \"TWOFACTOR_REQUIRED\",\n"
400 " \"message\": \"This account requires 2-factor authentication.\",\n"
401 " \"extra\": {}\n"
402 "}");
403 nam->setNextReply(reply);
404
405 QVariantMap userReply;
406 userReply[SSOUI_KEY_USERNAME] = "tom@example.com";
407 userReply[SSOUI_KEY_PASSWORD] = "s3cr3t";
408 m_testPlugin->userActionFinished(userReply);
409
410 /* Again the plugin should request user interaction, as OTP is required */
411 QTRY_COMPARE(userActionRequired.count(), 1);
412 data = userActionRequired.at(0).at(0).value<UiSessionData>().toMap();
413 expectedUserInteraction.clear();
414 expectedUserInteraction[SSOUI_KEY_USERNAME] = "tom@example.com";
415 expectedUserInteraction[SSOUI_KEY_PASSWORD] = "s3cr3t";
416 expectedUserInteraction[SSOUI_KEY_QUERY2FA] = true;
417 /* We want the map to contain the SSOUI_KEY_2FA_TEXT, but we don't care
418 * about the value */
419 QVERIFY(data.contains(SSOUI_KEY_2FA_TEXT));
420 data.remove(SSOUI_KEY_2FA_TEXT);
421 QCOMPARE(data, expectedUserInteraction);
422}
423
424void PluginTest::testTokenCreation_data()
425{
426 QTest::addColumn<QVariantMap>("sessionData");
427 QTest::addColumn<int>("networkError");
428 QTest::addColumn<int>("httpStatus");
429 QTest::addColumn<QString>("replyContents");
430 QTest::addColumn<int>("expectedErrorCode");
431 QTest::addColumn<QVariantMap>("expectedResponse");
432 QTest::addColumn<QVariantMap>("expectedStore");
433 QTest::addColumn<QVariantMap>("expectedUserInteraction");
434
435 UbuntuOne::PluginData sessionData;
436 UbuntuOne::PluginData response;
437 UbuntuOne::PluginData stored;
438 QVariantMap userInteraction;
439
440 // Successful creation, with password only
441 sessionData.setTokenName("helloworld");
442 sessionData.setUserName("jim@example.com");
443 sessionData.setSecret("s3cr3t");
444 response.setConsumerKey("aAa");
445 response.setConsumerSecret("bBb");
446 response.setTokenKey("cCc");
447 response.setTokenSecret("dDd");
448 QVariantMap storedData;
449 storedData[sessionData.TokenName()] = response.toMap();
450 stored.setStoredData(storedData);
451 response.setTokenName(sessionData.TokenName());
452 QTest::newRow("no OTP needed, 201") <<
453 sessionData.toMap() <<
454 -1 <<
455 201 << QString("{\n"
456 " \"href\": \"https://login.ubuntu.com/api/v2/tokens/oauth/the-key\",\n"
457 " \"token_key\": \"cCc\",\n"
458 " \"token_secret\": \"dDd\",\n"
459 " \"token_name\": \"helloworld\",\n"
460 " \"consumer_key\": \"aAa\",\n"
461 " \"consumer_secret\": \"bBb\",\n"
462 " \"date_created\": \"2013-01-11 12:43:23\",\n"
463 " \"date_updated\": \"2013-01-11 12:43:23\"\n"
464 "}") <<
465 -1 <<
466 response.toMap() << stored.toMap() << userInteraction;
467 sessionData = UbuntuOne::PluginData();
468 response = UbuntuOne::PluginData();
469 stored = UbuntuOne::PluginData();
470 storedData.clear();
471
472 // Wrong password
473 sessionData.setTokenName("helloworld");
474 sessionData.setUserName("jim@example.com");
475 sessionData.setSecret("s3cr3t");
476 userInteraction[SSOUI_KEY_QUERYUSERNAME] = true;
477 userInteraction[SSOUI_KEY_USERNAME] = "jim@example.com";
478 userInteraction[SSOUI_KEY_QUERYPASSWORD] = true;
479 QTest::newRow("wrong password") <<
480 sessionData.toMap() <<
481 -1 <<
482 401 << QString("{\n"
483 " \"code\": \"INVALID_CREDENTIALS\",\n"
484 " \"message\": \"Wrong password!\",\n"
485 " \"extra\": {}\n"
486 "}") <<
487 -1 <<
488 response.toMap() << stored.toMap() << userInteraction;
489 sessionData = UbuntuOne::PluginData();
490 userInteraction.clear();
491
492 // Network error while creating token
493 sessionData.setTokenName("helloworld");
494 sessionData.setUserName("jim@example.com");
495 sessionData.setSecret("s3cr3t");
496 QTest::newRow("network error") <<
497 sessionData.toMap() <<
498 int(QNetworkReply::SslHandshakeFailedError) <<
499 -1 << QString() <<
500 int(SignOn::Error::Ssl) <<
501 response.toMap() << stored.toMap() << userInteraction;
502 sessionData = UbuntuOne::PluginData();
503
504 // Account needs reset
505 sessionData.setTokenName("helloworld");
506 sessionData.setUserName("jim@example.com");
507 sessionData.setSecret("s3cr3t");
508 userInteraction[SSOUI_KEY_OPENURL] = "http://www.example.com/reset";
509 QTest::newRow("reset needed") <<
510 sessionData.toMap() <<
511 -1 <<
512 403 << QString("{\n"
513 " \"code\": \"PASSWORD_POLICY_ERROR\",\n"
514 " \"message\": \"Password too short\",\n"
515 " \"extra\": {\n"
516 " \"location\": \"http://www.example.com/reset\"\n"
517 " }\n"
518 "}") <<
519 -1 <<
520 response.toMap() << stored.toMap() << userInteraction;
521 sessionData = UbuntuOne::PluginData();
522 userInteraction.clear();
523}
524
525void PluginTest::testTokenCreation()
526{
527 QFETCH(QVariantMap, sessionData);
528 QFETCH(int, httpStatus);
529 QFETCH(int, networkError);
530 QFETCH(QString, replyContents);
531 QFETCH(int, expectedErrorCode);
532 QFETCH(QVariantMap, expectedResponse);
533 QFETCH(QVariantMap, expectedStore);
534 QFETCH(QVariantMap, expectedUserInteraction);
535
536 QSignalSpy result(m_testPlugin, SIGNAL(result(const SignOn::SessionData&)));
537 QSignalSpy error(m_testPlugin, SIGNAL(error(const SignOn::Error &)));
538 QSignalSpy userActionRequired(m_testPlugin,
539 SIGNAL(userActionRequired(const SignOn::UiSessionData&)));
540 QSignalSpy store(m_testPlugin, SIGNAL(store(const SignOn::SessionData&)));
541
542 /* Prepare network reply */
543 TestNetworkAccessManager *nam = new TestNetworkAccessManager;
544 m_testPlugin->m_networkAccessManager = nam;
545 TestNetworkReply *reply = new TestNetworkReply(this);
546 if (httpStatus > 0) {
547 reply->setStatusCode(httpStatus);
548 } else {
549 reply->setError(QNetworkReply::NetworkError(networkError),
550 "Network error");
551 }
552 reply->setContent(replyContents.toUtf8());
553 nam->setNextReply(reply);
554
555
556 m_testPlugin->process(sessionData, "ubuntuone");
557 if (expectedErrorCode < 0) {
558 if (!expectedUserInteraction.isEmpty()) {
559 QTRY_COMPARE(userActionRequired.count(), 1);
560 QVariantMap data =
561 userActionRequired.at(0).at(0).value<UiSessionData>().toMap();
562 QCOMPARE(data, expectedUserInteraction);
563 } else {
564 QCOMPARE(userActionRequired.count(), 0);
565 }
566
567 if (!expectedResponse.isEmpty()) {
568 QTRY_COMPARE(result.count(), 1);
569 QVariantMap resp = result.at(0).at(0).value<SessionData>().toMap();
570 QCOMPARE(resp, expectedResponse);
571 } else {
572 QCOMPARE(result.count(), 0);
573 }
574
575 if (!expectedStore.isEmpty()) {
576 QCOMPARE(store.count(), 1);
577 QVariantMap storedData =
578 store.at(0).at(0).value<SessionData>().toMap();
579 QCOMPARE(storedData, expectedStore);
580 } else {
581 QCOMPARE(store.count(), 0);
582 }
583
584 QCOMPARE(error.count(), 0);
585 } else {
586 QTRY_COMPARE(error.count(), 1);
587 Error err = error.at(0).at(0).value<Error>();
588 QCOMPARE(err.type(), expectedErrorCode);
589 }
590}
591
592#if 0
593void PluginTest::testPluginHmacSha1Process_data()
594{
595 QTest::addColumn<QString>("mechanism");
596 QTest::addColumn<QVariantMap>("sessionData");
597 QTest::addColumn<int>("replyStatusCode");
598 QTest::addColumn<QString>("replyContentType");
599 QTest::addColumn<QString>("replyContents");
600 QTest::addColumn<int>("errorCode");
601 QTest::addColumn<bool>("uiExpected");
602 QTest::addColumn<QVariantMap>("response");
603 QTest::addColumn<QVariantMap>("stored");
604
605 OAuth1PluginData hmacSha1Data;
606 hmacSha1Data.setRequestEndpoint("https://localhost/oauth/request_token");
607 hmacSha1Data.setTokenEndpoint("https://localhost/oauth/access_token");
608 hmacSha1Data.setAuthorizationEndpoint("https://localhost/oauth/authorize");
609 hmacSha1Data.setCallback("https://localhost/connect/login_success.html");
610 hmacSha1Data.setConsumerKey("104660106251471");
611 hmacSha1Data.setConsumerSecret("fa28f40b5a1f8c1d5628963d880636fbkjkjkj");
612 hmacSha1Data.setRealm("MyHost");
613
614 QTest::newRow("invalid mechanism") <<
615 "ANONYMOUS" <<
616 hmacSha1Data.toMap() <<
617 int(200) << "" << "" <<
618 int(Error::MechanismNotAvailable) <<
619 false << QVariantMap() << QVariantMap();
620
621 // Try without params
622 hmacSha1Data.setAuthorizationEndpoint(QString());
623 QTest::newRow("without params, HMAC-SHA1") <<
624 "HMAC-SHA1" <<
625 hmacSha1Data.toMap() <<
626 int(200) << "" << "" <<
627 int(Error::MissingData) <<
628 false << QVariantMap() << QVariantMap();
629
630 // Check for signon UI request for HMAC-SHA1
631 hmacSha1Data.setAuthorizationEndpoint("https://localhost/oauth/authorize");
632 QTest::newRow("ui-request, HMAC-SHA1") <<
633 "HMAC-SHA1" <<
634 hmacSha1Data.toMap() <<
635 int(200) << "text/plain" <<
636 "oauth_token=HiThere&oauth_token_secret=BigSecret" <<
637 -1 <<
638 true << QVariantMap() << QVariantMap();
639
640 QTest::newRow("ui-request, PLAINTEXT") <<
641 "PLAINTEXT" <<
642 hmacSha1Data.toMap() <<
643 int(200) << "text/plain" <<
644 "oauth_token=HiThere&oauth_token_secret=BigSecret" <<
645 -1 <<
646 true << QVariantMap() << QVariantMap();
647
648 /* Now store some tokens and test the responses */
649 hmacSha1Data.m_data.insert("UiPolicy", NoUserInteractionPolicy);
650 QVariantMap tokens; // ConsumerKey to Token map
651 QVariantMap token;
652 token.insert("oauth_token", QLatin1String("hmactokenfromtest"));
653 token.insert("oauth_token_secret", QLatin1String("hmacsecretfromtest"));
654 token.insert("timestamp", QDateTime::currentDateTime().toTime_t());
655 token.insert("Expiry", (uint)50000);
656 tokens.insert(QLatin1String("invalidid"), QVariant::fromValue(token));
657 hmacSha1Data.m_data.insert(QLatin1String("Tokens"), tokens);
658
659 // Try without cached token for our ConsumerKey
660 QTest::newRow("cached tokens, no ConsumerKey") <<
661 "HMAC-SHA1" <<
662 hmacSha1Data.toMap() <<
663 int(200) << "text/plain" <<
664 "oauth_token=HiThere&oauth_token_secret=BigSecret" <<
665 -1 <<
666 true << QVariantMap() << QVariantMap();
667
668 // Ensure that the cached token is returned as required
669 tokens.insert(hmacSha1Data.ConsumerKey(), QVariant::fromValue(token));
670 hmacSha1Data.m_data.insert(QLatin1String("Tokens"), tokens);
671 QVariantMap response;
672 response.insert("AccessToken", QLatin1String("hmactokenfromtest"));
673 QTest::newRow("cached tokens, with ConsumerKey") <<
674 "HMAC-SHA1" <<
675 hmacSha1Data.toMap() <<
676 int(200) << "" << "" <<
677 -1 <<
678 false << response << QVariantMap();
679
680 hmacSha1Data.m_data.insert("UiPolicy", RequestPasswordPolicy);
681 QTest::newRow("cached tokens, request password policy") <<
682 "HMAC-SHA1" <<
683 hmacSha1Data.toMap() <<
684 int(200) << "text/plain" <<
685 "oauth_token=HiThere&oauth_token_secret=BigSecret" <<
686 -1 <<
687 true << QVariantMap() << QVariantMap();
688 hmacSha1Data.m_data.remove("UiPolicy");
689
690 hmacSha1Data.setForceTokenRefresh(true);
691 QTest::newRow("cached tokens, force refresh") <<
692 "HMAC-SHA1" <<
693 hmacSha1Data.toMap() <<
694 int(200) << "text/plain" <<
695 "oauth_token=HiThere&oauth_token_secret=BigSecret" <<
696 -1 <<
697 true << QVariantMap() << QVariantMap();
698 hmacSha1Data.setForceTokenRefresh(false);
699
700 token.insert("timestamp", QDateTime::currentDateTime().toTime_t() - 50000);
701 token.insert("Expiry", (uint)100);
702 tokens.insert(hmacSha1Data.ConsumerKey(), QVariant::fromValue(token));
703 hmacSha1Data.m_data.insert(QLatin1String("Tokens"), tokens);
704 QTest::newRow("cached tokens, expired") <<
705 "HMAC-SHA1" <<
706 hmacSha1Data.toMap() <<
707 int(200) << "text/plain" <<
708 "oauth_token=HiThere&oauth_token_secret=BigSecret" <<
709 -1 <<
710 true << QVariantMap() << QVariantMap();
711
712 /* test the ProvidedTokens semantics */
713 OAuth1PluginData providedTokensHmacSha1Data;
714 providedTokensHmacSha1Data.setRequestEndpoint("https://localhost/oauth/request_token");
715 providedTokensHmacSha1Data.setTokenEndpoint("https://localhost/oauth/access_token");
716 providedTokensHmacSha1Data.setAuthorizationEndpoint("https://localhost/oauth/authorize");
717 providedTokensHmacSha1Data.setCallback("https://localhost/connect/login_success.html");
718 providedTokensHmacSha1Data.setConsumerKey("104660106251471");
719 providedTokensHmacSha1Data.setConsumerSecret("fa28f40b5a1f8c1d5628963d880636fbkjkjkj");
720 QVariantMap providedTokens;
721 providedTokens.insert("AccessToken", "providedhmactokenfromtest");
722 providedTokens.insert("TokenSecret", "providedhmacsecretfromtest");
723 providedTokens.insert("ScreenName", "providedhmacscreennamefromtest");
724 providedTokens.insert("UserId", "providedUserId");
725
726 // try providing tokens to be stored
727 providedTokensHmacSha1Data.m_data.insert("ProvidedTokens", providedTokens);
728 QVariantMap storedTokensForKey;
729 storedTokensForKey.insert("oauth_token", providedTokens.value("AccessToken"));
730 storedTokensForKey.insert("oauth_token_secret", providedTokens.value("TokenSecret"));
731 QVariantMap storedTokens;
732 storedTokens.insert(providedTokensHmacSha1Data.ConsumerKey(), storedTokensForKey);
733 QVariantMap stored;
734 stored.insert("Tokens", storedTokens);
735 QTest::newRow("provided tokens") <<
736 "HMAC-SHA1" <<
737 providedTokensHmacSha1Data.toMap() <<
738 int(200) << "" << "" <<
739 -1 <<
740 false << providedTokens << stored;
741
742 QTest::newRow("http error 401") <<
743 "HMAC-SHA1" <<
744 hmacSha1Data.toMap() <<
745 int(401) << "text/plain" <<
746 "oauth_token=HiThere&oauth_token_secret=BigSecret" <<
747 int(Error::OperationFailed) <<
748 false << QVariantMap() << QVariantMap();
749
750 QTest::newRow("no content returned") <<
751 "HMAC-SHA1" <<
752 hmacSha1Data.toMap() <<
753 int(200) << "" << "" <<
754 int(Error::OperationFailed) <<
755 false << QVariantMap() << QVariantMap();
756
757 QTest::newRow("no token returned") <<
758 "HMAC-SHA1" <<
759 hmacSha1Data.toMap() <<
760 int(200) << "text/plain" <<
761 "oauth_token=HiThere" <<
762 int(Error::OperationFailed) <<
763 false << QVariantMap() << QVariantMap();
764
765 /* Test handling of oauth_problem; this is a non standard extension:
766 * http://wiki.oauth.net/w/page/12238543/ProblemReporting
767 * https://developer.yahoo.com/oauth/guide/oauth-errors.html
768 */
769 QTest::newRow("problem user_refused") <<
770 "HMAC-SHA1" <<
771 hmacSha1Data.toMap() <<
772 int(400) << "text/plain" <<
773 "oauth_problem=user_refused" <<
774 int(Error::PermissionDenied) <<
775 false << QVariantMap() << QVariantMap();
776 QTest::newRow("problem permission_denied") <<
777 "HMAC-SHA1" <<
778 hmacSha1Data.toMap() <<
779 int(400) << "text/plain" <<
780 "oauth_problem=permission_denied" <<
781 int(Error::PermissionDenied) <<
782 false << QVariantMap() << QVariantMap();
783 QTest::newRow("problem signature_invalid") <<
784 "HMAC-SHA1" <<
785 hmacSha1Data.toMap() <<
786 int(400) << "text/plain" <<
787 "oauth_problem=signature_invalid" <<
788 int(Error::OperationFailed) <<
789 false << QVariantMap() << QVariantMap();
790}
791
792void PluginTest::testPluginHmacSha1Process()
793{
794 QFETCH(QString, mechanism);
795 QFETCH(QVariantMap, sessionData);
796 QFETCH(int, replyStatusCode);
797 QFETCH(QString, replyContentType);
798 QFETCH(QString, replyContents);
799 QFETCH(int, errorCode);
800 QFETCH(bool, uiExpected);
801 QFETCH(QVariantMap, response);
802 QFETCH(QVariantMap, stored);
803
804 QSignalSpy result(m_testPlugin, SIGNAL(result(const SignOn::SessionData&)));
805 QSignalSpy error(m_testPlugin, SIGNAL(error(const SignOn::Error &)));
806 QSignalSpy userActionRequired(m_testPlugin,
807 SIGNAL(userActionRequired(const SignOn::UiSessionData&)));
808 QSignalSpy store(m_testPlugin, SIGNAL(store(const SignOn::SessionData&)));
809
810 TestNetworkAccessManager *nam = new TestNetworkAccessManager;
811 m_testPlugin->m_networkAccessManager = nam;
812 TestNetworkReply *reply = new TestNetworkReply(this);
813 reply->setStatusCode(replyStatusCode);
814 if (!replyContentType.isEmpty()) {
815 reply->setContentType(replyContentType);
816 }
817 reply->setContent(replyContents.toUtf8());
818 nam->setNextReply(reply);
819
820 m_testPlugin->process(sessionData, mechanism);
821
822 QTRY_COMPARE(result.count(), response.isEmpty() ? 0 : 1);
823 /* In the test data sometimes we don't specify the expected stored data,
824 * but this doesn't mean that store() shouldn't be emitted. */
825 if (!stored.isEmpty()) { QTRY_COMPARE(store.count(), 1); }
826 QTRY_COMPARE(userActionRequired.count(), uiExpected ? 1 : 0);
827 QTRY_COMPARE(error.count(), errorCode < 0 ? 0 : 1);
828
829 if (errorCode < 0) {
830 QCOMPARE(error.count(), 0);
831
832 QVariantMap resp = result.count() > 0 ?
833 result.at(0).at(0).value<SessionData>().toMap() : QVariantMap();
834 QVariantMap storedData = store.count() > 0 ?
835 store.at(0).at(0).value<SessionData>().toMap() : QVariantMap();
836 /* We don't check the network request if a response was received,
837 * because a response can only be received if a cached token was
838 * found -- and that doesn't cause any network request to be made. */
839 if (resp.isEmpty()) {
840 QCOMPARE(nam->m_lastRequest.url(),
841 sessionData.value("RequestEndpoint").toUrl());
842 QVERIFY(nam->m_lastRequestData.isEmpty());
843
844 /* Check the authorization header */
845 QString authorizationHeader =
846 QString::fromUtf8(nam->m_lastRequest.rawHeader("Authorization"));
847 QStringList authorizationHeaderParts =
848 authorizationHeader.split(QRegExp(",?\\s+"));
849 QCOMPARE(authorizationHeaderParts[0], QString("OAuth"));
850
851 /* The rest of the header should be a mapping, let's parse it */
852 bool ok = true;
853 QVariantMap authMap =
854 parseAuthorizationHeader(authorizationHeaderParts.mid(1), &ok);
855 QVERIFY(ok);
856 QCOMPARE(authMap.value("oauth_signature_method").toString(), mechanism);
857 }
858
859 QVERIFY(mapIsSubset(response, resp));
860 QVERIFY(mapIsSubset(stored, storedData));
861 } else {
862 Error err = error.at(0).at(0).value<Error>();
863 QCOMPARE(err.type(), errorCode);
864 }
865}
866
867void PluginTest::testPluginUseragentUserActionFinished()
868{
869 SignOn::UiSessionData info;
870 PluginData data;
871 data.setHost("https://localhost");
872 data.setAuthPath("authorize");
873 data.setTokenPath("access_token");
874 data.setClientId("104660106251471");
875 data.setClientSecret("fa28f40b5a1f8c1d5628963d880636fbkjkjkj");
876 data.setRedirectUri("http://localhost/connect/login_success.html");
877 QStringList scopes = QStringList() << "scope1" << "scope2";
878 data.setScope(scopes);
879
880 QSignalSpy resultSpy(m_testPlugin, SIGNAL(result(const SignOn::SessionData&)));
881 QSignalSpy error(m_testPlugin, SIGNAL(error(const SignOn::Error &)));
882 QSignalSpy userActionRequired(m_testPlugin,
883 SIGNAL(userActionRequired(const SignOn::UiSessionData&)));
884 QSignalSpy store(m_testPlugin, SIGNAL(store(const SignOn::SessionData&)));
885
886 m_testPlugin->process(data, QString("user_agent"));
887
888 QTRY_COMPARE(userActionRequired.count(), 1);
889 QString state = parseState(userActionRequired);
890
891 //empty data
892 m_testPlugin->userActionFinished(info);
893 QTRY_COMPARE(error.count(), 1);
894 QCOMPARE(error.at(0).at(0).value<Error>().type(), int(Error::NotAuthorized));
895 error.clear();
896
897 //invalid data
898 info.setUrlResponse(QString("http://www.facebook.com/connect/login_success.html#access_token=&expires_in=4776"));
899 m_testPlugin->userActionFinished(info);
900 QTRY_COMPARE(error.count(), 1);
901 QCOMPARE(error.at(0).at(0).value<Error>().type(), int(Error::NotAuthorized));
902 error.clear();
903
904 //Invalid data
905 info.setUrlResponse(QString("http://www.facebook.com/connect/login_success.html"));
906 m_testPlugin->userActionFinished(info);
907 QTRY_COMPARE(error.count(), 1);
908 QCOMPARE(error.at(0).at(0).value<Error>().type(), int(Error::NotAuthorized));
909 error.clear();
910
911 // Wrong state
912 info.setUrlResponse(QString("http://www.facebook.com/connect/login_success.html"
913 "#access_token=123&expires_in=456&state=%1").
914 arg(state + "Boo"));
915 m_testPlugin->userActionFinished(info);
916 QTRY_COMPARE(error.count(), 1);
917 QCOMPARE(error.at(0).at(0).value<Error>().type(), int(Error::NotAuthorized));
918 error.clear();
919
920 //valid data
921 info.setUrlResponse(QString("http://www.facebook.com/connect/login_success.html#access_token=testtoken.&expires_in=4776&state=%1").
922 arg(state));
923 m_testPlugin->userActionFinished(info);
924 QTRY_COMPARE(resultSpy.count(), 1);
925 SessionData response = resultSpy.at(0).at(0).value<SessionData>();
926 PluginTokenData result = response.data<PluginTokenData>();
927 QCOMPARE(result.AccessToken(), QString("testtoken."));
928 QCOMPARE(result.ExpiresIn(), 4776);
929 QCOMPARE(result.Scope(), QStringList() << "scope1" << "scope2");
930 resultSpy.clear();
931 QTRY_COMPARE(store.count(), 1);
932 SessionData storedData = store.at(0).at(0).value<SessionData>();
933 QVariantMap storedTokenData = storedData.data<TokenData>().Tokens();
934 QVariantMap storedClientData =
935 storedTokenData.value(data.ClientId()).toMap();
936 QVERIFY(!storedClientData.isEmpty());
937 QCOMPARE(storedClientData["Scopes"].toStringList(), scopes);
938 store.clear();
939
940 //valid data, got scopes
941 info.setUrlResponse(QString("http://www.facebook.com/connect/login_success.html#access_token=testtoken.&expires_in=4776&state=%1&scope=scope2").
942 arg(state));
943 m_testPlugin->userActionFinished(info);
944 QTRY_COMPARE(resultSpy.count(), 1);
945 response = resultSpy.at(0).at(0).value<SessionData>();
946 result = response.data<PluginTokenData>();
947 QCOMPARE(result.AccessToken(), QString("testtoken."));
948 QCOMPARE(result.ExpiresIn(), 4776);
949 QCOMPARE(result.Scope(), QStringList() << "scope2");
950 resultSpy.clear();
951 store.clear();
952
953 //valid data
954 info.setUrlResponse(QString("http://www.facebook.com/connect/login_success.html"
955 "#state=%1&access_token=testtoken.").
956 arg(state));
957 m_testPlugin->userActionFinished(info);
958 QTRY_COMPARE(resultSpy.count(), 1);
959 response = resultSpy.at(0).at(0).value<SessionData>();
960 result = response.data<PluginTokenData>();
961 QCOMPARE(result.AccessToken(), QString("testtoken."));
962 QCOMPARE(result.ExpiresIn(), 0);
963 resultSpy.clear();
964 /* Check that the expiration time has not been stored, since the expiration
965 * time was not given (https://bugs.launchpad.net/bugs/1316021)
966 */
967 QTRY_COMPARE(store.count(), 1);
968 storedData = store.at(0).at(0).value<SessionData>();
969 storedTokenData = storedData.data<TokenData>().Tokens();
970 storedClientData = storedTokenData.value(data.ClientId()).toMap();
971 QVERIFY(!storedClientData.isEmpty());
972 QCOMPARE(storedClientData["Token"].toString(), QString("testtoken."));
973 QVERIFY(!storedClientData.contains("Expiry"));
974 store.clear();
975
976 //Permission denied
977 info.setUrlResponse(QString("http://www.facebook.com/connect/login_success.html?error=user_denied"));
978 m_testPlugin->userActionFinished(info);
979 QTRY_COMPARE(error.count(), 1);
980 QCOMPARE(error.at(0).at(0).value<Error>().type(), int(Error::NotAuthorized));
981 error.clear();
982}
983
984void PluginTest::testPluginWebserverUserActionFinished_data()
985{
986 QTest::addColumn<QString>("urlResponse");
987 QTest::addColumn<int>("errorCode");
988 QTest::addColumn<QString>("postUrl");
989 QTest::addColumn<QString>("postContents");
990 QTest::addColumn<bool>("disableStateParameter");
991 QTest::addColumn<int>("replyStatusCode");
992 QTest::addColumn<QString>("replyContentType");
993 QTest::addColumn<QString>("replyContents");
994 QTest::addColumn<QVariantMap>("response");
995
996 QVariantMap response;
997
998 QTest::newRow("empty data") <<
999 "" <<
1000 int(Error::NotAuthorized) <<
1001 "" << "" << false << 0 << "" << "" << QVariantMap();
1002
1003 QTest::newRow("no query data") <<
1004 "http://localhost/resp.html" <<
1005 int(Error::NotAuthorized) <<
1006 "" << "" << false << 0 << "" << "" << QVariantMap();
1007
1008 QTest::newRow("permission denied") <<
1009 "http://localhost/resp.html?error=user_denied&$state" <<
1010 int(Error::NotAuthorized) <<
1011 "" << "" << false << 0 << "" << "" << QVariantMap();
1012
1013 QTest::newRow("invalid data") <<
1014 "http://localhost/resp.html?sdsdsds=access.grant." <<
1015 int(Error::NotAuthorized) <<
1016 "" << "" << false << 0 << "" << "" << QVariantMap();
1017
1018 QTest::newRow("reply code, http error 401") <<
1019 "http://localhost/resp.html?code=c0d3&$state" <<
1020 int(Error::OperationFailed) <<
1021 "https://localhost/access_token" <<
1022 "grant_type=authorization_code&code=c0d3&redirect_uri=http://localhost/resp.html" <<
1023 false <<
1024 int(401) <<
1025 "application/json" <<
1026 "something else" <<
1027 QVariantMap();
1028
1029 QTest::newRow("reply code, empty reply") <<
1030 "http://localhost/resp.html?code=c0d3&$state" <<
1031 int(Error::NotAuthorized) <<
1032 "https://localhost/access_token" <<
1033 "grant_type=authorization_code&code=c0d3&redirect_uri=http://localhost/resp.html" <<
1034 false <<
1035 int(200) <<
1036 "application/json" <<
1037 "something else" <<
1038 QVariantMap();
1039
1040 QTest::newRow("reply code, no access token") <<
1041 "http://localhost/resp.html?code=c0d3&$state" <<
1042 int(Error::NotAuthorized) <<
1043 "https://localhost/access_token" <<
1044 "grant_type=authorization_code&code=c0d3&redirect_uri=http://localhost/resp.html" <<
1045 false <<
1046 int(200) <<
1047 "application/json" <<
1048 "{ \"expires_in\": 3600 }" <<
1049 QVariantMap();
1050
1051 QTest::newRow("reply code, no content type") <<
1052 "http://localhost/resp.html?code=c0d3&$state" <<
1053 int(Error::OperationFailed) <<
1054 "https://localhost/access_token" <<
1055 "grant_type=authorization_code&code=c0d3&redirect_uri=http://localhost/resp.html" <<
1056 false <<
1057 int(200) <<
1058 "" <<
1059 "something else" <<
1060 QVariantMap();
1061
1062 QTest::newRow("reply code, unsupported content type") <<
1063 "http://localhost/resp.html?code=c0d3&$state" <<
1064 int(Error::OperationFailed) <<
1065 "https://localhost/access_token" <<
1066 "grant_type=authorization_code&code=c0d3&redirect_uri=http://localhost/resp.html" <<
1067 false <<
1068 int(200) <<
1069 "image/jpeg" <<
1070 "something else" <<
1071 QVariantMap();
1072
1073 response.clear();
1074 response.insert("AccessToken", "t0k3n");
1075 response.insert("ExpiresIn", int(3600));
1076 response.insert("RefreshToken", QString());
1077 QTest::newRow("reply code, valid token, wrong state") <<
1078 "http://localhost/resp.html?code=c0d3&$wrongstate" <<
1079 int(Error::NotAuthorized) <<
1080 "" <<
1081 "" <<
1082 false <<
1083 int(200) <<
1084 "application/json" <<
1085 "{ \"access_token\":\"t0k3n\", \"expires_in\": 3600 }" <<
1086 response;
1087
1088 response.clear();
1089 response.insert("AccessToken", "t0k3n");
1090 response.insert("ExpiresIn", int(3600));
1091 response.insert("RefreshToken", QString());
1092 response.insert("Scope", QStringList() << "one" << "two");
1093 QTest::newRow("reply code, valid token, wrong state ignored") <<
1094 "http://localhost/resp.html?code=c0d3&$wrongstate" <<
1095 int(-1) <<
1096 "https://localhost/access_token" <<
1097 "grant_type=authorization_code&code=c0d3&redirect_uri=http://localhost/resp.html" <<
1098 true <<
1099 int(200) <<
1100 "application/json" <<
1101 "{ \"access_token\":\"t0k3n\", \"expires_in\": 3600, "
1102 "\"scope\": \"one two\" }" <<
1103 response;
1104
1105 response.clear();
1106 response.insert("AccessToken", "t0k3n");
1107 response.insert("ExpiresIn", int(3600));
1108 response.insert("RefreshToken", QString());
1109 response.insert("Scope", QStringList() << "one" << "two" << "three");
1110 QTest::newRow("reply code, valid token, no scope") <<
1111 "http://localhost/resp.html?code=c0d3&$state" <<
1112 int(-1) <<
1113 "https://localhost/access_token" <<
1114 "grant_type=authorization_code&code=c0d3&redirect_uri=http://localhost/resp.html" <<
1115 false <<
1116 int(200) <<
1117 "application/json" <<
1118 "{ \"access_token\":\"t0k3n\", \"expires_in\": 3600 }" <<
1119 response;
1120
1121 response.clear();
1122 response.insert("AccessToken", "t0k3n");
1123 response.insert("ExpiresIn", int(3600));
1124 response.insert("RefreshToken", QString());
1125 response.insert("Scope", QStringList());
1126 QTest::newRow("reply code, valid token, empty scope") <<
1127 "http://localhost/resp.html?code=c0d3&$state" <<
1128 int(-1) <<
1129 "https://localhost/access_token" <<
1130 "grant_type=authorization_code&code=c0d3&redirect_uri=http://localhost/resp.html" <<
1131 false <<
1132 int(200) <<
1133 "application/json" <<
1134 "{ \"access_token\":\"t0k3n\", \"expires_in\": 3600, \"scope\": \"\" }" <<
1135 response;
1136
1137 response.clear();
1138 response.insert("AccessToken", "t0k3n");
1139 response.insert("ExpiresIn", int(3600));
1140 response.insert("RefreshToken", QString());
1141 response.insert("Scope", QStringList() << "one" << "two");
1142 QTest::newRow("reply code, valid token, other scope") <<
1143 "http://localhost/resp.html?code=c0d3&$state" <<
1144 int(-1) <<
1145 "https://localhost/access_token" <<
1146 "grant_type=authorization_code&code=c0d3&redirect_uri=http://localhost/resp.html" <<
1147 false <<
1148 int(200) <<
1149 "application/json" <<
1150 "{ \"access_token\":\"t0k3n\", \"expires_in\": 3600, "
1151 "\"scope\": \"one two\" }" <<
1152 response;
1153
1154 response.clear();
1155 QTest::newRow("reply code, facebook, no token") <<
1156 "http://localhost/resp.html?code=c0d3&$state" <<
1157 int(Error::NotAuthorized) <<
1158 "https://localhost/access_token" <<
1159 "grant_type=authorization_code&code=c0d3&redirect_uri=http://localhost/resp.html" <<
1160 false <<
1161 int(200) <<
1162 "text/plain" <<
1163 "expires=3600" <<
1164 response;
1165
1166 response.clear();
1167 response.insert("AccessToken", "t0k3n");
1168 response.insert("ExpiresIn", int(3600));
1169 response.insert("RefreshToken", QString());
1170 response.insert("Scope", QStringList() << "one" << "two" << "three");
1171 QTest::newRow("reply code, facebook, valid token") <<
1172 "http://localhost/resp.html?code=c0d3&$state" <<
1173 int(-1) <<
1174 "https://localhost/access_token" <<
1175 "grant_type=authorization_code&code=c0d3&redirect_uri=http://localhost/resp.html" <<
1176 false <<
1177 int(200) <<
1178 "text/plain" <<
1179 "access_token=t0k3n&expires=3600" <<
1180 response;
1181
1182 response.clear();
1183 response.insert("AccessToken", "t0k3n");
1184 response.insert("ExpiresIn", int(3600));
1185 response.insert("RefreshToken", QString());
1186 response.insert("Scope", QStringList() << "one" << "two" << "three");
1187 QTest::newRow("username-password, valid token") <<
1188 "http://localhost/resp.html?username=us3r&password=s3cr3t" <<
1189 int(-1) <<
1190 "https://localhost/access_token" <<
1191 "grant_type=user_basic&username=us3r&password=s3cr3t" <<
1192 false <<
1193 int(200) <<
1194 "application/json" <<
1195 "{ \"access_token\":\"t0k3n\", \"expires_in\": 3600 }" <<
1196 response;
1197
1198 response.clear();
1199 response.insert("AccessToken", "t0k3n");
1200 response.insert("ExpiresIn", int(3600));
1201 response.insert("RefreshToken", QString());
1202 response.insert("Scope", QStringList() << "one" << "two" << "three");
1203 QTest::newRow("assertion, valid token") <<
1204 "http://localhost/resp.html?assertion_type=http://oauth.net/token/1.0"
1205 "&assertion=oauth1t0k3n" <<
1206 int(-1) <<
1207 "https://localhost/access_token" <<
1208 "grant_type=assertion&assertion_type=http://oauth.net/token/1.0&assertion=oauth1t0k3n" <<
1209 false <<
1210 int(200) <<
1211 "application/json" <<
1212 "{ \"access_token\":\"t0k3n\", \"expires_in\": 3600 }" <<
1213 response;
1214
1215 response.clear();
1216 response.insert("AccessToken", "t0k3n");
1217 response.insert("ExpiresIn", int(3600));
1218 response.insert("RefreshToken", QString());
1219 response.insert("Scope", QStringList() << "one" << "two" << "three");
1220 QTest::newRow("username-password, valid token, wrong content type") <<
1221 "http://localhost/resp.html?username=us3r&password=s3cr3t" <<
1222 int(-1) <<
1223 "https://localhost/access_token" <<
1224 "grant_type=user_basic&username=us3r&password=s3cr3t" <<
1225 false <<
1226 int(200) <<
1227 "text/plain" <<
1228 "{ \"access_token\":\"t0k3n\", \"expires_in\": 3600 }" <<
1229 response;
1230}
1231
1232void PluginTest::testPluginWebserverUserActionFinished()
1233{
1234 QFETCH(QString, urlResponse);
1235 QFETCH(int, errorCode);
1236 QFETCH(QString, postUrl);
1237 QFETCH(QString, postContents);
1238 QFETCH(bool, disableStateParameter);
1239 QFETCH(int, replyStatusCode);
1240 QFETCH(QString, replyContentType);
1241 QFETCH(QString, replyContents);
1242 QFETCH(QVariantMap, response);
1243
1244 SignOn::UiSessionData info;
1245 PluginData data;
1246 data.setHost("localhost");
1247 data.setAuthPath("authorize");
1248 data.setTokenPath("access_token");
1249 data.setClientId("104660106251471");
1250 data.setClientSecret("fa28f40b5a1f8c1d5628963d880636fbkjkjkj");
1251 data.setRedirectUri("http://localhost/resp.html");
1252 data.setScope(QStringList() << "one" << "two" << "three");
1253 data.setDisableStateParameter(disableStateParameter);
1254
1255 QSignalSpy result(m_testPlugin, SIGNAL(result(const SignOn::SessionData&)));
1256 QSignalSpy error(m_testPlugin, SIGNAL(error(const SignOn::Error &)));
1257 QSignalSpy userActionRequired(m_testPlugin,
1258 SIGNAL(userActionRequired(const SignOn::UiSessionData&)));
1259
1260 TestNetworkAccessManager *nam = new TestNetworkAccessManager;
1261 m_testPlugin->m_networkAccessManager = nam;
1262 TestNetworkReply *reply = new TestNetworkReply(this);
1263 reply->setStatusCode(replyStatusCode);
1264 if (!replyContentType.isEmpty()) {
1265 reply->setContentType(replyContentType);
1266 }
1267 reply->setContent(replyContents.toUtf8());
1268 nam->setNextReply(reply);
1269
1270 m_testPlugin->process(data, QString("web_server"));
1271 QTRY_COMPARE(userActionRequired.count(), 1);
1272 QString state = parseState(userActionRequired);
1273
1274 if (!urlResponse.isEmpty()) {
1275 urlResponse.replace("$state", QString("state=") + state);
1276 urlResponse.replace("$wrongstate", QString("state=12") + state);
1277 info.setUrlResponse(urlResponse);
1278 }
1279
1280 m_testPlugin->userActionFinished(info);
1281
1282 QTRY_COMPARE(error.count(), errorCode < 0 ? 0 : 1);
1283 QTRY_COMPARE(result.count(), errorCode < 0 ? 1 : 0);
1284 if (errorCode >= 0) {
1285 QCOMPARE(error.at(0).at(0).value<Error>().type(), errorCode);
1286 } else {
1287 QCOMPARE(result.at(0).at(0).value<SessionData>().toMap(), response);
1288 }
1289 QCOMPARE(nam->m_lastRequest.url(), QUrl(postUrl));
1290 QCOMPARE(QString::fromUtf8(nam->m_lastRequestData), postContents);
1291
1292 delete nam;
1293}
1294
1295void PluginTest::testUserActionFinishedErrors_data()
1296{
1297 QTest::addColumn<int>("uiError");
1298 QTest::addColumn<int>("expectedErrorCode");
1299
1300 QTest::newRow("user canceled") <<
1301 int(QUERY_ERROR_CANCELED) <<
1302 int(Error::SessionCanceled);
1303
1304 QTest::newRow("network error") <<
1305 int(QUERY_ERROR_NETWORK) <<
1306 int(Error::Network);
1307
1308 QTest::newRow("SSL error") <<
1309 int(QUERY_ERROR_SSL) <<
1310 int(Error::Ssl);
1311
1312 QTest::newRow("generic") <<
1313 int(QUERY_ERROR_NOT_AVAILABLE) <<
1314 int(Error::UserInteraction);
1315}
1316
1317void PluginTest::testUserActionFinishedErrors()
1318{
1319 QFETCH(int, uiError);
1320 QFETCH(int, expectedErrorCode);
1321
1322 SignOn::UiSessionData info;
1323 PluginData data;
1324 data.setHost("localhost");
1325 data.setAuthPath("authorize");
1326 data.setTokenPath("access_token");
1327 data.setClientId("104660106251471");
1328 data.setClientSecret("fa28f40b5a1f8c1d5628963d880636fbkjkjkj");
1329 data.setRedirectUri("http://localhost/resp.html");
1330
1331 QSignalSpy error(m_testPlugin, SIGNAL(error(const SignOn::Error &)));
1332 QSignalSpy userActionRequired(m_testPlugin,
1333 SIGNAL(userActionRequired(const SignOn::UiSessionData&)));
1334
1335 m_testPlugin->process(data, QString("web_server"));
1336 QTRY_COMPARE(userActionRequired.count(), 1);
1337
1338 info.setQueryErrorCode(uiError);
1339 m_testPlugin->userActionFinished(info);
1340
1341 QTRY_COMPARE(error.count(), 1);
1342 QCOMPARE(error.at(0).at(0).value<Error>().type(), expectedErrorCode);
1343}
1344
1345void PluginTest::testOauth1UserActionFinished_data()
1346{
1347 QTest::addColumn<QString>("mechanism");
1348 QTest::addColumn<QString>("urlResponse");
1349 QTest::addColumn<int>("errorCode");
1350 QTest::addColumn<QVariantMap>("expectedAuthMap");
1351 QTest::addColumn<int>("replyStatusCode");
1352 QTest::addColumn<QString>("replyContentType");
1353 QTest::addColumn<QString>("replyContents");
1354 QTest::addColumn<QVariantMap>("response");
1355
1356 QTest::newRow("empty data") <<
1357 "HMAC-SHA1" <<
1358 "" <<
1359 int(Error::NotAuthorized) <<
1360 QVariantMap() << 0 << "" << "" << QVariantMap();
1361
1362 QTest::newRow("auth error") <<
1363 "HMAC-SHA1" <<
1364 "http://localhost/resp.html?error=permission_denied" <<
1365 int(Error::NotAuthorized) <<
1366 QVariantMap() << 0 << "" << "" << QVariantMap();
1367
1368 QTest::newRow("auth problem") <<
1369 "HMAC-SHA1" <<
1370 "http://localhost/resp.html?oauth_problem=permission_denied" <<
1371 int(Error::PermissionDenied) <<
1372 QVariantMap() << 0 << "" << "" << QVariantMap();
1373
1374 QVariantMap authMap;
1375 authMap.insert("oauth_verifier", QString("v3r1f13r"));
1376 authMap.insert("oauth_token", QString("HiThere"));
1377 QTest::newRow("http error 401") <<
1378 "HMAC-SHA1" <<
1379 "http://localhost/resp.html?oauth_verifier=v3r1f13r" <<
1380 int(Error::OperationFailed) <<
1381 authMap <<
1382 int(401) <<
1383 "text/plain" <<
1384 "something else" <<
1385 QVariantMap();
1386
1387 QTest::newRow("empty reply") <<
1388 "HMAC-SHA1" <<
1389 "http://localhost/resp.html?oauth_verifier=v3r1f13r" <<
1390 int(Error::OperationFailed) <<
1391 authMap <<
1392 int(200) <<
1393 "text/plain" <<
1394 "" <<
1395 QVariantMap();
1396
1397 QTest::newRow("missing secret") <<
1398 "HMAC-SHA1" <<
1399 "http://localhost/resp.html?oauth_verifier=v3r1f13r" <<
1400 int(Error::OperationFailed) <<
1401 authMap <<
1402 int(200) <<
1403 "text/plain" <<
1404 "oauth_token=t0k3n" <<
1405 QVariantMap();
1406
1407 QVariantMap response;
1408 response.insert("AccessToken", QString("t0k3n"));
1409 response.insert("TokenSecret", QString("t0k3nS3cr3t"));
1410
1411 QTest::newRow("success") <<
1412 "HMAC-SHA1" <<
1413 "http://localhost/resp.html?oauth_verifier=v3r1f13r" <<
1414 int(-1) <<
1415 authMap <<
1416 int(200) <<
1417 "text/plain" <<
1418 "oauth_token=t0k3n&oauth_token_secret=t0k3nS3cr3t" <<
1419 response;
1420
1421 response.insert("ExtraField", QString("v4lu3"));
1422 QTest::newRow("success with data") <<
1423 "HMAC-SHA1" <<
1424 "http://localhost/resp.html?oauth_verifier=v3r1f13r" <<
1425 int(-1) <<
1426 authMap <<
1427 int(200) <<
1428 "text/plain" <<
1429 "oauth_token=t0k3n&oauth_token_secret=t0k3nS3cr3t"
1430 "&ExtraField=v4lu3" <<
1431 response;
1432}
1433
1434void PluginTest::testOauth1UserActionFinished()
1435{
1436 QFETCH(QString, mechanism);
1437 QFETCH(QString, urlResponse);
1438 QFETCH(int, errorCode);
1439 QFETCH(QVariantMap, expectedAuthMap);
1440 QFETCH(int, replyStatusCode);
1441 QFETCH(QString, replyContentType);
1442 QFETCH(QString, replyContents);
1443 QFETCH(QVariantMap, response);
1444
1445 SignOn::UiSessionData info;
1446 OAuth1PluginData data;
1447 data.setRequestEndpoint("https://localhost/oauth/request_token");
1448 data.setTokenEndpoint("https://localhost/oauth/access_token");
1449 data.setAuthorizationEndpoint("https://localhost/oauth/authorize");
1450 data.setCallback("http://localhost/resp.html");
1451 data.setConsumerKey("104660106251471");
1452 data.setConsumerSecret("fa28f40b5a1f8c1d5628963d880636fbkjkjkj");
1453 data.setRealm("MyHost");
1454
1455 QSignalSpy result(m_testPlugin, SIGNAL(result(const SignOn::SessionData&)));
1456 QSignalSpy error(m_testPlugin, SIGNAL(error(const SignOn::Error &)));
1457 QSignalSpy userActionRequired(m_testPlugin,
1458 SIGNAL(userActionRequired(const SignOn::UiSessionData&)));
1459
1460 TestNetworkAccessManager *nam = new TestNetworkAccessManager;
1461 m_testPlugin->m_networkAccessManager = nam;
1462 TestNetworkReply *reply = new TestNetworkReply(this);
1463 reply->setStatusCode(200);
1464 reply->setContentType("text/plain");
1465 reply->setContent("oauth_token=HiThere&oauth_token_secret=BigSecret");
1466 nam->setNextReply(reply);
1467
1468 m_testPlugin->process(data, mechanism);
1469 QTRY_COMPARE(userActionRequired.count(), 1);
1470
1471 nam->m_lastRequest = QNetworkRequest();
1472 nam->m_lastRequestData = QByteArray();
1473
1474 reply = new TestNetworkReply(this);
1475 reply->setStatusCode(replyStatusCode);
1476 if (!replyContentType.isEmpty()) {
1477 reply->setContentType(replyContentType);
1478 }
1479 reply->setContent(replyContents.toUtf8());
1480 nam->setNextReply(reply);
1481
1482 if (!urlResponse.isEmpty()) {
1483 info.setUrlResponse(urlResponse);
1484 }
1485
1486 m_testPlugin->userActionFinished(info);
1487 QTRY_COMPARE(error.count(), errorCode < 0 ? 0 : 1);
1488 QTRY_COMPARE(result.count(), errorCode < 0 ? 1 : 0);
1489 QVariantMap resp;
1490 if (errorCode >= 0) {
1491 QCOMPARE(error.at(0).at(0).value<Error>().type(), errorCode);
1492 } else {
1493 resp = result.at(0).at(0).value<SessionData>().toMap();
1494 QVERIFY(mapIsSubset(response, resp));
1495 }
1496
1497 if (!expectedAuthMap.isEmpty()) {
1498 QCOMPARE(nam->m_lastRequest.url().toString(), data.TokenEndpoint());
1499 QVERIFY(nam->m_lastRequestData.isEmpty());
1500
1501 QString authorizationHeader =
1502 QString::fromUtf8(nam->m_lastRequest.rawHeader("Authorization"));
1503 QStringList authorizationHeaderParts =
1504 authorizationHeader.split(QRegExp(",?\\s+"));
1505 QCOMPARE(authorizationHeaderParts[0], QString("OAuth"));
1506
1507 /* The rest of the header should be a mapping, let's parse it */
1508 bool ok = true;
1509 QVariantMap authMap =
1510 parseAuthorizationHeader(authorizationHeaderParts.mid(1), &ok);
1511 QVERIFY(ok);
1512 QCOMPARE(authMap.value("oauth_signature_method").toString(), mechanism);
1513 QVERIFY(mapIsSubset(expectedAuthMap, authMap));
1514 }
1515
1516 delete nam;
1517}
1518
1519void PluginTest::testErrors_data()
1520{
1521 QTest::addColumn<QString>("replyContents");
1522 QTest::addColumn<int>("expectedErrorCode");
1523
1524 QTest::newRow("incorrect_client_credentials") <<
1525 "{ \"error\": \"incorrect_client_credentials\" }" <<
1526 int(Error::InvalidCredentials);
1527
1528 QTest::newRow("redirect_uri_mismatch") <<
1529 "{ \"error\": \"redirect_uri_mismatch\" }" <<
1530 int(Error::InvalidCredentials);
1531
1532 QTest::newRow("bad_authorization_code") <<
1533 "{ \"error\": \"bad_authorization_code\" }" <<
1534 int(Error::InvalidCredentials);
1535
1536 QTest::newRow("invalid_client_credentials") <<
1537 "{ \"error\": \"invalid_client_credentials\" }" <<
1538 int(Error::InvalidCredentials);
1539
1540 QTest::newRow("unauthorized_client") <<
1541 "{ \"error\": \"unauthorized_client\" }" <<
1542 int(Error::NotAuthorized);
1543
1544 QTest::newRow("invalid_assertion") <<
1545 "{ \"error\": \"invalid_assertion\" }" <<
1546 int(Error::InvalidCredentials);
1547
1548 QTest::newRow("unknown_format") <<
1549 "{ \"error\": \"unknown_format\" }" <<
1550 int(Error::InvalidQuery);
1551
1552 QTest::newRow("authorization_expired") <<
1553 "{ \"error\": \"authorization_expired\" }" <<
1554 int(Error::InvalidCredentials);
1555
1556 QTest::newRow("multiple_credentials") <<
1557 "{ \"error\": \"multiple_credentials\" }" <<
1558 int(Error::InvalidQuery);
1559
1560 QTest::newRow("invalid_user_credentials") <<
1561 "{ \"error\": \"invalid_user_credentials\" }" <<
1562 int(Error::InvalidCredentials);
1563}
1564
1565void PluginTest::testErrors()
1566{
1567 QFETCH(QString, replyContents);
1568 QFETCH(int, expectedErrorCode);
1569
1570 SignOn::UiSessionData info;
1571 PluginData data;
1572 data.setHost("localhost");
1573 data.setAuthPath("authorize");
1574 data.setTokenPath("access_token");
1575 data.setClientId("104660106251471");
1576 data.setClientSecret("fa28f40b5a1f8c1d5628963d880636fbkjkjkj");
1577 data.setRedirectUri("http://localhost/resp.html");
1578
1579 QSignalSpy error(m_testPlugin, SIGNAL(error(const SignOn::Error &)));
1580 QSignalSpy userActionRequired(m_testPlugin,
1581 SIGNAL(userActionRequired(const SignOn::UiSessionData&)));
1582
1583 TestNetworkAccessManager *nam = new TestNetworkAccessManager;
1584 m_testPlugin->m_networkAccessManager = nam;
1585 TestNetworkReply *reply = new TestNetworkReply(this);
1586 reply->setStatusCode(401);
1587 reply->setContentType("application/json");
1588 reply->setContent(replyContents.toUtf8());
1589 nam->setNextReply(reply);
1590
1591 m_testPlugin->process(data, QString("web_server"));
1592 QTRY_COMPARE(userActionRequired.count(), 1);
1593 QString state = parseState(userActionRequired);
1594
1595 info.setUrlResponse("http://localhost/resp.html?code=c0d3&state=" + state);
1596 m_testPlugin->userActionFinished(info);
1597
1598 QTRY_COMPARE(error.count(), 1);
1599 QCOMPARE(error.at(0).at(0).value<Error>().type(), expectedErrorCode);
1600
1601 delete nam;
1602}
1603
1604void PluginTest::testRefreshToken_data()
1605{
1606 QTest::addColumn<QVariantMap>("sessionData");
1607 QTest::addColumn<QVariantMap>("expectedResponse");
1608
1609 PluginData data;
1610 data.setHost("localhost");
1611 data.setAuthPath("authorize");
1612 data.setTokenPath("access_token");
1613 data.setClientId("104660106251471");
1614 data.setClientSecret("fa28f40b5a1f8c1d5628963d880636fbkjkjkj");
1615 data.setRedirectUri("http://localhost/resp.html");
1616
1617 QVariantMap tokens;
1618 QVariantMap token;
1619 token.insert("Token", QLatin1String("tokenfromtest"));
1620 token.insert("timestamp", QDateTime::currentDateTime().toTime_t() - 10000);
1621 token.insert("Expiry", 1000);
1622 token.insert("refresh_token", QString("r3fr3sh"));
1623 tokens.insert(data.ClientId(), QVariant::fromValue(token));
1624 data.m_data.insert("Tokens", tokens);
1625
1626 QVariantMap response;
1627 response.insert("AccessToken", "n3w-t0k3n");
1628 response.insert("ExpiresIn", 3600);
1629 response.insert("RefreshToken", QString());
1630 response.insert("Scope", QStringList());
1631
1632 QTest::newRow("expired access token") << data.toMap() << response;
1633
1634 token.insert("timestamp", QDateTime::currentDateTime().toTime_t());
1635 token.insert("Expiry", 50000);
1636 tokens.insert(data.ClientId(), QVariant::fromValue(token));
1637 data.m_data.insert("Tokens", tokens);
1638 data.setForceTokenRefresh(true);
1639 QTest::newRow("valid access token, force refresh") << data.toMap() << response;
1640}
1641
1642void PluginTest::testRefreshToken()
1643{
1644 QFETCH(QVariantMap, sessionData);
1645 QFETCH(QVariantMap, expectedResponse);
1646
1647 SignOn::UiSessionData info;
1648
1649 QSignalSpy result(m_testPlugin, SIGNAL(result(const SignOn::SessionData&)));
1650 QSignalSpy error(m_testPlugin, SIGNAL(error(const SignOn::Error &)));
1651
1652 TestNetworkAccessManager *nam = new TestNetworkAccessManager;
1653 m_testPlugin->m_networkAccessManager = nam;
1654 TestNetworkReply *reply = new TestNetworkReply(this);
1655 reply->setStatusCode(200);
1656 reply->setContentType("application/json");
1657 reply->setContent("{ \"access_token\":\"n3w-t0k3n\", \"expires_in\": 3600 }");
1658 nam->setNextReply(reply);
1659
1660 m_testPlugin->process(sessionData, QString("web_server"));
1661 QTRY_COMPARE(result.count(), 1);
1662 QCOMPARE(error.count(), 0);
1663
1664 QCOMPARE(nam->m_lastRequest.url(), QUrl("https://localhost/access_token"));
1665 QCOMPARE(QString::fromUtf8(nam->m_lastRequestData),
1666 QString("grant_type=refresh_token&refresh_token=r3fr3sh"));
1667
1668 QCOMPARE(result.at(0).at(0).value<SessionData>().toMap(), expectedResponse);
1669
1670 delete nam;
1671}
1672
1673void PluginTest::testRefreshTokenError_data()
1674{
1675 QTest::addColumn<int>("replyErrorCode");
1676 QTest::addColumn<int>("replyStatusCode");
1677 QTest::addColumn<QString>("replyContents");
1678 QTest::addColumn<int>("expectedError");
1679
1680 QTest::newRow("invalid grant, 400") <<
1681 int(QNetworkReply::ProtocolInvalidOperationError) <<
1682 int(400) <<
1683 "{ \"error\":\"invalid_grant\" }" <<
1684 int(-1);
1685
1686 QTest::newRow("invalid grant, 401") <<
1687 int(QNetworkReply::ContentAccessDenied) <<
1688 int(401) <<
1689 "{ \"error\":\"invalid_grant\" }" <<
1690 int(-1);
1691
1692 QTest::newRow("invalid grant, 401, no error signal") <<
1693 int(-1) <<
1694 int(401) <<
1695 "{ \"error\":\"invalid_grant\" }" <<
1696 int(-1);
1697
1698 QTest::newRow("temporary network failure") <<
1699 int(QNetworkReply::TemporaryNetworkFailureError) <<
1700 int(-1) <<
1701 "" <<
1702 int(Error::NoConnection);
1703}
1704
1705void PluginTest::testRefreshTokenError()
1706{
1707 QFETCH(int, replyErrorCode);
1708 QFETCH(int, replyStatusCode);
1709 QFETCH(QString, replyContents);
1710 QFETCH(int, expectedError);
1711
1712 PluginData data;
1713 data.setHost("localhost");
1714 data.setAuthPath("authorize");
1715 data.setTokenPath("access_token");
1716 data.setClientId("104660106251471");
1717 data.setClientSecret("fa28f40b5a1f8c1d5628963d880636fbkjkjkj");
1718 data.setRedirectUri("http://localhost/resp.html");
1719
1720 QVariantMap tokens;
1721 QVariantMap token;
1722 token.insert("Token", QLatin1String("tokenfromtest"));
1723 token.insert("timestamp", QDateTime::currentDateTime().toTime_t() - 10000);
1724 token.insert("Expiry", 1000);
1725 token.insert("refresh_token", QString("r3fr3sh"));
1726 tokens.insert(data.ClientId(), QVariant::fromValue(token));
1727 data.m_data.insert("Tokens", tokens);
1728
1729 SignOn::UiSessionData info;
1730
1731 QSignalSpy error(m_testPlugin, SIGNAL(error(const SignOn::Error &)));
1732 QSignalSpy userActionRequired(m_testPlugin,
1733 SIGNAL(userActionRequired(const SignOn::UiSessionData&)));
1734
1735 TestNetworkAccessManager *nam = new TestNetworkAccessManager;
1736 m_testPlugin->m_networkAccessManager = nam;
1737 TestNetworkReply *reply = new TestNetworkReply(this);
1738 if (replyErrorCode >= 0) {
1739 reply->setError(QNetworkReply::NetworkError(replyErrorCode),
1740 "Dummy error", 5);
1741 }
1742 reply->setStatusCode(replyStatusCode);
1743 reply->setContentType("application/json");
1744 reply->setContent(replyContents.toUtf8());
1745 nam->setNextReply(reply);
1746
1747 m_testPlugin->process(data, QString("web_server"));
1748
1749 if (expectedError < 0) {
1750 QTRY_COMPARE(userActionRequired.count(), 1);
1751 QCOMPARE(error.count(), 0);
1752 } else {
1753 QTRY_COMPARE(error.count(), 1);
1754 QCOMPARE(error.at(0).at(0).value<Error>().type(), expectedError);
1755 }
1756
1757 delete nam;
1758}
1759
1760void PluginTest::testClientAuthentication_data()
1761{
1762 QTest::addColumn<QString>("clientSecret");
1763 QTest::addColumn<bool>("forceAuthViaRequestBody");
1764 QTest::addColumn<QString>("postContents");
1765 QTest::addColumn<QString>("postAuthorization");
1766
1767 QTest::newRow("no secret, std auth") <<
1768 "" << false <<
1769 "grant_type=authorization_code&code=c0d3"
1770 "&redirect_uri=http://localhost/resp.html&client_id=104660106251471" <<
1771 "";
1772 QTest::newRow("no secret, auth in body") <<
1773 "" << true <<
1774 "grant_type=authorization_code&code=c0d3"
1775 "&redirect_uri=http://localhost/resp.html&client_id=104660106251471" <<
1776 "";
1777
1778 QTest::newRow("with secret, std auth") <<
1779 "s3cr3t" << false <<
1780 "grant_type=authorization_code&code=c0d3&redirect_uri=http://localhost/resp.html" <<
1781 "Basic MTA0NjYwMTA2MjUxNDcxOnMzY3IzdA==";
1782 QTest::newRow("with secret, auth in body") <<
1783 "s3cr3t" << true <<
1784 "grant_type=authorization_code&code=c0d3"
1785 "&redirect_uri=http://localhost/resp.html"
1786 "&client_id=104660106251471&client_secret=s3cr3t" <<
1787 "";
1788}
1789
1790void PluginTest::testClientAuthentication()
1791{
1792 QFETCH(QString, clientSecret);
1793 QFETCH(bool, forceAuthViaRequestBody);
1794 QFETCH(QString, postContents);
1795 QFETCH(QString, postAuthorization);
1796
1797 SignOn::UiSessionData info;
1798 PluginData data;
1799 data.setHost("localhost");
1800 data.setAuthPath("authorize");
1801 data.setTokenPath("access_token");
1802 data.setClientId("104660106251471");
1803 data.setClientSecret(clientSecret);
1804 data.setRedirectUri("http://localhost/resp.html");
1805 data.setForceClientAuthViaRequestBody(forceAuthViaRequestBody);
1806
1807 QSignalSpy result(m_testPlugin, SIGNAL(result(const SignOn::SessionData&)));
1808 QSignalSpy error(m_testPlugin, SIGNAL(error(const SignOn::Error &)));
1809 QSignalSpy userActionRequired(m_testPlugin,
1810 SIGNAL(userActionRequired(const SignOn::UiSessionData&)));
1811
1812 TestNetworkAccessManager *nam = new TestNetworkAccessManager;
1813 m_testPlugin->m_networkAccessManager = nam;
1814 TestNetworkReply *reply = new TestNetworkReply(this);
1815 reply->setStatusCode(200);
1816 reply->setContentType("application/json");
1817 reply->setContent("{ \"access_token\":\"t0k3n\", \"expires_in\": 3600 }");
1818 nam->setNextReply(reply);
1819
1820 m_testPlugin->process(data, QString("web_server"));
1821 QTRY_COMPARE(userActionRequired.count(), 1);
1822 QString state = parseState(userActionRequired);
1823
1824 info.setUrlResponse("http://localhost/resp.html?code=c0d3&state=" + state);
1825 m_testPlugin->userActionFinished(info);
1826
1827 QTRY_COMPARE(result.count(), 1);
1828 QCOMPARE(error.count(), 0);
1829 QCOMPARE(nam->m_lastRequest.url(), QUrl("https://localhost/access_token"));
1830 QCOMPARE(QString::fromUtf8(nam->m_lastRequestData), postContents);
1831 QCOMPARE(QString::fromUtf8(nam->m_lastRequest.rawHeader("Authorization")),
1832 postAuthorization);
1833
1834 delete nam;
1835}
1836
1837#endif
1838
1839QTEST_MAIN(PluginTest)
1840#include "tst_plugin.moc"
01841
=== modified file 'signon-plugin/ubuntuone-plugin.cpp'
--- signon-plugin/ubuntuone-plugin.cpp 2013-08-12 21:16:29 +0000
+++ signon-plugin/ubuntuone-plugin.cpp 2016-04-22 09:51:32 +0000
@@ -15,15 +15,37 @@
15 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,15 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
16 * Boston, MA 02110-1301, USA.16 * Boston, MA 02110-1301, USA.
17 */17 */
18
19#include <QJsonDocument>
20#include <QJsonObject>
21#include <QNetworkAccessManager>
22#include <QNetworkReply>
23#include <QNetworkRequest>
24
25#include <SignOn/UiSessionData>
26#include <SignOn/uisessiondata_priv.h>
27
18#include <token.h>28#include <token.h>
1929
30#include "i18n.h"
20#include "ubuntuone-plugin.h"31#include "ubuntuone-plugin.h"
2132
33#define BASE_URL "https://login.ubuntu.com"
34
35#define ERR_INVALID_CREDENTIALS QLatin1String("INVALID_CREDENTIALS")
36#define ERR_INVALID_DATA QLatin1String("INVALID_DATA")
37#define ERR_TWOFACTOR_REQUIRED QLatin1String("TWOFACTOR_REQUIRED")
38#define ERR_TWOFACTOR_FAILURE QLatin1String("TWOFACTOR_FAILURE")
39#define ERR_PASSWORD_POLICY_ERROR QLatin1String("PASSWORD_POLICY_ERROR")
2240
23namespace UbuntuOne {41namespace UbuntuOne {
2442
25 SignOnPlugin::SignOnPlugin(QObject *parent)43 SignOnPlugin::SignOnPlugin(QObject *parent):
26 : AuthPluginInterface(parent)44 AuthPluginInterface(parent),
45 m_networkAccessManager(0),
46 m_reply(0),
47 m_didAskForPassword(false),
48 m_needsOtp(false)
27 {49 {
28 }50 }
2951
@@ -47,33 +69,319 @@
47 {69 {
48 }70 }
4971
72 bool SignOnPlugin::validateInput(const PluginData &data,
73 const QString &mechanism)
74 {
75 Q_UNUSED(mechanism);
76
77 if (data.TokenName().isEmpty()) {
78 return false;
79 }
80
81 return true;
82 }
83
84 bool SignOnPlugin::respondWithStoredData()
85 {
86 QVariantMap storedData = m_data.StoredData();
87
88 /* When U1 was using the password plugin, it was storing the token data
89 * in the password field. So, if we don't have any data stored in the
90 * plugin's data, try to get a token from the password field.
91 */
92 if (storedData.isEmpty() && !m_data.Secret().isEmpty()) {
93 Token *token = Token::fromQuery(m_data.Secret());
94 if (token->isValid()) {
95 PluginData tokenData;
96 tokenData.setConsumerKey(token->consumerKey());
97 tokenData.setConsumerSecret(token->consumerSecret());
98 tokenData.setTokenKey(token->tokenKey());
99 tokenData.setTokenSecret(token->tokenSecret());
100 storedData[token->name()] = tokenData.toMap();
101 PluginData pluginData;
102 pluginData.setStoredData(storedData);
103 Q_EMIT store(pluginData);
104
105 /* We know that the given secret is a valid token, so it cannot
106 * be a valid password as well: let's clear it out now, so that
107 * if it turns out that the token is no longer valid and that
108 * we need to create a new one, we won't make a useless attempt
109 * to create one with a wrong password.
110 */
111 m_data.setSecret(QString());
112 }
113 delete token;
114 }
115
116 /* Check if we have stored data for this token name */
117 PluginData tokenData(storedData[m_data.TokenName()].toMap());
118 Token token(tokenData.TokenKey(), tokenData.TokenSecret(),
119 tokenData.ConsumerKey(), tokenData.ConsumerSecret());
120 if (!token.isValid()) return false;
121 qDebug() << "Token is valid!" << tokenData.TokenKey();
122
123 tokenData.setTokenName(m_data.TokenName());
124 checkTokenValidity(token, tokenData);
125 return true;
126 }
127
128 void SignOnPlugin::emitErrorFromReply(QNetworkReply *reply)
129 {
130 int errorCode = reply->error();
131 int type = SignOn::Error::Network;
132 if (errorCode == QNetworkReply::SslHandshakeFailedError) {
133 type = SignOn::Error::Ssl;
134 } else if (errorCode == QNetworkReply::ServiceUnavailableError) {
135 type = SignOn::Error::ServiceNotAvailable;
136 } else if (errorCode == QNetworkReply::AuthenticationRequiredError) {
137 type = SignOn::Error::NotAuthorized;
138 } else if (errorCode <= QNetworkReply::UnknownNetworkError) {
139 type = SignOn::Error::NoConnection;
140 }
141
142 qDebug() << "Got error:" << reply->errorString();
143 Q_EMIT error(SignOn::Error(type, reply->errorString()));
144 }
145
146 void SignOnPlugin::onValidationFinished()
147 {
148 QNetworkReply *reply = m_reply;
149 m_reply->deleteLater();
150 m_reply = 0;
151
152 /* Note on error handling: we consider the token to be (in)valid only if
153 * we can parse the JSON response and it contains the response. If the
154 * validation has failed because of some other error (SSL error,
155 * unparsable server reply, etc.) then we report the error to the
156 * client.
157 */
158 QJsonDocument json = QJsonDocument::fromJson(reply->readAll());
159 QJsonObject object = json.object();
160 QJsonValue value = object.value("is_valid");
161
162 int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
163 if (statusCode == 200 && value.isBool()) {
164 bool isValid = value.toBool();
165 if (isValid) {
166 Q_EMIT result(m_checkedToken);
167 } else {
168 qDebug() << "Server verification failed";
169 getCredentialsAndCreateNewToken();
170 }
171 } else {
172 emitErrorFromReply(reply);
173 }
174 }
175
176 void SignOnPlugin::checkTokenValidity(const Token &token,
177 const PluginData &tokenData)
178 {
179 m_checkedToken = tokenData;
180
181 QNetworkRequest req(QUrl(BASE_URL "/api/v2/requests/validate"));
182 req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
183
184 QString httpUrl("http://www.example.com");
185 QString httpMethod("GET");
186
187 QJsonObject formData;
188 formData.insert("http_url", httpUrl);
189 formData.insert("http_method", httpMethod);
190 formData.insert("authorization", token.signUrl(httpUrl, httpMethod));
191
192 m_reply =
193 m_networkAccessManager->post(req, QJsonDocument(formData).toJson());
194 QObject::connect(m_reply, SIGNAL(finished()),
195 this, SLOT(onValidationFinished()));
196 }
197
50 void SignOnPlugin::process(const SignOn::SessionData &inData,198 void SignOnPlugin::process(const SignOn::SessionData &inData,
51 const QString &mechanism)199 const QString &mechanism)
52 {200 {
53 Q_UNUSED(mechanism);201 if (!m_networkAccessManager) {
202 m_networkAccessManager = new QNetworkAccessManager(this);
203 }
204
205 initTr("ubuntuone-credentials", NULL);
206
54 PluginData response;207 PluginData response;
55 m_data = inData.data<PluginData>();208 m_data = inData.data<PluginData>();
56209
57 if (!inData.Secret().isEmpty()) {210 if (!validateInput(m_data, mechanism)) {
58 response.setConsumer(m_data.Consumer());211 qWarning() << "Invalid parameters passed";
59 response.setConsumerSecret(m_data.ConsumerSecret());212 Q_EMIT error(SignOn::Error(SignOn::Error::MissingData));
60 response.setToken(m_data.Token());213 return;
61 response.setTokenSecret(m_data.TokenSecret());214 }
62215
63 response.setName(Token::buildTokenName());216 /* It may be that the stored token is valid; however, do the check only
64217 * if no OTP was provided (since the presence of an OTP is a clear
65 emit result(response);218 * signal that the caller wants to get a new token). */
66 return;219 if (m_data.OneTimePassword().isEmpty() &&
67 }220 respondWithStoredData()) {
68221 return;
69 SignOn::UiSessionData data;222 }
70 data.setRealm(inData.Realm());223
71 data.setShowRealm(!data.Realm().isEmpty());224 getCredentialsAndCreateNewToken();
72 emit userActionRequired(data);225 }
226
227 void SignOnPlugin::onCreationFinished()
228 {
229 QNetworkReply *reply = m_reply;
230 m_reply->deleteLater();
231 m_reply = 0;
232
233 QByteArray data = reply->readAll();
234 qDebug() << "Received" << data;
235 QJsonDocument json = QJsonDocument::fromJson(data);
236 QJsonObject object = json.object();
237
238 QString error = object.value("code").toString();
239
240 int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
241 qDebug() << "Status code:" << statusCode;
242 if (statusCode == 200 || statusCode == 201) {
243 QString tokenName = object.value("token_name").toString();
244 PluginData token;
245 token.setConsumerKey(object.value("consumer_key").toString());
246 token.setConsumerSecret(object.value("consumer_secret").toString());
247 token.setTokenKey(object.value("token_key").toString());
248 token.setTokenSecret(object.value("token_secret").toString());
249
250 /* Store the token */
251 QVariantMap storedData;
252 storedData[tokenName] = token.toMap();
253 PluginData pluginData;
254 pluginData.setStoredData(storedData);
255 Q_EMIT store(pluginData);
256
257 token.setTokenName(tokenName);
258 Q_EMIT result(token);
259 } else if (statusCode == 401 && error == ERR_INVALID_CREDENTIALS) {
260 m_data.setSecret(QString());
261 m_data.setOneTimePassword(QString());
262 getCredentialsAndCreateNewToken();
263 } else if (statusCode == 401 && error == ERR_TWOFACTOR_REQUIRED) {
264 m_needsOtp = true;
265 getCredentialsAndCreateNewToken();
266 } else if (statusCode == 403 && error == ERR_TWOFACTOR_FAILURE) {
267 m_data.setOneTimePassword(QString());
268 getCredentialsAndCreateNewToken();
269 } else if (statusCode == 403 && error == ERR_PASSWORD_POLICY_ERROR) {
270 QVariantMap data;
271 QJsonObject extra = object.value("extra").toObject();
272 data[SSOUI_KEY_OPENURL] = extra.value("location").toString();
273 Q_EMIT userActionRequired(data);
274 } else if (error == ERR_INVALID_DATA) {
275 // This error is received when the email address is invalid
276 m_data.setUserName(QString());
277 m_data.setSecret(QString());
278 m_data.setOneTimePassword(QString());
279 getCredentialsAndCreateNewToken();
280 } else {
281 emitErrorFromReply(reply);
282 }
283 }
284
285 void SignOnPlugin::createNewToken()
286 {
287 QNetworkRequest req(QUrl(BASE_URL "/api/v2/tokens/oauth"));
288 req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
289
290 QJsonObject formData;
291 formData.insert("email", m_data.UserName());
292 formData.insert("password", m_data.Secret());
293 formData.insert("token_name", m_data.TokenName());
294 if (!m_data.OneTimePassword().isEmpty()) {
295 formData.insert("otp", m_data.OneTimePassword());
296 }
297
298 qDebug() << "Sending data for token creation";
299 m_reply =
300 m_networkAccessManager->post(req, QJsonDocument(formData).toJson());
301 QObject::connect(m_reply, SIGNAL(finished()),
302 this, SLOT(onCreationFinished()));
303 }
304
305 void SignOnPlugin::getCredentialsAndCreateNewToken()
306 {
307 if (!m_data.Secret().isEmpty() &&
308 (!m_needsOtp || !m_data.OneTimePassword().isEmpty())) {
309 createNewToken();
310 } else if (m_data.Secret().isEmpty()) {
311 QVariantMap data;
312 data[SSOUI_KEY_TITLE] = _("UbuntuOne authentication");
313 data[SSOUI_KEY_QUERYUSERNAME] = true;
314 data[SSOUI_KEY_USERNAME] = m_data.UserName();
315 data[SSOUI_KEY_QUERYPASSWORD] = true;
316 m_didAskForPassword = true;
317 Q_EMIT userActionRequired(data);
318 } else {
319 QVariantMap data;
320 data[SSOUI_KEY_TITLE] = _("UbuntuOne authentication");
321 data[SSOUI_KEY_USERNAME] = m_data.UserName();
322 data[SSOUI_KEY_PASSWORD] = m_data.Secret();
323 data[SSOUI_KEY_QUERY2FA] = true;
324 data[SSOUI_KEY_2FA_TEXT] = _("2-factor device code");
325 Q_EMIT userActionRequired(data);
326 }
327 }
328
329 bool SignOnPlugin::handleUiError(const SignOn::UiSessionData &data)
330 {
331 using namespace SignOn;
332
333 int code = data.QueryErrorCode();
334 if (code == QUERY_ERROR_NONE) {
335 return false;
336 }
337
338 qDebug() << "userActionFinished with error: " << code;
339 if (code == QUERY_ERROR_CANCELED) {
340 Q_EMIT error(Error(Error::SessionCanceled,
341 QLatin1String("Cancelled by user")));
342 } else if (code == QUERY_ERROR_NETWORK) {
343 Q_EMIT error(Error(Error::Network, QLatin1String("Network error")));
344 } else if (code == QUERY_ERROR_SSL) {
345 Q_EMIT error(Error(Error::Ssl, QLatin1String("SSL error")));
346 } else {
347 QVariantMap map = data.toMap();
348 if (map.contains(SSOUI_KEY_QUERY2FA)) {
349 PluginData reply;
350 reply.setU1ErrorCode(PluginData::OneTimePasswordRequired);
351 Q_EMIT result(reply);
352 } else if (map.contains(SSOUI_KEY_QUERYPASSWORD)) {
353 PluginData reply;
354 reply.setU1ErrorCode(PluginData::InvalidPassword);
355 Q_EMIT result(reply);
356 } else {
357 Q_EMIT error(Error(Error::UserInteraction,
358 QString("userActionFinished error: ")
359 + QString::number(data.QueryErrorCode())));
360 }
361 }
362 return true;
73 }363 }
74364
75 void SignOnPlugin::userActionFinished(const SignOn::UiSessionData &data)365 void SignOnPlugin::userActionFinished(const SignOn::UiSessionData &data)
76 {366 {
367 if (handleUiError(data)) return;
368
369 PluginData uiData = data.data<PluginData>();
370 if (!uiData.UserName().isEmpty()) {
371 m_data.setUserName(uiData.UserName());
372 }
373
374 if (!uiData.Secret().isEmpty()) {
375 m_data.setSecret(uiData.Secret());
376 }
377
378 QVariantMap map = data.toMap();
379 QString oneTimePassword = map.value(SSOUI_KEY_2FA).toString();
380 if (!oneTimePassword.isEmpty()) {
381 m_data.setOneTimePassword(oneTimePassword);
382 }
383
384 getCredentialsAndCreateNewToken();
77 }385 }
78386
79 SIGNON_DECL_AUTH_PLUGIN(SignOnPlugin)387 SIGNON_DECL_AUTH_PLUGIN(SignOnPlugin)
80388
=== modified file 'signon-plugin/ubuntuone-plugin.h'
--- signon-plugin/ubuntuone-plugin.h 2013-08-12 21:16:29 +0000
+++ signon-plugin/ubuntuone-plugin.h 2016-04-22 09:51:32 +0000
@@ -27,9 +27,15 @@
2727
28#include "ubuntuonedata.h"28#include "ubuntuonedata.h"
2929
30class PluginTest;
31
32class QNetworkAccessManager;
33class QNetworkReply;
3034
31namespace UbuntuOne {35namespace UbuntuOne {
3236
37 class Token;
38
33 class SignOnPlugin : public AuthPluginInterface39 class SignOnPlugin : public AuthPluginInterface
34 {40 {
35 Q_OBJECT41 Q_OBJECT
@@ -40,17 +46,49 @@
40 virtual ~SignOnPlugin();46 virtual ~SignOnPlugin();
4147
42 public Q_SLOTS:48 public Q_SLOTS:
43 QString type() const;49 QString type() const Q_DECL_OVERRIDE;
44 QStringList mechanisms() const;50 QStringList mechanisms() const Q_DECL_OVERRIDE;
45 void cancel();51 void cancel() Q_DECL_OVERRIDE;
46 void process(const SignOn::SessionData &inData,52 void process(const SignOn::SessionData &inData,
47 const QString &mechanism = 0);53 const QString &mechanism = 0) Q_DECL_OVERRIDE;
48 void userActionFinished(const SignOn::UiSessionData &data);54 void userActionFinished(const SignOn::UiSessionData &data) Q_DECL_OVERRIDE;
4955
50 private:56 private:
57 bool validateInput(const PluginData &data, const QString &mechanism);
58 bool respondWithStoredData();
59 void checkTokenValidity(const Token &token,
60 const PluginData &tokenData);
61 void emitErrorFromReply(QNetworkReply *reply);
62 void createNewToken();
63 void getCredentialsAndCreateNewToken();
64 bool handleUiError(const SignOn::UiSessionData &data);
65
66 private Q_SLOTS:
67 void onValidationFinished();
68 void onCreationFinished();
69
70 private:
71 friend class ::PluginTest;
51 PluginData m_data;72 PluginData m_data;
73 PluginData m_checkedToken;
74 QNetworkAccessManager *m_networkAccessManager;
75 QNetworkReply *m_reply;
76 bool m_didAskForPassword;
77 bool m_needsOtp;
52 };78 };
5379
54} // namespace UbuntuOne80} // namespace UbuntuOne
5581
82/* These fields are temporarily defined here; they'll be eventually moved to
83 * signond's include files. */
84#define SSOUI_KEY_USERNAME_TEXT QLatin1String("UserNameText")
85#define SSOUI_KEY_PASSWORD_TEXT QLatin1String("PasswordText")
86#define SSOUI_KEY_REGISTER_URL QLatin1String("RegisterUrl")
87#define SSOUI_KEY_REGISTER_TEXT QLatin1String("RegisterText")
88#define SSOUI_KEY_LOGIN_TEXT QLatin1String("LoginText")
89#define SSOUI_KEY_QUERY2FA QLatin1String("Query2fa")
90#define SSOUI_KEY_2FA QLatin1String("2fa")
91#define SSOUI_KEY_2FA_TEXT QLatin1String("2faText")
92#define SSOUI_KEY_ERROR_MESSAGE QLatin1String("ErrorMessage")
93
56#endif94#endif
5795
=== modified file 'signon-plugin/ubuntuonedata.h'
--- signon-plugin/ubuntuonedata.h 2013-08-12 21:16:29 +0000
+++ signon-plugin/ubuntuonedata.h 2016-04-22 09:51:32 +0000
@@ -25,16 +25,33 @@
25 class PluginData : public SignOn::SessionData25 class PluginData : public SignOn::SessionData
26 {26 {
27 public:27 public:
28 PluginData(const QVariantMap &data = QVariantMap()):
29 SignOn::SessionData(data) {}
30
28 // The name of the token31 // The name of the token
29 SIGNON_SESSION_DECLARE_PROPERTY(QString, Name);32 SIGNON_SESSION_DECLARE_PROPERTY(QString, TokenName);
33
34 // The one-time password (optional)
35 SIGNON_SESSION_DECLARE_PROPERTY(QString, OneTimePassword);
3036
31 // The consumer key and secret for signing37 // The consumer key and secret for signing
32 SIGNON_SESSION_DECLARE_PROPERTY(QString, Consumer);38 SIGNON_SESSION_DECLARE_PROPERTY(QString, ConsumerKey);
33 SIGNON_SESSION_DECLARE_PROPERTY(QString, ConsumerSecret);39 SIGNON_SESSION_DECLARE_PROPERTY(QString, ConsumerSecret);
3440
35 // The access token and secret for signing41 // The access token and secret for signing
36 SIGNON_SESSION_DECLARE_PROPERTY(QString, Token);42 SIGNON_SESSION_DECLARE_PROPERTY(QString, TokenKey);
37 SIGNON_SESSION_DECLARE_PROPERTY(QString, TokenSecret);43 SIGNON_SESSION_DECLARE_PROPERTY(QString, TokenSecret);
44
45 // Error code
46 enum ErrorCode {
47 NoError = 0,
48 OneTimePasswordRequired,
49 InvalidPassword,
50 };
51 SIGNON_SESSION_DECLARE_PROPERTY(int, U1ErrorCode);
52
53 // Data which the plugin has stored into signond
54 SIGNON_SESSION_DECLARE_PROPERTY(QVariantMap, StoredData);
38 };55 };
3956
40} // namespace UbuntuOne57} // namespace UbuntuOne

Subscribers

People subscribed via source and target branches