Skip to main content

5 posts tagged with "OAuth"

View All Tags

· 8 min read

一昨日辺りからOAuth実装してる。
アクセストークンを取るところまでは躓きながらもなんとかなった。
今はそこら中に実装されてるコードがあるから、いざとなったらコピペなんてこともできる。

今後はこのアクセストークンを利用するわけだが、TwitterへのツイートやFlickrへのアップロードとなるとまた小さな壁が登場する。
アクセストークンを取得するには必要最低限のルールで済むけど、APIを利用する際にはさらにちょっとだけ必要な知識が増える。
知識はちょっとでいいんだけど、デバッグとなると知識は大切…と改めて思った。
普段は別にそんなことも気にしないんだけど、仕事で「こういう構成で進めましょう」って話をすることが多いから、なるべく誰でもなんとかなるように…と考えているせいか、「この知識がなかったら解決できたのか?」と自問することがある。

· 3 min read

OAuth 1.0を実装する際にありがちなミスを並べてみた。
ライブラリを使う人は遭遇することはないだろう。

  • KeyとSecretが違う
  • POSTとGETの指定ミス
  • URL encodeミス
  • encode忘れ
  • encodeしすぎ
  • 必要なヘッダーはある?
  • ヘッダーに付けすぎ
  • ヘッダーはカンマ区切り
  • ソート忘れ

· 6 min read

先日、Google Apps ScriptからGoogle App Engineのサービスを利用する際に管理者の判断を行いたかったのでそれについて調べていた。 自分ではある程度解決したと思っている。

発端

そんなとき、こんなmentionが飛んできた。

来月辺り、@vvakameにふやかされる可能性があるので、今のうちに親切に恩を売っておく目的でこれ書いてる。

今のところこうなのかな?と思っている

以下の確認はスクリプトエディタから実行したので、トリガーなどからの実行は未確認です。 テスト中なので一部は "???" 表記にしてます。

まずはOAuthによる認証について。

  • ???.appspot.comではOAuthによる認証が出来た
  • ???.mydomain.xyzではOAuthによる認証が出来なかった

続いて、OAuthが通った場合のユーザ情報について。

  • AppEngine側ではOAuthServiceFactoryからユーザ情報が取得できた
  • AppEngine側ではUserServiceFactoryからユーザ情報が取得できなかった
  • UserServiceFactoryからUser情報が取得できなかったので、管理者かどうか判断できない

所感

mydomain.xyzで認証が通らないのはそのドメインの信頼性を確保できないからということなのだろうか? だとしたら、証明書を登録することによってmydomain.xyzでもOAuthによる認証が通るのかな?と思いつつ、試してない。時間があったらやってみたいところだけど、優先度が下がってる。 mydomain.xyzのToken(key, secret)使ってても、appspot.comでの認証が通ってる。mydomain.xyzはDNSでgoogleの方を向いているから結局は同じということなのだろうか?これはなんとなくありのような、無しのような、微妙な感覚。

ソースコード(apps script)

以下がapps scriptのソースコード。
function initApps_() {
ScriptProperties.setProperty("http", "https://");
ScriptProperties.setProperty("appid", "?????");
ScriptProperties.setProperty("d", "appspot.com");
ScriptProperties.setProperty("consumerKey", "?????.appspot.com");
ScriptProperties.setProperty("consumerSecret", "appssecret");
}

function initMydomain_() {
ScriptProperties.setProperty("http", "https://");
ScriptProperties.setProperty("appid", "?????");
ScriptProperties.setProperty("d", "mydomain.xyz");
ScriptProperties.setProperty("consumerKey", "?????.mydomain.xyz");
ScriptProperties.setProperty("sonsumerSecret", "mydomainsecret");
}

function initOAuthUrl_() {
var http = ScriptProperties.getProperty("http");
var appid = ScriptProperties.getProperty("appid");
var domain = ScriptProperties.getProperty("d");

var ah = http + appid + "." + domain + "/_ah/";
ScriptProperties.setProperty("accessTokenUrl", ah + "OAuthGetAccessToken");
ScriptProperties.setProperty("requestTokenUrl", ah + "OAuthGetRequestToken");
ScriptProperties.setProperty("authUrl", ah + "OAuthAuthorizeToken");
}

// appspotへOAuth --> oauth: ken@mydomain.xyz / user: unknown
function oAuthAppspot() {
initApps_();
var url = "http://?????.appspot.com/oauth/auth";
oAuthTest_(url);
}

// mydomainへOAuth --> Unknown(OAuthRequestExceptionが発生している)
function oAuthmydomain.xyz() {
initMydomain_();
var url = "http://?????.mydomain.xyz/oauth/auth";
oAuthTest_(url);
}

// appspotへOAuth(mydomainのtoken) --> oauth: ken@mydomain.xyz / user: unknown
function oAuthAppspotMyKey() {
initMydomain_();
var url = "http://?????.appspot.com/oauth/auth";
oAuthTest_(url);
}

// mydomainへOAuth(appspotのtoken) --> Unknown(OAuthRequestExceptionが発生している)
function oAuthmydomain.xyzAppspotKey() {
initApps_();
var url = "http://?????.mydomain.xyz/oauth/auth";
oAuthTest_(url);
}

