401 when trying to create a tweet on Twitter/X

Hi, i'm just started learning Rust last week and right now i'm trying to create a tweet, but not sure what i'm doing wrong. I keep getting 401 response. Everytime works fine when i test it on postman but have no luck with my code. I've been trying to debug but not sure what i'm doing wrong. Any help would be appreciated.

this is the response that i got:

signature: OAuth oauth_consumer_key="xxxxxxxxxxxxxxxxxxxx", oauth_nonce="MPEZKcVz0mUAAAAApp%2B6ca4OGRdjYCRE", oauth_signature="%2Bh6wp5P0bD2dr74k6yX4XB3SY5Y%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1708291013", oauth_token="xxxxxxxxxxxxxxxxxxxxxxxx", oauth_version="1.0"


tweet response: Response {
    url: Url {
        scheme: "https",
        cannot_be_a_base: false,
        username: "",
        password: None,
        host: Some(
            Domain(
                "api.twitter.com",
            ),
        ),
        port: None,
        path: "/2/tweets",
        query: None,
        fragment: None,
    },
    status: 401,
    headers: {
        "perf": "7469935968",
        "content-type": "application/problem+json",
        "cache-control": "no-cache, no-store, max-age=0",
        "content-length": "99",
        "x-transaction-id": "7d70178eb8f70f5b",
        "x-response-time": "2",
        "x-connection-hash": "d9a1212b0290a23c211d027c9d3988b0f3845ab5439e4a05a68ffae8839c897a",
        "date": "Sun, 18 Feb 2024 21:16:54 GMT",
        "server": "tsa_b",
    },
}
/// ./generate_signature.rs
use base64::prelude::*;
use chrono::Utc;
use dotenv::dotenv;
use hmac::{Hmac, Mac};
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use sha1::Sha1;
use std::env;
use textnonce::TextNonce;

pub fn generate_nonce() -> String {
    let nonce = TextNonce::new();
    nonce.to_string()
}

pub fn generate_oauth_signature() -> String {
    dotenv().ok();
    let timestamp = (Utc::now().timestamp_millis() / 1000).to_string();
    let consumer_key = env::var("CONSUMER_KEY").expect("consumer key is missing");
    let consumer_secret = env::var("CONSUMER_SECRET").expect("cosumer secret is missing");
    let access_token = env::var("ACCESS_TOKEN").expect("access token is missing");
    let token_secret = env::var("TOKEN_SECRET").expect("token secret is missing");
    let http_method = "POST";
    let base_url = "https://api.twitter.com/2/tweets";
    let nonce = generate_nonce();

    let mut signing_params: Vec<(String, String)> = Vec::new();
    signing_params.push(("oauth_consumer_key".to_string(), consumer_key.to_string()));
    signing_params.push(("oauth_nonce".to_string(), nonce.to_string()));
    signing_params.push((
        "oauth_signature_method".to_string(),
        "HMAC-SHA1".to_string(),
    ));
    signing_params.push(("oauth_timestamp".to_string(), timestamp.to_string()));
    signing_params.push(("oauth_token".to_string(), access_token.to_string()));
    signing_params.push(("oauth_version".to_string(), "1.0".to_string()));

    signing_params.sort();
    let param_string = signing_params
        .into_iter()
        .map(|(key, value)| {
            format!(
                "{}={}",
                utf8_percent_encode(&key, NON_ALPHANUMERIC),
                utf8_percent_encode(&value, NON_ALPHANUMERIC)
            )
        })
        .collect::<Vec<String>>()
        .join("&");
    let signature_base_string = format!(
        "{}&{}&{}",
        http_method,
        utf8_percent_encode(base_url, NON_ALPHANUMERIC),
        utf8_percent_encode(&param_string, NON_ALPHANUMERIC)
    );

    let signing_key = format!("{}&{}", consumer_secret, token_secret);
    let mut mac = Hmac::<Sha1>::new_from_slice(signing_key.as_bytes())
        .expect("Hmac can take key of any size");
    mac.update(signature_base_string.as_bytes());
    let signature = BASE64_STANDARD.encode(mac.finalize().into_bytes());

    format!("OAuth oauth_consumer_key=\"{}\", oauth_nonce=\"{}\", oauth_signature=\"{}\", oauth_signature_method=\"HMAC-SHA1\", oauth_timestamp=\"{}\", oauth_token=\"{}\", oauth_version=\"1.0\"",
    utf8_percent_encode(&consumer_key, NON_ALPHANUMERIC),
    utf8_percent_encode(&nonce, NON_ALPHANUMERIC),
    utf8_percent_encode(&signature, NON_ALPHANUMERIC),
    utf8_percent_encode(&timestamp, NON_ALPHANUMERIC),
    utf8_percent_encode(&access_token, NON_ALPHANUMERIC)
)
}
/// ./tweet.rs
use serde_json::json;

use crate::generate_signature::generate_oauth_signature;

pub async fn create_tweet() -> Result<(), Box<dyn std::error::Error>> {
    let signature = generate_oauth_signature();

    println!("signature: {}", signature);

    let client = reqwest::Client::new();
    let response = client
        .post("https://api.twitter.com/2/tweets")
        .header("Authorization", signature)
        .header("Content-Type", "application/json")
        .json(&json!({"text": "Hello"}))
        .send()
        .await?;

    println!("tweet response: {:#?}", response);

    Ok(())
}

