diff --git a/resources/ews/ewsclient/auth/ewsoauth.cpp b/resources/ews/ewsclient/auth/ewsoauth.cpp index 44b82ee44..1ae83340c 100644 --- a/resources/ews/ewsclient/auth/ewsoauth.cpp +++ b/resources/ews/ewsclient/auth/ewsoauth.cpp @@ -1,432 +1,503 @@ /* Copyright (C) 2018 Krzysztof Nowicki This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ewsoauth.h" #ifdef EWSOAUTH_UNITTEST #include "ewsoauth_ut_mock.h" using namespace Mock; #else #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#ifdef HAVE_QCA +#include "ewspkeyauthjob.h" +#endif #endif #include "ewsclient_debug.h" static const auto o365AuthorizationUrl = QUrl(QStringLiteral("https://login.microsoftonline.com/common/oauth2/authorize")); static const auto o365AccessTokenUrl = QUrl(QStringLiteral("https://login.microsoftonline.com/common/oauth2/token")); static const auto o365FakeUserAgent = QStringLiteral("Mozilla/5.0 (Linux; Android 7.0; SM-G930V Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36"); static const auto o365Resource = QStringLiteral("https%3A%2F%2Foutlook.office365.com%2F"); +#ifdef HAVE_QCA +static const auto pkeyAuthSuffix = QStringLiteral(" PKeyAuth/1.0"); +static const auto pkeyRedirectUri = QStringLiteral("urn:http-auth:PKeyAuth"); +static const QString pkeyPasswordMapKey = QStringLiteral("pkey-password"); +#endif + static const QString accessTokenMapKey = QStringLiteral("access-token"); static const QString refreshTokenMapKey = QStringLiteral("refresh-token"); class EwsOAuthUrlSchemeHandler final : public QWebEngineUrlSchemeHandler { Q_OBJECT public: EwsOAuthUrlSchemeHandler(QObject *parent = Q_NULLPTR) : QWebEngineUrlSchemeHandler(parent) {}; ~EwsOAuthUrlSchemeHandler() override = default; void requestStarted(QWebEngineUrlRequestJob *request) override; signals: void returnUriReceived(QUrl url); }; class EwsOAuthReplyHandler final : public QAbstractOAuthReplyHandler { Q_OBJECT public: EwsOAuthReplyHandler(QObject *parent, const QString &returnUri) : QAbstractOAuthReplyHandler(parent), mReturnUri(returnUri) {}; ~EwsOAuthReplyHandler() override = default; QString callback() const override { return mReturnUri; }; void networkReplyFinished(QNetworkReply *reply) override; Q_SIGNALS: void replyError(const QString &error); private: const QString mReturnUri; }; class EwsOAuthRequestInterceptor final : public QWebEngineUrlRequestInterceptor { Q_OBJECT public: EwsOAuthRequestInterceptor(QObject *parent, const QString &redirectUri) : QWebEngineUrlRequestInterceptor(parent), mRedirectUri(redirectUri) { }; ~EwsOAuthRequestInterceptor() override = default; void interceptRequest(QWebEngineUrlRequestInfo &info) override; Q_SIGNALS: void redirectUriIntercepted(const QUrl &url); private: const QString mRedirectUri; }; class EwsOAuthPrivate final : public QObject { Q_OBJECT public: EwsOAuthPrivate(EwsOAuth *parent, const QString &email, const QString &appId, const QString &redirectUri); ~EwsOAuthPrivate() override = default; bool authenticate(bool interactive); void modifyParametersFunction(QAbstractOAuth::Stage stage, QVariantMap *parameters); void authorizeWithBrowser(const QUrl &url); void redirectUriIntercepted(const QUrl &url); void granted(); void error(const QString &error, const QString &errorDescription, const QUrl &uri); + QVariantMap queryToVarmap(const QUrl &url); +#ifdef HAVE_QCA + void pkeyAuthResult(KJob *job); +#endif QWebEngineView mWebView; QWebEngineProfile mWebProfile; QWebEnginePage mWebPage; QOAuth2AuthorizationCodeFlow mOAuth2; EwsOAuthReplyHandler mReplyHandler; EwsOAuthRequestInterceptor mRequestInterceptor; EwsOAuthUrlSchemeHandler mSchemeHandler; QString mToken; const QString mEmail; const QString mRedirectUri; bool mAuthenticated; QPointer mWebDialog; +#ifdef HAVE_QCA + QString mPKeyPassword; +#endif EwsOAuth *q_ptr = nullptr; Q_DECLARE_PUBLIC(EwsOAuth) }; void EwsOAuthUrlSchemeHandler::requestStarted(QWebEngineUrlRequestJob *request) { returnUriReceived(request->requestUrl()); } void EwsOAuthReplyHandler::networkReplyFinished(QNetworkReply *reply) { if (reply->error() != QNetworkReply::NoError) { Q_EMIT replyError(reply->errorString()); return; } else if (reply->header(QNetworkRequest::ContentTypeHeader).isNull()) { Q_EMIT replyError(QStringLiteral("Empty or no Content-type header")); return; } const auto cth = reply->header(QNetworkRequest::ContentTypeHeader); const auto ct = cth.isNull() ? QStringLiteral("text/html") : cth.toString(); const auto data = reply->readAll(); if (data.isEmpty()) { Q_EMIT replyError(QStringLiteral("No data received")); return; } Q_EMIT replyDataReceived(data); QVariantMap tokens; if (ct.startsWith(QStringLiteral("text/html")) || ct.startsWith(QStringLiteral("application/x-www-form-urlencoded"))) { QUrlQuery q(QString::fromUtf8(data)); const auto items = q.queryItems(QUrl::FullyDecoded); for (const auto it : items) { tokens.insert(it.first, it.second); } } else if (ct.startsWith(QStringLiteral("application/json")) || ct.startsWith(QStringLiteral("text/javascript"))) { const auto document = QJsonDocument::fromJson(data); if (!document.isObject()) { Q_EMIT replyError(QStringLiteral("Invalid JSON data received")); return; } const auto object = document.object(); if (object.isEmpty()) { Q_EMIT replyError(QStringLiteral("Empty JSON data received")); return; } tokens = object.toVariantMap(); } else { Q_EMIT replyError(QStringLiteral("Unknown content type")); return; } const auto error = tokens.value(QStringLiteral("error")); if (error.isValid()) { Q_EMIT replyError(QStringLiteral("Received error response: ") + error.toString()); return; } const auto accessToken = tokens.value(QStringLiteral("access_token")); if (!accessToken.isValid() || accessToken.toString().isEmpty()) { Q_EMIT replyError(QStringLiteral("Received empty or no access token")); return; } Q_EMIT tokensReceived(tokens); } void EwsOAuthRequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo &info) { const auto url = info.requestUrl(); - qCDebug(EWSCLI_LOG) << QStringLiteral("Intercepted browser navigation to ") << url; + qCDebugNC(EWSCLI_LOG) << QStringLiteral("Intercepted browser navigation to ") << url; - if (url.toString(QUrl::RemoveQuery) == mRedirectUri) { + if ((url.toString(QUrl::RemoveQuery) == mRedirectUri) +#ifdef HAVE_QCA + || (url.toString(QUrl::RemoveQuery) == pkeyRedirectUri) +#endif + ) { qCDebug(EWSCLI_LOG) << QStringLiteral("Found redirect URI - blocking request"); redirectUriIntercepted(url); info.block(true); } } EwsOAuthPrivate::EwsOAuthPrivate(EwsOAuth *parent, const QString &email, const QString &appId, const QString &redirectUri) : QObject(nullptr), mWebView(nullptr), mWebProfile(), mWebPage(&mWebProfile), mReplyHandler(this, redirectUri), - mRequestInterceptor(this, redirectUri), mEmail(email), mRedirectUri(redirectUri), mAuthenticated(false), q_ptr(parent) + mRequestInterceptor(this, redirectUri), mEmail(email), mRedirectUri(redirectUri), mAuthenticated(false), + q_ptr(parent) { mOAuth2.setReplyHandler(&mReplyHandler); mOAuth2.setAuthorizationUrl(o365AuthorizationUrl); mOAuth2.setAccessTokenUrl(o365AccessTokenUrl); mOAuth2.setClientIdentifier(appId); - /* Bad bad Microsoft... - * When Conditional Access is enabled on the server the OAuth2 authentication server only supports Windows, - * MacOSX, Android and iOS. No option to include Linux. Support (i.e. guarantee that it works) - * is one thing, but blocking unsupported browsers completely is just wrong. - * Fortunately enough this can be worked around by faking the user agent to something "supported". - */ - mWebProfile.setHttpUserAgent(o365FakeUserAgent); - mWebProfile.setRequestInterceptor(&mRequestInterceptor); mWebProfile.installUrlSchemeHandler("urn", &mSchemeHandler); mWebView.setPage(&mWebPage); mOAuth2.setModifyParametersFunction([&](QAbstractOAuth::Stage stage, QVariantMap *parameters) { modifyParametersFunction(stage, parameters); }); connect(&mOAuth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this, &EwsOAuthPrivate::authorizeWithBrowser); connect(&mOAuth2, &QOAuth2AuthorizationCodeFlow::granted, this, &EwsOAuthPrivate::granted); connect(&mOAuth2, &QOAuth2AuthorizationCodeFlow::error, this, &EwsOAuthPrivate::error); connect(&mRequestInterceptor, &EwsOAuthRequestInterceptor::redirectUriIntercepted, this, &EwsOAuthPrivate::redirectUriIntercepted, Qt::QueuedConnection); connect(&mReplyHandler, &EwsOAuthReplyHandler::replyError, this, [this](const QString &err) { error(QStringLiteral("Network reply error"), err, QUrl()); }); } bool EwsOAuthPrivate::authenticate(bool interactive) { Q_Q(EwsOAuth); qCInfoNC(EWSCLI_LOG) << QStringLiteral("Starting OAuth2 authentication"); #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) if (!mOAuth2.refreshToken().isEmpty()) { #else if (mOAuth2.status() == QAbstractOAuth::Status::Granted) { #endif mOAuth2.refreshAccessToken(); return true; } else if (interactive) { mOAuth2.grant(); return true; } else { return false; } } void EwsOAuthPrivate::modifyParametersFunction(QAbstractOAuth::Stage stage, QVariantMap *parameters) { switch (stage) { case QAbstractOAuth::Stage::RequestingAccessToken: parameters->insert(QStringLiteral("resource"), o365Resource); break; case QAbstractOAuth::Stage::RequestingAuthorization: parameters->insert(QStringLiteral("prompt"), QStringLiteral("login")); parameters->insert(QStringLiteral("login_hint"), mEmail); parameters->insert(QStringLiteral("resource"), o365Resource); break; default: break; } } void EwsOAuthPrivate::authorizeWithBrowser(const QUrl &url) { Q_Q(EwsOAuth); qCInfoNC(EWSCLI_LOG) << QStringLiteral("Launching browser for authentication"); + /* Bad bad Microsoft... + * When Conditional Access is enabled on the server the OAuth2 authentication server only supports Windows, + * MacOSX, Android and iOS. No option to include Linux. Support (i.e. guarantee that it works) + * is one thing, but blocking unsupported browsers completely is just wrong. + * Fortunately enough this can be worked around by faking the user agent to something "supported". + */ + auto userAgent = o365FakeUserAgent; +#ifdef HAVE_QCA + if (!q->mPKeyCertFile.isNull() && !q->mPKeyKeyFile.isNull()) { + qCInfoNC(EWSCLI_LOG) << QStringLiteral("Found PKeyAuth certificates"); + userAgent += pkeyAuthSuffix; + } else { + qCInfoNC(EWSCLI_LOG) << QStringLiteral("PKeyAuth certificates not found"); + } +#endif + mWebProfile.setHttpUserAgent(userAgent); + mWebDialog = new QDialog(q->mAuthParentWidget); mWebDialog->setObjectName(QStringLiteral("Akonadi EWS Resource - Authentication")); mWebDialog->setWindowIcon(QIcon("akonadi-ews")); mWebDialog->resize(400, 500); auto layout = new QHBoxLayout(mWebDialog); layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(&mWebView); mWebView.show(); connect(mWebDialog.data(), &QDialog::rejected, this, [this]() { error(QStringLiteral("User cancellation"), QStringLiteral("The authentication browser was closed"), QUrl()); }); mWebView.load(url); mWebDialog->show(); } -void EwsOAuthPrivate::redirectUriIntercepted(const QUrl &url) +QVariantMap EwsOAuthPrivate::queryToVarmap(const QUrl &url) { - qCDebug(EWSCLI_LOG) << QStringLiteral("Intercepted redirect URI from browser"); - QUrlQuery query(url); QVariantMap varmap; for (const auto item : query.queryItems()) { varmap[item.first] = item.second; } - mOAuth2.authorizationCallbackReceived(varmap); + return varmap; +} + +void EwsOAuthPrivate::redirectUriIntercepted(const QUrl &url) +{ + qCDebugNC(EWSCLI_LOG) << QStringLiteral("Intercepted redirect URI from browser: ") << url; + mWebView.stop(); mWebDialog->hide(); + +#ifdef HAVE_QCA + Q_Q(EwsOAuth); + if (url.toString(QUrl::RemoveQuery) == pkeyRedirectUri) { + qCDebugNC(EWSCLI_LOG) << QStringLiteral("Found PKeyAuth URI"); + + auto pkeyAuthJob = new EwsPKeyAuthJob(url, q->mPKeyCertFile, q->mPKeyKeyFile, mPKeyPassword, this); + + connect(pkeyAuthJob, &KJob::result, this, &EwsOAuthPrivate::pkeyAuthResult); + + pkeyAuthJob->start(); + + return; + } +#endif + mOAuth2.authorizationCallbackReceived(queryToVarmap(url)); +} + +#ifdef HAVE_QCA +void EwsOAuthPrivate::pkeyAuthResult(KJob *j) +{ + EwsPKeyAuthJob *job = qobject_cast(j); + + qCDebugNC(EWSCLI_LOG) << QStringLiteral("PKeyAuth result: %1").arg(job->error()); + QVariantMap varmap; + if (job->error() == 0) { + varmap = queryToVarmap(job->resultUri()); + } else { + varmap[QStringLiteral("error")] = job->errorString(); + } + mOAuth2.authorizationCallbackReceived(varmap); } +#endif void EwsOAuthPrivate::granted() { Q_Q(EwsOAuth); qCInfoNC(EWSCLI_LOG) << QStringLiteral("Authentication succeeded"); mAuthenticated = true; QMap map; map[accessTokenMapKey] = mOAuth2.token(); #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) map[refreshTokenMapKey] = mOAuth2.refreshToken(); #endif Q_EMIT q->setWalletMap(map); Q_EMIT q->authSucceeded(); } void EwsOAuthPrivate::error(const QString &error, const QString &errorDescription, const QUrl &uri) { Q_Q(EwsOAuth); Q_UNUSED(uri); mAuthenticated = false; #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) mOAuth2.setRefreshToken(QString()); #endif qCInfoNC(EWSCLI_LOG) << QStringLiteral("Authentication failed: ") << error << errorDescription; Q_EMIT q->authFailed(error); } EwsOAuth::EwsOAuth(QObject *parent, const QString &email, const QString &appId, const QString &redirectUri) : EwsAbstractAuth(parent), d_ptr(new EwsOAuthPrivate(this, email, appId, redirectUri)) { } EwsOAuth::~EwsOAuth() { } void EwsOAuth::init() { requestWalletMap(); } bool EwsOAuth::getAuthData(QString &username, QString &password, QStringList &customHeaders) { Q_D(const EwsOAuth); Q_UNUSED(username); Q_UNUSED(password); if (d->mAuthenticated) { customHeaders.append(QStringLiteral("Authorization: Bearer ") + d->mOAuth2.token()); return true; } else { return false; } } void EwsOAuth::notifyRequestAuthFailed() { Q_D(EwsOAuth); d->mOAuth2.setToken(QString()); d->mAuthenticated = false; EwsAbstractAuth::notifyRequestAuthFailed(); } bool EwsOAuth::authenticate(bool interactive) { Q_D(EwsOAuth); return d->authenticate(interactive); } const QString &EwsOAuth::reauthPrompt() const { static const QString prompt = i18nc("@info", "Microsoft Exchange credentials for the account %1 are no longer valid. You need to authenticate in order to continue using it."); return prompt; } const QString &EwsOAuth::authFailedPrompt() const { static const QString prompt = i18nc("@info", "Failed to obtain credentials for Microsoft Exchange account %1. Please update it in the account settings page."); return prompt; } void EwsOAuth::walletPasswordRequestFinished(const QString &password) { Q_UNUSED(password); } void EwsOAuth::walletMapRequestFinished(const QMap &map) { Q_D(EwsOAuth); +#ifdef HAVE_QCA + if (map.contains(pkeyPasswordMapKey)) { + d->mPKeyPassword = map[pkeyPasswordMapKey]; + } +#endif #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) if (map.contains(refreshTokenMapKey)) { d->mOAuth2.setRefreshToken(map[refreshTokenMapKey]); } #endif if (map.contains(accessTokenMapKey)) { d->mOAuth2.setToken(map[accessTokenMapKey]); d->mAuthenticated = true; Q_EMIT authSucceeded(); } else { Q_EMIT authFailed(QStringLiteral("Access token request failed")); } } #include "ewsoauth.moc" diff --git a/resources/ews/test/unittests/ewsoauth_ut_mock.cpp b/resources/ews/test/unittests/ewsoauth_ut_mock.cpp index be4bc7dd1..3fbe44b34 100644 --- a/resources/ews/test/unittests/ewsoauth_ut_mock.cpp +++ b/resources/ews/test/unittests/ewsoauth_ut_mock.cpp @@ -1,455 +1,477 @@ /* Copyright (C) 2018 Krzysztof Nowicki This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ewsoauth_ut_mock.h" Q_LOGGING_CATEGORY(EWSCLI_LOG, "org.kde.pim.ews.client", QtInfoMsg) namespace Mock { QPointer QWebEngineView::instance; QPointer QOAuth2AuthorizationCodeFlow::instance; QUrl QWebEngineUrlRequestJob::requestUrl() const { return mUrl; } QUrl QWebEngineUrlRequestInfo::requestUrl() const { return mUrl; } void QWebEngineUrlRequestInfo::block(bool) { mBlocked = true; } QWebEngineUrlRequestInterceptor::QWebEngineUrlRequestInterceptor(QObject *parent) : QObject(parent) { } QWebEngineUrlRequestInterceptor::~QWebEngineUrlRequestInterceptor() { } QWebEngineUrlSchemeHandler::QWebEngineUrlSchemeHandler(QObject *parent) : QObject(parent) { } QWebEngineUrlSchemeHandler::~QWebEngineUrlSchemeHandler() { } QWebEngineProfile::QWebEngineProfile(QObject *parent) : QObject(parent), mInterceptor(nullptr), mHandler(nullptr) { } QWebEngineProfile::~QWebEngineProfile() { } void QWebEngineProfile::setHttpUserAgent(const QString &ua) { mUserAgent = ua; } void QWebEngineProfile::setRequestInterceptor(QWebEngineUrlRequestInterceptor *interceptor) { mInterceptor = interceptor; } void QWebEngineProfile::installUrlSchemeHandler(QByteArray const &scheme, QWebEngineUrlSchemeHandler *handler) { mScheme = scheme; mHandler = handler; } QWebEnginePage::QWebEnginePage(QWebEngineProfile *profile, QObject *parent) : QObject(parent), mProfile(profile) { connect(profile, &QWebEngineProfile::logEvent, this, &QWebEnginePage::logEvent); } QWebEnginePage::~QWebEnginePage() { } QWebEngineView::QWebEngineView(QWidget *parent) : QWidget(parent), mPage(nullptr) { if (!instance) { instance = this; } else { qDebug() << "QWebEngineView instance already exists!"; } } QWebEngineView::~QWebEngineView() { } void QWebEngineView::load(const QUrl &url) { Q_EMIT logEvent(QStringLiteral("LoadWebPage:") + url.toString()); simulatePageLoad(url); QVariantMap params; if (mAuthFunction) { mAuthFunction(url, params); simulatePageLoad(QUrl(mRedirectUri + QStringLiteral("?") + QOAuth2AuthorizationCodeFlow::mapToSortedQuery(params).toString())); } else { qWarning() << "No authentication callback defined"; } } void QWebEngineView::simulatePageLoad(const QUrl &url) { if (mPage && mPage->mProfile && mPage->mProfile->mInterceptor) { QWebEngineUrlRequestInfo info(url, this); Q_EMIT logEvent(QStringLiteral("InterceptRequest:") + url.toString()); mPage->mProfile->mInterceptor->interceptRequest(info); Q_EMIT logEvent(QStringLiteral("InterceptRequestBlocked:%1").arg(info.mBlocked)); } else { qWarning() << "Cannot reach to request interceptor"; } } void QWebEngineView::setPage(QWebEnginePage *page) { mPage = page; connect(page, &QWebEnginePage::logEvent, this, &QWebEngineView::logEvent); } void QWebEngineView::stop() { } void QWebEngineView::setAuthFunction(const AuthFunc &func) { mAuthFunction = func; } void QWebEngineView::setRedirectUri(const QString &uri) { mRedirectUri = uri; } QNetworkReply::NetworkError QNetworkReply::error() const { return NoError; } QVariant QNetworkReply::header(QNetworkRequest::KnownHeaders header) const { return mHeaders[header]; } QAbstractOAuthReplyHandler::QAbstractOAuthReplyHandler(QObject *parent) : QObject(parent) { } QAbstractOAuthReplyHandler::~QAbstractOAuthReplyHandler() { } QAbstractOAuth::QAbstractOAuth(QObject *parent) : QObject(parent), mStatus(Status::NotAuthenticated) { } void QAbstractOAuth::setReplyHandler(QAbstractOAuthReplyHandler *handler) { mReplyHandler = handler; } void QAbstractOAuth::setAuthorizationUrl(const QUrl &url) { mAuthUrl = url; } void QAbstractOAuth::setClientIdentifier(const QString &identifier) { mClientId = identifier; } void QAbstractOAuth::setModifyParametersFunction(const std::function*)> &func) { mModifyParamsFunc = func; } QString QAbstractOAuth::token() const { return mToken; } void QAbstractOAuth::setToken(const QString &token) { mToken = token; } QAbstractOAuth::Status QAbstractOAuth::status() const { return mStatus; } QAbstractOAuth2::QAbstractOAuth2(QObject *parent) : QAbstractOAuth(parent) { } QString QAbstractOAuth2::refreshToken() const { return mRefreshToken; } void QAbstractOAuth2::setRefreshToken(const QString &token) { mRefreshToken = token; } QOAuth2AuthorizationCodeFlow::QOAuth2AuthorizationCodeFlow(QObject *parent) : QAbstractOAuth2(parent) { if (!instance) { instance = this; } else { qDebug() << "QOAuth2AuthorizationCodeFlow instance already exists!"; } } QOAuth2AuthorizationCodeFlow::~QOAuth2AuthorizationCodeFlow() { } void QOAuth2AuthorizationCodeFlow::setAccessTokenUrl(const QUrl &url) { mTokenUrl = url; } void QOAuth2AuthorizationCodeFlow::grant() { QMap map; map[QStringLiteral("response_type")] = QStringLiteral("code"); map[QStringLiteral("client_id")] = QUrl::toPercentEncoding(mClientId); map[QStringLiteral("redirect_uri")] = QUrl::toPercentEncoding(mReplyHandler->callback()); map[QStringLiteral("scope")] = QString(); map[QStringLiteral("state")] = mState; Q_EMIT logEvent(QStringLiteral("ModifyParams:RequestingAuthorization:") + mapToSortedQuery(map).toString()); if (mModifyParamsFunc) { mModifyParamsFunc(Stage::RequestingAuthorization, &map); } mResource = QUrl::fromPercentEncoding(map[QStringLiteral("resource")].toByteArray()); QUrl url(mAuthUrl); url.setQuery(mapToSortedQuery(map)); Q_EMIT logEvent(QStringLiteral("AuthorizeWithBrowser:") + url.toString()); connect(this, &QAbstractOAuth2::authorizationCallbackReceived, this, &QOAuth2AuthorizationCodeFlow::authCallbackReceived, Qt::UniqueConnection); Q_EMIT authorizeWithBrowser(url); } void QOAuth2AuthorizationCodeFlow::refreshAccessToken() { mStatus = Status::RefreshingToken; doRefreshAccessToken(); } void QOAuth2AuthorizationCodeFlow::doRefreshAccessToken() { QMap map; map[QStringLiteral("grant_type")] = QStringLiteral("authorization_code"); map[QStringLiteral("code")] = QUrl::toPercentEncoding(mRefreshToken); map[QStringLiteral("client_id")] = QUrl::toPercentEncoding(mClientId); map[QStringLiteral("redirect_uri")] = QUrl::toPercentEncoding(mReplyHandler->callback()); Q_EMIT logEvent(QStringLiteral("ModifyParams:RequestingAccessToken:") + mapToSortedQuery(map).toString()); if (mModifyParamsFunc) { mModifyParamsFunc(Stage::RequestingAccessToken, &map); } connect(mReplyHandler, &QAbstractOAuthReplyHandler::tokensReceived, this, &QOAuth2AuthorizationCodeFlow::tokenCallbackReceived, Qt::UniqueConnection); connect(mReplyHandler, &QAbstractOAuthReplyHandler::replyDataReceived, this, &QOAuth2AuthorizationCodeFlow::replyDataCallbackReceived, Qt::UniqueConnection); if (mTokenFunc) { QNetworkReply reply(this); QString data; reply.mError = mTokenFunc(data, reply.mHeaders); reply.setData(data.toUtf8()); reply.open(QIODevice::ReadOnly); Q_EMIT logEvent(QStringLiteral("NetworkReplyFinished:") + data); mReplyHandler->networkReplyFinished(&reply); } else { qWarning() << "No token function defined"; } } QUrlQuery QOAuth2AuthorizationCodeFlow::mapToSortedQuery(QMap const &map) { QUrlQuery query; QStringList keys = map.keys(); keys.sort(); for (const auto key : keys) { query.addQueryItem(key, map[key].toString()); } return query; } void QOAuth2AuthorizationCodeFlow::authCallbackReceived(QMap const ¶ms) { Q_EMIT logEvent(QStringLiteral("AuthorizatioCallbackReceived:") + mapToSortedQuery(params).toString()); mRefreshToken = params[QStringLiteral("code")].toString(); if (!mRefreshToken.isEmpty()) { mStatus = Status::TemporaryCredentialsReceived; doRefreshAccessToken(); } else { Q_EMIT error(QString(), QString(), QUrl()); } } void QOAuth2AuthorizationCodeFlow::tokenCallbackReceived(const QVariantMap &tokens) { Q_EMIT logEvent(QStringLiteral("TokenCallback:") + mapToSortedQuery(tokens).toString()); mToken = tokens["access_token"].toString(); mRefreshToken = tokens["refresh_token"].toString(); mStatus = Status::Granted; Q_EMIT granted(); } void QOAuth2AuthorizationCodeFlow::replyDataCallbackReceived(const QByteArray &data) { Q_EMIT logEvent(QStringLiteral("ReplyDataCallback:") + data); } QString QOAuth2AuthorizationCodeFlow::redirectUri() const { return mReplyHandler->callback(); } void QOAuth2AuthorizationCodeFlow::setTokenFunction(const TokenFunc &func) { mTokenFunc = func; } void QOAuth2AuthorizationCodeFlow::setState(const QString &state) { mState = state; } QString browserDisplayRequestString() { return QStringLiteral("BrowserDisplayRequest"); } QString modifyParamsAuthString(const QString &clientId, const QString &returnUri, const QString &state) { return QStringLiteral("ModifyParams:RequestingAuthorization:client_id=%1&redirect_uri=%2&response_type=code&scope&state=%3") .arg(QString::fromUtf8(QUrl::toPercentEncoding(clientId)), QUrl::toPercentEncoding(returnUri), QUrl::toPercentEncoding(state)); } QString authUrlString(const QString &authUrl, const QString &clientId, const QString &returnUri, const QString &email, const QString &resource, const QString &state) { return QStringLiteral("%1?client_id=%2&login_hint=%3&prompt=login&redirect_uri=%4&resource=%5&response_type=code&scope&state=%6") .arg(authUrl, QUrl::toPercentEncoding(clientId), email, QUrl::toPercentEncoding(returnUri), QUrl::toPercentEncoding(resource), QUrl::toPercentEncoding(state)); } QString authorizeWithBrowserString(const QString &url) { return QStringLiteral("AuthorizeWithBrowser:") + url; } QString loadWebPageString(const QString &url) { return QStringLiteral("LoadWebPage:") + url; } QString interceptRequestString(const QString &url) { return QStringLiteral("InterceptRequest:") + url; } QString interceptRequestBlockedString(bool blocked) { return QStringLiteral("InterceptRequestBlocked:%1").arg(blocked); } QString authorizationCallbackReceivedString(const QString &code) { return QStringLiteral("AuthorizatioCallbackReceived:code=%1").arg(code); } QString modifyParamsTokenString(const QString &clientId, const QString &returnUri, const QString &code) { return QStringLiteral("ModifyParams:RequestingAccessToken:client_id=%1&code=%2&grant_type=authorization_code&redirect_uri=%3") .arg(QString::fromUtf8(QUrl::toPercentEncoding(clientId)), QUrl::toPercentEncoding(code), QUrl::toPercentEncoding(returnUri)); } QString networkReplyFinishedString(const QString &data) { return QStringLiteral("NetworkReplyFinished:") + data; } QString replyDataCallbackString(const QString &data) { return QStringLiteral("ReplyDataCallback:") + data; } QString tokenCallbackString(const QString &accessToken, const QString &refreshToken, const QString &idToken, quint64 time, unsigned int tokenLifetime, unsigned int extTokenLifetime, const QString &resource) { return QStringLiteral("TokenCallback:access_token=%1&expires_in=%2&expires_on=%3&ext_expires_in=%4&foci=1&id_token=%5¬_before=%6&refresh_token=%7&resource=%8&scope=ReadWrite.All&token_type=Bearer") .arg(accessToken).arg(tokenLifetime).arg(time + tokenLifetime).arg(extTokenLifetime).arg(idToken).arg(time) .arg(refreshToken).arg(resource); } QString requestWalletMapString() { return QStringLiteral("RequestWalletMap"); } +const QString &KJob::errorString() const +{ + static const QString empty; + return empty; +} + +EwsPKeyAuthJob::EwsPKeyAuthJob(const QUrl &pkeyUri, const QString &certFile, const QString &keyFile, const QString &keyPassword, + QObject *parent) + : KJob(parent) +{ + Q_UNUSED(pkeyUri); + Q_UNUSED(certFile); + Q_UNUSED(keyFile); + Q_UNUSED(keyPassword); +} + +const QUrl &EwsPKeyAuthJob::resultUri() const +{ + static const QUrl empty; + return empty; +} + } diff --git a/resources/ews/test/unittests/ewsoauth_ut_mock.h b/resources/ews/test/unittests/ewsoauth_ut_mock.h index 23419dad2..ff27a9317 100644 --- a/resources/ews/test/unittests/ewsoauth_ut_mock.h +++ b/resources/ews/test/unittests/ewsoauth_ut_mock.h @@ -1,297 +1,322 @@ /* Copyright (C) 2018 Krzysztof Nowicki This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef EWSOAUTH_UT_MOCK_H #define EWSOAUTH_UT_MOCK_H #include #include #include #include #include #include #include #include #include #include #include #include Q_DECLARE_LOGGING_CATEGORY(EWSCLI_LOG) namespace Mock { class QWebEngineUrlRequestJob : public QObject { Q_OBJECT public: QWebEngineUrlRequestJob(const QUrl &url, QObject *parent) : QObject(parent), mUrl(url) {}; ~QWebEngineUrlRequestJob() override = default; QUrl requestUrl() const; QUrl mUrl; }; class QWebEngineUrlRequestInfo : public QObject { Q_OBJECT public: QWebEngineUrlRequestInfo(const QUrl &url, QObject *parent) : QObject(parent), mBlocked(false), mUrl(url) {}; ~QWebEngineUrlRequestInfo() override = default; QUrl requestUrl() const; void block(bool shouldBlock); bool mBlocked; QUrl mUrl; }; class QWebEngineUrlRequestInterceptor : public QObject { Q_OBJECT public: QWebEngineUrlRequestInterceptor(QObject *parent); ~QWebEngineUrlRequestInterceptor() override; virtual void interceptRequest(QWebEngineUrlRequestInfo &info) = 0; }; class QWebEngineUrlSchemeHandler : public QObject { Q_OBJECT public: QWebEngineUrlSchemeHandler(QObject *parent); ~QWebEngineUrlSchemeHandler() override; virtual void requestStarted(QWebEngineUrlRequestJob *request) = 0; }; class QWebEngineProfile : public QObject { Q_OBJECT public: QWebEngineProfile(QObject *parent = nullptr); ~QWebEngineProfile() override; void setHttpUserAgent(const QString &ua); void setRequestInterceptor(QWebEngineUrlRequestInterceptor *interceptor); void installUrlSchemeHandler(QByteArray const &scheme, QWebEngineUrlSchemeHandler *handler); Q_SIGNALS: void logEvent(const QString &event); public: QString mUserAgent; QWebEngineUrlRequestInterceptor *mInterceptor; QString mScheme; QWebEngineUrlSchemeHandler *mHandler; }; class QWebEnginePage : public QObject { Q_OBJECT public: QWebEnginePage(QWebEngineProfile *profile, QObject *parent = nullptr); ~QWebEnginePage() override; Q_SIGNALS: void logEvent(const QString &event); public: QWebEngineProfile *mProfile; }; class QWebEngineView : public QWidget { Q_OBJECT public: typedef std::function AuthFunc; QWebEngineView(QWidget *parent); ~QWebEngineView() override; void load(const QUrl &url); void setPage(QWebEnginePage *page); void stop(); void setAuthFunction(const AuthFunc &func); void setRedirectUri(const QString &uri); static QPointer instance; Q_SIGNALS: void logEvent(const QString &event); protected: void simulatePageLoad(const QUrl &url); QWebEnginePage *mPage; QString mRedirectUri; AuthFunc mAuthFunction; }; class QNetworkRequest { public: enum KnownHeaders { ContentTypeHeader }; }; class QNetworkReply : public QBuffer { Q_OBJECT public: enum NetworkError { NoError = 0, }; Q_ENUM(NetworkError) QNetworkReply(QObject *parent) : QBuffer(parent) {}; ~QNetworkReply() override = default; NetworkError error() const; QVariant header(QNetworkRequest::KnownHeaders header) const; QMap mHeaders; NetworkError mError; }; class QAbstractOAuthReplyHandler : public QObject { Q_OBJECT public: QAbstractOAuthReplyHandler(QObject *parent); ~QAbstractOAuthReplyHandler() override; virtual QString callback() const = 0; virtual void networkReplyFinished(QNetworkReply *reply) = 0; Q_SIGNALS: void replyDataReceived(const QByteArray &data); void tokensReceived(const QVariantMap &tokens); }; class QAbstractOAuth : public QObject { Q_OBJECT public: Q_ENUMS(Stage) enum class Stage { RequestingTemporaryCredentials, RequestingAuthorization, RequestingAccessToken, RefreshingAccessToken }; Q_ENUMS(Status) enum class Status { NotAuthenticated, TemporaryCredentialsReceived, Granted, RefreshingToken }; QAbstractOAuth(QObject *parent); ~QAbstractOAuth() override = default; void setReplyHandler(QAbstractOAuthReplyHandler *handler); void setAuthorizationUrl(const QUrl &url); void setClientIdentifier(const QString &identifier); void setModifyParametersFunction(const std::function*)> &func); QString token() const; void setToken(const QString &token); Status status() const; Q_SIGNALS: void authorizeWithBrowser(const QUrl &url); void granted(); void logEvent(const QString &event); protected: QAbstractOAuthReplyHandler *mReplyHandler; QUrl mAuthUrl; QString mClientId; std::function*)> mModifyParamsFunc; QString mToken; QString mRefreshToken; QUrl mTokenUrl; QString mResource; Status mStatus; }; class QAbstractOAuth2 : public QAbstractOAuth { Q_OBJECT public: QAbstractOAuth2(QObject *parent); ~QAbstractOAuth2() override = default; QString refreshToken() const; void setRefreshToken(const QString &token); Q_SIGNALS: void authorizationCallbackReceived(QMap const ¶ms); void error(const QString &error, const QString &errorDescription, const QUrl &uri); }; class QOAuth2AuthorizationCodeFlow : public QAbstractOAuth2 { Q_OBJECT public: typedef std::function &)> TokenFunc; QOAuth2AuthorizationCodeFlow(QObject *parent = nullptr); ~QOAuth2AuthorizationCodeFlow() override; void setAccessTokenUrl(const QUrl &url); void grant(); void refreshAccessToken(); QString redirectUri() const; void setTokenFunction(const TokenFunc &func); void setState(const QString &state); static QUrlQuery mapToSortedQuery(QMap const &map); static QPointer instance; protected: void authCallbackReceived(QMap const ¶ms); void replyDataCallbackReceived(const QByteArray &data); void tokenCallbackReceived(const QVariantMap &tokens); void doRefreshAccessToken(); TokenFunc mTokenFunc; QString mState; }; +class KJob : public QObject +{ + Q_OBJECT +public: + explicit KJob(QObject *) {}; + ~KJob() override = default; + + int error() const { return 0; }; + const QString &errorString() const; +Q_SIGNALS: + void result(KJob *job); +}; + +class EwsPKeyAuthJob : public KJob +{ + Q_OBJECT +public: + EwsPKeyAuthJob(const QUrl &pkeyUri, const QString &certFile, const QString &keyFile, const QString &keyPassword, + QObject *parent); + ~EwsPKeyAuthJob() override = default; + void start() {}; + + const QUrl &resultUri() const; +}; + QString browserDisplayRequestString(); QString modifyParamsAuthString(const QString &clientId, const QString &returnUri, const QString &state); QString authUrlString(const QString &authUrl, const QString &clientId, const QString &returnUri, const QString &email, const QString &resource, const QString &state); QString authorizeWithBrowserString(const QString &url); QString loadWebPageString(const QString &url); QString interceptRequestString(const QString &url); QString interceptRequestBlockedString(bool blocked); QString authorizationCallbackReceivedString(const QString &code); QString modifyParamsTokenString(const QString &clientId, const QString &returnUri, const QString &code); QString networkReplyFinishedString(const QString &data); QString replyDataCallbackString(const QString &data); QString tokenCallbackString(const QString &accessToken, const QString &refreshToken, const QString &idToken, quint64 time, unsigned int tokenLifetime, unsigned int extTokenLifetime, const QString &resource); QString requestWalletMapString(); } #endif /* EWSOAUTH_UT_MOCK_H */