tinyhumansai/openhuman · error · Error

Login token invalid or expired

Error message

Login token invalid or expired

What it means

Thrown by consumeLoginToken() when the core RPC 'openhuman.auth.consume_login_token' resolves but the result has no jwtToken string. The core validates the one-time login token (Telegram/OAuth verified-link flow) against the backend; a missing JWT in a successful envelope means the core considered the token unusable — invalid, already consumed, or expired — or the token string passed in was garbage. This is the generic client-side translation for 'token did not yield a session'.

Solutions

  1. Request a fresh link/token and consume it promptly — expiry and single-use consumption are the dominant causes.
  2. Verify the login token extracted from the URL is complete (no truncation, no HTML-escaping artifacts like &).
  3. Ensure the consume call happens exactly once per token — guard against React double-invoke (StrictMode) re-consuming.
  4. If it always fails with fresh tokens, check backend clock sync and the token TTL configured on the backend.

Example fix

// before
const jwtToken = response.result?.jwtToken;
if (!jwtToken) {
  throw new Error('Login token invalid or expired');
}

// after — one-shot consume with double-invoke guard
let consumed = false;
async function consumeOnce(token: string) {
  if (consumed) return;
  consumed = true;
  const response = await callCoreRpc<{ result: { jwtToken: string } }>({
    method: 'openhuman.auth.consume_login_token',
    params: { loginToken: token },
  });
  const jwtToken = response.result?.jwtToken;
  if (!jwtToken) {
    consumed = false; // allow re-request of a NEW token, not replay of this one
    throw new Error('Login token invalid or expired — request a new link');
  }
  return jwtToken;
}
Defensive patterns

Strategy: validation

Validate before calling

const token = new URL(loginUrl).searchParams.get('token') ?? '';
if (!token || token.length < 16) {
  // link is mangled/expired — request a fresh one instead of consuming
  throw new Error('Login link is malformed — request a new one');
}
const jwt = await consumeLoginToken(token);

Type guard

function hasJwtToken(res: unknown): res is { result: { jwtToken: string } } {
  return typeof (res as { result?: { jwtToken?: unknown } })?.result?.jwtToken === 'string'
    && ((res as { result: { jwtToken: string } }).result.jwtToken.length > 0);
}

Try / catch

try {
  return await consumeLoginToken(token);
} catch (e) {
  if (e instanceof Error && e.message === 'Login token invalid or expired') {
    redirect('/auth?expired=1'); // prompt for a fresh link
  } else throw e;
}

Prevention

When it happens

Trigger: User clicks an old magic/Telegram login link whose token already expired or was consumed on another device; the token was truncated when copied out of the URL; calling consumeLoginToken twice with the same token (one-time use); clock skew between client and backend shortening the validity window.

Common situations: Stale browser tab with a login link reopened later; email clients mangling long query strings; dev flows where the same fixture token is reused across test runs; user pasting only part of the URL.

Understand the failure class

Related errors


AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17). Data as JSON: /api/errors/9b222b18f825ccd5. Report an issue: GitHub.

Appendix: source

Thrown at app/src/services/api/authApi.ts:60

    throw error;
  } finally {
    window.clearTimeout(timeoutId);
  }
}

/**
 * Consume a verified login token and return the JWT.
 * Works for both Telegram and OAuth login tokens.
 * POST /telegram/login-tokens/:token/consume (no auth required)
 */
export async function consumeLoginToken(loginToken: string): Promise<string> {
  const response = await callCoreRpc<{ result: { jwtToken: string } }>({
    method: 'openhuman.auth.consume_login_token',
    params: { loginToken },
  });
  const jwtToken = response.result?.jwtToken;
  if (!jwtToken) {
    throw new Error('Login token invalid or expired');
  }
  return jwtToken;
}

View on GitHub (pinned to 7491200858)