2010年3月2日火曜日

第18回 OAuth認証に対応する

OAuthってなんなの?という方にすごく簡単に説明すると、
twitterと連携したサイトやクライアントに
ユーザーがIDとパスワードを渡すことなく、
twitterの情報へのアクセスをできるようにする仕組みです。
またtwitterでOAuth認証が推奨されていて、
OAuth認証でログインするとAPI制限の上限が増加します。
さらに、将来的にはユーザーIDとパスワードでの認証(BASIC認証)は廃止されるそうです。

OAuth認証の流れ
■開発時にやること
開発者がTwitterに申請して、自分のアプリケーション用の
ConsumerKey、ConsumerSecretを入手する。

■ユーザーがログインするときのOAuth認証の流れ
1.始めてOAuth認証でログインするときは、
アプリ固有の認証用URL(ConsumerKey、ConsumerSecretから生成される)を
ブラウザで開く。(そのようなボタンなどをアプリで用意しておく)
2.ユーザーはブラウザで「許可する」を選択する
3.ブラウザ上に数字が表示されるので、それをアプリに入力してもらう。
4.それらの情報を元にAccessToken(キーワードのようなもの)を作る。
5.以後、twitterにアクセスするときはAccessTokenを付与してやる。
6.twitterのAccessTokenは有効期限が切れることがないので、AccessTokenを保存しておく。
そうすることで、コードに埋め込まれたConsumerKey、ConsumerSecretと保存したAccessTokenで、
次回以降の起動はユーザーの操作無しで認証が通るようにできる。

では実際に作ります。
ですが、1から自分で作ると結構複雑なのでOAuthLibというものを利用させてもらいます。
http://oauthlib.codeplex.com/
使い方は一緒に入っているサンプルコードを見るとすぐわかると思います。

