Carpe Diem

備忘録

GoでFacebook OAuthログインをセキュアに実装する

概要

Facebookを用いたOAuthログインの実装方法はネット上に多々ありますが、セキュリティに関してあまり考慮されてないものが多いです。
今回はセキュアに実装する例を示します。

環境

成果物

今回のソースコードはこちらです。

github.com

主な対応

大きく以下の2つの対応をします

  • stateパラメータを付ける
  • appsecret_proofを付ける

stateパラメータを付ける

christina04.hatenablog.com

こちらで紹介したAuthorization Flowで、CSRF攻撃を防ぐためにstateパラメータを付けて正しいリクエストか検証します。

まず最初のリクエストでstateパラメータを生成し、facebookのredirectURLに渡します。
後で検証に使うので、生成したstateパラメータはセッションストアに保持しておきます。
oauth2パッケージAuthCodeURL()というメソッドでstateパラメータを付けたURLを生成できます。

func LoginHandler(c *gin.Context) {
    u4, _ := uuid.NewV4()
    state := u4.String()

    session := sessions.Default(c)
    session.Set("state", state)  // あとで検証するので保存
    session.Save()

    url := GetAuthCodeURL(state)
    c.Redirect(http.StatusMovedPermanently, url)
    return
}

func NewConfig() *oauth2.Config {
    c := &oauth2.Config{
        ClientID:     fbClientID,
        ClientSecret: fbClientSecret,
        RedirectURL:  callbackURL,
        Endpoint: oauth2.Endpoint{
            AuthURL:  fbAuthURL,
            TokenURL: fbTokenURL,
        },
        Scopes: fbScope,
    }

    return c
}

func GetAuthCodeURL(state string) string {
    oc := NewConfig()
    url := oc.AuthCodeURL(state, oauth2.AccessTypeOnline)  // state付きURLを生成
    return url
}

その後facebook側で認可が済むとコールバックが走りauthorization codeを渡されます。
その時にstateパラメータが付いているので、それを検証します。

func CallbackHandler(c *gin.Context) {
    code := c.DefaultQuery("code", "")
    state := c.DefaultQuery("state", "")

    session := sessions.Default(c)
    v := session.Get("state")

    err := ValidateFacebookCode(code, state, v)
    if err != nil {
        c.String(http.StatusBadRequest, "%s", err)
        return
    }

    ...
}

func ValidateFacebookCode(code string, state string, session interface{}) (err error) {
    if code == "" {
        err = errors.New("code should be set on query string")
        return
    }

    if state == "" {
        err = errors.New("state should be set on query string")
        return
    }

    if session == nil {
        err = errors.New("state hasn't be set")
        return
    }

    ss := session.(string)
    if state != ss {  // stateパラメータが正しいか検証
        err = errors.New("state is invalid")
        return
    }

    return
}

これでCSRF対策ができました。

appsecret_proofを付ける

OAuth2のアクセストークンはどのサービスで使用するか、どこで生成したかなど関係なく、APIトークンとして用いられるので通常漏洩した場合に悪用される可能性があります。

アクセストークンは移植できます。FacebookSDKによってクライアント上で生成されたアクセストークンを取得し、そのトークンを任意のサーバーに送信して、クライアントに代わってサーバーから呼び出しを行うことも可能です。 アクセストークンは、利用者のコンピュータ上の悪意のあるソフトウェアまたは中間者攻撃によって盗まれることもあります。 その場合、盗まれたアクセストークンは、クライアントでもお使いのサーバーでもないまったく別のシステムから利用でき、スパムを生成したりデータを盗んだりといった目的で利用されます。

ref: Graph APIリクエストの保護 - グラフAPI - ドキュメンテーション - 開発者向けFacebook

christina04.hatenablog.com

またこちらで説明しているImplicit Flowはまさしくそのケースで、他サービスで取得した不正なアクセストークンでログインが可能になります。

しかしFacebookはシークレットキーを用いたHMAC認証を使うことで悪用を防ぐ手段を提供しています。それがappsecret_proofパラメータです。

アプリ設定

APIリクエストでappsecret_proofを強制するためには以下の部分をONにする必要があります。

f:id:quoll00:20180319122727p:plain

実装側

実装側ではドキュメント通りに、シークレットキーをキーとしたSHA-256のHMACを生成してパラメータとして付加します。

func addAppSecretProofHMAC(url string, accessToken string) string {
    mac := hmac.New(sha256.New, []byte(fbClientSecret))
    mac.Write([]byte(accessToken))
    hash := hex.EncodeToString(mac.Sum(nil))

    if strings.Contains(url, "?") {
        url += "&"
    } else {
        url += "?"
    }
    url += "appsecret_proof=" + hash
    return url
}

検証

appsecret proofをONにしてパラメータがない場合

code:100 message:API calls from the server require an appsecret_proof argument

このようなエラーが出ます。

appsecret_proofが不正な場合

適当な値を入れると、

code:100 message:Invalid appsecret_proof provided in the API argument

このようなエラーが返ります。これはアプリ側をON設定しなくてもこうなります。

まとめ

OAuth2を用いたログインはセキュリティ面で抜け漏れしやすいので、こういった点に気をつけて実装する必要があります。

ソース