Skip to content

Commit

Permalink
fix: split tokens expirations in OAuth2 login (fix #3076)
Browse files Browse the repository at this point in the history
  • Loading branch information
Bionus committed Dec 29, 2023
1 parent 0c818d2 commit fcc6e59
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 33 deletions.
109 changes: 77 additions & 32 deletions src/lib/src/#/oauth2-login.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand Down Expand Up @@ -430,42 +473,44 @@ 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;
}

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<OAuth2Login*>(this)->refresh(false);
}

Expand Down
3 changes: 2 additions & 1 deletion src/lib/src/#/oauth2-login.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down

0 comments on commit fcc6e59

Please # to comment.