diff --git a/.gitignore b/.gitignore index b25a02da..86c7a11e 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,6 @@ Makefile* build out exportToOffice.sh + +/temp + diff --git a/App/Server/QJWT.cpp b/App/Server/QJWT.cpp index 69f39832..255fd547 100644 --- a/App/Server/QJWT.cpp +++ b/App/Server/QJWT.cpp @@ -18,6 +18,7 @@ ******************************************************************************/ /** * @author S.Mehran M.Ziabary + * @author Kambiz Zandi */ #include @@ -27,13 +28,12 @@ #include "QJWT.h" +#include "libTargomanDBM/clsDAC.h" #include "libTargomanCommon/Configuration/Validators.hpp" #include "ServerConfigs.h" #include "clsSimpleCrypt.h" -namespace Targoman { -namespace API { -namespace Server { +namespace Targoman::API::Server { using namespace Common; using namespace Targoman::Common::Configuration; @@ -99,68 +99,178 @@ tmplConfigurable QJWT::RememberLoginTTL( enuConfigSource::Arg | enuConfigSource::File); thread_local static clsSimpleCrypt* SimpleCryptInstance = nullptr; -static clsSimpleCrypt* simpleCryptInstance(){ - if(Q_UNLIKELY(!SimpleCryptInstance)){ + +static clsSimpleCrypt* simpleCryptInstance() +{ + if (Q_UNLIKELY(!SimpleCryptInstance)) + { SimpleCryptInstance = new clsSimpleCrypt(QJWT::SimpleCryptKey.value()); SimpleCryptInstance->setIntegrityProtectionMode(clsSimpleCrypt::ProtectionHash); } + return SimpleCryptInstance; } -QString QJWT::createSigned(QJsonObject _payload, QJsonObject _privatePayload, const qint32 _expiry, const QString& _sessionID) +QString QJWT::createSigned( + QJsonObject _payload, + QJsonObject _privatePayload, + const qint32 _expiry, + const QString &_sessionID, + const QString &_remoteIP + ) { const QString Header = QString("{\"typ\":\"JWT\",\"alg\":\"%1\"}").arg(enuJWTHashAlgs::toStr(QJWT::HashAlgorithm.value())); _payload["iat"] = static_cast(QDateTime::currentDateTime().toTime_t()); - if(_expiry >= 0) + + if (_expiry >= 0) _payload["exp"] = _payload["iat"].toInt() + _expiry; else _payload.remove("exp"); - if(_sessionID.size()) + if (_sessionID.size()) _payload["jti"] = _sessionID; else _payload.remove("jti"); - if (!_privatePayload.isEmpty()) + if (_remoteIP.isEmpty() == false) + _privatePayload.insert("cip", _remoteIP); + + if (_privatePayload.isEmpty() == false) _payload["prv"] = simpleCryptInstance()->encryptToString(QJsonDocument(_privatePayload).toJson()); else _payload.remove("prv"); - QByteArray Data = Header.toUtf8().toBase64() + "." + QJsonDocument(_payload).toJson().toBase64(); + QByteArray Data = Header.toUtf8().toBase64() + "." + QJsonDocument(_payload).toJson().toBase64(); return Data + "." + QJWT::hash(Data).toBase64(); } -QJsonObject QJWT::verifyReturnPayload(const QString& _jwt) +QJsonObject QJWT::verifyReturnPayload( + QString &_jwt, + const QString &_remoteIP, + bool _renewIfExpired + ) { QStringList JWTParts = _jwt.split('.'); - if(JWTParts.length() != 3) + + if (JWTParts.length() != 3) throw exHTTPForbidden("Invalid JWT Token"); - if(QJWT::hash((JWTParts.at(0) + "." + JWTParts.at(1)).toUtf8()).toBase64() != JWTParts[2]) + + if (QJWT::hash((JWTParts.at(0) + "." + JWTParts.at(1)).toUtf8()).toBase64() != JWTParts[2]) throw exHTTPForbidden("JWT signature verification failed"); + QJsonParseError Error; QJsonDocument Payload = QJsonDocument::fromJson(QByteArray::fromBase64(JWTParts.at(1).toLatin1()), &Error); - if(Payload.isNull()) + + if (Payload.isNull()) throw exHTTPForbidden("Invalid JWT payload: " + Error.errorString()); QJsonObject JWTPayload = Payload.object(); - if(JWTPayload.empty()) + + if (JWTPayload.empty()) throw exHTTPForbidden("Invalid JWT payload: empty object"); - if(JWTPayload.contains("exp") && - static_cast(JWTPayload.value("exp").toInt()) <= QDateTime::currentDateTime().toTime_t()) - throw exHTTPUnauthorized("JWT expired"); - if(JWTPayload.contains("prv")){ - QString Decrypted= simpleCryptInstance()->decryptToString(JWTPayload.value("prv").toString()); - if(Decrypted.isEmpty()) - throw exHTTPExpectationFailed(QString("Invalid empty private JWT payload: DEC ErrNo: %1").arg(simpleCryptInstance()->lastError())); + if (JWTPayload.contains("prv")) + { + QString Decrypted = simpleCryptInstance()->decryptToString(JWTPayload.value("prv").toString()); + + if (Decrypted.isEmpty()) + throw exHTTPExpectationFailed(QString("Invalid empty private JWT payload: DEC ErrNo: %1") + .arg(simpleCryptInstance()->lastError())); QJsonDocument Private = QJsonDocument::fromJson(Decrypted.toUtf8(), &Error); - if(Private.isNull()) + + if (Private.isNull()) throw exHTTPExpectationFailed("Invalid private JWT payload: " + Error.errorString()); - JWTPayload["prv"] = Private.object(); + QJsonObject PrivateObject = Private.object(); + + JWTPayload["prv"] = PrivateObject; + + // check client ip --------------- + if (PrivateObject.contains("cip")) + { + if (PrivateObject["cip"].toString() != _remoteIP) + throw exHTTPForbidden("Invalid client IP"); + } + } + + if (JWTPayload.contains("exp") + && static_cast(JWTPayload.value("exp").toInt()) <= QDateTime::currentDateTime().toTime_t() + ) + { + if (_renewIfExpired == false) + throw exHTTPUnauthorized("JWT expired"); + + QString SessionKey = JWTPayload["jti"].toString(); + + DBManager::clsDAC DAC("AAA"); //master db dac -> AAA + + //check session + QString Qry = R"( + SELECT TIME_TO_SEC((TIMEDIFF(NOW(), ssnCreationDateTime))) AS LifeSeconds + , tblActiveSessions.* + FROM tblActiveSessions + WHERE ssnKey=? + AND ssnStatus='A' +)"; + QJsonDocument Result = DAC.execQuery({}, Qry, { SessionKey }) + .toJson(true); + + if (Result.object().isEmpty()) + throw exHTTPUnauthorized("Active session not found"); + + QVariantMap SessionInfo = Result.toVariant().toMap(); + + //check old JWT + QString ssnJWT = SessionInfo["ssnJWT"].toString(); + if ((ssnJWT.isEmpty() == false) && (_jwt != ssnJWT)) + { + _jwt = ssnJWT; //this will add response header X-AUTH-NEW-TOKEN + throw exHTTPForbidden("JWT not replaced by client"); + } + + //check large expiration + quint64 LifeSeconds = SessionInfo["LifeSeconds"].toUInt(); + bool ssnRemember = (SessionInfo["ssnRemember"].toInt() == 1); + + if (LifeSeconds >= (ssnRemember ? QJWT::RememberLoginTTL.value() : QJWT::NormalLoginTTL.value())) + { + Qry = R"( + UPDATE tblActiveSessions + SET ssnStatus='E' + WHERE ssnKey=? +)"; + DAC.execQuery({}, Qry, { SessionKey }); + + throw exHTTPUnauthorized("Session expired"); + } + + // + if (SessionInfo["ssnIPReadable"] != _remoteIP) + throw exHTTPForbidden("Invalid IP"); + + //ssn_usrID + //ssnFingerPrint + + //TODO: check user ban or large expiration + + //---------------------------------------- + _jwt = QJWT::createSigned( + JWTPayload, + JWTPayload.contains("prv") ? JWTPayload["prv"].toObject() : QJsonObject(), + JWTPayload["exp"].toInt() - JWTPayload["iat"].toInt(), + JWTPayload["jti"].toString(), + _remoteIP + ); + + Qry = R"( + UPDATE tblActiveSessions + SET ssnJWT=? + WHERE ssnKey=? +)"; + DAC.execQuery({}, Qry, { _jwt, SessionKey }); } return JWTPayload; @@ -168,19 +278,22 @@ QJsonObject QJWT::verifyReturnPayload(const QString& _jwt) QByteArray QJWT::hash(const QByteArray& _data) { - switch(QJWT::HashAlgorithm.value()){ - case enuJWTHashAlgs::HS256: - return QMessageAuthenticationCode::hash(_data, QJWT::Secret.value().toUtf8(), QCryptographicHash::Sha256); - case enuJWTHashAlgs::HS384: - return QMessageAuthenticationCode::hash(_data, QJWT::Secret.value().toUtf8(), QCryptographicHash::Sha384); - case enuJWTHashAlgs::HS512: - return QMessageAuthenticationCode::hash(_data, QJWT::Secret.value().toUtf8(), QCryptographicHash::Sha512); - default: - throw exHTTPInternalServerError("Invalid JWT encryption algorithm"); + switch (QJWT::HashAlgorithm.value()) + { + case enuJWTHashAlgs::HS256: + return QMessageAuthenticationCode::hash(_data, QJWT::Secret.value().toUtf8(), QCryptographicHash::Sha256); + + case enuJWTHashAlgs::HS384: + return QMessageAuthenticationCode::hash(_data, QJWT::Secret.value().toUtf8(), QCryptographicHash::Sha384); + + case enuJWTHashAlgs::HS512: + return QMessageAuthenticationCode::hash(_data, QJWT::Secret.value().toUtf8(), QCryptographicHash::Sha512); + + default: + throw exHTTPInternalServerError("Invalid JWT encryption algorithm"); } } -} -} -} +} //namespace Targoman::API::Server + ENUM_CONFIGURABLE_IMPL(Targoman::API::Server::enuJWTHashAlgs) diff --git a/App/Server/QJWT.h b/App/Server/QJWT.h index 66cd7b87..2c95cfb0 100644 --- a/App/Server/QJWT.h +++ b/App/Server/QJWT.h @@ -18,20 +18,19 @@ ******************************************************************************/ /** * @author S.Mehran M.Ziabary + * @author Kambiz Zandi */ - #ifndef TARGOMAN_API_SERVER_CLSJWT_H #define TARGOMAN_API_SERVER_CLSJWT_H #include "libTargomanCommon/Configuration/tmplConfigurable.h" -namespace Targoman { -namespace API { -namespace AAA { +namespace Targoman::API::AAA { class clsJWT; } -namespace Server { + +namespace Targoman::API::Server { TARGOMAN_DEFINE_ENHANCED_ENUM(enuJWTHashAlgs, HS256, @@ -39,21 +38,28 @@ TARGOMAN_DEFINE_ENHANCED_ENUM(enuJWTHashAlgs, HS512) class QJWT { - static inline QString makeConfig(const QString& _name){return "/JWT/" + _name;} +public: + static inline QString makeConfig(const QString& _name) { return "/JWT/" + _name; } static Targoman::Common::Configuration::tmplConfigurable Secret; static Targoman::Common::Configuration::tmplConfigurable HashAlgorithm; static Targoman::Common::Configuration::tmplConfigurable TTL; static Targoman::Common::Configuration::tmplConfigurable NormalLoginTTL; static Targoman::Common::Configuration::tmplConfigurable RememberLoginTTL; -public: static Targoman::Common::Configuration::tmplConfigurable SimpleCryptKey; -public: - static QString createSigned(QJsonObject _payload, - QJsonObject _privatePayload = QJsonObject(), - const qint32 _expiry = -1, - const QString& _sessionID = QString()); - static QJsonObject verifyReturnPayload(const QString& _jwt); + static QString createSigned( + QJsonObject _payload, + QJsonObject _privatePayload = QJsonObject(), + const qint32 _expiry = -1, + const QString &_sessionID = {}, + const QString &_remoteIP = {} + ); + + static QJsonObject verifyReturnPayload( + /*INOUT*/ QString &_jwt, + const QString &_remoteIP, + bool _renewIfExpired = false + ); private: static QByteArray hash(const QByteArray& _data); @@ -61,9 +67,7 @@ class QJWT friend class Targoman::API::AAA::clsJWT; }; -} -} -} +} //namespace Targoman::API::Server ENUM_CONFIGURABLE(Targoman::API::Server::enuJWTHashAlgs) diff --git a/App/Server/clsRequestHandler.cpp b/App/Server/clsRequestHandler.cpp index 69c9cbc7..202914f4 100644 --- a/App/Server/clsRequestHandler.cpp +++ b/App/Server/clsRequestHandler.cpp @@ -48,7 +48,9 @@ clsRequestHandler::clsRequestHandler(QHttpRequest *_req, QHttpResponse *_res, QO QObject(_parent), Request(_req), Response(_res) -{} +{ + this->ElapsedTimer.start(); +} void clsRequestHandler::process(const QString& _api) { @@ -320,8 +322,29 @@ clsRequestHandler::stuResult clsRequestHandler::run(clsAPIObject* _apiObject, QS QString Auth = Headers.value("authorization"); if (Auth.startsWith("Bearer ")) { - JWT = QJWT::verifyReturnPayload(Auth.mid(sizeof("Bearer"))); + QString BearerToken = Auth.mid(sizeof("Bearer")); Headers.remove("authorization"); + + QString OldBearerToken = BearerToken; + + try + { + JWT = QJWT::verifyReturnPayload( + BearerToken, + this->toIPv4(this->Request->remoteAddress()), + true + ); + + if (BearerToken != OldBearerToken) + ResponseHeaders.insert("X-AUTH-NEW-TOKEN", BearerToken); + } + catch (...) + { + if (BearerToken != OldBearerToken) + ResponseHeaders.insert("X-AUTH-NEW-TOKEN", BearerToken); + + throw; + } } else throw exHTTPForbidden("No valid authentication header is present"); @@ -352,23 +375,23 @@ clsRequestHandler::stuResult clsRequestHandler::run(clsAPIObject* _apiObject, QS return stuResult(Result, ResponseHeaders); } - catch(exTargomanBase& ex) + catch (exTargomanBase& ex) { return stuResult(ex.what(), ResponseHeaders, static_cast(ex.httpCode())); } - catch(exQFVRequiredParam &ex) + catch (exQFVRequiredParam &ex) { return stuResult(ex.what(), ResponseHeaders, qhttp::ESTATUS_BAD_REQUEST); } - catch(exQFVInvalidValue &ex) + catch (exQFVInvalidValue &ex) { return stuResult(ex.what(), ResponseHeaders, qhttp::ESTATUS_BAD_REQUEST); } - catch(std::exception &ex) + catch (std::exception &ex) { return stuResult(ex.what(), ResponseHeaders, qhttp::ESTATUS_INTERNAL_SERVER_ERROR); } - catch(...) + catch (...) { return stuResult("INTERNAL SERVER ERROR!!!", ResponseHeaders, qhttp::ESTATUS_INTERNAL_SERVER_ERROR); } @@ -410,7 +433,9 @@ bool clsRequestHandler::callStaticAPI(QString _api) if (_api == "/openAPI.json") { gServerStats.Success.inc(); - this->sendResponseBase(qhttp::ESTATUS_OK, OpenAPIGenerator::retrieveJson(this->host(), this->port())); + this->sendResponseBase( + qhttp::ESTATUS_OK, + OpenAPIGenerator::retrieveJson(this->host(), this->port())); return true; } @@ -480,9 +505,6 @@ void clsRequestHandler::findAndCallAPI(QString _api) return; //----------------------------------------------------- - QElapsedTimer ElapsedTimer; - ElapsedTimer.start(); - QStringList Queries = this->Request->url().query().split('&', QString::SkipEmptyParts); QString ExtraAPIPath; @@ -511,11 +533,9 @@ void clsRequestHandler::findAndCallAPI(QString _api) this->sendError(qhttp::ESTATUS_REQUEST_TIMEOUT, "Request Timed Out"); }); - this->connect(&this->FutureWatcher, &QFutureWatcher::finished, [this, ElapsedTimer]() { + this->connect(&this->FutureWatcher, &QFutureWatcher::finished, [this]() { stuResult Result = this->FutureWatcher.result(); - Result.ResponseHeader.insert("X-DEBUG-TIME-ELAPSED-MS", ElapsedTimer.elapsed()); - if (Result.StatusCode == qhttp::ESTATUS_OK) this->sendResponse(StatusCodeOnMethod[this->Request->method()], Result.Result, Result.ResponseHeader); else @@ -554,22 +574,28 @@ void clsRequestHandler::sendError( void clsRequestHandler::sendFile(const QString& _basePath, const QString _path) { - if(QFile::exists(_basePath + _path) == false) + if (QFile::exists(_basePath + _path) == false) throw exHTTPNotFound(_path); + Q_ASSERT(this->FileHandler.isNull()); + this->FileHandler.reset(new QFile(_basePath + _path)); this->FileHandler->open(QFile::ReadOnly); - if(this->FileHandler->isReadable() == false) + if (this->FileHandler->isReadable() == false) throw exHTTPForbidden(_path); QMimeType FileMIME = this->MIMEDB.mimeTypeForFile(_basePath + _path); qint64 FileSize = QFileInfo(*this->FileHandler).size(); + this->Response->addHeaderValue("content-type", FileMIME.name()); this->Response->addHeaderValue("content-length", QString("%1").arg(FileSize)); this->Response->addHeaderValue("Connection", QString("keep-alive")); + this->Response->setStatusCode(qhttp::ESTATUS_OK); + this->Response->addHeaderValue("X-DEBUG-TIME-ELAPSED", QString::number(this->ElapsedTimer.elapsed()) + " ms"); + QTimer::singleShot(10, this, &clsRequestHandler::slotSendFileData); } @@ -636,6 +662,9 @@ void clsRequestHandler::sendResponse( this->addHeaderValues(_responseHeaders); + if (_responseHeaders.contains("X-DEBUG-TIME-ELAPSED") == false) + this->Response->addHeaderValue("X-DEBUG-TIME-ELAPSED", QString::number(this->ElapsedTimer.elapsed()) + " ms"); + this->Response->end(RawData.data()); this->deleteLater(); @@ -668,6 +697,8 @@ void clsRequestHandler::sendCORSOptions() this->Response->setStatusCode(qhttp::ESTATUS_NO_CONTENT); + this->Response->addHeaderValue("X-DEBUG-TIME-ELAPSED", QString::number(this->ElapsedTimer.elapsed()) + " ms"); + this->Response->end(); this->deleteLater(); @@ -723,6 +754,9 @@ void clsRequestHandler::sendResponseBase( this->addHeaderValues(_responseHeaders); + if (_responseHeaders.contains("X-DEBUG-TIME-ELAPSED") == false) + this->Response->addHeaderValue("X-DEBUG-TIME-ELAPSED", QString::number(this->ElapsedTimer.elapsed()) + " ms"); + this->Response->end(Data.constData()); this->deleteLater(); diff --git a/App/Server/clsRequestHandler.h b/App/Server/clsRequestHandler.h index 178a0668..267779e5 100644 --- a/App/Server/clsRequestHandler.h +++ b/App/Server/clsRequestHandler.h @@ -147,6 +147,7 @@ private slots: QTimer FutureTimer; QMimeDatabase MIMEDB; QScopedPointer FileHandler; + QElapsedTimer ElapsedTimer; friend class clsMultipartFormDataRequestHandler; }; diff --git a/Modules/Account/migrations/db/m20220401_144613_AAA_add_jwt_to_session.sql b/Modules/Account/migrations/db/m20220401_144613_AAA_add_jwt_to_session.sql new file mode 100644 index 00000000..9824b85b --- /dev/null +++ b/Modules/Account/migrations/db/m20220401_144613_AAA_add_jwt_to_session.sql @@ -0,0 +1,4 @@ +/* Migration File: m20220401_144613_AAA_add_jwt_to_session.sql */ + +ALTER TABLE `tblActiveSessions` + ADD COLUMN `ssnJWT` TEXT NULL AFTER `ssnRemember`; diff --git a/Modules/Account/moduleSrc/ORM/ActiveSessions.cpp b/Modules/Account/moduleSrc/ORM/ActiveSessions.cpp index cbcd6bd1..5dfd9592 100644 --- a/Modules/Account/moduleSrc/ORM/ActiveSessions.cpp +++ b/Modules/Account/moduleSrc/ORM/ActiveSessions.cpp @@ -43,6 +43,7 @@ ActiveSessions::ActiveSessions() : { tblActiveSessions::ssnFingerPrint, S(TAPI::MD5_t), QFV.allwaysInvalid(), QNull, UPNone, false, false }, { tblActiveSessions::ssnLastActivity, S(TAPI::DateTime_t), QFV, QNull, UPNone }, { tblActiveSessions::ssnRemember, S(bool), QFV, false, UPNone }, + { tblActiveSessions::ssnJWT, S(QString), QFV, QNull, UPAdmin, false, false }, { tblActiveSessions::ssnStatus, ORM_STATUS_FIELD(Targoman::API::AccountModule::enuSessionStatus, Targoman::API::AccountModule::enuSessionStatus::Active) }, { tblActiveSessions::ssnCreationDateTime, ORM_CREATED_ON }, { tblActiveSessions::ssnUpdatedBy_usrID, ORM_UPDATED_BY }, diff --git a/Modules/Account/moduleSrc/ORM/ActiveSessions.h b/Modules/Account/moduleSrc/ORM/ActiveSessions.h index a9b86f9f..7945319f 100644 --- a/Modules/Account/moduleSrc/ORM/ActiveSessions.h +++ b/Modules/Account/moduleSrc/ORM/ActiveSessions.h @@ -55,8 +55,9 @@ TARGOMAN_CREATE_CONSTEXPR(ssnInfo); TARGOMAN_CREATE_CONSTEXPR(ssnFingerPrint); TARGOMAN_CREATE_CONSTEXPR(ssnLastActivity); TARGOMAN_CREATE_CONSTEXPR(ssnRemember); -TARGOMAN_CREATE_CONSTEXPR(ssnUpdatedBy_usrID); +TARGOMAN_CREATE_CONSTEXPR(ssnJWT); TARGOMAN_CREATE_CONSTEXPR(ssnStatus); +TARGOMAN_CREATE_CONSTEXPR(ssnUpdatedBy_usrID); } #pragma GCC diagnostic pop diff --git a/TargomanAPI.pro b/TargomanAPI.pro index 56455e11..233141b6 100644 --- a/TargomanAPI.pro +++ b/TargomanAPI.pro @@ -42,4 +42,5 @@ OTHER_FILES += \ migrations/I18N/local/.migrations \ migrations/MT/db/* \ migrations/MT/local/* \ - migrations/MT/local/.migrations + migrations/MT/local/.migrations \ + temp/sql-change-scripts.txt