GAE / Go でもTwitterのユーザーログイン認証がしたい!

# 動機 :　GAE / Go でもTwitterのユーザーログインがしたい! but 認証ライブラリがそのままでは使えない!
React + API (GAE/Go) でシステムを作っていて、ログイン機能としてTwitter連携したい。
GoogleとTwitter Botの記事が多いがなんとかユーザー認証の仕方を学ぶ。

フロー自体については割愛。
[公式ページの概要](https://developer.twitter.com/en/docs/twitter-for-websites/log-in-with-twitter/guides/implementing-sign-in-with-twitter)

# 以下、長いので結論を先に
goのバージョンは1.0 Standardな環境
ユーザー認証のための dghubble/oauth1　では http.DefaultClient.Transportを上書きで対応でき流が、

```go
ctx := appengine.NewContext(r)
http.DefaultClient.Transport = &urlfetch.Transport{Context: ctx}

requestToken, requestSecret, err := config.RequestToken()
```

go-twitterでoauth1からhttp.Clientを生成するところはコンテキスト経由でurlfetchのトランスポートを食わせてやると良い

```go
ctx := appengine.NewContext(r)

// コンテキストのValue経由でクライアントを渡す
newCtx := context.WithValue(ctx, oauth1.HTTPClient, urlfetch.Client(ctx))
token := oauth1.NewToken(accessToken, secret)

// ここで単に config.Client(ctx, token) としても処理本体であるurlfetch.Transport.RoundTripが渡らないためエラーとなる
httpClient := config.Client(newCtx, token)

client := lib.NewClient(httpClient)
user, _, err := client.Accounts.VerifyCredentials(nil)
// userには問題なくログインしたユーザーのtwitterのアカウント情報が入っている
```

# Go言語用のTwitterクライアントは二つ
ざっと探すと[anaconda](https://github.com/ChimeraCoder/anaconda)が定番のようだが、ユーザー認証によるアクセストークンの取得方法がよくわからなかったのでパス
さらにVerifyCredentialsでboolしか取れないようなので、ユーザー認証は想定していないのかも?

そしてもう一つが[go-twitter](https://github.com/dghubble/go-twitter)
同じ開発者のライブラリに [oauth1](https://github.com/dghubble/oauth1) があり、いい感じにtwitterログインによるアクセストークン取得のサンプルが書いてある。
つまり、oauth1 でアクセストークンを取得して、そのあとgo-twiiterでAPIを叩けば良い。

# ところが、GAE/Goでは外部へのhttpアクセスは対応しないとエラーになる
http.DefaultClientなどの通常のアクセス方法では以下のようなエラーが出る。

```
http.DefaultTransport and http.DefaultClient are not available in App Engine. See https://developers.google.com/appengine/docs/go/urlfetch/overview
```

これについては公式のどこかのページにurlfetchを使えと書いてあり、ふた通りの対応がある。
## 1. http.Clientを切り替える方法
ライブラリが対応している必要がある。

```go
import "google.golang.org/appengine/urlfetch"

func handler(r *http.Request, ...) {
    ctx := appengine.NewContext(r)
    newClient := urlfetch.Client(ctx)

   // anacondaなどのhttp.Client切り替えに対応している場合は渡すことができる
   api.Client = newClient
}
```

## 2. http.DefaultClientのTransportを上書きする
ライブラリがhttp.DefaultClient.Do(req)とかしていてClientの上書きができない場合はこの方法が使える。
oauth1でtwitterのアクセストークンを取得するところまではこの方法で対応できる。

```go
func GetCallbackURL(r *http.Request) (string, error) {
    ctx := appengine.NewContext(r)
    http.DefaultClient.Transport = &urlfetch.Transport{Context: ctx}

    // 問題なくアクセスできる
    requestToken, requestSecret, err := config.RequestToken()
}
```

以上の二つの方法はググればすぐ出てくるし、[公式ページ](https://cloud.google.com/appengine/docs/standard/go/urlfetch/reference)からも見て取ることができる。

# ここで問題。oauth1のconfig.Client()にどう対応する?

client.Transport = &urlfetch.Transport({Context: ctx}) とやると、oauth1のための認証ヘッダーの作成などが行われなくなる。
ソースを見るとTransportを使って処理しているため。urlfetch.Transportとバッティングしてしまう。
ソースコードを追っかけているとTransport.Base という感じに継承できるようになっていている。

```go:transport.go
func (t *Transport) RoundTrip (req ...) {

    // 認証ヘッダーとかトークンの処理とか。。。

    return t.base().RoundTrip(req2)
}

func (t *Transport) base() http.RoundTripper {
    // ↓このBaseにurlfetch.Transportが入っていれば行けそう。
    if t.Base != nil {
         return t.Base
    }
    // ↓こいつのせいでエラーになる
    return http.DefaultTransport
}
```
で、このBaseを設定しているのはconfig.go のNewClient()のとこ
config.Client(ctx, token) が NewClient() に渡って、contextTransport()に渡されている。
context.ContextのValueに入れておく、ということらしい。
直接渡せるようにしてもよかったのでは?

```go:context.go
type contextKey struct{}
var HTTPClient contextKey

func contextTransport(ctx context.Context) http.RoundTripper {
  if client, ok := ctx.Value(HTTPClient).(*http.Client); ok {
    return client.Transport
  }
  return nil 
}
```

というわけで、oauth1のconfig.Client()から帰ってきたhttp.Clientにurlfetchの処理を入れるためには、Valueにcurlfetch.Client()を入れたcontextをたすせば良いと。
というわけで上記の結論になるのでした。
システムの奥の部分を上書きできるになってる設計ってよくできてるなと勉強になりました。
欲を言えばコンパイル時にチェックができるとより良いんだろうけど。

それにしてもGoでtwitterでユーザ認証の情報がほとんどなかった。
GAE/Goはbot用途に使う人がほとんどだから需要はないんだろうなぁ。