Carpe Diem

備忘録

GoでTwitterのOAuth1.0を用いた認証

概要

GoでTwitterのOAuthを用いた認証をします。
oauth1.0のライブラリはいくつかありますが、今回はgaryburd/go-oauthを利用します。

github.com

環境

認証フロー

f:id:quoll00:20160711140344p:plain

Twitterへのアプリ登録作業

Twitter Application Managementへアクセス

f:id:quoll00:20160711152618p:plain

各種情報を記入。 f:id:quoll00:20160711153028p:plain

この時WebsiteCallback URLはポート等が入ると怒られます。

問題なければ同意して作成ボタン。 f:id:quoll00:20160711153642p:plain

Details

f:id:quoll00:20160711154009p:plain

Setting

f:id:quoll00:20160711153717p:plain

注意なのがEnable Callback Lockingの部分で、これをチェックするとサーバ側で扱えるCallbackURLが、この画面のCallbackURLに限定されます。
もちろんセキュアにする上でこれはやった方が良いですが、複数の開発環境で扱うときなどは外していないとちゃんと通ってくれないので注意。

Keys and Access Tokens

f:id:quoll00:20160711154053p:plain

ここの

  • Consumer Key
  • Consumer Secret

をサーバ側の実装で使います。

サーバ側実装

最終形は以下にcommitしてあります。

github.com

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されて認可画面に飛ばされます。

認可画面

f:id:quoll00:20160711154303p:plain

この時のURLは以下。 https://api.twitter.com/oauth/authenticate?oauth_token=sjoGKwAAAAAAwB6XAAABVdihGsA

request tokenがoauth_tokenとしてくっついています。OKを押します。

認可後

http://localhost:8080/login/twitter/auth/callback?oauth_token=sjoGKwAAAAAAwB6XAAABVdihGsA&oauth_verifier=NENw6GtZBz6cZ0VuJLovm5I6X5KOc1JK

に飛ばされます。このoauth_tokenoauth_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

以上です。

ソース