Have you tried comparing the raw HTTP send by Postman and your app?

hey, yes i have. On the doc it said to sort the params alphabetically but on post man log the signature is not alphabetically but still worked, so i tried to send both alphabetically and the order that the postman has and still got 401 both time.

Can you show all request headers from your app as well?

here's the request response header from my app

tweet response: Response {
    url: Url {
        scheme: "https",
        cannot_be_a_base: false,
        username: "",
        password: None,
        host: Some(
            Domain(
                "api.twitter.com",
            ),
        ),
        port: None,
        path: "/2/tweets",
        query: None,
        fragment: None,
    },
    status: 401,
    headers: {
        "perf": "7469935968",
        "content-type": "application/problem+json",
        "cache-control": "no-cache, no-store, max-age=0",
        "content-length": "99",
        "x-transaction-id": "d4af45e63ff044c5",
        "x-response-time": "2",
        "x-connection-hash": "aa1854e4954bf289954def2b16fd0003ce7b486ae00da8134a2974acbfcfe019",
        "date": "Sun, 18 Feb 2024 22:40:59 GMT",
        "server": "tsa_b",
    },
}

this is from postman

date: Sun, 18 Feb 2024 21:51:40 UTC
perf: 7469935968
server: tsa_b
location: https://api.twitter.com/2/tweets/1759334895455781037
api-version: 2.93
content-type: application/json; charset=utf-8
cache-control: no-cache, no-store, max-age=0
content-length: 108
x-access-level: read-write
x-frame-options: SAMEORIGIN
content-encoding: gzip
x-transaction-id: f0a3f2a85a60cddf
x-xss-protection: 0
x-rate-limit-limit: 1080000
x-rate-limit-reset: 1708293706
content-disposition: attachment; filename=json.json
x-content-type-options: nosniff
x-rate-limit-remaining: 1079998
x-app-limit-24hour-limit: 50
x-app-limit-24hour-reset: 1708356228
strict-transport-security: max-age=631138519
x-user-limit-24hour-limit: 50
x-user-limit-24hour-reset: 1708356228
x-app-limit-24hour-remaining: 45
x-user-limit-24hour-remaining: 45
x-response-time: 135
x-connection-hash: b1ab6b0ef34f54809985b4469439f8bdd2e00a8fc7b2656f8fb6daaf05f7ec7d```

Those are the response headers

Oh sorry lol. so this is what i'm sending. signature is what's returned from my generate_oauth_signature function and then i call this in my create_tweet function

let response = client
        .post("https://api.twitter.com/2/tweets")
        .header("Authorization", signature)
        .header("Content-Type", "application/json")
        .json(&json!({"text": "Hello"}))
        .send()
        .await?;```

Posting text is always best, especially for those using screen readers. Here is info on formatting.

Ok just fixed it. thank you

1 Like

From twitters doc, they don't seem to require any special headers or something like that. So, I suspect the error is in your oauth signature code. There probably are tested crates to do that, have you tried that instead of implementing it yourself?

Try using base64 URL for encoding the signature. Maybe your request is being rejected because its malformed.

Do you know any crate? i tried looking but couldn't see any which is why i decided to try and implementing it myself. Found egg-mode but it was already abandoned

so i tried using base64-url = "2.0.2" and replacing

let signature = base64_url::encode(&mac.finalize().into_bytes());

but still got the same error

signature: OAuth oauth_consumer_key="xxxxxxxxxxx",oauth_token="xxxxxxxxx",oauth_signature_method="HMAC-SHA1",oauth_timestamp="1708297603",oauth_nonce="oPW0KION0mUAAAAA",oauth_version="1.0",oauth_signature="hO8viP%2D0xTT6SmcjF5Vdlo02wZo"
tweet response: Response {
    url: Url {
        scheme: "https",
        cannot_be_a_base: false,
        username: "",
        password: None,
        host: Some(
            Domain(
                "api.twitter.com",
            ),
        ),
        port: None,
        path: "/2/tweets",
        query: None,
        fragment: None,
    },
    status: 401,
    headers: {
        "perf": "7469935968",
        "content-type": "application/problem+json",
        "cache-control": "no-cache, no-store, max-age=0",
        "content-length": "99",
        "x-transaction-id": "97c69fc1bdd2f146",
        "x-response-time": "2",
        "x-connection-hash": "0b194e25606f19875274c53f9327babcd39af09f16c101f179cebfc7853a134d",
        "date": "Sun, 18 Feb 2024 23:06:44 GMT",
        "server": "tsa_b",
    },
}

Can you post the response's body?

this is the response i got 401 when trying to create a tweet on Twitter/X - #14 by irregular

That's the Debug representation of the response, and it does not include the body. That's why I'm asking.

super noob question, how do i get the reponse body? lol do i do response.text().await to get it?

Yes. There's a json method to get the body as JSON as well.

so this is what i got

Error response body: {
  "title": "Unauthorized",
  "type": "about:blank",
  "status": 401,
  "detail": "Unauthorized"
}```

The way you are generating the so called parameter string seems to be the issue. It should look like this according to the documentation:

include_entities=true&oauth_consumer_key=xvz1evFS4wEEPTGEFPHBog&oauth_nonce=kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1318622958&oauth_token=370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb&oauth_version=1.0&status=Hello%20Ladies%20%2B%20Gentlemen%2C%20a%20signed%20OAuth%20request%21