7#include "outlookoauthtokenrequester.h"
8#include "mailtransport_debug.h"
10#include <QCryptographicHash>
11#include <QDesktopServices>
12#include <QHostAddress>
13#include <QJsonDocument>
16#include <QNetworkAccessManager>
17#include <QNetworkReply>
18#include <QNetworkRequest>
19#include <QRandomGenerator64>
25using namespace MailTransport;
27TokenResult::TokenResult(ErrorCode errorCode,
const QString &errorText)
28 : mErrorCode(errorCode)
29 , mErrorText(errorText)
33TokenResult::TokenResult(
const QString &accessToken,
const QString &refreshToken)
34 : mAccessToken(accessToken)
35 , mRefreshToken(refreshToken)
39QString TokenResult::accessToken()
const
44QString TokenResult::refreshToken()
const
49bool TokenResult::hasError()
const
51 return mErrorCode != 0;
54TokenResult::ErrorCode TokenResult::errorCode()
const
59QString TokenResult::errorText()
const
64Q_DECLARE_METATYPE(TokenResult);
68namespace MailTransport
77 mVerifier = generateRandomString(128);
78 mChallenge = generateChallenge(mVerifier);
81 QString challenge()
const
86 QString verifier()
const
92 QString generateRandomString(std::size_t length)
94 static const char charset[] =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._`~";
99 for (
size_t i = 0; i < length; ++i) {
100 const int idx = generator.bounded(
static_cast<int>(
sizeof(charset) - 1));
106 QString generateChallenge(
const QString &verifier)
121OutlookOAuthTokenRequester::OutlookOAuthTokenRequester(
const QString &clientId,
const QString &tenantId,
const QStringList &scopes, QObject *parent)
123 , mClientId(clientId)
124 , mTenantId(tenantId)
126 , mPkce(std::make_unique<PKCE>())
130OutlookOAuthTokenRequester::~OutlookOAuthTokenRequester() =
default;
132void OutlookOAuthTokenRequester::requestToken(
const QString &usernameHint)
134 qCDebug(MAILTRANSPORT_LOG) <<
"Requesting new Outlook OAuth2 access token";
136 auto redirectUri = startLocalHttpServer();
137 if (!redirectUri.has_value()) {
138 Q_EMIT finished({TokenResult::InternalError, QStringLiteral(
"Failed to start local HTTP server to receive Outlook OAuth2 authorization code")});
140 mRedirectUri = *redirectUri;
142 QUrl url(QStringLiteral(
"https://login.microsoftonline.com/%1/oauth2/v2.0/authorize").arg(mTenantId));
143 QUrlQuery
query{{QStringLiteral(
"client_id"), mClientId},
144 {QStringLiteral(
"redirect_uri"), mRedirectUri.toString()},
145 {QStringLiteral(
"response_type"), QStringLiteral(
"code")},
146 {QStringLiteral(
"response_mode"), QStringLiteral(
"query")},
147 {QStringLiteral(
"scope"), mScopes.join(u
' ')},
148 {QStringLiteral(
"code_challenge"), mPkce->challenge()},
149 {QStringLiteral(
"code_challenge_method"), QStringLiteral(
"S256")}};
151 query.addQueryItem(QStringLiteral(
"login_hint"), usernameHint);
153 query.addQueryItem(QStringLiteral(
"prompt"), QStringLiteral(
"select_account"));
157 qCDebug(MAILTRANSPORT_LOG) <<
"Browser opened, waiting for Outlook OAuth2 authorization code...";
161void OutlookOAuthTokenRequester::refreshToken(
const QString &refreshToken)
163 qCDebug(MAILTRANSPORT_LOG) <<
"Refreshing Outlook OAuth2 access token";
165 QUrl url(QStringLiteral(
"https://login.microsoftonline.com/%1/oauth2/v2.0/token").arg(mTenantId));
167 QNetworkRequest request{url};
169 mNam = std::make_unique<QNetworkAccessManager>();
170 auto *reply = mNam->post(request,
171 QUrlQuery{{QStringLiteral(
"client_id"), mClientId},
172 {QStringLiteral(
"grant_type"), QStringLiteral(
"refresh_token")},
173 {QStringLiteral(
"scope"), mScopes.join(u
' ')},
174 {QStringLiteral(
"refresh_token"), refreshToken}}
178 handleTokenResponse(reply,
true);
182std::optional<QUrl> OutlookOAuthTokenRequester::startLocalHttpServer()
184 mHttpServer = std::make_unique<QTcpServer>();
189 qCDebug(MAILTRANSPORT_LOG) <<
"Local Outlook OAuth2 server listening on port" << mHttpServer->serverPort();
191 return QUrl(QStringLiteral(
"http://localhost:%1").arg(mHttpServer->serverPort()));
194void OutlookOAuthTokenRequester::handleNewConnection()
196 qCDebug(MAILTRANSPORT_LOG) <<
"New incoming connection from Outlook OAuth2";
197 mSocket = std::unique_ptr<QTcpSocket>(mHttpServer->nextPendingConnection());
201void OutlookOAuthTokenRequester::handleSocketReadyRead()
203 auto request = mSocket->readLine();
206 sendResponseToBrowserAndCloseSocket();
208 if (!request.startsWith(
"GET /?") && !request.endsWith(
"HTTP/1.1")) {
209 Q_EMIT finished({TokenResult::InvalidAuthorizationResponse, QStringLiteral(
"Invalid authorization response from server")});
214 request.remove(0,
sizeof(
"GET ") - 1);
215 request.truncate(request.size() -
sizeof(
" HTTP/1.1") - 1);
217 request.prepend(
"http://localhost");
221 if (!url.isValid()) {
222 qCWarning(MAILTRANSPORT_LOG) <<
"Failed to extract valid URL from initial HTTP request line from Outlook OAuth2:" << request;
223 Q_EMIT finished({TokenResult::InvalidAuthorizationResponse, QStringLiteral(
"Invalid authorization response from server")});
228 const QUrlQuery
query(url);
229 if (
query.hasQueryItem(QStringLiteral(
"error"))) {
230 const auto error =
query.queryItemValue(QStringLiteral(
"error"));
231 const auto errorDescription =
query.queryItemValue(QStringLiteral(
"error_description"));
232 qCWarning(MAILTRANSPORT_LOG) <<
"Authorization server returned error:" <<
error << errorDescription;
233 Q_EMIT finished({TokenResult::AuthorizationFailed, errorDescription});
237 const auto code =
query.queryItemValue(QStringLiteral(
"code"));
238 if (code.isEmpty()) {
239 qCWarning(MAILTRANSPORT_LOG) <<
"Failed to extract authorization code from Outlook OAuth2 response:" << request;
240 Q_EMIT finished({TokenResult::InvalidAuthorizationResponse, QStringLiteral(
"Invalid authorization response from server")});
244 qCDebug(MAILTRANSPORT_LOG) <<
"Extracted Outlook OAuth2 autorization token from response, requesting access token...";
245 requestIdToken(code);
248void OutlookOAuthTokenRequester::requestIdToken(
const QString &code)
250 QUrl url(QStringLiteral(
"https://login.microsoftonline.com/%1/oauth2/v2.0/token").arg(mTenantId));
252 QNetworkRequest request{url};
254 mNam = std::make_unique<QNetworkAccessManager>();
255 auto *reply = mNam->post(request,
256 QUrlQuery{{QStringLiteral(
"client_id"), mClientId},
257 {QStringLiteral(
"scope"), mScopes.join(u
' ')},
258 {QStringLiteral(
"code"), code},
259 {QStringLiteral(
"redirect_uri"), mRedirectUri.toString()},
260 {QStringLiteral(
"grant_type"), QStringLiteral(
"authorization_code")},
261 {QStringLiteral(
"code_verifier"), mPkce->verifier()}}
265 handleTokenResponse(reply);
267 qCDebug(MAILTRANSPORT_LOG) <<
"Requested Outlook OAuth2 access token, waiting for response...";
270void OutlookOAuthTokenRequester::handleTokenResponse(QNetworkReply *reply,
bool isTokenRefresh)
272 const auto responseData = reply->
readAll();
276 if (!response.isObject()) {
277 qCWarning(MAILTRANSPORT_LOG) <<
"Failed to parse token response:" << responseData;
278 Q_EMIT finished({TokenResult::InvalidAuthorizationResponse, QStringLiteral(
"Failed to parse token response")});
282 if (response[QStringView{u
"error"}].isString()) {
283 const auto error = response[QStringView{u
"error"}].toString();
284 const auto errorDescription = response[QStringView{u
"error_description"}].toString();
285 qCWarning(MAILTRANSPORT_LOG) <<
"Outlook OAuth2 authorization server returned error:" <<
error << errorDescription;
287 if (isTokenRefresh && error == u
"invalid_grant") {
288 qCDebug(MAILTRANSPORT_LOG) <<
"Outlook OAuth2 refresh token is invalid, requesting new token...";
291 Q_EMIT finished({TokenResult::AuthorizationFailed, errorDescription});
296 const auto accessToken = response[QStringView{u
"access_token"}].toString();
297 const auto refreshToken = response[QStringView{u
"refresh_token"}].toString();
299 qCDebug(MAILTRANSPORT_LOG) <<
"Received Outlook OAuth2 access and refresh tokens";
301 Q_EMIT finished({accessToken, refreshToken});
304void OutlookOAuthTokenRequester::sendResponseToBrowserAndCloseSocket()
306 const auto response =
307 "HTTP/1.1 200 OK\r\n"
308 "Content-Type: text/html; charset=utf-8\r\n"
309 "Connection: close\r\n"
312 "<head><title>KDE PIM Authorization</title></head>"
314 "<h1>You can close the browser window now and return to the application.</h1>"
318 mSocket->write(response);
322 mSocket.release()->deleteLater();
324 mHttpServer->close();
325 mHttpServer.release()->deleteLater();
327 qCDebug(MAILTRANSPORT_LOG) <<
"Sent HTTP OK response to browser and closed our local HTTP server.";
330#include "moc_outlookoauthtokenrequester.cpp"
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
QByteArray hash(QByteArrayView data, Algorithm method)
bool openUrl(const QUrl &url)
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QRandomGenerator securelySeeded()
QString & append(QChar ch)
QString fromLatin1(QByteArrayView str)
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
void reserve(qsizetype size)