まずUserInfoクラスをBasic認証とOAuth認証のどっちの情報でも持てるようにします。


 public class UserInfo {
  public bool IsUseOAuth { get; set; }

  #region OAuth 認証用
  private AccessToken _accessToken;
  [XmlIgnore]
  public AccessToken AccessToken {
   set {
    if (_accessToken != value) {
     _accessToken = value;
    }
   }
   get {
    if (_accessToken == null) {
     if (!string.IsNullOrEmpty(_accessTokenSecret) && !string.IsNullOrEmpty(_accessTokenValue)) {
      _accessToken = new AccessToken(_accessTokenValue, _accessTokenSecret);
     }
    }
    return _accessToken;
   }
  }

  private string _accessTokenSecret;
  //ファイルへの保存と読み込みにしか使用しない
  public string AccessTokenSecret {
   get {
    if (AccessToken == null) {
     return "";
    } else {
     return Convert.ToBase64String(Encoding.ASCII.GetBytes(AccessToken.TokenSecret));
    }
   }
   set {
    _accessTokenSecret = Encoding.ASCII.GetString(Convert.FromBase64String(value));
   }
  }

  //ファイルへの保存と読み込みにしか使用しない
  private string _accessTokenValue;
  public string AccessTokenValue {
   get {
    if (AccessToken == null) {
     return "";
    } else {
     return Convert.ToBase64String(Encoding.ASCII.GetBytes(AccessToken.TokenValue));
    }
   }
   set {
    _accessTokenValue = Encoding.ASCII.GetString(Convert.FromBase64String(value));
   }
  }

半分の情報は使われないことになるので、
抽象化したクラスを1つ作り、BasicとOAuthの2つの派生クラスを作った方がいいかもしれませんが、
とりあえずはこれで。

twitte(http://twitter.com/oauth_clients)からもらった情報を入れておくクラスを作ります。
一応ConsumerKeyとConsumerSecretは公開するとまずいかなと思って
ローカルにXMLファイルで保存しておいて読み出すようにしています。
(Readmeなどを参照に各自作ってください)


 class OAuthData {
<OAuthData>
    <ConsumerKey>アプリ固有の文字列</ConsumerKey>
    <ConsumerSecret>アプリ固有の文字列</ConsumerSecret>
</OAuthData>
   */

  static OAuthData() {
   var doc = XDocument.Load("OAuthData.xml");
   ConsumerKey = doc.Element("OAuthData").Element("ConsumerKey").Value;
   ConsumerSecret = doc.Element("OAuthData").Element("ConsumerSecret").Value;
  }
  public static readonly string ConsumerKey ;
  public static readonly string ConsumerSecret ;
  public static readonly string RequestTokenURL = "http://twitter.com/oauth/request_token";
  public static readonly string AccessTokenURL = "http://twitter.com/oauth/access_token";
  public static readonly string AuthorizeURL = "http://twitter.com/oauth/authorize";
  public static readonly string AuthorizationRealm = "http://twitter.com";
 }



次にユーザーにオプションダイアログから認証情報を入力してもらう必要があるので
OAuth認証用のURLをブラウザで開くコマンドと
ユーザーにテキストボックスに数字を入力してもらって、それからAccessTokenを作るコマンドを作ります。


   ShowOAuthURLCommand = new RelayCommand(
    param => {
     reqToken = consumer.ObtainUnauthorizedRequestToken(OAuthData.RequestTokenURL, OAuthData.AuthorizationRealm);
     oAuthUserAuthorizationURL = Consumer.BuildUserAuthorizationURL(OAuthData.AuthorizeURL, reqToken);
     Process.Start(oAuthUserAuthorizationURL);}
    );
   GetAccessTokenCommand = new RelayCommand(
    param => {
     string verifier = param as string;
     accessToken = consumer.RequestAccessToken(verifier, reqToken, OAuthData.AccessTokenURL, OAuthData.AuthorizationRealm);
     _option.AccessTokenTmp = accessToken;
    }

ちなみにConsumerクラスはOAuthLibで用意されているクラスで


private Consumer consumer = new Consumer(OAuthData.ConsumerKey, OAuthData.ConsumerSecret);

のようにConsumerKeyとConsumerSecretから作ります。(最初の説明の1の部分)

あとはGETやPOSTのコードが散らばっていたので、
WebUtilityクラスを作り、そこにアクセスする手順をまとめます。
OAuth版とBasic版メソッド両方作りましたが、
OAuth版だけ載せます。


 struct RequestParameter {
  /// <summary>
  /// パラメータの名前
  /// </summary>
  public string Name;
  /// <summary>
  /// パラメータの値
  /// ValueにはURLエンコードする前の文字列をいれる
  /// </summary>
  public string Value;
 }



 class WebUtility {
  public static WebResponse GetResponseByOAuth(string url, string method, RequestParameter[] parameters, UserInfo info) {
   Debug.Assert(method == "GET" || method == "POST");
   Debug.Assert(info != null);

   //パラメータ配列をOAuthLib用に変換
   var OAuthParams = (
    from item in parameters
     select new Parameter(item.Name, item.Value)
    ).ToArray<Parameter>();

   var consumer = new Consumer(OAuthData.ConsumerKey, OAuthData.ConsumerSecret);
   var response = consumer.AccessProtectedResource(
    info.AccessToken,
    url,
    method,
    OAuthData.AuthorizationRealm,
    OAuthParams
    );
   return response;
  }

OAuthLibはParameter型でパラメータを要求しますが、
アプリ全体でOAuthLibに依存するわけにもいかないので、
冗長な感じもしますが、RequestParameter構造体を定義しています。
そしてメソッドの最初で変換しています。
from~select~の部分は、
RequestParameter型の配列parametersの
1個1個に対してParamete型でインスタンスを作り直しています。

あとはUserInfoの値次第でBasicかOAuthを振り分けるようなメソッドを作って、


  public static WebResponse GetResponse(string url, string method, RequestParameter[] parameters, UserInfo info) {
   if (info == null) {
    return GetResponseByBasicAuth(url, method, parameters, null);
   } else if (info.IsUseOAuth) {
    return GetResponseByOAuth(url, method, parameters, info);
   } else {
    return GetResponseByBasicAuth(url, method, parameters, info);
   }
  }

呼び出す部分を以下のように書き換えます。
呼び出し部分は何カ所か書き換えましたが、以下は一例。(投稿の部分)


  public static Status Update(string text, UserInfo info) {
   Status result = null;
   var response = WebUtility.GetResponse(
    UpdateUrl,
    "POST",
    new RequestParameter[] {
     new RequestParameter() { Name = "status", Value= text}
    }
    , info
   );
  
   //Json形式の応答を分解
   using (var stream = response.GetResponseStream()) {
    var serializer = new DataContractJsonSerializer(typeof(Status));
    result = serializer.ReadObject(stream) as Status;
   }
   response.Close();
   return result;
  }




今回のソースコード
http://wtwitter.codeplex.com/SourceControl/changeset/view/42109

1 件のコメント:

  1. すみません。
    UserInfoクラスのIsValidもOAuth対応して修正する必要があります。
    リンク先のソースコードは漏れています。

    返信削除