tinyhumansai/openhuman · error

login token is required

Error message

login token is required

What it means

Thrown by BackendOAuthClient::consume_login_token when the one-time login token is empty after trimming. These tokens (e.g. from a Telegram bot deep link) are exchanged at POST /auth/login-token/consume; an empty token means the caller never extracted it from its carrier (URL param, bot payload) before calling. Pure input validation, no request is made.

Source

Thrown at src/api/rest.rs:516

        let state = value
            .get("state")
            .and_then(Value::as_str)
            .filter(|state| !state.is_empty())
            .map(str::to_owned)
            .context("missing state")?;
        Ok(ConnectResponse { oauth_url, state })
    }

    /// Fetches the current authenticated user profile using the provided JWT.
    pub async fn fetch_current_user(&self, bearer_jwt: &str) -> Result<Value> {
        self.authed_json(bearer_jwt, Method::GET, "auth/me", None)
            .await
    }

    /// Exchanges a one-time login token (e.g. from Telegram) for a long-lived JWT.
    pub async fn consume_login_token(&self, login_token: &str) -> Result<String> {
        let token = login_token.trim();
        anyhow::ensure!(!token.is_empty(), "login token is required");

        // Backend serves `POST /auth/login-token/consume` with the token in a JSON
        // body `{ token, audience? }` and returns `{ success, data: { jwt } }`
        // (see backend `routes/auth.ts`). The legacy
        // `telegram/login-tokens/{token}/consume` path-param route was removed, so
        // the old call 404'd and Telegram/OAuth-token login could never complete
        // (WIRING_GAPS_AUDIT C1/C2).
        let response = self
            .sdk
            .auth()
            .consume_login_token(&tinyhumans_sdk::api::types::LoginTokenRequest {
                token: token.to_string(),
            })
            .await
            .context("consume login token through TinyHumans SDK")?;
        let jwt = response
            .get("jwt")
            .and_then(Value::as_str)

View on GitHub (pinned to a221052e0d)

Solutions

  1. Extract the token from its carrier first and confirm it is non-empty before calling
  2. Log the raw incoming deep link / payload (redacted) when the token is missing to see which parse step lost it
  3. If the token is genuinely absent, re-issue the login link instead of calling consume

Example fix

// before
let jwt = client.consume_login_token(&token).await?; // token = ""

// after
let token = params.get("token").map(str::trim).unwrap_or("");
anyhow::ensure!(!token.is_empty(), "login link is missing its token; request a new one");
let jwt = client.consume_login_token(token).await?;
Defensive patterns

Strategy: validation

Validate before calling

let token = login_token.trim();
anyhow::ensure!(!token.is_empty(), "login token missing from deep link; request a new one");
let jwt = client.consume_login_token(token).await?;

Type guard

fn extract_login_token(url: &str) -> Option<String> {
    url::Url::parse(url).ok()?
        .query_pairs()
        .find(|(k, _)| k == "token")
        .map(|(_, v)| v.trim().to_string())
        .filter(|t| !t.is_empty())
}

Prevention

When it happens

Trigger: Calling consume_login_token("") or consume_login_token(" ") — e.g. a deep-link handler parsed the URL but the token query param was absent, or a bot start payload regex matched an empty capture group.

Common situations: Deep-link format changed so the token param moved or was dropped, the bot payload regex no longer matches, or a test fixture left the token field blank.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16). Data as JSON: /api/errors/ce62b0f0b78b0057. Report an issue: GitHub.