概要
Facebookを用いたOAuthログインの実装方法はネット上に多々ありますが、セキュリティに関してあまり考慮されてないものが多いです。
今回はセキュアに実装する例を示します。
環境
成果物
今回のソースコードはこちらです。
主な対応
大きく以下の2つの対応をします
- stateパラメータを付ける
- appsecret_proofを付ける
stateパラメータを付ける
こちらで紹介した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のトークンとして用いられるので通常漏洩した場合に悪用される可能性があります。
アクセストークンは移植できます。FacebookのSDKによってクライアント上で生成されたアクセストークンを取得し、そのトークンを任意のサーバーに送信して、クライアントに代わってサーバーから呼び出しを行うことも可能です。 アクセストークンは、利用者のコンピュータ上の悪意のあるソフトウェアまたは中間者攻撃によって盗まれることもあります。 その場合、盗まれたアクセストークンは、クライアントでもお使いのサーバーでもないまったく別のシステムから利用でき、スパムを生成したりデータを盗んだりといった目的で利用されます。
ref: Graph APIリクエストの保護 - グラフAPI - ドキュメンテーション - 開発者向けFacebook
またこちらで説明しているImplicit Flowはまさしくそのケースで、他サービスで取得した不正なアクセストークンでログインが可能になります。
しかしFacebookはシークレットキーを用いたHMAC認証を使うことで悪用を防ぐ手段を提供しています。それがappsecret_proof
パラメータです。
アプリ設定
全APIリクエストでappsecret_proof
を強制するためには以下の部分をONにする必要があります。
実装側
実装側ではドキュメント通りに、シークレットキーをキーとした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を用いたログインはセキュリティ面で抜け漏れしやすいので、こういった点に気をつけて実装する必要があります。