30
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-09-17

はじめに

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

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

外部認証ログインし、その外部サービスからの情報を表示するだけのシンプルなWEBアプリをgothを利用し作ってみました。GitHubページリンク。(gothのGitHubページにもサンプルアプリが載っています。link)

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

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

事前準備

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

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

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

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

スクリーンショット 2017-09-17 02.21.48.png

環境変数の設定

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

bash, zshの場合

.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を利用しており、それを利用する上で必要な秘密鍵です。また、goth利用者側でそのCookie設定を上書きすることができます。

実装

ファイル構成

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

階層構成は上記のようになっています。gothの初期設定、リクエストのルーティング設定、HTMLレンダリングをmain.goに、認証の制御とgothを使ったOAuth2認証機能をauth.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))
}
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を利用します。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から取得できるユーザー情報はこちらのファイルから参照する必要があります。

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

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>
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>

動かす

ビルドして起動します。

run
$ go build -o app
$ ./app

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

スクリーンショット 2017-09-17 18.06.48.png

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

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

スクリーンショット 2017-09-17 18.18.12.png

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

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

スクリーンショット 2017-09-17 01.18.38.png

参考

30
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?