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.