mirror of
https://github.com/nextcloud/desktop.git
synced 2025-10-26 11:17:43 +00:00
Login Flow V2: 1st implementation, cleanup
This is the first draft of the Login Flow V2 authorization method. See: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2 - Adds the Login Fĺow V2 auth method - Adds ability to reinitiate a new request via UI TODO: - Implement re-auth upon logout -> login - Improve UI - SSL: Client certificate login is possible at the first time only but missing after relaunch Signed-off-by: Michael Schuster <michael@schuster.ms>
This commit is contained in:
parent
2742411abd
commit
8fa55b97b4
@ -14,14 +14,12 @@
|
||||
*/
|
||||
|
||||
#include <QDesktopServices>
|
||||
//#include <QNetworkReply>
|
||||
#include <QTimer>
|
||||
#include <QBuffer>
|
||||
#include "account.h"
|
||||
#include "creds/flow2auth.h"
|
||||
#include <QJsonObject>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include "theme.h"
|
||||
#include "networkjobs.h"
|
||||
#include "configfile.h"
|
||||
@ -36,23 +34,35 @@ Flow2Auth::~Flow2Auth()
|
||||
|
||||
void Flow2Auth::start()
|
||||
{
|
||||
// Note: All startup code is in openBrowser() to allow reinitiate a new request with
|
||||
// fresh tokens. Opening the same pollEndpoint link twice triggers an expiration
|
||||
// message by the server (security, intended design).
|
||||
openBrowser();
|
||||
}
|
||||
|
||||
QUrl Flow2Auth::authorisationLink() const
|
||||
{
|
||||
return _loginUrl;
|
||||
}
|
||||
|
||||
void Flow2Auth::openBrowser()
|
||||
{
|
||||
_pollTimer.stop();
|
||||
|
||||
// Step 1: Initiate a login, do an anonymous POST request
|
||||
QUrl url = Utility::concatUrlPath(_account->url().toString(), QLatin1String("/index.php/login/v2"));
|
||||
|
||||
auto job = _account->sendRequest("POST", url);
|
||||
job->setTimeout(qMin(30 * 1000ll, job->timeoutMsec()));
|
||||
|
||||
QObject::connect(job, &SimpleNetworkJob::finishedSignal, this, [this](QNetworkReply *reply) {
|
||||
auto jsonData = reply->readAll();
|
||||
QJsonParseError jsonParseError;
|
||||
QJsonObject json = QJsonDocument::fromJson(jsonData, &jsonParseError).object();
|
||||
//MessageBoxA(0, jsonData.toStdString().c_str(), "Flow2Auth::start()", 0);
|
||||
|
||||
QString pollToken = json.value("poll").toObject().value("token").toString();
|
||||
QString pollToken = json.value("poll").toObject().value("token").toString();
|
||||
QString pollEndpoint = json.value("poll").toObject().value("endpoint").toString();
|
||||
QUrl loginUrl = json["login"].toString();
|
||||
/*MessageBoxA(0, pollToken.toStdString().c_str(), "pollToken", 0);
|
||||
MessageBoxA(0, pollEndpoint.toStdString().c_str(), "pollEndpoint", 0);
|
||||
MessageBoxA(0, loginUrl.toString().toStdString().c_str(), "loginUrl", 0);*/
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError || jsonParseError.error != QJsonParseError::NoError
|
||||
|| json.isEmpty() || pollToken.isEmpty() || pollEndpoint.isEmpty() || loginUrl.isEmpty()) {
|
||||
@ -76,153 +86,32 @@ void Flow2Auth::start()
|
||||
}
|
||||
|
||||
|
||||
_loginUrl = loginUrl;
|
||||
_loginUrl = loginUrl;
|
||||
_pollToken = pollToken;
|
||||
_pollEndpoint = pollEndpoint;
|
||||
|
||||
|
||||
ConfigFile cfg;
|
||||
ConfigFile cfg;
|
||||
std::chrono::milliseconds polltime = cfg.remotePollInterval();
|
||||
qCInfo(lcFlow2auth) << "setting remote poll timer interval to" << polltime.count() << "msec";
|
||||
_pollTimer.setInterval(polltime.count());
|
||||
_pollTimer.setInterval(polltime.count());
|
||||
QObject::connect(&_pollTimer, &QTimer::timeout, this, &Flow2Auth::slotPollTimerTimeout);
|
||||
_pollTimer.start();
|
||||
|
||||
|
||||
if (!openBrowser())
|
||||
return;
|
||||
if (!QDesktopServices::openUrl(authorisationLink())) {
|
||||
// TODO: Ask the user to copy and open the link instead of failing here!
|
||||
|
||||
|
||||
/*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);*/
|
||||
});
|
||||
#if 0
|
||||
return;
|
||||
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
if (!openBrowser())
|
||||
return;
|
||||
|
||||
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] {
|
||||
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
|
||||
QRegExp rx("^GET /\\?code=([a-zA-Z0-9]+)[& ]"); // Match a /?code=... URL
|
||||
if (rx.indexIn(peek) != 0) {
|
||||
httpReplyAndClose(socket, "404 Not Found", "<html><head><title>404 Not Found</title></head><body><center><h1>404 Not Found</h1></center></body></html>");
|
||||
return;
|
||||
}
|
||||
|
||||
QString code = rx.cap(1); // The 'code' is the first capture of the regexp
|
||||
|
||||
QUrl requestToken = Utility::concatUrlPath(_account->url().toString(), QLatin1String("/index.php/apps/oauth2/api/v1/token"));
|
||||
QNetworkRequest req;
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
|
||||
QString basicAuth = QString("%1:%2").arg(
|
||||
Theme::instance()->oauthClientId(), Theme::instance()->oauthClientSecret());
|
||||
req.setRawHeader("Authorization", "Basic " + basicAuth.toUtf8().toBase64());
|
||||
|
||||
auto requestBody = new QBuffer;
|
||||
QUrlQuery arguments(QString(
|
||||
"grant_type=authorization_code&code=%1&redirect_uri=http://localhost:%2")
|
||||
.arg(code, QString::number(_server.serverPort())));
|
||||
requestBody->setData(arguments.query(QUrl::FullyEncoded).toLatin1());
|
||||
|
||||
auto job = _account->sendRequest("POST", requestToken, req, requestBody);
|
||||
job->setTimeout(qMin(30 * 1000ll, job->timeoutMsec()));
|
||||
QObject::connect(job, &SimpleNetworkJob::finishedSignal, this, [this, socket](QNetworkReply *reply) {
|
||||
auto jsonData = reply->readAll();
|
||||
QJsonParseError jsonParseError;
|
||||
QJsonObject json = QJsonDocument::fromJson(jsonData, &jsonParseError).object();
|
||||
QString accessToken = json["access_token"].toString();
|
||||
QString refreshToken = json["refresh_token"].toString();
|
||||
QString user = json["user_id"].toString();
|
||||
QUrl messageUrl = json["message_url"].toString();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError || jsonParseError.error != QJsonParseError::NoError
|
||||
|| json.isEmpty() || refreshToken.isEmpty() || accessToken.isEmpty()
|
||||
|| json["token_type"].toString() != QLatin1String("Bearer")) {
|
||||
QString errorReason;
|
||||
QString 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 (jsonParseError.error != QJsonParseError::NoError) {
|
||||
errorReason = tr("Could not parse the JSON returned from the server: <br><em>%1</em>")
|
||||
.arg(jsonParseError.errorString());
|
||||
} else {
|
||||
errorReason = tr("The reply from the server did not contain all expected fields");
|
||||
}
|
||||
qCWarning(lcFlow2auth) << "Error when getting the accessToken" << json << errorReason;
|
||||
httpReplyAndClose(socket, "500 Internal Server Error",
|
||||
tr("<h1>Login Error</h1><p>%1</p>").arg(errorReason).toUtf8().constData());
|
||||
emit result(Error);
|
||||
return;
|
||||
}
|
||||
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);
|
||||
});
|
||||
});
|
||||
// We cannot open the browser, then we claim we don't support OAuth.
|
||||
emit result(NotSupported, QString());
|
||||
}
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
void Flow2Auth::slotPollTimerTimeout()
|
||||
{
|
||||
_pollTimer.stop();
|
||||
|
||||
// qCInfo(lcFlow2auth) << "reached poll timout";
|
||||
|
||||
// Step 2: Poll
|
||||
QNetworkRequest req;
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
@ -259,8 +148,8 @@ void Flow2Auth::slotPollTimerTimeout()
|
||||
errorReason = tr("The reply from the server did not contain all expected fields");
|
||||
}
|
||||
qCDebug(lcFlow2auth) << "Error when polling for the appPassword" << json << errorReason;
|
||||
// emit result(Error);
|
||||
_pollTimer.start();
|
||||
|
||||
_pollTimer.start();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -272,28 +161,4 @@ void Flow2Auth::slotPollTimerTimeout()
|
||||
});
|
||||
}
|
||||
|
||||
QUrl Flow2Auth::authorisationLink() const
|
||||
{
|
||||
return _loginUrl;
|
||||
/*Q_ASSERT(_server.isListening());
|
||||
QUrlQuery query;
|
||||
query.setQueryItems({ { QLatin1String("response_type"), QLatin1String("code") },
|
||||
{ QLatin1String("client_id"), Theme::instance()->oauthClientId() },
|
||||
{ QLatin1String("redirect_uri"), QLatin1String("http://localhost:") + QString::number(_server.serverPort()) } });
|
||||
if (!_expectedUser.isNull())
|
||||
query.addQueryItem("user", _expectedUser);
|
||||
QUrl url = Utility::concatUrlPath(_account->url(), QLatin1String("/index.php/apps/oauth2/authorize"), query);
|
||||
return url;*/
|
||||
}
|
||||
|
||||
bool Flow2Auth::openBrowser()
|
||||
{
|
||||
if (!QDesktopServices::openUrl(authorisationLink())) {
|
||||
// We cannot open the browser, then we claim we don't support OAuth.
|
||||
emit result(NotSupported, QString());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
|
||||
@ -22,21 +22,9 @@
|
||||
namespace OCC {
|
||||
|
||||
/**
|
||||
* Job that does the authorization grant and fetch the access token
|
||||
* Job that does the authorization, grants and fetches the access token via Login Flow v2
|
||||
*
|
||||
* Normal workflow:
|
||||
*
|
||||
* --> start()
|
||||
* |
|
||||
* +----> openBrowser() open the browser to the login page, redirects 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'
|
||||
* |
|
||||
* v
|
||||
* emit result(...)
|
||||
* See: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2
|
||||
*
|
||||
*/
|
||||
class Flow2Auth : public QObject
|
||||
@ -55,7 +43,7 @@ public:
|
||||
Error };
|
||||
Q_ENUM(Result);
|
||||
void start();
|
||||
bool openBrowser();
|
||||
void openBrowser();
|
||||
QUrl authorisationLink() const;
|
||||
|
||||
signals:
|
||||
@ -63,6 +51,7 @@ signals:
|
||||
* The state has changed.
|
||||
* when logged in, token has the value of the token.
|
||||
*/
|
||||
// TODO: Remove refreshToken
|
||||
void result(Flow2Auth::Result result, const QString &user = QString(), const QString &token = QString(), const QString &refreshToken = QString());
|
||||
|
||||
private slots:
|
||||
@ -74,9 +63,6 @@ private:
|
||||
QString _pollToken;
|
||||
QString _pollEndpoint;
|
||||
QTimer _pollTimer;
|
||||
|
||||
public:
|
||||
QString _expectedUser;
|
||||
};
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user