diff options
author | Hannah von Reth <hannah.vonreth@owncloud.com> | 2020-01-16 19:27:50 +0300 |
---|---|---|
committer | Hannah von Reth <vonreth@kde.org> | 2020-01-24 16:00:43 +0300 |
commit | d0943439b7e6b6c001e3097471927358a14ad0cc (patch) | |
tree | 4ce0d3a1e5c4b7d556ec8ff2b84b57251cb1f376 /src/gui/creds | |
parent | ca09cba42c52ec6540361e7c499bc1fb4bd6aa5f (diff) |
Move oauth from ui -> libsync
Diffstat (limited to 'src/gui/creds')
-rw-r--r-- | src/gui/creds/oauth.cpp | 305 | ||||
-rw-r--r-- | src/gui/creds/oauth.h | 105 |
2 files changed, 0 insertions, 410 deletions
diff --git a/src/gui/creds/oauth.cpp b/src/gui/creds/oauth.cpp deleted file mode 100644 index b4b793afe..000000000 --- a/src/gui/creds/oauth.cpp +++ /dev/null @@ -1,305 +0,0 @@ -/* - * Copyright (C) by Olivier Goffart <ogoffart@woboq.com> - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License - * for more details. - */ - -#include <QDesktopServices> -#include <QNetworkReply> -#include <QTimer> -#include <QBuffer> -#include "account.h" -#include "creds/oauth.h" -#include <QJsonObject> -#include <QJsonDocument> -#include "theme.h" -#include "networkjobs.h" -#include "creds/httpcredentials.h" -#include <QRandomGenerator> - -namespace OCC { - -Q_LOGGING_CATEGORY(lcOauth, "sync.credentials.oauth", QtInfoMsg) - -OAuth::~OAuth() -{ -} - -static void httpReplyAndClose(QTcpSocket *socket, const char *code, const char *html, - const char *moreHeaders = nullptr) -{ - if (!socket) - return; // socket can have been deleted if the browser was closed - socket->write("HTTP/1.1 "); - socket->write(code); - socket->write("\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\nContent-Length: "); - socket->write(QByteArray::number(qstrlen(html))); - if (moreHeaders) { - socket->write("\r\n"); - socket->write(moreHeaders); - } - socket->write("\r\n\r\n"); - socket->write(html); - socket->disconnectFromHost(); - // We don't want that deleting the server too early prevent queued data to be sent on this socket. - // The socket will be deleted after disconnection because disconnected is connected to deleteLater - socket->setParent(nullptr); -} - -void OAuth::start() -{ - // Listen on the socket to get a port which will be used in the redirect_uri - if (!_server.listen(QHostAddress::LocalHost)) { - emit result(NotSupported, QString()); - return; - } - - _pkceCodeVerifier = generateRandomString(24); - ASSERT(_pkceCodeVerifier.size() == 128) - _state = generateRandomString(8); - - fetchWellKnown(); - - openBrowser(); - - QObject::connect(&_server, &QTcpServer::newConnection, this, [this] { - while (QPointer<QTcpSocket> socket = _server.nextPendingConnection()) { - QObject::connect(socket.data(), &QTcpSocket::disconnected, socket.data(), &QTcpSocket::deleteLater); - QObject::connect(socket.data(), &QIODevice::readyRead, this, [this, socket] { - const QByteArray peek = socket->peek(qMin(socket->bytesAvailable(), 4000LL)); //The code should always be within the first 4K - if (peek.indexOf('\n') < 0) - return; // wait until we find a \n - if (!peek.startsWith("GET /?")) { - httpReplyAndClose(socket, "404 Not Found", "<html><head><title>404 Not Found</title></head><body><center><h1>404 Not Found</h1></center></body></html>"); - return; - } - const int offset = 6; - const QUrlQuery args(peek.mid(offset, peek.indexOf(' ', offset) - offset)); - if (args.queryItemValue(QStringLiteral("state")) != _state) { - httpReplyAndClose(socket, "400 Bad Request", "<html><head><title>400 Bad Request</title></head><body><center><h1>400 Bad Request</h1></center></body></html>"); - return; - } - - const QUrl requestTokenUrl = _tokenEndpoint.isValid() - ? _tokenEndpoint - : Utility::concatUrlPath(_account->url(), QLatin1String("/index.php/apps/oauth2/api/v1/token")); - - QNetworkRequest req; - req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded; charset=UTF-8"); - - const QString basicAuth = QStringLiteral("%1:%2").arg( - Theme::instance()->oauthClientId(), Theme::instance()->oauthClientSecret()); - req.setRawHeader("Authorization", "Basic " + basicAuth.toUtf8().toBase64()); - // We just added the Authorization header, don't let HttpCredentialsAccessManager tamper with it - req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true); - - auto requestBody = new QBuffer; - QUrlQuery arguments; - arguments.setQueryItems({ - { QStringLiteral("grant_type"), "authorization_code" }, - { QStringLiteral("code"), args.queryItemValue(QStringLiteral("code")) }, - { QStringLiteral("client_id"), Theme::instance()->oauthClientId() }, - { QStringLiteral("client_secret"), Theme::instance()->oauthClientSecret() }, - { QStringLiteral("redirect_uri"), QStringLiteral("http://localhost:%1").arg(_server.serverPort()) }, - { QStringLiteral("code_verifier"), _pkceCodeVerifier }, - { QStringLiteral("scope"), args.queryItemValue(QStringLiteral("scope")) }, - }); - requestBody->setData(arguments.query(QUrl::FullyEncoded).toUtf8()); - const auto job = _account->sendRequest("POST", requestTokenUrl, req, requestBody); - job->setTimeout(qMin(30 * 1000ll, job->timeoutMsec())); - QObject::connect(job, &SimpleNetworkJob::finishedSignal, this, [this, socket](QNetworkReply *reply) { - const auto jsonData = reply->readAll(); - QJsonParseError jsonParseError; - const QJsonObject json = QJsonDocument::fromJson(jsonData, &jsonParseError).object(); - QString fieldsError; - const auto getRequiredField = [&json, &fieldsError](const QString &s) { - const auto out = json.constFind(s); - if (out == json.constEnd()) { - fieldsError.append(tr("\tError: Missing field %1\n").arg(s)); - return QJsonValue(); - } - return *out; - }; - const QString accessToken = getRequiredField(QStringLiteral("access_token")).toString(); - const QString refreshToken = getRequiredField(QStringLiteral("refresh_token")).toString(); - const QString tokenType = getRequiredField(QStringLiteral("token_type")).toString().toLower(); - const QString user = json[QStringLiteral("user_id")].toString(); - const QUrl messageUrl = json[QStringLiteral("message_url")].toString(); - - if (reply->error() != QNetworkReply::NoError || jsonParseError.error != QJsonParseError::NoError - || !fieldsError.isEmpty() - || tokenType != "bearer") { - QString errorReason; - QString errorFromJson = json["error_description"].toString(); - if (errorFromJson.isEmpty()) - errorFromJson = json["error"].toString(); - if (!errorFromJson.isEmpty()) { - errorReason = tr("Error returned from the server: <em>%1</em>") - .arg(errorFromJson.toHtmlEscaped()); - } else if (reply->error() != QNetworkReply::NoError) { - errorReason = tr("There was an error accessing the 'token' endpoint: <br><em>%1</em>") - .arg(reply->errorString().toHtmlEscaped()); - } else if (jsonData.isEmpty()) { - // Can happen if a funky load balancer strips away POST data, e.g. BigIP APM my.policy - errorReason = tr("Empty JSON from OAuth2 redirect"); - // We explicitly have this as error case since the json qcWarning output below is misleading, - // it will show a fake json will null values that actually never was received like this as - // soon as you access json["whatever"] the debug output json will claim to have "whatever":null - } else if (jsonParseError.error != QJsonParseError::NoError) { - errorReason = tr("Could not parse the JSON returned from the server: <br><em>%1</em>") - .arg(jsonParseError.errorString()); - } else if (tokenType != QStringLiteral("bearer")) { - errorReason = tr("Unsupported token type: %1").arg(tokenType); - } else if (!fieldsError.isEmpty()) { - errorReason = tr("The reply from the server did not contain all expected fields\n:%1").arg(fieldsError); - } else { - errorReason = tr("Unknown Error"); - } - qCWarning(lcOauth) << "Error when getting the accessToken" << jsonData << errorReason; - httpReplyAndClose(socket, "500 Internal Server Error", - tr("<h1>Login Error</h1><p>%1</p>").arg(errorReason).toUtf8().constData()); - emit result(Error); - return; - } - if (!user.isEmpty()) { - finalize(socket, accessToken, refreshToken, user, messageUrl); - return; - } - // If the reply don't contains the user id, we must do another call to query it - JsonApiJob *job = new JsonApiJob(_account->sharedFromThis(), QLatin1String("ocs/v1.php/cloud/user"), this); - job->setTimeout(qMin(30 * 1000ll, job->timeoutMsec())); - QNetworkRequest req; - // We are not connected yet so we need to handle the authentication manually - req.setRawHeader("Authorization", "Bearer " + accessToken.toUtf8()); - // We just added the Authorization header, don't let HttpCredentialsAccessManager tamper with it - req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true); - job->startWithRequest(req); - QObject::connect(job, &JsonApiJob::jsonReceived, this, [=](const QJsonDocument &json) { - QString user = json.object().value("ocs").toObject().value("data").toObject().value("id").toString(); - finalize(socket, accessToken, refreshToken, user, messageUrl); - }); - }); - }); - } - }); -} - -void OAuth::finalize(QPointer<QTcpSocket> socket, const QString &accessToken, - const QString &refreshToken, const QString &user, const QUrl &messageUrl) { - if (!_expectedUser.isNull() && user != _expectedUser) { - // Connected with the wrong user - QString message = tr("<h1>Wrong user</h1>" - "<p>You logged-in with user <em>%1</em>, but must login with user <em>%2</em>.<br>" - "Please log out of %3 in another tab, then <a href='%4'>click here</a> " - "and log in as user %2</p>") - .arg(user, _expectedUser, Theme::instance()->appNameGUI(), - authorisationLink().toString(QUrl::FullyEncoded)); - httpReplyAndClose(socket, "200 OK", message.toUtf8().constData()); - // We are still listening on the socket so we will get the new connection - return; - } - const char *loginSuccessfullHtml = "<h1>Login Successful</h1><p>You can close this window.</p>"; - if (messageUrl.isValid()) { - httpReplyAndClose(socket, "303 See Other", loginSuccessfullHtml, - QByteArray("Location: " + messageUrl.toEncoded()).constData()); - } else { - httpReplyAndClose(socket, "200 OK", loginSuccessfullHtml); - } - emit result(LoggedIn, user, accessToken, refreshToken); -} - -QByteArray OAuth::generateRandomString(size_t size) const -{ - // TODO: do we need a varaible size? - std::vector<quint32> buffer(size, 0); - QRandomGenerator::global()->fillRange(buffer.data(), static_cast<qsizetype>(size)); - return QByteArray(reinterpret_cast<char *>(buffer.data()), static_cast<int>(size * sizeof(quint32))).toBase64(QByteArray::Base64UrlEncoding); -} - -QUrl OAuth::authorisationLink() const -{ - Q_ASSERT(_server.isListening()); - QUrlQuery query; - QByteArray code_challenge = QCryptographicHash::hash(_pkceCodeVerifier, QCryptographicHash::Sha256) - .toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); - query.setQueryItems({ - { QLatin1String("response_type"), QLatin1String("code") }, - { QLatin1String("client_id"), Theme::instance()->oauthClientId() }, - { QLatin1String("redirect_uri"), QLatin1String("http://localhost:") + QString::number(_server.serverPort()) }, - { QLatin1String("code_challenge"), QString::fromLatin1(code_challenge) }, - { QLatin1String("code_challenge_method"), QLatin1String("S256") }, - { QLatin1String("scope"), QLatin1String("openid offline_access") }, - { QLatin1String("prompt"), QLatin1String("consent") }, - { QStringLiteral("state"), _state }, - }); - if (!_expectedUser.isNull()) - query.addQueryItem("user", _expectedUser); - const QUrl url = _authEndpoint.isValid() - ? Utility::concatUrlPath(_authEndpoint, {}, query) - : Utility::concatUrlPath(_account->url(), QLatin1String("/index.php/apps/oauth2/authorize"), query); - return url; -} - -void OAuth::authorisationLinkAsync(std::function<void (const QUrl &)> callback) const -{ - if (_wellKnownFinished) { - callback(authorisationLink()); - } else { - connect(this, &OAuth::authorisationLinkChanged, callback); - } -} - -void OAuth::fetchWellKnown() -{ - QUrl wellKnownUrl = Utility::concatUrlPath(_account->url().toString(), QLatin1String("/.well-known/openid-configuration")); - QNetworkRequest req; - auto job = _account->sendRequest("GET", wellKnownUrl); - job->setTimeout(qMin(30 * 1000ll, job->timeoutMsec())); - QObject::connect(job, &SimpleNetworkJob::finishedSignal, this, [this](QNetworkReply *reply) { - _wellKnownFinished = true; - if (reply->error() != QNetworkReply::NoError) { - // Most likely the file does not exist, default to the normal endpoint - emit this->authorisationLinkChanged(authorisationLink()); - return; - } - const auto jsonData = reply->readAll(); - QJsonParseError jsonParseError; - const QJsonObject json = QJsonDocument::fromJson(jsonData, &jsonParseError).object(); - - if (jsonParseError.error == QJsonParseError::NoError) { - QString authEp = json["authorization_endpoint"].toString(); - if (!authEp.isEmpty()) - this->_authEndpoint = authEp; - QString tokenEp = json["token_endpoint"].toString(); - if (!tokenEp.isEmpty()) - this->_tokenEndpoint = tokenEp; - } else if (jsonParseError.error == QJsonParseError::IllegalValue) { - qCDebug(lcOauth) << ".well-known did not return json, the server most does not support oidc"; - } else { - qCWarning(lcOauth) << "Json parse error in well-known: " << jsonParseError.errorString(); - } - - emit this->authorisationLinkChanged(authorisationLink()); - }); -} - -void OAuth::openBrowser() -{ - authorisationLinkAsync([this](const QUrl &link) { - if (!QDesktopServices::openUrl(link)) { - qCWarning(lcOauth) << "QDesktopServices::openUrl Failed"; - // We cannot open the browser, then we claim we don't support OAuth. - emit result(NotSupported, QString()); - } - }); -} - -} // namespace OCC diff --git a/src/gui/creds/oauth.h b/src/gui/creds/oauth.h deleted file mode 100644 index e5863d6db..000000000 --- a/src/gui/creds/oauth.h +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (C) by Olivier Goffart <ogoffart@woboq.com> - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License - * for more details. - */ - -#pragma once -#include <QPointer> -#include <QTcpServer> -#include <QUrl> -#include "accountfwd.h" - -namespace OCC { - -/** - * Job that do the authorization grant and fetch the access token - * - * Normal workflow: - * - * --> start() - * | - * +----> fetchWellKnown() query the ".well-known/openid-configuration" endpoint - * | - * +----> openBrowser() open the browser after fetchWellKnown finished to the specified page - * | (or the default 'oauth2/authorize' if fetchWellKnown does not exist) - * | Then the browser will redirect to http://localhost:xxx - * | - * +----> _server starts listening on a TCP port waiting for an HTTP request with a 'code' - * | - * v - * request the access_token and the refresh_token via 'apps/oauth2/api/v1/token' - * | - * +-> Request the user_id is not present - * | | - * v v - * finalize(...): emit result(...) - * - */ -class OAuth : public QObject -{ - Q_OBJECT -public: - OAuth(Account *account, QObject *parent) - : QObject(parent) - , _account(account) - { - } - ~OAuth(); - - enum Result { NotSupported, - LoggedIn, - Error }; - Q_ENUM(Result); - void start(); - void openBrowser(); - QUrl authorisationLink() const; - /** - * Call the callback when the call to the well-known endpoint finishes. - * (or immediatly if it is ready) - * The callback will not be called if this object gets destroyed - */ - void authorisationLinkAsync(std::function<void(const QUrl&)> callback) const; - -signals: - /** - * The state has changed. - * when logged in, token has the value of the token. - */ - void result(OAuth::Result result, const QString &user = QString(), const QString &token = QString(), const QString &refreshToken = QString()); - - /** - * emitted when the call to the well-known endpoint is finished - */ - void authorisationLinkChanged(const QUrl &); - -private: - - void fetchWellKnown(); - void finalize(QPointer<QTcpSocket> socket, const QString &accessToken, - const QString &refreshToken, const QString &userId, const QUrl &messageUrl); - - QByteArray generateRandomString(size_t size) const; - - Account* _account; - QTcpServer _server; - bool _wellKnownFinished = false; - QUrl _authEndpoint; - QUrl _tokenEndpoint; - QByteArray _pkceCodeVerifier; - QByteArray _state; - -public: - QString _expectedUser; -}; - - -} // namespace OCC |