Commit 81864271 authored by Krzysztof Nowicki's avatar Krzysztof Nowicki Committed by Laurent Montel
Browse files

Fix PKeyAuth after changes on Microsoft side



Initially when PKey authentication was implemented, the OAuth2 flow
was handled by the browser for the first steps until a redirection to
PKey authentication was requested, after which the browser was closed
and the flow continued GUI-less. This was possible as the PKey
authentication was the last step and sending a request to the
SubmitUrl resulted in a redirect to the final OAuth2 redirect URL and
ended the authentication flow.

Some time ago Microsoft made a change to this flow and introduced
another confirmation page after the PKey authentication step. This
meant that the PKey authentication request could not be done outside
of the browser any more, as a request to the SubmitUrl would now
return an actual web page, which demanded further action from the
user.

To fix this problem the PKey authentication logic was moved to the
browser and wired into the request interceptor.

Unfortunately a Qt bug (reported as QTBUG-88861) was encountered,
which prevented the Authorization header from being set on a
redirected request. This bug has been worked around using an external
redirection issued by a minimalistic web server implemented in the
resource itself.
Signed-off-by: Krzysztof Nowicki's avatarKrzysztof Nowicki <krissn@op.pl>
parent e9a0697b
......@@ -30,6 +30,8 @@ using namespace Mock;
#include "ewsclient_debug.h"
#include <KLocalizedString>
#include <QJsonDocument>
#include <QTcpServer>
#include <QTcpSocket>
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"));
......@@ -88,20 +90,24 @@ class EwsOAuthRequestInterceptor final : public QWebEngineUrlRequestInterceptor
{
Q_OBJECT
public:
EwsOAuthRequestInterceptor(QObject *parent, const QString &redirectUri)
: QWebEngineUrlRequestInterceptor(parent)
, mRedirectUri(redirectUri)
{
}
EwsOAuthRequestInterceptor(QObject *parent, const QString &redirectUri, const QString &clientId);
~EwsOAuthRequestInterceptor() override = default;
void interceptRequest(QWebEngineUrlRequestInfo &info) override;
public Q_SLOTS:
void setPKeyAuthInputArguments(const QString &pkeyCertFile, const QString &pkeyKeyFile, const QString &pkeyPassword);
Q_SIGNALS:
void redirectUriIntercepted(const QUrl &url);
private:
const QString mRedirectUri;
const QString mClientId;
QString mPKeyCertFile;
QString mPKeyKeyFile;
QString mPKeyPassword;
QString mPKeyAuthResponse;
QString mPKeyAuthSubmitUrl;
QTcpServer mRedirectServer;
};
class EwsOAuthPrivate final : public QObject
......@@ -199,13 +205,65 @@ void EwsOAuthReplyHandler::networkReplyFinished(QNetworkReply *reply)
Q_EMIT tokensReceived(tokens);
}
EwsOAuthRequestInterceptor::EwsOAuthRequestInterceptor(QObject *parent, const QString &redirectUri, const QString &clientId)
: QWebEngineUrlRequestInterceptor(parent)
, mRedirectUri(redirectUri)
, mClientId(clientId)
{
/* Workaround for QTBUG-88861 - start a trivial HTTP server to serve the redirect.
* The redirection must be done using JavaScript as HTTP-protocol redirections (301, 302)
* do not cause QWebEngineUrlRequestInterceptor::interceptRequest() to fire. */
connect(&mRedirectServer, &QTcpServer::newConnection, this, [this]() {
const auto socket = mRedirectServer.nextPendingConnection();
if (socket) {
connect(socket, &QIODevice::readyRead, this, [this, socket]() {
const auto response = QStringLiteral(
"HTTP/1.1 200 OK\n\n<!DOCTYPE html>\n<html><body><p>You will be redirected "
"shortly.</p><script>window.location.href=\"%1\";</script></body></html>\n")
.arg(mPKeyAuthSubmitUrl);
socket->write(response.toLocal8Bit());
});
connect(socket, &QIODevice::bytesWritten, this, [socket]() {
socket->deleteLater();
});
}
});
mRedirectServer.listen(QHostAddress::LocalHost);
}
void EwsOAuthRequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo &info)
{
const auto url = info.requestUrl();
qCDebugNC(EWSCLI_LOG) << QStringLiteral("Intercepted browser navigation to ") << url;
if ((url.toString(QUrl::RemoveQuery) == mRedirectUri) || (url.toString(QUrl::RemoveQuery) == pkeyRedirectUri)) {
if (url.toString(QUrl::RemoveQuery) == pkeyRedirectUri) {
qCDebugNC(EWSCLI_LOG) << QStringLiteral("Found PKeyAuth URI");
auto pkeyAuthJob = new EwsPKeyAuthJob(url, mPKeyCertFile, mPKeyKeyFile, mPKeyPassword, this);
mPKeyAuthResponse = pkeyAuthJob->getAuthHeader();
QUrlQuery query(url.query());
if (!mPKeyAuthResponse.isEmpty() && query.hasQueryItem(QLatin1String("SubmitUrl"))) {
mPKeyAuthSubmitUrl = query.queryItemValue(QLatin1String("SubmitUrl"), QUrl::FullyDecoded);
/* Workaround for QTBUG-88861
* When the PKey authentication starts, the server issues a request for a "special" PKey URL
* containing the challenge arguments and expects that a response is composed and the browser
* then redirected to the URL found in the SubmitUrl argument with the response passed using
* the HTTP Authorization header.
* Unfortunately the Qt WebEngine request interception mechanism will ignore custom HTTP headers
* when issuing a redirect.
* To work around that the EWS Resource launches a minimalistic HTTP server to serve a
* simple webpage with redirection. This way the redirection happens externally to the
* Qt Web Engine and the submit URL can be captured by the request interceptor again, this time
* only to add the missing Authorization header. */
qCDebugNC(EWSCLI_LOG) << QStringLiteral("Redirecting to PKey SubmitUrl via QTBUG-88861 workaround");
info.redirect(QUrl(QStringLiteral("http://localhost:%1/").arg(mRedirectServer.serverPort())));
} else {
qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to retrieve PKey authorization header");
}
} else if (url.toString(QUrl::RemoveQuery) == mPKeyAuthSubmitUrl) {
info.setHttpHeader(QByteArray("Authorization"), mPKeyAuthResponse.toLocal8Bit());
} else if (url.toString(QUrl::RemoveQuery) == mRedirectUri) {
qCDebug(EWSCLI_LOG) << QStringLiteral("Found redirect URI - blocking request");
Q_EMIT redirectUriIntercepted(url);
......@@ -213,13 +271,20 @@ void EwsOAuthRequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo &info
}
}
void EwsOAuthRequestInterceptor::setPKeyAuthInputArguments(const QString &pkeyCertFile, const QString &pkeyKeyFile, const QString &pkeyPassword)
{
mPKeyCertFile = pkeyCertFile;
mPKeyKeyFile = pkeyKeyFile;
mPKeyPassword = pkeyPassword;
}
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)
, mRequestInterceptor(this, redirectUri, appId)
, mEmail(email)
, mRedirectUri(redirectUri)
, mAuthenticated(false)
......@@ -300,6 +365,8 @@ void EwsOAuthPrivate::authorizeWithBrowser(const QUrl &url)
}
mWebProfile.setHttpUserAgent(userAgent);
mRequestInterceptor.setPKeyAuthInputArguments(q->mPKeyCertFile, q->mPKeyKeyFile, mPKeyPassword);
mWebDialog = new QDialog(q->mAuthParentWidget);
mWebDialog->setObjectName(QStringLiteral("Akonadi EWS Resource - Authentication"));
mWebDialog->setWindowIcon(QIcon(QStringLiteral("akonadi-ews")));
......
......@@ -173,3 +173,26 @@ const QUrl &EwsPKeyAuthJob::resultUri() const
{
return mResultUri;
}
QString EwsPKeyAuthJob::getAuthHeader()
{
const QUrlQuery query(mPKeyUri);
QMap<QString, QString> params;
for (const auto &it : query.queryItems()) {
params[it.first.toLower()] = QUrl::fromPercentEncoding(it.second.toLatin1());
}
if (params.contains(QStringLiteral("submiturl")) && params.contains(QStringLiteral("nonce")) && params.contains(QStringLiteral("certauthorities"))
&& params.contains(QStringLiteral("context")) && params.contains(QStringLiteral("version"))) {
const auto respToken = buildAuthResponse(params);
if (!respToken.isEmpty()) {
return QLatin1String("PKeyAuth AuthToken=\"%1\",Context=\"%2\",Version=\"1.0\"")
.arg(QString::fromLatin1(respToken), params[QStringLiteral("context")]);
} else {
return {};
}
} else {
return {};
}
}
......@@ -24,6 +24,8 @@ public:
const QUrl &resultUri() const;
void start() override;
QString getAuthHeader();
private:
QByteArray buildAuthResponse(const QMap<QString, QString> &params);
void sendAuthRequest(const QByteArray &respToken, const QUrl &submitUrl, const QString &context);
......
......@@ -29,6 +29,14 @@ void QWebEngineUrlRequestInfo::block(bool)
mBlocked = true;
}
void QWebEngineUrlRequestInfo::redirect(const QUrl &)
{
}
void QWebEngineUrlRequestInfo::setHttpHeader(const QByteArray &header, const QByteArray &value)
{
}
QWebEngineUrlRequestInterceptor::QWebEngineUrlRequestInterceptor(QObject *parent)
: QObject(parent)
{
......@@ -484,4 +492,9 @@ const QUrl &EwsPKeyAuthJob::resultUri() const
static const QUrl empty;
return empty;
}
QString EwsPKeyAuthJob::getAuthHeader()
{
return QString();
}
}
......@@ -56,6 +56,8 @@ public:
QUrl requestUrl() const;
void block(bool shouldBlock);
void setHttpHeader(const QByteArray &name, const QByteArray &value);
void redirect(const QUrl &url);
bool mBlocked;
QUrl mUrl;
......@@ -311,6 +313,7 @@ public:
}
const QUrl &resultUri() const;
QString getAuthHeader();
};
QString browserDisplayRequestString();
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment