2013年6月17日月曜日

GAE + Go で Sign in with Twitter (3)

Go + GAE で OAuth1.0a の Authorization Header を作成します。

ソースコードは GitHub にアップロードしてあります。
https://github.com/yokano/escape3ds_angularjs/blob/master/server/oauth1.go

まず、通信を行うための構造体 OAuth を作成します。
クラスっぽく扱うのでコメントではクラスと書いています。
送信するパラメータなどを保存しておけるようにしておきます。
/**
 * OAuthの通信を行うクラス
 * @class
 */
type OAuth struct {
    params map[string]string
}

インスタンス化する関数を作成します。
/**
 * OAuthクラスのインスタンス化
 * @function
 * @returns {*OAuth} OAuthインスタンス
 */
func NewOAuth() *OAuth {
    params := make(map[string]string, 7)
    params["oauth_callback"] = "http://localhost:8080/oauth_callback"
    params["oauth_consumer_key"] = config["consumer_key"]
    params["oauth_signature_method"] = "HMAC-SHA1"
    params["oauth_version"] = "1.0"
 
    oauth := new(OAuth)
    oauth.params = params
    return oauth
}

初期化する際に、固定できるパラメータを指定しています。

  • oauth_callback
  • oauth_consumer_key
  • oauth_signature_method
  • oauth_version

上の4つはリクエストによらず不変なので初期化時に指定しておきます。

  • oauth_nonce
  • oauth_timestamp
  • oauth_signature

この3つはリクエストごとに異なる値を取ります。
そのため、初期化時に指定せずリクエスト毎に作り直します。


まずは、oauth_nonce からです。
oauth_nonce はリクエストごとに異なるランダムな文字列を指定します。
この値は、クライアントから同じリクエストが過去に送られてきたかどうかをサーバが判断するための値です。
ここでは仕様書に載っている例と同様に生成してみます。
32byte のランダムなデータを Base64 エンコードして記号を削除した文字列を oauth_nonce とします。8byte で同じ事をする関数を別で作ったので、それを4回繰り返しています。
/**
 * oauth_nonce を作成する
 * @method
 * @memberof OAuth
 * @returns {string} 作成したoauth_nonce
 */
func (this *OAuth) createNonce() string {
    nonce := ""
    for i := 0; i < 4; i++ {
        nonce = strings.Join([]string{nonce, string(getRandomizedString())}, "")
    }
    return nonce
}
/**
 * ランダムな文字列を取得する
 * 64bit のランダムデータを Base64 エンコードして記号を抜いたもの
 * @function
 * @returns {string} ランダムな文字列
 */
func getRandomizedString() string {
    r := rand.Int63()
    b := make([]byte, binary.MaxVarintLen64)
    binary.PutVarint(b, int64(r))
    e := base64.StdEncoding.EncodeToString(b)
    e = strings.Replace(e, "+", "", -1)
    e = strings.Replace(e, "/", "", -1)
    e = strings.Replace(e, "=", "", -1)
    return e
}

次に oauth_timestamp です。
これはリクエストが作成された時の Unix タイムです。
現在時刻から離れていると Twitter からエラーが来るので注意が必要です。
oauth_nonce と oauth_timestamp はワンタイムパスワードのような役割を持ち、反射攻撃を防ぎます。
タイムスタンプをとるだけなのでコードは以下のように簡単になります。
this.params["oauth_timestamp"] = strconv.Itoa(int(time.Now().Unix()))

次に oauth_signature です。
oauth_signature は他のパラメータと比べると複雑でバグが出やすいです。
oauth_signature は送信するデータと秘密鍵 consumer_secret を使って oauth_signature_method 方式で暗号化した文字列です。
Twitter の場合は HMAC-SHA1 が使われています。
サーバ側でも同じように oauth_signature を計算して、クライアント側から送られてきた oauth_signature と一致するかどうか調べます。
これによって consumer_secret を知っている人(アプリ開発者)だけのアクセスが受け入れられます。

暗号化する前の文字列(base string)は以下のようになっています。

メソッド名&送信先URL&パラメータストリング

メソッド名は GET or POST。必ず大文字です。
送信先URL はリクエストの送信先の URL をパーセントエンコードしたものです。
パラメータストリングは oauth_signature 以外の oauth パラメータをまとめた文字列です。

パラメータストリングの作り方
  1. oauth パラメータをアルファベット順にならべる
  2. パラメータ名と値をパーセントエンコードする
  3. パラメータ名と値を = でくっつける
  4. さらにパラメータ同士を & でくっつける
oauth_signature の作成部分を Go で書くと以下のようになります。
/**
 * oauth_signature を作成する
 * @method
 * @memberof OAuth
 * @param {string} targetUrl リクエスト送信先のURL
 * @returns {string} oauth_signature
 */
func (this *OAuth) createSignature(targetUrl string) string {
    // アルファベット順に並び替える
    keys := make([]string, 0)
    for key := range this.params {
        keys = append(keys, key)
    }
    sort.Strings(keys)
 
    // パラメータストリングの作成
    params := make([]string, len(keys))
    for i := 0; i < len(keys); i++ {
        key := keys[i]
        val := this.params[key]
        params[i] = fmt.Sprintf("%s=%s", url.QueryEscape(key), url.QueryEscape(val))
    }
    paramString := strings.Join(params, "&")
 
    // ベース文字列の作成
    baseString := fmt.Sprintf("POST&%s&%s", url.QueryEscape(targetUrl), url.QueryEscape(paramString))
 
    // consumer secret をエンコード
    signatureKey := fmt.Sprintf("%s&", url.QueryEscape(config["consumer_secret"]))
 
    // HMAC-SHA1 で暗号化
    hash := hmac.New(sha1.New, []byte(signatureKey))
    hash.Write([]byte(baseString))
    signature := hash.Sum(nil)
    return base64.StdEncoding.EncodeToString(signature)
}


アルファベット順に並び替えるのは sort パッケージ
文字列の連結は strings パッケージ
URL エスケープは url パッケージ
HMAC-SHA1 は hmac パッケージと sha1 パッケージが使えます。

 sort package - Go lang

 url package - Go lang

 sha1 package - Go lang

 hmac package - Go lang


以上で 7 つの oauth パラメータが揃いました。
最後に Authorization Header を作ります。

  1. oauth パラメータ名と値をパーセントエンコードする
  2. 「パラメータ名="値"」 という感じでくっつける(ダブルクォート必須)
  3. すべてのパラメータを 「, 」で区切る(カンマの後のスペースはなくてもOK)
  4. 先頭に 「OAuth 」をくっつける

/**
 * ヘッダを作成する
 * @method
 * @memberof OAuth
 * @returns {string} ヘッダ
 */
func (this *OAuth) createHeader() string {
    params := make([]string, 0)
    for key, val := range this.params {
        key = url.QueryEscape(key)
        val = url.QueryEscape(val)
        set := fmt.Sprintf(`%s="%s"`, key, val)
        params = append(params, set)
    }
    header := strings.Join(params, ", ")
    header = fmt.Sprintf("OAuth %s", header)
    return header
}


Authorization Header が作れたら、ほぼできたも同然です。
今日はここまで。

0 件のコメント:

コメントを投稿