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

  1. Create or update the user in Actual's server user-management so user_name exactly matches the IdP identity claim
  2. Re-enable the user if enabled = 0 (UPDATE users SET enabled = 1 WHERE user_name = ...)
  3. Verify the claim used for identity mapping matches case and format (check server logs for the value)
  4. 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

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


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/d120f2f218f19519. Report an issue: GitHub.