function oAuthTest_(url) {
url += "?" + Utilities.formatDate(new Date(), "GMT", "yyyy-MM-dd HH:mm:ss");
Logger.log(url);
try {
var response = UrlFetchApp.fetch(url, option_());
Browser.msgBox(response.getContentText());
} catch(e) {
var msg = "";
for (var i in e) {
msg += e[i] + "n";
}
Browser.msgBox(msg);
}
}

function option_() {
initOAuthUrl_();
var oAuthServiceName = "apps";
var accessTokenUrl = ScriptProperties.getProperty("accessTokenUrl");
var requestTokenUrl = ScriptProperties.getProperty("requestTokenUrl");
var authUrl = ScriptProperties.getProperty("authUrl");
var key = ScriptProperties.getProperty("consumerKey");
var secret = ScriptProperties.getProperty("consumerSecret");

var oAuthConfig = UrlFetchApp.addOAuthService(oAuthServiceName);
oAuthConfig.setAccessTokenUrl(accessTokenUrl);
oAuthConfig.setRequestTokenUrl(requestTokenUrl);
oAuthConfig.setAuthorizationUrl(authUrl);
oAuthConfig.setConsumerKey(key);
oAuthConfig.setConsumerSecret(secret);

var options = {
"oAuthServiceName" : oAuthServiceName,
"oAuthUseToken" : "always"
};

return options;
}

oAuthTest_メソッドでURLに時刻を付けてるのは、キャッシュを返されるような動作があったので、それを回避するため。

スクリプトでひとつ疑問。 ScriptPropertiesで "domain" をキーに指定した場合、getPropertyするとnullになる。これはうちだけ?

ソースコード(App Engine)

以下がApp Engineのソースコード。 言語はJavaじゃなくて、Scala使ってるし、オレオレフレームワーク使ってる。
class OauthController(request:Request, response:Response) extends Controller(request, response) {

def auth = {
try {
val oauth = OAuthServiceFactory.getOAuthService();
val oauthUser = oauth.getCurrentUser match {
case user if user != null => user.getNickname;
case _ => "unknown"
}
val user = UserServiceFactory.getUserService.getCurrentUser match {
case user if user != null => user.getNickname
case _ => "unknown"
}
val msg = "oauth: %s / user: %s".format(oauthUser, user)
response.getWriter.write(msg)
} catch {
case e:OAuthRequestException =>
response.getWriter.write(e.getMessage)
}
}
}

まとめ

今のところ、管理者かどうか判断は出来ていないけど、ユーザは判断できているので、それで凌ごうと思っている。必ずしも開発者(管理者)がアプリの管理者とは限らないので、これはこれでいいや…と思ってる。 お仕事じゃないので、うるさく言う人いないし(笑)

· 7 min read

OAuthを利用して画像を投稿したかったので、まずは画像投稿でよさげなサービスを探すことにした。 そうは言っても、ひとつひとつ調査してたら大変なので、Twitterでつぶやいてみたら、ありがたいことにアドバイスいただきました。

色々とアドバイスいただいたんだけど、重要なのは以下の2つ。

  • TweetPhotoがいいよ
  • 認証はOAuth Echoを使うのがよさそう

早速、OAuth Echoを利用し、TweetPhotoへ画像を投稿しようと思ったら・・・出来ない! 結果から言うと、TweetPhotoへの投稿は未だに出来て無くて、Twitpicへの投稿は出来たという状態。

まずは、OAuth Echoの手順を大雑把説明するとこんな感じ。

  1. OAuthの署名を作成
  2. 署名をヘッダに付けて画像を投稿

TweetPhotoへ投稿する前にTwitterへ投げて、AccessToken(OAuth Token)がもらえたら署名は間違いないだろうということで、ここから確認。ここは問題なく出来た。では、続いて画像を投稿・・・出来ない。

エラーの内容も親切ではなく、よくわからない。

http://groups.google.com/group/tweetphoto/web/upload-v2-0-api を見る限りでは、認証情報とその他ヘッダーと画像を送ればいいのかな?と思っていたのだがエラー。 しかも、このエラーが不親切で、原因を追うにも役立たず。その後、色々と調べてみるも、TweetPhotoの情報が少なく、Twitpicの情報が多いことに気付く。 そこで、Twitpicを試してみて、画像の投稿が出来れば認証も出来ていることだし、少しは切り分けに役立ちそうだったので、急遽Twitpicに変更。

実際、Twitpicの方がAPIの情報が整理されててわかりやすい! TwitpicのAPI情報: http://dev.twitpic.com/docs/2/upload/ TweetPhotoのAPI情報: http://groups.google.com/group/tweetphoto/web/upload-v2-0-api そもそも、このgoogle groupのAPI情報はオフィシャルなのか?って話はあるものの、一番有力そうに見えた。 APIが整理されてるって大事よね・・・と改めて実感(そして、軽くTweetPhotoへ疑念)。

