Oauth #とは
「Oauth1とかOauth2とか、よく聞くけど何もわからん」な状態の人、多分多いと思うんですよね。自分もなんもわかりません。
というわけでRustでOauth1.0を実装してみたいと思います。
Rustなのは趣味です。全人類がRustをやりましょう。
題材が何もないのはつまらないので、今回はTwitter APIを叩いてrequest tokenを取得するところまでやってみます。
ぜひ手元にコーヒーとConsumer key, Consumer secret keyを用意して本記事をお読みください。
対象とする読者
Oauth何もわからん人
RustでOauthを実装してみたい人
Rustで実装しますが、特にRustらしい凝った操作などをしているところはないので、Rust初心者の人でも十分に流れや処理の意味を掴めると思います。
実行環境
- macOS Mojave: 10.14.6
- rustc: 1.36.0
- cargo: 1.36.0
前提
特にありません。でも、プログラミングのプの字くらいは知っていてほしいなという気持ちがあります。
Rustをインストールしていない?今すぐインストールしましょう。
いや、一瞬なので、ちょっとだけで終わりますんで、インストールだけでも、あの、先っちょだけでいいから...
また、本記事は以下のリポジトリに、記事の節目ごとにv1, v2, ...というようにブランチを切って残してあります。
記事内でわからなくなってしまったら、弊リポジトリをご参照ください。
それではやっていきましょう
Oauthって?
ここらへんにいい感じに書かれています。感謝です。
Oauth1.0:フロー
OAuth1.0の署名(Signature)を作成する方法
今更OAuth1.0についてRFC読んで理解してみた - SlideShare
簡単に言うと、ユーザーとサーバの間にいるクライアント(Webアプリケーションなど)がユーザのリソースにアクセス出来るよう認可を行う方式、という感じです(多分)
普通にHTTPリクエストを行って、リクエストヘッダーに色々な情報を付加してあげることで手続きを行います。
Oauth1.0に必要なパラメータ
基本的には以下の通りになると思います。
-
oauth_consumer_key
- APIから開発者に渡される鍵、とても大事なやつその1。
- Twitter APIはこれを手に入れるまでがめんどくさいです。(読書感想文を思い出します)
-
oauth_nonce
- リプレイ攻撃を防ぐためのパラメータです。
- 値はランダム文字列だったりナノ秒だったり、リクエストごとに一意なら何でもよさそうです。
-
oauth_timestamp
- UNIXタイムスタンプです。時間です。
- アプリケーション側と自分のマシンでの時間がズレていることによるエラーが多い(らしい)。
-
oauth_token
- リクエストトークンとかアクセストークンとか言われるやつです。
- ユーザーごとに存在しています。Twitter APIなら、自分用のアクセストークンはDashboardから簡単に拾ってこれます。
-
oauth_signature_method
- oauth_signatureというものを算出する時のアルゴリズム名を入れます。
- Twitterは"HMAC-SHA1"というものを使っているようです。
-
oauth_version
- 使用するOauthのバージョンを書きます。
- 今回はOauth1.0を使うので"1.0"です。
-
oauth_signature
- リクエスト時に必要な値を一切合切ブチ込んでさらにconsumer_secret_keyなども総動員して計算するパラメータです。
- 個人的に、Oauth1.0のめんどくささの2億割がこいつだと思っています。
上記のパラメータのうち、oauth_signatureだけ少し性質が違います。
signatureは、自分が持っている秘密鍵も使用して計算を行います。秘密鍵はパラメータに載せません。大事にしましょう。
#早速実装していく
やっていきます。
適当にプロジェクトを作成します。以下のコマンドを実行してください。
cargo new oauth_trial
cd oauth_trial
コードを書いていく前にまず、TwitterにOauthでリクエストする時の形式を確認します。
公式APIリファレンスを参考にすると、以下のような感じでリクエストを飛ばせばいいらしいです。
Request URL
POST https://api.twitter.com/oauth/request_token
Request POST Body
N/A(何もなくて大丈夫です)
Authorization Header (可読性のため改行を含めています)
OAuth
oauth_nonce="K7ny27JTpKVsTgdyLdDfmQQWVLERj2zAK5BslRsqyw",
oauth_callback="http%3A%2F%2Fmyapp.com%3A3005%2Ftwitter%2Fprocess_callback",
oauth_signature_method="HMAC-SHA1",
oauth_timestamp="1300228849",
oauth_consumer_key="OqEqJeafRSF11jBMStrZz",
oauth_signature="Pc%2BMLdv028fxCErFyi8KXFM%2BddU%3D",
oauth_version="1.0"
頑張ってここら辺の値をリクエストヘッダに入れてあげて、TwtterにPOSTを飛ばせばいけそうです。
ちなみに、ここのoauth_callbackというのはTwitter APIに飛ばした後に帰りたいURLを指定してあげればいいです。
Twtter developerアカウントに登録してあるcallback URLと同じにしてあげる必要があります。(じゃないと403エラーなどが返ると思います)
今回はhttp://127.0.0.1
を使用すればOKです。
また、Responseはこんな感じになります(可読性のため改行を含めています)。
oauth_token=Z6eEdO8MOmk394WozF5oKyuAv855l4Mlqo7hhlSLik
&oauth_token_secret=Kd75W4OQfb2oJTV0vzGzeXftVAwgMnEK9MumzYcM
&oauth_callback_confirmed=true
値としては、oauth_token, oauth_token_secret, oauth_callback_confirmedという値が帰ってきます。
oauth_token, oauth_token_secretの値は、場合によってリクエストトークンの値だったり、アクセストークンの値が入ったりしています。
今回はリクエストトークンの値が帰ってくるはずです。
##はじめの一歩
デンプシーロールを決めながらこの返り値を保存するための構造体を作っていきます。
生の値を返してもいいですが、構造体としてちゃんとまとめてあげたほうが迷子にならなくていいかなと思います。
cargoで勝手に生成されたsrcディレクトリのmain.rsに書いていきます!
#[derive(Clone,Debug)]
struct RequestToken {
oauth_token: String,
oauth_token_secret: String,
oauth_callback_confirmed: String,
}
こんな感じです。整理整頓できて素晴らしいです。
次にこの構造体を返すような関数を作っていきます。
main.rsは全体としてこんな感じになります。
#[derive(Clone,Debug)]
struct RequestToken {
oauth_token: String,
oauth_token_secret: String,
oauth_callback_confirmed: String,
}
fn get_request_token() -> RequestToken {
RequestToken {
oauth_token: "".to_string(),
oauth_token_secret: "".to_string(),
oauth_callback_confirmed: "".to_string()
}
}
fn main() {
let req = get_request_token();
println!("{:?}", req);
}
みたままですが、引数を取らず、RequestToken型の何かを返す何かです。
今はデバッグ用に空文字を詰めて返すようにしています。
まだ形だけですが、何となくイメージは掴めるかなと思います。
この関数を呼び出すと、いい感じにリクエストトークンを返してくれる、までが理想の動作です。
試しにデバッグしてみます。
プロジェクトのルートディレクトリでcargo run
すると結果が確認できます。
$ cargo run
RequestToken { oauth_token: "", oauth_token_secret: "", oauth_callback_confirmed: "" }
##雛形のお造り
次に中身を書いていきます
rustでHTTP通信を行う際は、reqwest(typoでない)というcrateを使うのが便利です。
以下のようにしてreqwestをインストールします。
cargo add reqwest
また、main.rsの先頭に以下のように追記してください。
use reqwest::header::*;
#[derive(Clone,Debug)]
struct RequestToken {
oauth_token: String,
oauth_token_secret: String,
oauth_callback_confirmed: String,
}
...
ではget_request_token()
の中に処理を書いていきます。
まずは以下のような感じにします。
fn get_request_token() -> RequestToken {
let endpoint = "https://api.twitter.com/oauth/request_token";
let header_auth = get_request_header(endpoint);
let mut headers = HeaderMap::new();
headers.insert(AUTHORIZATION, header_auth.parse().unwrap());
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/x-www-form-urlencoded"));
RequestToken {
oauth_token: "".to_string(),
oauth_token_secret: "".to_string(),
oauth_callback_confirmed: "".to_string()
}
}
endpointはこのプログラムでアクセスしたいTwitter APIのURLです。今回はRequest tokenを取得したいのでそれ用のURLをここに入れておきます。
ここで、get_request_header()
はまだ作成していない関数です。上記Oauth用のヘッダーを作成するためにendpointの情報も必要なので、ここで渡しています。
またヘッダー用の値を作成する作業がかなり重いので外に関数として逃しています。
reqwestでは、HashMapのような構造体に値をいい感じに入れると勝手にリクエストヘッダーを作成してくれます。
headersという変数はそのためのものです。これにinsertメソッドをしていくことでヘッダーを作ります。
左側に指定するフィールドを、右側に実際の値を入れます。reqwestのValue
ではget_request_header()
を書いていきます。
とりあえずこんな感じで書きます。
fn get_request_header(endpoint: &str) -> String {
"".to_string()
}
Stringを返すだけです、楽ですね。
このget_request_header()という関数では、oauth_signatureの計算のためにconsumer_keyとconsumer_secret_keyが必要なのですが、プログラム上にそれをベタ書きするのは怖くてやってられません。
そこで今回はfrom_env()
という関数を作って簡単に環境変数を使えるようにします。
具体的には以下のようになります。
fn from_env(name: &str) -> String {
match std::env::var(name) {
Ok(val) => val,
Err(err) => {
println!("{}: {}", err, name);
std::process::exit(1);
}
}
}
nameという名前で&str型の値を受け取り、その名前を元に環境変数から値を読み込む簡単な関数です。値が存在しないと死にます。取得した値をString型で返します。
このままcargo run
しても環境変数に期待する値が存在していないのは明白なので、以下のように.env
というファイルを作成してそこに環境変数にしたい値を書き込み、source
コマンドを使って環境変数に設定する、というのが楽かなと思います。
まず.env
ファイルを作成して以下のように書き("="の前後は開けないでください)
export CONSUMERKEY=****
export CONSUMERSECRET=****
ターミナルで以下のコマンドを実行します。
source .env
それでは次に、get_request_header()の中に、以下のようにササッと値を設定してみます。
今回はお試しなので適当です。(セキュリティ的によくないかも)
UNIX時間取得のためにchronoというcrateを用います。
use chrono::Utc;
...
fn get_request_header(endpoint: &str) -> String {
let oauth_consumer_key: &str= &from_env("CONSUMERKEY");
let oauth_consumer_secret: &str= &from_env("CONSUMERSECRET");
let oauth_nonce: &str = &format!("nonce{}", Utc::now().timestamp());
let oauth_callback: &str = "http://127.0.0.1";
let oauth_signature_method: &str = "HMAC-SHA1";
let oauth_timestamp: &str = &format!("{}", Utc::now().timestamp());
let oauth_version: &str = "1.0";
"".to_string()
}
順番に説明していきます。
-
oauth_consumer_key, oauth_consumer_secret
- from_env()で取得した値はString型なので、とりあえず周りと合わせるために&をつけて&str型にしています。
-
oauth_nonce
- ここも適当にUNIX時間を入れるだけにしてあります。リクエストの一意性が保たれればいいっぽいので、十分な長さのランダム文字列を使っている実装をよくみます。
-
oauth_callback
- 先述の通り
http://127.0.0.1
を入れています。
- 先述の通り
-
oauth_signature_method
- 先述の通り
HMAC-SHA1
を入れています。oauth1.0には他に2つの方式があるらしいです。
- 先述の通り
-
oauth_timestamp
- UNIX時間です。chronoというcrateを使って現在時刻を取得しています。
一応必要なのはこんな感じです。
ここからoauth_signatureの計算に入っていきます。計算するのがとてつもなくめんどくさいので、これまた別の関数を作って逃します。その関数は、create_oauth_signature()
という名前にして後から作成します。
では、以下のようにget_request_header()
を修正します。
fn get_request_header(endpoint: &str) -> String {
let oauth_consumer_key: &str= &from_env("CONSUMERKEY");
let oauth_consumer_secret: &str= &from_env("CONSUMERSECRET");
let oauth_nonce: &str = &format!("nonce{}", Utc::now().timestamp());
let oauth_callback: &str = "http://127.0.0.1";
let oauth_signature_method: &str = "HMAC-SHA1";
let oauth_timestamp: &str = &format!("{}", Utc::now().timestamp());
let mut map: std::collections::HashMap<&str, &str> = std::collections::HashMap::new();
map.insert("oauth_nonce", oauth_nonce);
map.insert("oauth_callback", oauth_callback);
map.insert("oauth_signature_method", oauth_signature_method);
map.insert("oauth_timestamp", oauth_timestamp);
map.insert("oauth_version", oauth_version);
map.insert("oauth_consumer_key", oauth_consumer_key);
let oauth_signature: &str = &create_oauth_signature(
"POST",
&endpoint,
oauth_consumer_secret,
"",
&map
);
"".to_string()
}
oauth_signatureを計算するためには、リクエストヘッダーに載せるoauth用の値の全てと、HTTPメソッド名, endpoint, consumer_secret_key, oauth_token_secret の情報が必要です。
今回はリクエストトークンを取得するタイミングなのでまだoauth_token_secretとして渡す値が存在していません。なので空文字を渡しています。
oauth系の値を全て渡すのはめんどくさいので、全てHashMapに入れて渡すことにしてみました。
oauth_signature自体は他の値と同じように&str型で受け取れると嬉しいので、create_oauth_signature()
をString型を返す関数として、&をつけて&str型にしています。
なぜString型を返すかですが、自分は寿命などの関係でStringのほうが適当に扱えて楽だからです。
今回もとりあえずモックとしてcreate_oauth_signature()
を空文字を返す関数として置いておきます。
以下のような感じです。
fn create_oauth_signature(
http_method: &str,
endpoint: &str,
oauth_consumer_secret: &str,
oauth_token_secret: &str,
params: &std::collections::HashMap<&str, &str>
) -> String {
"".to_string()
}
それでは、さらに次節でoauth_signatureをつくっていきます。
現時点での確認用リポジトリのブランチはv1です。
hppRC/oauth_tutorial
今現在のプログラム全文を載せるとこんな感じです。すでに結構長いですね。
use reqwest::header::*;
use chrono::Utc;
#[derive(Clone,Debug)]
struct RequestToken {
oauth_token: String,
oauth_token_secret: String,
oauth_callback_confirmed: String,
}
fn from_env(name: &str) -> String {
match std::env::var(name) {
Ok(val) => val,
Err(err) => {
println!("{}: {}", err, name);
std::process::exit(1);
}
}
}
fn create_oauth_signature(
http_method: &str,
endpoint: &str,
oauth_consumer_secret: &str,
oauth_token_secret: &str,
params: &std::collections::HashMap<&str, &str>
) -> String {
"".to_string()
}
fn get_request_header(endpoint: &str) -> String {
let oauth_consumer_key: &str= &from_env("CONSUMERKEY");
let oauth_consumer_secret: &str= &from_env("CONSUMERSECRET");
let oauth_nonce: &str = &format!("nonce{}", Utc::now().timestamp());
let oauth_callback: &str = "http://127.0.0.1";
let oauth_signature_method: &str = "HMAC-SHA1";
let oauth_timestamp: &str = &format!("{}", Utc::now().timestamp());
let oauth_version: &str = "1.0";
let mut params: std::collections::HashMap<&str, &str> = std::collections::HashMap::new();
params.insert("oauth_nonce", oauth_nonce);
params.insert("oauth_callback", oauth_callback);
params.insert("oauth_signature_method", oauth_signature_method);
params.insert("oauth_timestamp", oauth_timestamp);
params.insert("oauth_version", oauth_version);
params.insert("oauth_consumer_key", oauth_consumer_key);
let oauth_signature: &str = &create_oauth_signature(
"POST",
&endpoint,
oauth_consumer_secret,
"",
¶ms
);
"".to_string()
}
fn get_request_token() -> RequestToken {
let endpoint = "https://api.twitter.com/oauth/request_token";
let header_auth = get_request_header(endpoint);
let mut headers = HeaderMap::new();
headers.insert(AUTHORIZATION, header_auth.parse().unwrap());
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/x-www-form-urlencoded"));
RequestToken {
oauth_token: "".to_string(),
oauth_token_secret: "".to_string(),
oauth_callback_confirmed: "".to_string()
}
}
fn main() {
let req = get_request_token();
println!("{:?}", req);
}
それでは確認もしたところで次節に進みます。
##signatureの計算をやっていき
それでは早速create_oauth_signature()
の中身をいじっていく...その前に色々準備を整えます。
まずoauth_signatureをどう計算するか説明します。
oauth_signatureはざっくり言えば、リクエストヘッダーに載せるoauth関連の値全てと、HTTPメソッド、そしてendpointを全てパーセントエンコードしながら"&"で繋げていき、さらにそれをHMAC_SHA1で変換します。
HMAC_SHA1で吐き出された値をさらにbase64エンコードすれば完成です。
めちゃくちゃめんどくさい
上記からもわかるように、oauth_signatureの計算にはpercent encode(url encode)が必要不可欠です。それ用のcrateを用います。
まずcrateを追加します。
cargo add percent-encoding
さらにmain.rsの先頭に以下のように追記してください。
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
use reqwest::header::*;
use chrono::Utc;
これでpercent encodeができるようになったんですが、このままだとoauth_signatureの計算に使えるような感じでうまくエンコードしてくれません。
エンコードしてほしい文字をエンコードしなかったり、その逆になったりします。
というわけで、しょうがなく自分でエンコードする文字をカスタマイズします。
以下の記述を (コードブロック下により簡潔な記法あり)use
などの下に記述してください。
const FRAGMENT: &AsciiSet = &CONTROLS
.add(b'!').add(b'"').add(b'#').add(b'$').add(b'%')
.add(b'&').add(b'\'').add(b'(').add(b')').add(b'+')
.add(b',').add(b'/').add(b':').add(b';').add(b'<')
.add(b'=').add(b'>').add(b'?').add(b'@').add(b'[')
.add(b'\\').add(b']').add(b'^').add(b'`').add(b'{')
.add(b'|').add(b'}').add(b'~').add(b' ');
かなり汚いですが、ここに追加した文字をpercent encodingの対象とする、という風に指定できます。そのルールをFRAGMENTという定数として宣言している感じです。
9/8追記分
「percent_encodeしたくない値を除けばいい」という観点から、コメント欄にて以下のように書くとより簡潔と教えていただきました!
@tric さん、ありがとうございます!!
const FRAGMENT: &AsciiSet = &percent_encoding::NON_ALPHANUMERIC
.remove(b'*')
.remove(b'-')
.remove(b'.')
.remove(b'_');
この場合percent_encoding crateのCONTROLSが不要になるので削除してあげてください。
ではcreate_oauth_signature
を弄ります。
以下のように追記してください。(引数などは省略しています)
fn create_oauth_signature (...) -> String {
let cs_encoded = utf8_percent_encode(oauth_consumer_secret, FRAGMENT);
let ts_encoded = utf8_percent_encode(oauth_token_secret, FRAGMENT);
let key: String = format!("{}&{}", cs_encoded, ts_encoded);
"".to_string()
}
ここでやっているのは、oauth_consumer_secretとoauth_token_secretをエンコードした後に、それらの文字列を'&'で繋げるという処理です。今回はoauth_token_secretに何も入っていないはずなので、oauth_token_secretは空文字です。
大体oauth_consumer_keyもoauth_token_secretも元から英数字だと思いますが、一応形式的にやっておきましょう。
繋げた文字列はkey
という名前で取っておきます。
さて、先ほど引数として受け取ったparamsを"&"で繋げていきたいんですが、その場合には一つルールが存在します。
それはparamsとして渡されたキーと値のペアが、キーに関して辞書順(a, ab, c, ...のような順)に並んでいなくてはいけないというものです。
よって、paramsをmutableなVectorとしてから、paramsをソートすることで順番を正しいものにします。(RustのHashMapは値の順序が不定です)
では上記ルールに気をつけて、さらに処理を増やしていきます。
以下のように記述してください。 (※コードブロック下部に追記あり)
fn create_oauth_signature (...) -> String {
...
let mut param: String = format!("");
let mut params: Vec<(&&str, &&str)> = params.into_iter().collect();
params.sort();
for i in 0..params.len()-1 {
let &(k, v) = ¶ms[i];
param = format!("{}{}={}&",
param,
utf8_percent_encode(k, FRAGMENT),
utf8_percent_encode(v, FRAGMENT),
);
}
let &(k, v) = ¶ms[params.len()-1];
param = format!("{}{}={}",
param,
utf8_percent_encode(k, FRAGMENT),
utf8_percent_encode(v, FRAGMENT),
);
"".to_string()
}
9/8追記
コメント欄にて以下のような、より簡潔な記述法を教えていただきました!
let mut params: Vec<(&&str, &&str)> = params.into_iter().collect();
params.sort();
let param = params
.into_iter()
.map(|(k, v)| {
format!(
"{}={}",
utf8_percent_encode(k, FRAGMENT),
utf8_percent_encode(v, FRAGMENT)
)
})
.collect::<Vec<String>>()
.join("&");
処理の大体の内容は理解できるかと思います。
引数のparams(oauthに必要な値が入ったHashMap)から1組ずつキーと値を取り出し、それを"="で結んでいきます。さらにそのペアを"&"で繋げていきます。
最後の組だけ文末の"&"が不要なので、そこをいい感じにするためにちょっと変な処理をしています。
さらに追記していきます。
fn create_oauth_signature (...) -> String {
...
let http_method_encoded = utf8_percent_encode(http_method, FRAGMENT);
let endpoint_encoded = utf8_percent_encode(endpoint, FRAGMENT);
let param_encoded = utf8_percent_encode(¶m, FRAGMENT);
let data = format!("{}&{}&{}", http_method_encoded, endpoint_encoded, param_encoded);
"".to_string()
}
ここでは、今までに繋げてきた値を全て"&"で繋げていきます。
全てpercent_encodeしています。
ここはそんなに難しくなくて嬉しくなりますね。
最後に、HMAC_SHA1で鍵付きハッシングを行います。
この時に使用する鍵が、最初に作成したoauth_consumer_secretとoauth_token_secretを用いたキーです。
rustでHMAC_SHA1とbase64を使うにはそれ用のcrateを用いるだけでいいので便利です。ターミナルで以下のコマンドを実行してください。
cargo add hmac-sha1
cargo add base64
処理としては簡単です。以下のように追記し、末尾の空文字を返している部分を削除してください。
fn create_oauth_signature (...) -> String {
...
let hash = hmacsha1::hmac_sha1(key.as_bytes(), data.as_bytes());
base64::encode(&hash)
}
これで完成です!!
計算した内容が気になる方は適当な場所でprintln!()
してみましょう。
現在のコードは以下のような感じになっています。
本節ではcreate_oauth_signature()
しか弄っていないので、create_oauth_signature()
のみ記載します。
fn create_oauth_signature(
http_method: &str,
endpoint: &str,
oauth_consumer_secret: &str,
oauth_token_secret: &str,
params: &std::collections::HashMap<&str, &str>
) -> String {
let cs_encoded = utf8_percent_encode(oauth_consumer_secret, FRAGMENT);
let ts_encoded = utf8_percent_encode(oauth_token_secret, FRAGMENT);
let key: String = format!("{}&{}", cs_encoded, ts_encoded);
let mut params: Vec<(&&str, &&str)> = params.into_iter().collect();
params.sort();
let param = params
.into_iter()
.map(|(k, v)| {
format!(
"{}={}",
utf8_percent_encode(k, FRAGMENT),
utf8_percent_encode(v, FRAGMENT)
)
})
.collect::<Vec<String>>()
.join("&");
let http_method_encoded = utf8_percent_encode(http_method, FRAGMENT);
let endpoint_encoded = utf8_percent_encode(endpoint, FRAGMENT);
let param_encoded = utf8_percent_encode(¶m, FRAGMENT);
let data = format!("{}&{}&{}", http_method_encoded, endpoint_encoded, param_encoded);
let hash = hmacsha1::hmac_sha1(key.as_bytes(), data.as_bytes());
base64::encode(&hash)
}
また、ここまでのコードはこのリポジトリのv2ブランチとして残してあります。
それでは次節でいよいよHTTP通信を実際に行っていきます!
##HTTP通信したい
といっても、最大の山場はもう超えたので、あとはリクエストヘッダーに適切な形で値を載せて、リクエストを飛ばすだけです。
以下のようにget_request_header()
を修正・追記してください。
fn get_request_header(endpoint: &str) -> String {
...
format!(
"OAuth oauth_nonce=\"{}\", oauth_callback=\"{}\", oauth_signature_method=\"{}\", oauth_timestamp=\"{}\", oauth_consumer_key=\"{}\", oauth_signature=\"{}\", oauth_version=\"{}\"",
utf8_percent_encode(oauth_nonce, FRAGMENT),
utf8_percent_encode(oauth_callback, FRAGMENT),
utf8_percent_encode(oauth_signature_method, FRAGMENT),
utf8_percent_encode(oauth_timestamp, FRAGMENT),
utf8_percent_encode(oauth_consumer_key, FRAGMENT),
utf8_percent_encode(oauth_signature, FRAGMENT),
utf8_percent_encode(oauth_version, FRAGMENT),
)
}
ここでは、twitter APIのリファレンス通りに各値を並べて、その値を文字列に入れています。
文字列の中で「"」を使いたいので、ダブルクオーテーションをエスケープしています。
9/8追記
また、上記の書き方は冗長になるので、以下のように書くことも可能だとコメント欄にてご指摘いただきました!
以下は生文字を使用した記法です。
format!(
r#"OAuth oauth_nonce="{}", oauth_callback="{}", oauth_signature_method="{}", oauth_timestamp="{}", oauth_consumer_key="{}", oauth_signature="{}", oauth_version="{}""#,
utf8_percent_encode(oauth_nonce, FRAGMENT),
utf8_percent_encode(oauth_callback, FRAGMENT),
utf8_percent_encode(oauth_signature_method, FRAGMENT),
utf8_percent_encode(oauth_timestamp, FRAGMENT),
utf8_percent_encode(oauth_consumer_key, FRAGMENT),
utf8_percent_encode(oauth_signature, FRAGMENT),
utf8_percent_encode(oauth_version, FRAGMENT),
)
さらに、以下は適切に改行を行なって整形した記法です。
format!(
"OAuth oauth_nonce=\"{}\", \
oauth_callback=\"{}\", \
oauth_signature_method=\"{}\", \
oauth_timestamp=\"{}\", \
oauth_consumer_key=\"{}\", \
oauth_signature=\"{}\", \
oauth_version=\"{}\"",
utf8_percent_encode(oauth_nonce, FRAGMENT),
utf8_percent_encode(oauth_callback, FRAGMENT),
utf8_percent_encode(oauth_signature_method, FRAGMENT),
utf8_percent_encode(oauth_timestamp, FRAGMENT),
utf8_percent_encode(oauth_consumer_key, FRAGMENT),
utf8_percent_encode(oauth_signature, FRAGMENT),
utf8_percent_encode(oauth_version, FRAGMENT),
)
書き方一つとっても奥が深い...!
リクエストヘッダーに必要な値はこれで用意できました。
次はリクエストを飛ばしてみます。
以下のようにreqwest crateを利用してget_request_token()
に追記します。
fn get_request_token() -> RequestToken {
...
let client = reqwest::Client::new();
let res: String = client
.post(endpoint)
.headers(headers)
.send()
.unwrap()
.text()
.unwrap();
...
}
ここでは、endpointに先ほどまでで作ったheaderを載せてリクエストをPOSTし、さらにその結果をres
という変数に格納しています。
res
はString型の変数です。
Twitter APIからのResponseの形式を再掲すると以下のような感じです。
oauth_token=Z6eEdO8MOmk394WozF5oKyuAv855l4Mlqo7hhlSLik
&oauth_token_secret=Kd75W4OQfb2oJTV0vzGzeXftVAwgMnEK9MumzYcM
&oauth_callback_confirmed=true
これらが全てString型として格納されているので、String型のメソッドを使ってうまくパースしてあげましょう。
以下の処理を追記してください。
fn get_request_token() -> RequestToken {
...
let res_values: Vec<&str> = (&res)
.split('&')
.map(|s| s.split('=').collect::<Vec<&str>>()[1])
.collect();
...
}
ここでは、まず返り値を"&"でsplit、さらにそれぞれを"="でパースして=の右辺のみを持つVectorを作っています。
あとはこれを構造体に詰めて返すだけですね。
以下のように最後に構造体を返している部分を修正します。
fn get_request_token() -> RequestToken {
...
RequestToken {
oauth_token: res_values[0].to_string(),
oauth_token_secret: res_values[1].to_string().to_string(),
oauth_callback_confirmed: res_values[2].to_string(),
}
}
以上で終了です!!!!
全体のプログラムは以下のような感じになりました。
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
use reqwest::header::*;
use chrono::Utc;
const FRAGMENT: &AsciiSet = &percent_encoding::NON_ALPHANUMERIC
.remove(b'*')
.remove(b'-')
.remove(b'.')
.remove(b'_');
#[derive(Clone,Debug)]
struct RequestToken {
oauth_token: String,
oauth_token_secret: String,
oauth_callback_confirmed: String,
}
fn from_env(name: &str) -> String {
match std::env::var(name) {
Ok(val) => val,
Err(err) => {
println!("{}: {}", err, name);
std::process::exit(1);
}
}
}
fn create_oauth_signature(
http_method: &str,
endpoint: &str,
oauth_consumer_secret: &str,
oauth_token_secret: &str,
params: &std::collections::HashMap<&str, &str>
) -> String {
let cs_encoded = utf8_percent_encode(oauth_consumer_secret, FRAGMENT);
let ts_encoded = utf8_percent_encode(oauth_token_secret, FRAGMENT);
let key: String = format!("{}&{}", cs_encoded, ts_encoded);
let mut params: Vec<(&&str, &&str)> = params.into_iter().collect();
params.sort();
let param = params
.into_iter()
.map(|(k, v)| {
format!(
"{}={}",
utf8_percent_encode(k, FRAGMENT),
utf8_percent_encode(v, FRAGMENT)
)
})
.collect::<Vec<String>>()
.join("&");
let http_method_encoded = utf8_percent_encode(http_method, FRAGMENT);
let endpoint_encoded = utf8_percent_encode(endpoint, FRAGMENT);
let param_encoded = utf8_percent_encode(¶m, FRAGMENT);
let data = format!("{}&{}&{}", http_method_encoded, endpoint_encoded, param_encoded);
let hash = hmacsha1::hmac_sha1(key.as_bytes(), data.as_bytes());
base64::encode(&hash)
}
fn get_request_header(endpoint: &str) -> String {
let oauth_consumer_key: &str= &from_env("CONSUMERKEY");
let oauth_consumer_secret: &str= &from_env("CONSUMERSECRET");
let oauth_nonce: &str = &format!("nonce{}", Utc::now().timestamp());
let oauth_callback: &str = "http://127.0.0.1";
let oauth_signature_method: &str = "HMAC-SHA1";
let oauth_timestamp: &str = &format!("{}", Utc::now().timestamp());
let oauth_version: &str = "1.0";
let mut params: std::collections::HashMap<&str, &str> = std::collections::HashMap::new();
params.insert("oauth_nonce", oauth_nonce);
params.insert("oauth_callback", oauth_callback);
params.insert("oauth_signature_method", oauth_signature_method);
params.insert("oauth_timestamp", oauth_timestamp);
params.insert("oauth_version", oauth_version);
params.insert("oauth_consumer_key", oauth_consumer_key);
let oauth_signature: &str = &create_oauth_signature(
"POST",
&endpoint,
oauth_consumer_secret,
"",
¶ms
);
format!(
"OAuth oauth_nonce=\"{}\", oauth_callback=\"{}\", oauth_signature_method=\"{}\", oauth_timestamp=\"{}\", oauth_consumer_key=\"{}\", oauth_signature=\"{}\", oauth_version=\"{}\"",
utf8_percent_encode(oauth_nonce, FRAGMENT),
utf8_percent_encode(oauth_callback, FRAGMENT),
utf8_percent_encode(oauth_signature_method, FRAGMENT),
utf8_percent_encode(oauth_timestamp, FRAGMENT),
utf8_percent_encode(oauth_consumer_key, FRAGMENT),
utf8_percent_encode(oauth_signature, FRAGMENT),
utf8_percent_encode(oauth_version, FRAGMENT),
)
}
fn get_request_token() -> RequestToken {
let endpoint = "https://api.twitter.com/oauth/request_token";
let header_auth = get_request_header(endpoint);
let mut headers = HeaderMap::new();
headers.insert(AUTHORIZATION, header_auth.parse().unwrap());
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/x-www-form-urlencoded"));
let client = reqwest::Client::new();
let res: String = client
.post(endpoint)
.headers(headers)
.send()
.unwrap()
.text()
.unwrap();
let res_values: Vec<&str> = (&res)
.split('&')
.map(|s| s.split('=').collect::<Vec<&str>>()[1])
.collect();
RequestToken {
oauth_token: res_values[0].to_string(),
oauth_token_secret: res_values[1].to_string().to_string(),
oauth_callback_confirmed: res_values[2].to_string(),
}
}
fn main() {
let req = get_request_token();
println!("{:?}", req);
}
実際にcargo run
してみてください。
今までとは違って謎の文字列が出力されたと思います。いい感じに値が取れていれば完了です!!
確認として、ここまでのコードをこのリポジトリのv3ブランチにまとめています。よければご覧ください。
コメント欄にてご指摘頂いた事項を追記し、v4ブランチとして切ってあります。
よろしければそちらをご覧ください。
あとは今回取得したリクエストトークンを使ってユーザのアクセストークンを取得し、それを用いて色々なサービスを利用してみましょう!! (それも難しい)
おわりに
スペクタクル巨編になってしまいました、Oauth1.0はめんどくさいですね。
走り書きなので抜け漏れかなりあると思います。
もし気づいたことがあれば、お気軽にコメント欄などでお教えいただけますと幸いです。
全人類Rustをやろう!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!