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}]}