はじめに
昨今、スタートアップなサービスをつくる場合、ログイン認証にはOAuth2規格を用いたFacebookやGoogle等の外部サービスの認証ログインを利用するのが基本です。
Go言語での外部認証ログイン機能の実装には goth というOSSパッケージを利用するのが現在主流のようです。
OAuth2認証に伴うサービスサーバとの複雑なやり取りを、こういったパッケージに任せることで、プログラマは自身のアプリケーションの実装に集中できます。
外部認証ログインし、その外部サービスからの情報を表示するだけのシンプルなWEBアプリをgothを利用し作ってみました。GitHubページリンク。(gothのGitHubページにもサンプルアプリが載っています。link)
アプリを開発環境で動かすには、認証に使う外部サービスからIDとシークレットキーを取得し、それらを環境変数に設定する必要があります。
本記事ではそれらも踏まえてアプリを動かすまでを書いていきます。
事前準備
外部サービスからアプリID、シークレットキーを取得
外部サービス認証機能を利用するにはその外部サービスからアプリ用のIDとシークレットキーを取得する必要があります。
取得方法は各サービスごとに用意されています。本アプリでは、Facebook認証を利用します。FacebookサービスからIDとシークレットキーを取得し、さらに、開発環境で動作させるために必要な設定を以下に示します。
- facebook for developersにアクセスする
- 右上の「マイアプリ」>「新しいアプリを追加」でアプリ表示名を設定しアプリを作成する
- アプリの設定ページにある「アプリID」と「app secret」を控える
- 設定ページにある「プラットフォームを追加」から「ウェブサイト」を選択する
- 「サイトURL」に開発環境で起動させるWebサーバのURLを設定する。(localhostの3000番ポートで起動させる場合は
http://localhost:3000/
と設定する。)
環境変数の設定
外部サービス(Facebook)から取得した「アプリID」と「app secret」、さらにgothを利用する上で必要な値を環境変数として開発環境に設定します。
bash, zshの場合
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
に実装しています。
コード
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))
}
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ファイル
<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>
<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>
動かす
ビルドして起動します。
$ go build -o app
$ ./app
http://localhost:3000/
へブラウザからアクセスするとhttp://localhost:3000/login
のログイン画面へ遷移します。
facebookをクリックするとアプリの許可画面ページが出るので許可を選択します。
メイン画面が表示され、ユーザー名とFacebookでのトップ画が表示されていることを確認できました!ログイン完了です。
サインアウトするとアプリ用のCookie情報を消すことでログアウトし、ログイン画面へ再度遷移します。
【おまけ】ブラウザからCookieを見るとアプリのコード側で作ったもの(auth)とgothが生成したもの(facebook_gothic_session)がそれぞれ存在することが確認できます。