From fcc6e596d237de5087aea2e27519c9b39763a1be Mon Sep 17 00:00:00 2001 From: Bionus Date: Fri, 29 Dec 2023 22:12:18 +0100 Subject: [PATCH] fix: split tokens expirations in OAuth2 login (fix #3076) --- src/lib/src/login/oauth2-login.cpp | 109 ++++++++++++++++++++--------- src/lib/src/login/oauth2-login.h | 3 +- 2 files changed, 79 insertions(+), 33 deletions(-) diff --git a/src/lib/src/login/oauth2-login.cpp b/src/lib/src/login/oauth2-login.cpp index c3812a65b..0f5793664 100644 --- a/src/lib/src/login/oauth2-login.cpp +++ b/src/lib/src/login/oauth2-login.cpp @@ -27,7 +27,8 @@ OAuth2Login::OAuth2Login(OAuth2Auth *auth, Site *site, NetworkManager *manager, { m_accessToken = m_settings->value("auth/accessToken").toString(); m_refreshToken = m_settings->value("auth/refreshToken").toString(); - m_expires = m_settings->value("auth/accessTokenExpiration").toDateTime(); + m_accessTokenExpiration = m_settings->value("auth/accessTokenExpiration").toDateTime(); + m_refreshTokenExpiration = m_settings->value("auth/refreshTokenExpiration").toDateTime(); } bool OAuth2Login::isTestable() const @@ -201,10 +202,12 @@ void OAuth2Login::loginAuthorizationCode() m_accessToken = flow->token(); m_refreshToken = flow->refreshToken(); - m_expires = flow->expirationAt(); + m_accessTokenExpiration = flow->expirationAt(); + m_refreshTokenExpiration = QDateTime(); m_settings->setValue("auth/accessToken", m_accessToken); m_settings->setValue("auth/refreshToken", m_refreshToken); - m_settings->setValue("auth/accessTokenExpiration", m_expires); + m_settings->setValue("auth/accessTokenExpiration", m_accessTokenExpiration); + m_settings->remove("auth/refreshTokenExpiration"); emit loggedIn(Result::Success); @@ -265,13 +268,16 @@ void OAuth2Login::loginAuthorizationCode() void OAuth2Login::login() { - const QDateTime now = QDateTime::currentDateTime(); - if (!m_refreshToken.isEmpty() && (!m_expires.isValid() || m_expires < now)) { + // If the current access token is invalid, but we have a valid refresh token, perform a refresh instead of a login flow + const QDateTime now = QDateTime::currentDateTimeUtc(); + const bool validRefreshToken = !m_refreshToken.isEmpty() && (!m_refreshTokenExpiration.isValid() || m_accessTokenExpiration <= now); + if (validRefreshToken && (!m_accessTokenExpiration.isValid() || m_accessTokenExpiration <= now)) { refresh(true); return; } - if (!m_accessToken.isEmpty()) { + // If we still have a valid access token, we can skip the login flow + if (!m_accessToken.isEmpty() && m_accessTokenExpiration > now) { emit loggedIn(Result::Success); return; } @@ -367,9 +373,12 @@ void OAuth2Login::refreshFinished() { const bool ok = readResponse(m_refreshReply); m_refreshing = false; + + // If we're not doing a refresh triggered by a login flow, we can stop here and not emit anything if (!m_refreshForLogin) { return; } + if (!ok) { if (m_auth->authType() == "refresh_token") { log(QStringLiteral("[%1] Refresh failed").arg(m_site->url()), Logger::Warning); @@ -382,14 +391,48 @@ void OAuth2Login::refreshFinished() m_settings->remove("auth/accessToken"); m_refreshToken.clear(); m_settings->remove("auth/refreshToken"); - m_expires = QDateTime(); + m_accessTokenExpiration = QDateTime(); m_settings->remove("auth/accessTokenExpiration"); + m_refreshTokenExpiration = QDateTime(); + m_settings->remove("auth/refreshTokenExpiration"); login(); } else { emit loggedIn(Result::Success); } } +/** + * Extract the "exp" claim from a JSON Web Token. + * @param jwt The JWT to parse. + * @return The "exp" claim from the token, parsed as a datetime on success. A null datetime on error. + */ +QDateTime extractJwtExpiration(const QString &jwt) +{ + // Ignore strings that do not look like a JWT + if (jwt.count('.') != 2) { + return {}; + } + + // Parse the JWT payload as JSON + const QStringList parts = jwt.split('.'); + const QJsonDocument jsonPayloadDoc = QJsonDocument::fromJson(QByteArray::fromBase64(parts[1].toUtf8())); + if (jsonPayloadDoc.isNull()) { + return {}; + } + + // Extract the "exp" claim from the payload + const QJsonObject jsonPayload = jsonPayloadDoc.object(); + const QJsonValue jsonExp = jsonPayload.value("exp"); + if (!jsonExp.isUndefined()) { + QDateTime exp = QDateTime::fromSecsSinceEpoch(jsonExp.toInt(), Qt::UTC); + if (exp.isValid()) { + return exp; + } + } + + return {}; +} + bool OAuth2Login::readResponse(NetworkReply *reply) { const QString result = reply->readAll(); @@ -430,33 +473,33 @@ bool OAuth2Login::readResponse(NetworkReply *reply) if (jsonObject.contains("refresh_token")) { m_refreshToken = jsonObject.value("refresh_token").toString(); + m_refreshTokenExpiration = extractJwtExpiration(m_refreshToken); m_settings->setValue("auth/refreshToken", m_refreshToken); log(QStringLiteral("[%1] Successfully received OAuth2 refresh token '%2'").arg(m_site->url(), m_refreshToken), Logger::Debug); + } - bool expires = jsonObject.contains("expires"); - bool expires_in = jsonObject.contains("expires_in"); - if (expires || expires_in) { - int expiresSecond = jsonObject.value(expires ? "expires" : "expires_in").toInt(); - m_expires = QDateTime::currentDateTime().addSecs(expiresSecond); - } - if (m_refreshToken.count('.') == 2) { - const QStringList parts = m_refreshToken.split('.'); - const QJsonDocument jsonPayloadDoc = QJsonDocument::fromJson(QByteArray::fromBase64(parts[1].toUtf8())); - if (!jsonPayloadDoc.isNull()) { - const QJsonObject jsonPayload = jsonPayloadDoc.object(); - const QJsonValue jsonExp = jsonPayload.value("exp"); - if (!jsonExp.isUndefined()) { - m_expires = QDateTime::fromSecsSinceEpoch(jsonExp.toInt(), Qt::UTC); - } - } - } + bool expires = jsonObject.contains("expires"); + bool expires_in = jsonObject.contains("expires_in"); + if (expires || expires_in) { + int expiresSecond = jsonObject.value(expires ? "expires" : "expires_in").toInt(); + m_accessTokenExpiration = QDateTime::currentDateTime().addSecs(expiresSecond); + } else { + m_accessTokenExpiration = extractJwtExpiration(m_accessToken); + } - if (!m_expires.isNull()) { - const int expiresSecond = QDateTime::currentDateTime().secsTo(m_expires); - QTimer::singleShot((expiresSecond / 2) * 1000, this, SIGNAL(basicRefresh())); - log(QStringLiteral("[%1] Token will expire at '%2'").arg(m_site->url(), m_expires.toString("yyyy-MM-dd HH:mm:ss")), Logger::Debug); - m_settings->setValue("auth/accessTokenExpiration", m_expires); - } + if (!m_accessTokenExpiration.isNull()) { + const int expiresSecond = QDateTime::currentDateTime().secsTo(m_accessTokenExpiration); + QTimer::singleShot((expiresSecond / 2) * 1000, this, SIGNAL(basicRefresh())); + log(QStringLiteral("[%1] Token will expire at '%2'").arg(m_site->url(), m_accessTokenExpiration.toString("yyyy-MM-dd HH:mm:ss")), Logger::Debug); + m_settings->setValue("auth/accessTokenExpiration", m_accessTokenExpiration); + } else { + m_settings->remove("auth/accessTokenExpiration"); + } + + if (!m_refreshTokenExpiration.isNull()) { + m_settings->setValue("auth/refreshTokenExpiration", m_refreshTokenExpiration); + } else { + m_settings->remove("auth/refreshTokenExpiration"); } return true; @@ -464,8 +507,10 @@ bool OAuth2Login::readResponse(NetworkReply *reply) void OAuth2Login::complementRequest(QNetworkRequest *request) const { - // Trigger a token refresh in the background if the token is expired - if (!m_refreshToken.isEmpty() && (!m_expires.isValid() || m_expires < QDateTime::currentDateTime())) { + // Trigger a token refresh in the background if the access token is expired and we have a valid refresh token + const QDateTime now = QDateTime::currentDateTimeUtc(); + const bool validRefreshToken = !m_refreshToken.isEmpty() && (!m_refreshTokenExpiration.isValid() || m_accessTokenExpiration <= now); + if (validRefreshToken && (!m_accessTokenExpiration.isValid() || m_accessTokenExpiration <= now)) { const_cast(this)->refresh(false); } diff --git a/src/lib/src/login/oauth2-login.h b/src/lib/src/login/oauth2-login.h index 3bfd97343..47715910c 100644 --- a/src/lib/src/login/oauth2-login.h +++ b/src/lib/src/login/oauth2-login.h @@ -52,7 +52,8 @@ class OAuth2Login : public Login NetworkReply *m_refreshReply = nullptr; QString m_accessToken; QString m_refreshToken; - QDateTime m_expires; + QDateTime m_accessTokenExpiration; + QDateTime m_refreshTokenExpiration; bool m_refreshing = false; bool m_refreshForLogin = false; };