https://bugs.kde.org/show_bug.cgi?id=520588

            Bug ID: 520588
           Summary: KMail GMail authentication: invalid_grant token
                    refresh failure does not trigger re-authentication,
                    leaving user permanently stuck
    Classification: Frameworks and Libraries
           Product: libkgapi
      Version First unspecified
       Reported In:
          Platform: Arch Linux
                OS: Linux
            Status: REPORTED
          Severity: normal
          Priority: NOR
         Component: General
          Assignee: [email protected]
          Reporter: [email protected]
  Target Milestone: ---

(I've been having the issue with adding Gmail account in KMail for over a year.
I used Claude AI to debug and fix the issue based on Akonadi logs and generate
the bug report)

Description:

When a stored OAuth2 refresh token is invalidated (e.g. because the user
revoked and re-granted access to "Akonadi Resources for Google Services" in
their Google account settings), KMail/Akonadi permanently fails to connect to
Gmail with the error "Failed to authenticate additional scopes". No browser
window opens to allow re-authentication. The only fix is to manually delete the
stored token from KWallet via D-Bus.

Steps to reproduce:

Add a Gmail account to KMail using OAuth2.
Observe that KMail shows "Failed to authenticate additional scopes" and never
opens a browser for re-authentication.

Expected behaviour:

When the token refresh fails with invalid_grant, libkgapi should clear the
stale token from KWallet and fall back to a full browser-based OAuth2 flow,
prompting the user to log in again.

Actual behaviour:

The error is surfaced to the user as "Failed to authenticate additional scopes"
and the connection permanently fails. The user has no way to recover without
manually running:

HANDLE=$(qdbus org.kde.kwalletd6 /modules/kwalletd6 org.kde.KWallet.open
kdewallet 0 "test-app")
qdbus org.kde.kwalletd6 /modules/kwalletd6 org.kde.KWallet.removeEntry $HANDLE
"LibKGAPI" "554041944266.apps.googleusercontent.com" "test-app"

After running this, restarting Akonadi and KMail triggers a fresh browser-based
login which succeeds.

Debugging information:

The journal confirms the root cause — a 400 invalid_grant response from
Google's token endpoint:
akonadi_imap_resource: Requesting token refresh.
akonadi_imap_resource: Received reply from
https://accounts.google.com/o/oauth2/token
akonadi_imap_resource: Status code: 400
akonadi_imap_resource: Bad request, Google replied '{"error": "invalid_grant",
"error_description": "Bad Request"}'
D-Bus monitoring confirms that readMap is called on the LibKGAPI KWallet folder
(finding the stale token), but no writeMap ever follows — the new token from
the re-granted OAuth flow is never saved.

Root cause:

In src/core/accountmanager.cpp, the updateAccount method's completion handler
simply gives up on any AuthJob error:
if (job->error() != KGAPI2::NoError) {
    promise->d->setError(tr("Failed to authenticate additional scopes"));
    return;
}
It does not distinguish between different error types, and does not attempt to
recover from invalid_grant by clearing the stale token and retrying with a full
browser-based authentication.

Meanwhile, src/core/authjob.cpp already contains the correct logic to trigger a
full FullAuthenticationJob (browser login) when the refresh token is empty:
if (d->account->refreshToken().isEmpty() || (d->account->m_scopesChanged ==
true)) {
    auto job = new FullAuthenticationJob(...);  // opens browser
} else {
    auto job = new RefreshTokensJob(...);  // no fallback on failure
}
And accountmanager.cpp already uses the correct pattern for clearing tokens in
the scope-change case (lines 171-176):
cppaccount->setAccessToken({});
account->setRefreshToken({});
account->setExpireDateTime({});
d->updateAccount(promise, apiKey, apiSecret, account, scopes);

Suggested fix:

In src/core/accountmanager.cpp, replace the error handler in updateAccount with
logic that clears the stale token and retries, triggering
FullAuthenticationJob:

cppconnect(job, &AuthJob::finished, q, [this, job, apiKey, apiSecret,
promise]() {
    job->deleteLater();
    if (job->error() != KGAPI2::NoError) {
        const auto account = job->account();
        if (account && !account->refreshToken().isEmpty()) {
            // Token refresh failed (e.g. invalid_grant) — clear stale
            // token and retry, which will trigger full browser login
            account->setAccessToken({});
            account->setRefreshToken({});
            account->setExpireDateTime({});
            mStore->storeAccount(apiKey, account);
            updateAccount(promise, apiKey, apiSecret, account, {});
        } else {
            promise->d->setError(tr("Failed to authenticate additional
scopes"));
        }
        return;
    }

    mStore->storeAccount(apiKey, job->account());
    promise->d->setAccount(job->account());
});

-- 
You are receiving this mail because:
You are watching all bug changes.

Reply via email to