How to authorize Twitter API?

Hi. Now I’m trying to authorize as an Twitter user account with 3-legged OAuth.
But the scripts throws 401 unauthorized error. I would like anyone to find which are wrong.

iCloudDrive/Scriptable
├ TwitterAuthKit.js
└ @TwitterAuthKit/
  └ Util.js

  • Util.js

class Util {
  constructor(){};
  
  static buildQueryString(param){
    return Object.keys(param)
      .sort()
      .map(key=>{
        return this.rfc3986(key) + '=' + this.rfc3986(param[key]);
    }).join('&');
  };

  static async hmacSha1(base, key){
    const wv = new WebView();
    const html = `
    <script>
      async function main(BASE, KEY, CALLBACK){
        const te = new TextEncoder('utf-8');
        const cryptoKey = await crypto.subtle.importKey(
          'raw',
          te.encode(KEY),
          {name: 'HMAC', hash: {name: 'SHA-1'}},
          false,
          ['sign']
        );
        const signature = await crypto.subtle.sign(
          'HMAC',
          cryptoKey,
          te.encode(BASE)
        );
        const buff = new Uint8Array(signature);
        const str = btoa(String.fromCharCode(...buff))
        return str;
      };
    </script>
    `;
    const js = `main('${base}', '${key}').then(completion); ''`;
    await wv.loadHTML(html);
    const result = await wv.evaluateJavaScript(js, true);
    return result;
  };
  // MDN Web Docs: encodeURIComponent()
  // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
  static rfc3986(str){
    return encodeURIComponent(str).replace(/[!'()*]/g, function(c) {
      return '%' + c.charCodeAt(0).toString(16);
    });
  };
  
  static async nonce(){
    const wv = new WebView();
    const n = await wv.evaluateJavaScript(`
	  const array = new Uint32Array(1);
	  crypto.getRandomValues(array);
	  completion(array[0]);
    `, true
    );
    return n.toString()
  };
  
  static unix(){
	const date = new Date();
	return Math.floor(date.getTime()/1000);
  };
};


module.exports = Util;
  • TwitterAuthKit.js
/* --- ENVIROMENT --- */

const CK = Keychain.get('TW.CK');  // API_KEY
const CS = Keychain.get('TW.CS');  // API_SECRET
const CBURL = 'https://open.scriptable.app/run/TwitterAuthKit';

/* --- Util --- */
const Util = importModule('@TwitterAuthKit/Util');

/* --- TwitterAuthKit --- */
class TwitterAuthKit {
  constructor(CK, CS, AT='', AS=''){
    this.CK = CK;
    this.CS = CS;
    this.AT = AT;
    this.AS = AS;
  };
  
  async generateRequestUrl(callbackUrl){
    const requestUrl = 'https://api.twitter.com/oauth/request_token';
    const baseParams = {
      ... {oauth_callback: callbackUrl},
      oauth_consumer_key: this.CK,
      oauth_signature_method: 'HMAC-SHA1',
      oauth_timestamp: Util.unix(),
      oauth_nonce: await Util.nonce(),
      oauth_version: '1.0'
    };
    const signature = await this.generateSignature(
      'POST',
      requestUrl,
      baseParams,
      (params) => {
        return Object.keys(params)
          // .sort()
          .map(key => {
            return key === 'oauth_callback'
              ? key + '=' + params[key]
              : key + '=' + Util.rfc3986(params[key]);
          })
          .sort((a, b) => {
            if(a < b) return -1;
            if(a > b) return 1;
            return 0;
          })
          .join('&');
      }
    );
    const oauthParams = {
      ...baseParams, 
      ...{oauth_signature: signature}
    };
    // authString
    const authString = 'OAuth ' + Object.keys(oauthParams)
      .map(key => {
        return key + '=' + Util.rfc3986(oauthParams[key]);
      })
      .sort((a, b) => {
        if(a < b) return -1;
        if(a > b) return 1;
        return 0;
      })
      .join(', ');
    // request
    const r = new Request(requestUrl);
    r.method = 'POST';
    r.headers = {
      Authorization: authString
    };
    const result = await r.loadJSON();
    log(JSON.stringify(r.response, null, 2))
    return result;
  };
  
  async generateSignature(method, url, opt_param={}, opt_builder){
    const builder = opt_builder || Util.buildQueryString;
    const key = [
      Util.rfc3986(this.CS),
      Util.rfc3986(this.AS)
    ].join('&');
    const data = ((param)=>{
      const queryString = builder(param);
      return [
        Util.rfc3986(method),
        Util.rfc3986(url),
        Util.rfc3986(queryString)
      ].join('&');
    })(opt_param);
    const signature = await Util.hmacSha1(data, key);
    return signature;
  };
}

const kit = new TwitterAuthKit(CK, CS);

const result = await kit.generateRequestUrl(CBURL);
log(result)

  • Response Body
{"errors":[{"message":"Could not authenticate you.","code":32}]}