【Go言語】gothでWebアプリを外部サービス認証ログインできるようにする


# はじめに

昨今、スタートアップなサービスをつくる場合、ログイン認証にはOAuth2規格を用いたFacebookやGoogle等の外部サービスの認証ログインを利用するのが基本です。

Go言語での外部認証ログイン機能の実装には **[goth](https://github.com/markbates/goth)** というOSSパッケージを利用するのが現在主流のようです。
OAuth2認証に伴うサービスサーバとの複雑なやり取りを、こういったパッケージに任せることで、プログラマは自身のアプリケーションの実装に集中できます。

外部認証ログインし、その外部サービスからの情報を表示するだけのシンプルなWEBアプリをgothを利用し作ってみました。[GitHubページリンク](https://github.com/momotaro98/go-sample-apps/tree/master/simple-auth-webapp)。(gothのGitHubページにもサンプルアプリが載っています。[link](https://github.com/markbates/goth/blob/master/examples/main.go))

アプリを開発環境で動かすには、認証に使う外部サービスからIDとシークレットキーを取得し、それらを環境変数に設定する必要があります。

本記事ではそれらも踏まえてアプリを動かすまでを書いていきます。

# 事前準備

## 外部サービスからアプリID、シークレットキーを取得

外部サービス認証機能を利用するにはその外部サービスからアプリ用のIDとシークレットキーを取得する必要があります。

取得方法は各サービスごとに用意されています。本アプリでは、Facebook認証を利用します。FacebookサービスからIDとシークレットキーを取得し、さらに、開発環境で動作させるために必要な設定を以下に示します。

1. [facebook for developers](https://developers.facebook.com/)にアクセスする
2. 右上の「マイアプリ」>「新しいアプリを追加」でアプリ表示名を設定しアプリを作成する
3. アプリの設定ページにある「アプリID」と「app secret」を控える
4. 設定ページにある「プラットフォームを追加」から「ウェブサイト」を選択する
5. 「サイトURL」に開発環境で起動させるWebサーバのURLを設定する。(localhostの3000番ポートで起動させる場合は`http://localhost:3000/`と設定する。)

![スクリーンショット 2017-09-17 02.21.48.png](https://qiita-image-store.s3.amazonaws.com/0/91562/861410b4-a886-7566-8589-00f680eadf5e.png "スクリーンショット 2017-09-17 02.21.48.png")

## 環境変数の設定

外部サービス(Facebook)から取得した「アプリID」と「app secret」、さらにgothを利用する上で必要な値を環境変数として開発環境に設定します。

bash, zshの場合

```bash:.bashrc
export SESSION_SECRET=session-secret
export GOSIMPLEWEBAPP_FACEBOOK_ID=取得したアプリID
export GOSIMPLEWEBAPP_FACEBOOK_SECRET=取得したapp secret
```

取得した「アプリID」と「app secret」を設定し、gothがCookieを生成するために必要となる秘密鍵`SESSION_SECRET `をランダムな乱数等を使って設定します。※

※ gothは内部で[Gorilla web toolkitのsessions package](http://www.gorillatoolkit.org/pkg/sessions)を利用しており、それを利用する上で必要な秘密鍵です。また、goth利用者側でそのCookie設定を上書きすることができます。

# 実装

## ファイル構成

```bash
└── simple-auth-webapp
    ├── auth.go
    ├── main.go
    └── templates
        ├── index.html
        └── login.html
```

階層構成は上記のようになっています。gothの初期設定、リクエストのルーティング設定、HTMLレンダリングを`main.go`に、認証の制御とgothを使ったOAuth2認証機能を`auth.go`に実装しています。

## コード

```go:main.go
package main

import (
	"log"
	"net/http"
	"os"
	"path/filepath"
	"sync"
	"text/template"

	"github.com/gorilla/pat"
	// "github.com/gorilla/sessions"
	"github.com/markbates/goth"
	"github.com/markbates/goth/gothic"
	"github.com/markbates/goth/providers/facebook"
	"github.com/stretchr/objx"
)

func init() {
	/*
		// gothで利用するCookieの設定を上書きする場合
		store := sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET")))
		store.MaxAge(86400 * 60) // セッション期限の設定(60日) デフォルトでは30日
		store.Options.Secure = true // Cookieのセキュア設定 デフォルトではfalse
		gothic.Store = store // 上書きする
	*/
	
	// ①
	goth.UseProviders(
		facebook.New(os.Getenv("GOSIMPLEWEBAPP_FACEBOOK_ID"), os.Getenv("GOSIMPLEWEBAPP_FACEBOOK_SECRET"), "http://localhost:3000/auth/facebook/callback"),
	)
}

type templateHandler struct {
	filename string
	once     sync.Once
	templ    *template.Template
}

func (t *templateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	t.once.Do(func() {
		t.templ = template.Must(
			template.ParseFiles(filepath.Join("templates", t.filename)))
	})

	data := make(map[string]interface{})

	// ⑥ アプリ用Cookieからユーザー情報を取得する
	if authCookie, err := r.Cookie("auth"); err == nil {
		data["UserData"] = objx.MustFromBase64(authCookie.Value)
	}

	t.templ.Execute(w, data)
}

func main() {
	// ② patを使ってルーティング設定
	p := pat.New()
	p.Get("/auth/{provider}/callback", callbackHandler)
	p.Get("/auth/{provider}", gothic.BeginAuthHandler)
	p.Get("/logout", logoutHandler)
	p.Add("GET", "/login", &templateHandler{filename: "login.html"})
	p.Add("GET", "/", MustAuth(&templateHandler{filename: "index.html"}))

	// WEBサーバを起動
	log.Fatal(http.ListenAndServe(":3000", p))
}
```

```go:auth.go
package main

import (
	"fmt"
	"net/http"

	"github.com/markbates/goth/gothic"
	"github.com/stretchr/objx"
)

// MustAuth forces user to be authenticated
func MustAuth(handler http.Handler) http.Handler {
	return &authHandler{next: handler}
}

type authHandler struct {
	next http.Handler
}

// ③
func (h *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if cookie, err := r.Cookie("auth"); err == http.ErrNoCookie || cookie.Value == "" {
		// 未承認時はログイン画面へリダイレクト
		w.Header().Set("Location", "/login")
		w.WriteHeader(http.StatusTemporaryRedirect)
	} else {
		// 承認成功。ラップされたハンドラを呼び出す
		h.next.ServeHTTP(w, r)
	}
}

func callbackHandler(w http.ResponseWriter, r *http.Request) {
	// ④ 外部サービスからの認証結果を判定
	user, err := gothic.CompleteUserAuth(w, r)
	if err != nil {
		fmt.Fprintln(w, err)
		return
	}

	// ⑤ 外部サービスから取得した情報をアプリ用データとしてCookieにしこむ
	authCookieValue := objx.New(map[string]interface{}{
		"name":       user.Name,
		"avatar_url": user.AvatarURL,
	}).MustBase64()
	http.SetCookie(w, &http.Cookie{
		Name:  "auth",
		Value: authCookieValue,
		Path:  "/",
	})

	// メイン画面へリダイレクト
	w.Header()["Location"] = []string{"/"}
	w.WriteHeader(http.StatusTemporaryRedirect)
}

func logoutHandler(w http.ResponseWriter, r *http.Request) {
	http.SetCookie(w, &http.Cookie{
		Name:   "auth",
		Value:  "",
		Path:   "/",
		MaxAge: -1,
	})

	w.Header()["Location"] = []string{"/"}
	w.WriteHeader(http.StatusTemporaryRedirect)
}
```

### ポイント

①: アプリ初期化時に`goth.UseProviders`で利用する外部サービスを設定します。本記事ではFacebook認証を利用しています。第一引数に「アプリID」の値、第二引数に「app secrert」の値、そして、第三引数には外部サービスが認証結果を返すコールバックURLを指定し、アプリ側でそのURLに対してハンドリング処理を記述しています(本アプリでは`callbackHandler`が処理するように指定している)。

②: ルーティングには[Gorilla web toolkitのpat](http://www.gorillatoolkit.org/pkg/pat)を利用します。gothを利用する上ではこのパッケージを利用することが推奨されるようです。※

※ gothに各外部サービス(provider)を判定させるためには、URLに`provider`パラメータを指定し、gothへURLを受け渡す必要があります。patパッケージは`{provider}`のように`{}`でくくった部分をパラメータとしてURL変換してくれるので、簡単にgothとの連携に必要な記述ができます。

③: 認証制御用のHTTPハンドラ(`authHandler`)を実装し、`MustAuth`関数でアプリのメイン画面へ遷移させるHTTPハンドラ(`templateHandler `)をラップします。

④: 外部サービス(facebook)からの認証結果を受取り、`user, err := gothic.CompleteUserAuth(w, r)`でその結果を判定します。認証に成功していれば、`user`には外部サービスから受け取ったユーザー情報が入っています。※

※ gothから取得できるユーザー情報は[こちら](https://github.com/markbates/goth/blob/master/user.go)のファイルから参照する必要があります。

⑤⑥: 外部サービスから取得したユーザー情報をアプリで利用するために、Cookieに必要な情報を入れます。そのCookieによって`authHandler`で承認され、⑥にてユーザー情報を取得します。[`objx`](https://godoc.org/github.com/stretchr/objx)はCookieに入れる情報をBase64形式で扱うことをサポートするOSSパッケージです。

## HTMLファイル

```html:templates/index.html
<html>
  <head>
    <title>メイン画面</title>
  </head>
  <body>
    <div>ユーザー名: {{.UserData.name}}</div>
    <div><img src="{{.UserData.avatar_url}}" title="avatar_img" width="128" height="128"></div>
    <div><a href="/logout">サインアウト</a></div>
  </body>
</html>
```

```html:templates/login.html
<html>
  <head>
    <title>ログイン画面</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  </head>
  <body>
    <div class="container">
      <div class="page-header">
        <h1>サインインしてください</h1>
      </div>
      <div class="panel panel-danger">
        <div class="panel-heading">
          <h3 class="panel-title">サインインが必要です</h3>
        </div>
        <div>
          <p>サインインに使用するサービスを選んでください:</p>
          <ul>
            <li>
              <a href="/auth/facebook">Facebook</a>
            </li>
          </ul>
        </div>
      </div>
    </div>
  </body>
</html>
```

# 動かす

ビルドして起動します。

```bash:run
$ go build -o app
$ ./app
```

`http://localhost:3000/`へブラウザからアクセスすると`http://localhost:3000/login`のログイン画面へ遷移します。

![スクリーンショット 2017-09-17 18.06.48.png](https://qiita-image-store.s3.amazonaws.com/0/91562/22b8863b-5ca7-da19-da81-cffa0bfdda09.png "スクリーンショット 2017-09-17 18.06.48.png")

facebookをクリックするとアプリの許可画面ページが出るので許可を選択します。

メイン画面が表示され、ユーザー名とFacebookでのトップ画が表示されていることを確認できました！ログイン完了です。

![スクリーンショット 2017-09-17 18.18.12.png](https://qiita-image-store.s3.amazonaws.com/0/91562/33396df2-bec9-5e14-2a48-75d869cb8635.png "スクリーンショット 2017-09-17 18.18.12.png")

サインアウトするとアプリ用のCookie情報を消すことでログアウトし、ログイン画面へ再度遷移します。

【おまけ】ブラウザからCookieを見るとアプリのコード側で作ったもの(auth)とgothが生成したもの(facebook_gothic_session)がそれぞれ存在することが確認できます。

![スクリーンショット 2017-09-17 01.18.38.png](https://qiita-image-store.s3.amazonaws.com/0/91562/25932d4b-4f3d-7964-45af-3f44d4d8a055.png "スクリーンショット 2017-09-17 01.18.38.png")


# 参考

* [【Qiita】OAuth 2.0 全フローの図解と動画](http://qiita.com/TakahikoKawasaki/items/200951e5b5929f840a1f)
* [【書籍】Go言語によるWebアプリケーション開発](https://www.oreilly.co.jp/books/9784873117522/)
* [https://goldplugins.com/documentation/wp-social-pro-documentation/how-to-get-an-app-id-and-secret-key-from-facebook/](https://goldplugins.com/documentation/wp-social-pro-documentation/how-to-get-an-app-id-and-secret-key-from-facebook/)
* [https://github.com/rodreegez/goth-example](https://github.com/rodreegez/goth-example)