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

  • 26
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

要点

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