概要
GoでTwitterのOAuthを用いた認証をします。
oauth1.0のライブラリはいくつかありますが、今回はgaryburd/go-oauthを利用します。
環境
- golang v1.6.2
認証フロー
Twitterへのアプリ登録作業
Twitter Application Managementへアクセス
各種情報を記入。
この時Website
やCallback URL
はポート等が入ると怒られます。
問題なければ同意して作成ボタン。
Details
Setting
注意なのがEnable Callback Locking
の部分で、これをチェックするとサーバ側で扱えるCallbackURLが、この画面のCallbackURLに限定されます。
もちろんセキュアにする上でこれはやった方が良いですが、複数の開発環境で扱うときなどは外していないとちゃんと通ってくれないので注意。
Keys and Access Tokens
ここの
- Consumer Key
- Consumer Secret
をサーバ側の実装で使います。
サーバ側実装
最終形は以下にcommitしてあります。
main.go
func main() { store, _ := sessions.NewRedisStore(10, "tcp", "localhost:6379", "", []byte("redis_secret")) r := gin.New() r.Use(gin.Logger()) r.Use(gin.Recovery()) r.Use(sessions.Sessions("session", store)) r.GET("/login/twitter/auth", LoginByTwitter) r.GET("/login/twitter/auth/callback", TwitterCallback) r.Run() }
ポイントは以下
- session storeを使っている
- request token取得のためのエンドポイント
- 認可callback後のエンドポイント
login.go
func LoginByTwitter(c *gin.Context) { oc := NewTWClient() rt, err := oc.RequestTemporaryCredentials(nil, callbackURL, nil) if err != nil { c.JSON(http.StatusBadRequest, nil) return } session := sessions.Default(c) session.Set("request_token", rt.Token) session.Set("request_token_secret", rt.Secret) session.Save() url := oc.AuthorizationURL(rt, nil) c.Redirect(http.StatusMovedPermanently, url) return } func TwitterCallback(c *gin.Context) { tok := c.DefaultQuery("oauth_token", "") if tok == "" { c.JSON(http.StatusBadRequest, nil) return } ov := c.DefaultQuery("oauth_verifier", "") if ov == "" { c.JSON(http.StatusBadRequest, nil) return } session := sessions.Default(c) v := session.Get("request_token") if v == nil { c.JSON(http.StatusBadRequest, nil) return } rt := v.(string) if tok != rt { c.JSON(http.StatusBadRequest, nil) return } v = session.Get("request_token_secret") if v == nil { c.JSON(http.StatusBadRequest, nil) return } rts := v.(string) if rts == "" { c.JSON(http.StatusBadRequest, nil) return } code, at, err := GetAccessToken(&oauth.Credentials{Token: rt, Secret: rts}, ov) if err != nil { c.JSON(code, nil) return } account := struct { ID string `json:"id_str"` ScreenName string `json:"screen_name"` }{} code, err = GetMe(at, &account) if err != nil { c.JSON(code, nil) return } // TODO use id to make user login. fmt.Println(account) c.JSON(http.StatusOK, nil) return }
ポイントは以下
- request token取得後に、request token secretをセッションに保存
- 認可callback時にクライアントからのoauth_tokenとセッションのrequest tokenのチェック
- 認可callback時にセッションにrequest token secretがあるかチェック(CSRF対策)
twitter.go
const ( refreshTokenURL = "https://api.twitter.com/oauth/request_token" authorizationURL = "https://api.twitter.com/oauth/authenticate" accessTokenURL = "https://api.twitter.com/oauth/access_token" accountURL = "https://api.twitter.com/1.1/account/verify_credentials.json" callbackURL = "http://localhost:8080/login/twitter/auth/callback" ) var ( twitterKey string twitterSecret string ) func init() { twitterKey = os.Getenv("TEST_KEY") twitterSecret = os.Getenv("TEST_SECRET") } func NewTWClient() *oauth.Client { oc := &oauth.Client{ TemporaryCredentialRequestURI: refreshTokenURL, ResourceOwnerAuthorizationURI: authorizationURL, TokenRequestURI: accessTokenURL, Credentials: oauth.Credentials{ Token: twitterKey, Secret: twitterSecret, }, } return oc } func GetAccessToken(rt *oauth.Credentials, oauthVerifier string) (int, *oauth.Credentials, error) { oc := NewTWClient() at, _, err := oc.RequestToken(nil, rt, oauthVerifier) if err != nil { err := errors.Wrap(err, "Failed to get access token.") return http.StatusBadRequest, nil, err } return http.StatusOK, at, nil } func GetMe(at *oauth.Credentials, user interface{}) (int, error) { oc := NewTWClient() resp, err := oc.Get(nil, at, accountURL, nil) if err != nil { err = errors.Wrap(err, "Failed to send twitter request.") return http.StatusInternalServerError, err } defer resp.Body.Close() if resp.StatusCode >= 500 { err = errors.New("Twitter is unavailable") return http.StatusInternalServerError, err } if resp.StatusCode >= 400 { err = errors.New("Twitter request is invalid") return http.StatusBadRequest, err } err = json.NewDecoder(resp.Body).Decode(user) if err != nil { err = errors.Wrap(err, "Failed to decode user account response.") return http.StatusInternalServerError, err } return http.StatusOK, nil }
ポイントは以下
https://api.twitter.com/oauth/authorize
でなくhttps://api.twitter.com/oauth/authenticate
- アクセストークンを使ったtwitterAPIへのリクエストは署名などが必要になるのでoauthライブラリのhttpクライアントを利用
動作確認
アクセス
http://localhost:8080/login/twitter/auth
にアクセス。するとcallbackされて認可画面に飛ばされます。
認可画面
この時のURLは以下。 https://api.twitter.com/oauth/authenticate?oauth_token=sjoGKwAAAAAAwB6XAAABVdihGsA
request tokenがoauth_token
としてくっついています。OKを押します。
認可後
に飛ばされます。このoauth_token
、oauth_verifier
を使ってアクセストークンを取得します。
コンソールログを見ると、アクセストークンを使ってuserIdやscreen_nameがちゃんと取得できています。
[GIN] 2016/07/11 - 15:46:28 | 301 | 311.494613ms | ::1 | GET /login/twitter/auth {75231543886xxxxxx screen_name_hoge} [GIN] 2016/07/11 - 15:46:31 | 200 | 994.449589ms | ::1 | GET /login/twitter/auth/callback
以上です。