actualbudget/actual · error
openid-grant-failed
openid-grant-failed
Error message
openid-grant-failed
What it means
loginWithOpenIdFinalize completes the OpenID Connect callback: it exchanges the code, reads the identity claim, and looks up an enabled user matching that identity in the accounts DB. If no enabled user matches, it throws Error('openid-grant-failed') (code openid-grant-failed), aborting the SSO login.
Source
Thrown at packages/sync-server/src/accounts/openid.ts:292
countUsersWithUserName === 0 ? 'ADMIN' : 'BASIC',
],
);
if (countUsersWithUserName === 0) {
const userFromPasswordMethod = getUserByUsername('');
if (userFromPasswordMethod) {
transferAllFilesFromUser(userId, userFromPasswordMethod.user_id);
}
}
} else {
const { id: userIdFromDb, display_name: displayName } =
accountDb.first(
'SELECT id, display_name FROM users WHERE user_name = ? and enabled = 1',
[identity],
) || {};
if (userIdFromDb == null) {
throw new Error('openid-grant-failed');
}
if (!displayName && userInfo.name) {
accountDb.mutate('UPDATE users set display_name = ? WHERE id = ?', [
userInfo.name,
userIdFromDb,
]);
}
userId = userIdFromDb;
}
});
} catch (error) {
if (error.message === 'user-already-exists') {
return { error: 'user-already-exists' };
} else if (error.message === 'openid-grant-failed') {
return { error: 'openid-grant-failed' };
} else {View on GitHub (pinned to d4334cb6e6)
Solutions
- Create or update the user in Actual's server user-management so user_name exactly matches the IdP identity claim
- Re-enable the user if enabled = 0 (UPDATE users SET enabled = 1 WHERE user_name = ...)
- Verify the claim used for identity mapping matches case and format (check server logs for the value)
- Confirm the OpenID issuer/client config so the expected claim is being read
Example fix
// before: IdP sends 'jane.doe@corp.com', Actual user_name = 'jdoe' // after (server user management or SQL) UPDATE users SET user_name = 'jane.doe@corp.com' WHERE user_name = 'jdoe'; UPDATE users SET enabled = 1 WHERE user_name = 'jane.doe@corp.com';
Defensive patterns
Strategy: try-catch
Try / catch
try {
const { error, url } = await loginWithOpenIdFinalize(...);
} catch (e) {
if (e.message === 'openid-grant-failed') {
render('Your account is not enabled on this Actual server or your username does not match. Contact the server admin.');
} else throw e;
} Prevention
- Keep Actual's users table in sync with the IdP (same identity claim values)
- Enable users in Actual before granting them access in the IdP
- After IdP username changes, update user_name in Actual immediately
- Check server logs for the identity value received during failed grants
When it happens
Trigger: A user completes the IdP login but the 'preferred_username'/identity claim does not match any user_name in the users table, or the matching user row has enabled = 0 (disabled account).
Common situations: IdP username changed (e.g. company rename to email-based logins) while Actual's user_name stayed the old value; user created in the IdP but never added in Actual's users management screen; user disabled in Actual but still active in the IdP.
Related errors
- responseData.description || responseData.reason || 'unknown'
- unknown
- Invalid user IDs
- New owner not found
- Failed to transfer files: ${error.message}
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/d120f2f218f19519.
Report an issue: GitHub.