では、Twitpicへ投稿・・・出来ない!! ディスプレイに穴が開くんじゃないかってくらい、サンプルコード(Java, PHPなど)を見比べても署名の違いはなさそう。署名でないとするとパラメータを疑うんだけど、パラメータはシンプルで間違いなさそう。 悩み疲れたので風呂にでも入るか・・・と席を立った瞬間にふと思い当たることがひとつ思い浮かんだ。 それが verify_credentials の拡張子(種類)。 XMLの方が扱いやすかったので https://api.twitter.com/1/account/verify_credentials.xml を利用していたのだが、そう言えば、サンプルコードではjsonしか見なかったな・・・と。 その場で拡張子だけをxmlからjsonに変えたらエラー内容が変わった! 今まではTwitterの認証で弾かれてる風だったのが、keyが無いと言われるようになった!

key情報がなかったのはx-www-form-urlencodedで送っていたためで、multipart-formdataに変えたら無事に投稿できました♪ 実際は、GAE/JのURLFetchでmultipartで送る方法がなさそうだったので仕方なく書いたので、一手間かかってる。

再度、拡張子をjsonからxmlに戻してもやっぱりエラーでした。

http://twitterapi.pbworks.com/Twitter-REST-API-Method%3A-account%C2%A0verify_credentials↑こちらはxml, json両対応と書いてある。http://dev.twitter.com/pages/oauth_echo↑こちらはjsonしか書いてない。 ということは、OAuthの時はxml, json両対応で、OAuth Echoの場合はjsonしか対応してないってこと?

無事にTwitpic投稿が出来たので、続いてTwitter4jを使ってTwitterへtweetを・・・と思ったら、今度はこちらがエラー発生。 java.lang.NoSuchMethodError: org.apache.log4j.Logger.infoが出たり、java.lang.NoClassDefFoundError: Could not initialize class twitter4j.http.OAuthAuthorizationが出たり。 Twitpicをいじる前はtweet出来るを確認していたんだけど、その後、GAEのバージョンを1.3.4から1.3.5に変えたし、明確な原因不明。 使っていたのはstatuses/updateだけだったので、これだけ書きました。OAuth Echoの実装があったので、汎用的に修正して完了。 なんとお手軽OAuth。

まだTwitpicへの投稿で文字化けしてるし、TweetPhotoへの投稿は出来てないけど、アドバイスをいただいたおかげで方向性が定まってよかった。

最後に簡単にまとめ。

  • APIはわかりやすく明確に。
  • OAuth Echoはjsonしか対応してなさそうだよ。

· 5 min read

Twitter絡みで、OAuth Echoを使いたかったので書いてみた。

ネットの情報を色々と見ていると、人それぞれというか、動くのかな?っていうのもあったりして、軽く振り回された感あり :| 書いたソースコードはgithubに置いておきます&下の方に貼り付けておきます。

http://gist.github.com/497821お役に立つようでしたら、ご自由にどうぞ。

以下、RequestTokenを取得するURLの作り方を簡単にまとめます。 RequestTokenはTwitterに署名付きのパラメータを送ることによってもらうことが出来きます。

http://twitter.com/oauth/request_token?&<各種パラメータ>&<署名>

この各種パラメータは次の通り。

  • oauth_consumer_key: 各自のConsumerKey
  • oauth_signature_method: "HMAC-SHA1" 他にもあるらしいが今回はこれ固定。
  • oauth_nonce: その場限りの値。ランダム文字列を利用。
  • oauth_timestamp: 現在時刻を秒で表した値。1900年を起算にっていう例のやつ。
  • oauth_version: OAuthのバージョン値。"1.0"固定。

URL構築時にはお約束の key=value を&で連結。

署名がちょっと面倒。 まずは署名の準備。署名にはHMAC-SHA1形式を利用。 キーを作成するときは "ConsumerSecret&OAuthToken" とする。もちろん、ConsumerSecretもOAuthTokenも自分の値で書き換えてください。 初回はOAuthTokenをまだ取得してないので空文字列となり、結果的に "ConsumerSecret&" となります。

続いて、署名するための文字列を作成します。

  1. パラメータを名称順にソート。
  2. パラメータを連結(key1=value&key2=value2&...)。これを仮に<Params>とする。keyとvalueをそれぞれエンコードする必要があるという情報も見たが、今回はエンコードしても同じなので、してません。
  3. "GET&encode(http://twitter.com/oauth/request_token)&encode(<Params>)" を作成。encodeはURLエンコード。

ここで作成した "GET..." という文字列を先程作成したキーで署名します。 署名した値をBase64エンコードし、その後、URLエンコードします。 これがRequestTokenをもらうためのURLの一部となる署名です。

生成したURLを叩いて "oauth_token=..." というレスポンスが帰ってくれば成功です。

もし、うまくいかなかった場合に見直した方がいいと思われるところ。

  • 署名用のキーを生成するところ。ConsumerSecretとOAuthTokenを利用しているか?OAuthTokenが無い場合は "ConsumerSecret&" のように&で終わっているか?
  • 署名する値("GET&http://...")の生成は正しいか?
  • 署名した値をBase64→URLエンコードしているか?

それほど難しいわけじゃないけど、はまるとミスっている箇所を見つけるのがちょっと大変かも。