google認証のrefresh_tokenの扱いでハマったポイント

## はじめに
ある案件の要件に「Googleでログインしているユーザーで、スプレッドシートに帳票を出力してほしい」とありましたので、その実現のためにはまったポイントについて、refresh_tokenの「取り方」「保管の仕方」「使い方」の３つの観点で整理しようと思います。
サーバー側のロジックはGoで記述し、Webフレームワークは一部echoを使用しています。


## 全体概要
解説のためmain.goに全て記述しました。
著書『Go言語によるWebアプリケーション開発』（以下、「教科書」）の内容を元に実装しています。

```
package main

import (
	"fmt"
	"github.com/gomodule/redigo/redis"
	"github.com/google/uuid"
	"github.com/joho/godotenv"
	"github.com/labstack/echo"
	"github.com/stretchr/gomniauth"
	"github.com/stretchr/gomniauth/providers/google"
	"github.com/stretchr/objx"
	"golang.org/x/oauth2"
	google2 "golang.org/x/oauth2/google"
	"log"
	"net/http"
	"os"
	"strings"
)

// 認証情報はRedisに保存する
var conn redis.Conn

/*
CLIENT_ID プロジェクトのクライアントID
SECRET_VALUE プロジェクトのシークレットValue
REDIRECT_URL プロジェクトで設定したリダイレクトURL
SECURITY_KEY 任意の文字列
*/

func main(){
	log.Println("server start...")

	_ = godotenv.Load()

	var err error
	conn, err = redis.Dial("tcp", os.Getenv("REDIS_CONN"))
	if err != nil{
		panic(err)
	}

	// google認証機能の設定　
	gomniauth.SetSecurityKey(os.Getenv("SECURITY_KEY"))
	gomniauth.WithProviders(
		google.New(os.Getenv("CLIENT_ID"), os.Getenv("SECRET_VALUE"), os.Getenv("REDIRECT_URL")),
	)

	e := echo.New()

	// ハンドラの登録
	e.GET("/login", Login)
	e.GET("/callback", Callback)


	g := e.Group("")
	g.Use(AuthGuard())
	// ログインしていないと見れないページ
	g.GET("/private", Private)

	// echoサーバーの起動
	e.Logger.Fatal(e.Start(":1323"))
}

// Login GoogleログインのURLの発行処理
func Login(c echo.Context) error{
	log.Println("Login is invoked")

    // ！ refresh_tokenを取り出すときのポイント
	// provider, err := gomniauth.Provider("google")　教科書ではここでこれを使っているが、それだとScorpesをいじれないし、refresh_tokenも取れない。
	// url, err := provider.GetBeginAuthURL(nil, nil)

	// 代わりにoauth2でconfigを直接いじる。
	config := oauth2.Config{
		ClientID:     os.Getenv("CLIENT_ID"),
		ClientSecret: os.Getenv("SECRET_VALUE"),
		Endpoint:     google2.Endpoint,
		RedirectURL:  os.Getenv("REDIRECT_URL"),
		Scopes: []string{
			"https://www.googleapis.com/auth/userinfo.email",
			"https://www.googleapis.com/auth/spreadsheets", // スプレッドシートの使用権限を追加
		},
	}

	// こうするとログイン後にスプレッドシートの操作権限を渡して良いかの確認ダイアログが出るurlを取得でき、「refreshToken」が取れる。
	url := config.AuthCodeURL(os.Getenv("SECURITY_KEY"), oauth2.AccessTypeOffline, oauth2.ApprovalForce)

	// 生成したurlを返す。フロントはこれにリクエスト、リダイレクトしてもらう。
	return c.String(http.StatusOK, url)
}

/*
googleログイン後はリダイレクトURLに対してクエリ「?code=XXXX」がついた状態でリダイレクトする
教科書では直接GoのCallback関数にリダイレクトさせるように記述されているが、
フロントとAPIでソースコードが別れている場合ではうまくいかなかった。
そのため、一度フロントにURLを返して、改めて再度queryにcodeの値をセットして、Goの/callbackにリクエストを投げてもらっている。
*/

// Callback ログイン後のコールバック処理
func Callback(c echo.Context) error{
	log.Println("Callback is invoked")

	provider, err := gomniauth.Provider("google")
	if err != nil{
		return c.String(http.StatusInternalServerError, err.Error())
	}

	// codeの値を取り出す。Googleの認可処理
	code := c.QueryParam("code")
	cred, err := provider.CompleteAuth(objx.MustFromURLQuery("code=" + code))
	if err != nil {
		return c.String(http.StatusInternalServerError, err.Error())
	}

	// 変数「user」にGoogleで保持しているユーザー情報が取れる。access_token、emailと、アバターの画像、名前、jwtトークンに加えて、上記の対応によりここでrefresh_tokenが含まれるようになる。
	user, err := provider.GetUser(cred)
	if err != nil {
		return c.String(http.StatusInternalServerError, err.Error())
	}


    // refresh_tokenを保管するときのポイント 
	// redisにその情報を保存するため、keyを生成し、cred,userからほしい情報だけ取り出して保存する。
	uuID, err := uuid.NewRandom()
	if err != nil {
		panic(err)
	}	
    uuidStr := uuID.String()
	refreshToken := fmt.Sprintf("%v", cred.Get("refresh_token"))
	jsonStr := fmt.Sprintf(`{"refresh_token":"%s","email":"%s"}`,
		refreshToken, user.Email())

	log.Println(jsonStr)
	_, err = conn.Do("SET", uuidStr, jsonStr, "NX")
	if err != nil{
		return c.String(http.StatusInternalServerError, err.Error())
	}

	// フロントにredisのキーを返す。フロントは今後Header["Authorization"]に入れてリクエストしてもらう。
	return c.String(http.StatusOK, uuidStr)
}

// Private ログインしていない場合、"success"は表示されない。
func Private(c echo.Context) error{
	log.Println("Private is invoked")

	// context からトークンを取り出す
	token := c.Get("token").(string)
	log.Println(token)

	// tokenを表示させる。
	return c.JSON(http.StatusOK, token)
	//return c.String(http.StatusOK, "success")
}

// AuthGuard ログイン状態の確認
func AuthGuard() echo.MiddlewareFunc{
	return func(next echo.HandlerFunc) echo.HandlerFunc{
		return func(c echo.Context) error{
			log.Println("AuthGuard is invoked")

			// Header["Authorization"]からredisのキーを取り出す。
			token := c.Request().Header.Get("Authorization")
			key := strings.ReplaceAll(token, "Bearer ", "")

			// redisから情報を取り出す。
			res, _ := redis.String(conn.Do("GET", key))
			if res == ""{
				// ない場合、ログインが必要だと表示
				return c.String(http.StatusUnauthorized, "login required")
			}

			// contextに値を詰めておく。
			c.Set("token", res)

			// ログインしているので、その関数を実行する。
			if err := next(c); err != nil{
				return c.String(http.StatusInternalServerError, err.Error())
			}
			return nil
		}
	}
}

// ！ refresh_tokenの使い方のポイント
// リフレッシュトークンを元にアクセストークンを取得する
func getClient(refreshToken string) *http.Client {
	log.Println(model.IsInvoked())
	urlValue := url.Values{
		"client_id":     {os.Getenv("CLIENT_ID")},
		"client_secret": {os.Getenv("SECRET_VALUE")},
		"refresh_token": {refreshToken},
		"grant_type":    {"refresh_token"},
	}

	resp, err := http.PostForm("https://www.googleapis.com/oauth2/v3/token", urlValue)
	if err != nil {
		log.Fatalf("Error when renew token %v", err)
	}

	body, err := ioutil.ReadAll(resp.Body)
	_ = resp.Body.Close()
	if err != nil {
		log.Fatal(err)
	}
	var token oauth2.Token
	_ = json.Unmarshal(body, &token)
	config := oauth2.Config{}
	return config.Client(context.Background(), &token)
}

```

