Google App Engine for GoでOAuth2による認証を用意する

## 要点
Google App EngineでGoogle AccountによるOAuth2認証を用意します。
実例のあるOpenID Connectを使用します。
App Engineは基本無料なので安心して使えます。
`Enable Billing`のようなものが出てきたら速攻で脱出してください :scream: 

## OAuth2 for App Engine
GoでのOAuth認証手順。
https://cloud.google.com/appengine/docs/go/oauth/

実例があるOpenID Connectで用意します
https://developers.google.com/identity/protocols/OpenIDConnect

## Google Developer ConsoleでAPI Credentialを取得
1. https://console.cloud.google.com
2. サイドバーのAPI Managerをクリック
3. Credentialsをクリック
4. Create credentialsをクリック
5. OAuth client IDをクリック
6. Web applicationを選択後、createをクリック
7. 作成されたclientをクリックして`Client ID`と`Client secret`をメモ
8. 念のためAuthorized redirect URIsにエンドポイントを指定する

## 実装

使用したLibraryはこちらになります。
未インストールのものは`go get -u -v`してください。

```go:
import (
	"fmt"
	"net/http"
	"strings"

	"github.com/satori/go.uuid"
	"golang.org/x/oauth2"
	v2 "google.golang.org/api/oauth2/v2"
	"google.golang.org/appengine"
	appenginelog "google.golang.org/appengine/log"
)
```

まずは、OAuth2用のConfigを作成します。

```go:
var (
	conf = oauth2.Config{
		ClientID:     "先ほどのClient ID",
		ClientSecret: "先ほどのClient secret",
		Scopes:       []string{"openid", "email", "profile"}, // 6/19 update: openidのscopeが漏れていたので追加
		Endpoint: oauth2.Endpoint{
			AuthURL:  "https://accounts.google.com/o/oauth2/v2/auth",
			TokenURL: "https://www.googleapis.com/oauth2/v4/token",
		},
	}
    // 後で使います
    // 6/11 update:globalで持つのは誤りでした
	state string
)

```

次に認証用のhandlerを作成します。

```go:
func init() {
	http.HandleFunc("/api/oauth2", oauth2Handler)
	http.HandleFunc("/oauth2callback", tokenHandler)
}

func oauth2Handler(w http.ResponseWriter, r *http.Request) {
    // ランダムな文字列作成に
    // github.com/satori/go.uuid
    // を使用しています
    state = uuid.NewV4().String()

    // 6/11 update:stateをredirect後と比較する場合はcookieに入れるのが無難です
    sc := &http.Cookie{
        Name:   "hogehoge",
        Value:  state,
        MaxAge: 60,
        Path:   "/",
    }
    redierctURL := getRedirectURL(r.URL.Host)
    conf.RedirectURL = redierctURL
    url := conf.AuthCodeURL(state)
    http.Redirect(w, r, url, 302)
}

func getRedirectURL(host string) string {
	return fmt.Sprintf("https://%s/oauth2callback", host)
}
```

次に、トークン用のhandlerを作成します。

```go:
func tokenHandler(w http.ResponseWriter, r *http.Request) {
    // redirectされたstateと生成したstateが等しいかを確認します
    // 6/11 update:cookieと比較するようにします
    sc, err := r.Cookie("hogehoge")
    if err != nil ||  sc.Value != r.FormValue("state") {
        http.Error(w, "state is invalid.", http.StatusUnauthorized)
        return
    }

    // 認証コードを取得します
	code := r.FormValue("code")
    // appengineのcontextを取得します
	context := appengine.NewContext(r)

    // 認証コードからtokenを取得します
	tok, err := conf.Exchange(context, code)
	if err != nil {
		http.Error(w, err.Error(), http.StatusUnauthorized)
		return
	}

    // tokenが正しいことを確認します
	if tok.Valid() == false {
		http.Error(w, "token is invalid.", http.StatusUnauthorized)
		return
	}

    // oauth2 clinet serviceを取得します
    // 特にuserの情報が必要ない場合はスルーです
	service, err := v2.New(conf.Client(context, tok))
	if err != nil {
		http.Error(w, err.Error(), http.StatusUnauthorized)
		return
	}

    // token情報を取得します
    // ここにEmailやUser IDなどが入っています
    // 特にuserの情報が必要ない場合はスルーです
	tokenInfo, err := service.Tokeninfo().AccessToken(tok.AccessToken).Context(context).Do()
	if err != nil {
		http.Error(w, err.Error(), http.StatusUnauthorized)
		return
	}

    // indexページにリダイレクトします
    // 6/11 update: ハードコードではなく定数を使用するようにした
	http.Redirect(w, r, "/", http.StatusMovedPermanently)
}
```

これでGoogle Accountが登録されているユーザであることは認証されました。
以上になります。