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
- Request a fresh link/token and consume it promptly — expiry and single-use consumption are the dominant causes.
- Verify the login token extracted from the URL is complete (no truncation, no HTML-escaping artifacts like &).
- Ensure the consume call happens exactly once per token — guard against React double-invoke (StrictMode) re-consuming.
- 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
- Consume each token exactly once; guard against React StrictMode double-invoke.
- Extract the token with URL/searchParams to avoid truncation.
- Treat any failure as 'request a new link' UX rather than a dead end.
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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Channel connect response missing status
- Core returned an empty backend URL
- Core RPC returned an error
- Discord link complete response missing required boolean…
- Discord link start response missing required string field…
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)