Edited at

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

More than 3 years have passed since last update.


要点

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 IDClient secretをメモ

  8. 念のためAuthorized redirect URIsにエンドポイントを指定する


実装

使用したLibraryはこちらになります。

未インストールのものはgo get -u -vしてください。

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を作成します。

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を作成します。

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を作成します。

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が登録されているユーザであることは認証されました。

以上になります。