## refresh_tokenの「取り方」のポイント
にてログイン機能の実装は解説されているのですが、その通りに実装すると、refresh_tokenを変数credの中から取得することができませんでした。（Login関数内のポイント①を参照）
そのため、config.AuthCodeURLのnilを渡している二箇所にそれぞれ、oauth2.AccessTypeOffline, oauth2.ApprovalForceを引数に渡すと、ユーザーがGoogleログインするときに「〇〇 が Google アカウントへのアクセスをリクエストしています」というダイアログを出すようになります。
すると、Callback関数のprovider.GetUser(cred)の返り値のなかに、refresh_tokenが含まれるようになります。

## refresh_tokenの「保管の仕方」のポイント
refresh_tokenが流出した場合、その人の権限でスコープで記述した範囲のことができてしまうので、ユーザーに直接見える形で管理するというのは避けなければなりません。
そのため、今回はuuID.String()で生成した値をキーに、メールアドレスと一緒にredisに保管しています。キーをフロントとやり取りするときはAuthorizationヘッダー内に入れます。

## refresh_tokenの「使い方」のポイント
いざ使うときにはrefresh_tokenをそのまま使うのではなく、その都度access_tokenを生成しなければなりません。
getClient関数にて、その記述をしました。



## さいごに
まだまだ経験の浅いエンジニアです。上記がベストプラクティスなのかは確証がありません・・・。
他にもっと良い方法があった場合、ぜひぜひおしえてください。