Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PIN-Based Auth の request_token 取得を実装 #3

Merged
merged 6 commits into from Dec 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/twitter.ts
@@ -0,0 +1,3 @@
import { fetchRequestToken } from "./twitter/oauth/request_token.ts";

console.log(await fetchRequestToken());
94 changes: 94 additions & 0 deletions src/twitter/oauth/oauth_headers.ts
@@ -0,0 +1,94 @@
import { config } from "https://deno.land/x/dotenv/mod.ts";
import { v4 } from "https://deno.land/std@0.51.0/uuid/mod.ts";
import { hmac } from "https://deno.land/x/hmac@v2.0.1/mod.ts";
import { percentEncode, toQueryParams } from "../../util.ts";

type MethodType = "GET" | "POST";

interface Options {
[key: string]: string;
}

class OAuthHeader {
method: MethodType;
url: string;
options: Options;

conf = config();
oauthVersion = "1.0";
oauthSignatureMethod = "HMAC-SHA1";
oauthNonce = this.generateNonce();
oauthTimestamp = this.getCurrentTimestamp();

constructor(method: MethodType, url: string, options: Options = {}) {
this.method = method;
this.url = url;
this.options = options;
}

create() {
const oauthSignature = this.createSignature();
const authorizationHeader = [
"OAuth",
`oauth_consumer_key="${this.conf.consumerApiKey}",`,
`oauth_nonce="${this.oauthNonce}",`,
`oauth_signature="${oauthSignature}",`,
`oauth_signature_method="${this.oauthSignatureMethod}",`,
`oauth_timestamp="${this.oauthTimestamp}",`,
`oauth_version="${this.oauthVersion}"`,
].join(" ");

return new Headers({
"Authorization": authorizationHeader,
"Content-Type": "application/json",
});
}

private generateNonce(): string {
return v4.generate().replace(/-/g, "");
}

private getCurrentTimestamp(): string {
return Math.floor(Date.now() / 1000).toString();
}

// https://developer.twitter.com/en/docs/authentication/oauth-1-0a/creating-a-signature
private createSignature() {
// oauth_token is unnecessary for PIN-Based OAuth
const allParams: Options = {
...this.options,
"oauth_consumer_key": this.conf.consumerApiKey,
"oauth_nonce": this.oauthNonce,
"oauth_signature_method": this.oauthSignatureMethod,
"oauth_timestamp": this.oauthTimestamp,
"oauth_version": this.oauthVersion,
};

const encodedParamPairs = toQueryParams(allParams, true);

const signatureBaseString = `${this.method}&${percentEncode(this.url)}&${
percentEncode(encodedParamPairs)
}`;

const signingKey = `${percentEncode(this.conf.consumerApiSecret)}&`;

const signature = hmac(
"sha1",
signingKey,
signatureBaseString,
"utf8",
"base64",
).toString();

return percentEncode(signature);
}
}

export const createOAuthHeaders = (
method: MethodType,
url: string,
options: Options,
) => {
const headers = new OAuthHeader(method, url, options);
return headers.create();
};
16 changes: 16 additions & 0 deletions src/twitter/oauth/request_token.ts
@@ -0,0 +1,16 @@
import { createOAuthHeaders } from "./oauth_headers.ts";
import { toQueryParams } from "../../util.ts";

const requestTokenUrl = "https://api.twitter.com/oauth/request_token";

export const fetchRequestToken = async (): Promise<string> => {
const method = "POST";
const options = { "oauth_callback": "oob" };
const headers = createOAuthHeaders(method, requestTokenUrl, options);

const response = await fetch(requestTokenUrl + "?" + toQueryParams(options), {
method,
headers,
});
return await response.text();
};
49 changes: 49 additions & 0 deletions src/util.ts
@@ -0,0 +1,49 @@
export const percentEncode = (val: string): string => {
const encodedVal: string = encodeURIComponent(val);

// Adjust for RFC 3986 section 2.2 Reserved Characters
const reservedChars: { match: RegExp; replace: string }[] = [
{ match: /\!/g, replace: "%21" },
{ match: /\#/g, replace: "%23" },
{ match: /\$/g, replace: "%24" },
{ match: /\&/g, replace: "%26" },
{ match: /\'/g, replace: "%27" },
{ match: /\(/g, replace: "%28" },
{ match: /\)/g, replace: "%29" },
{ match: /\*/g, replace: "%2A" },
{ match: /\+/g, replace: "%2B" },
{ match: /\,/g, replace: "%2C" },
{ match: /\//g, replace: "%2F" },
{ match: /\:/g, replace: "%3A" },
{ match: /\;/g, replace: "%3B" },
{ match: /\=/g, replace: "%3D" },
{ match: /\?/g, replace: "%3F" },
{ match: /\@/g, replace: "%40" },
{ match: /\[/g, replace: "%5B" },
{ match: /\]/g, replace: "%5D" },
];

const percentEncodedVal = reservedChars.reduce(
(tot, { match, replace }) => {
return tot.replace(match, replace);
},
encodedVal,
);

return percentEncodedVal;
};

type Object = { [key: string]: string };

export const toQueryParams = (
object: Object,
sort = false,
): string => {
const strings = Object.entries(object).map(([key, val]) => {
const encodedKey = percentEncode(key);
const encodedVal = percentEncode(val);
return `${encodedKey}=${encodedVal}`;
});
if (sort) strings.sort();
return strings.join("&");
};