🦔

【GO】GoogleOAuthの管理パッケージがいい感じにできた

2025/02/24に公開
2

ソースの流れとOAuthについて

最後に全文載せてるので使ってください。
たぶんほぼそのまま動くと思います。

OAuth 2.0 は、ユーザーが安全に認証情報を提供せずに、サードパーティアプリケーションにアクセスを許可するための標準的な認可プロトコルです。

このパッケージは Google OAuth を利用し、アクセストークンの取得やリフレッシュ、ユーザー情報の取得を管理する機能を提供します。基本的な流れは以下の通り。

  1. NewGoogleOAuthManager で Google OAuth 設定を生成。
  2. GetAccessToken で認可コードを使用してアクセストークンを取得。
  3. RefreshAccessToken でリフレッシュトークンを用いて新しいアクセストークンを取得。
  4. createOAuthInfo でアクセストークンを元にユーザー情報を取得。

メソッドの説明

NewGoogleOAuthManager(redirectURL string) *GoogleOAuthManager

環境変数から ClientIDClientSecret を取得し、Google OAuth 設定を生成する。

GetAccessToken(code string) (*OAuthInfo, error)

Google OAuth 認可コードを使用し、アクセストークンを取得。ユーザー情報を OAuthInfo 構造体に格納して返す。

RefreshAccessToken(refreshToken string) (*OAuthInfo, error)

リフレッシュトークンを利用し、新しいアクセストークンを取得する。

createOAuthInfo(ctx context.Context, tokenInfo *oauth2.Token) (*OAuthInfo, error)

アクセストークンを元に Google のユーザー情報を取得し、OAuthInfo 構造体に格納する。

インポートの書き方に注意

Google OAuth ライブラリを使用しています。
https://pkg.go.dev/golang.org/x/oauth2/google

import (
	"golang.org/x/oauth2"
	googleOAuth "golang.org/x/oauth2/google"
	v2 "google.golang.org/api/oauth2/v2"
)

必要なライブラリのインストール

以下のコマンドを実行して、必要なライブラリを取得してください。

go get golang.org/x/oauth2
go get golang.org/x/oauth2/google

注意点

  • googleOAuth というエイリアスを付けて google.Endpoint を使用する。
  • v2 "google.golang.org/api/oauth2/v2" を用いて Google API の Tokeninfo を取得する。

-- 追記 ここから --
この記事を拝見してくださった方からご助言をいただきました。(引用)

この方法で取得した値はあくまでアクセストークンから引いた情報であること、利用サービスがユーザー認証機能で利用する場合はOIDCの一連のフローで取得した情報を利用すべき

OAuthでログインとかを実装する際は注意してください。
この処理は『使われているGoogleアカウントの主は本人だと信じていいよ』ということ過ぎないので、『自身のアプリのユーザーであるか?』には無関係です。
別途、自身のアプリのログイン処理等と組み合すなりの処理が必要です。

さらにコメントでは、この注釈を踏まえて『Emailとユーザー識別子の情報』を本パッケージ内で閉じてしまえばアプリの認証として使いやすいのではというご助言をいただきました m(_ _)m

-- 追記 ここまで --

ソースコード全文

package infra

import (
	"context"
	"develop-app/utils"
	"errors"
	"os"

	"golang.org/x/oauth2"
	googleOAuth "golang.org/x/oauth2/google"
	v2 "google.golang.org/api/oauth2/v2"
)

type IGoogleOAuthManager interface {
	GetAccessToken(code string) (oAuthInfo *OAuthInfo, err error)
}

type GoogleOAuthManager struct {
	Config *oauth2.Config
}

func NewGoogleOAuthManager(redirectURL string) *GoogleOAuthManager {
	return newGoogleOAuthManager(
		os.Getenv("GOOGLE_CLIENT_ID"),
		os.Getenv("GOOGLE_CLIENT_SECRET"),
		redirectURL,
	)
}

func newGoogleOAuthManager(clientId string, clientSecret string, redirectURL string) *GoogleOAuthManager {
	googleOAuthManager := &GoogleOAuthManager{
		Config: &oauth2.Config{
			ClientID:     clientId,
			ClientSecret: clientSecret,
			Endpoint:     googleOAuth.Endpoint,
			Scopes:       []string{"openid", "email"},
			RedirectURL:  redirectURL,
		},
	}

	if googleOAuthManager.Config == nil {
		panic("==== invalid key. google api ====")
	}

	return googleOAuthManager
}

func (g *GoogleOAuthManager) GetAccessToken(code string) (oAuthInfo *OAuthInfo, err error) {
	utils.CustomLogger(utils.Debug, "GetAccessToken")
	cxt := context.Background()
	tokenInfo, _ := g.Config.Exchange(cxt, code)
	if tokenInfo == nil {
		utils.CustomLogger(utils.Error, utils.FailedGetGoogleToken)
		return nil, errors.New(utils.FailedGetGoogleToken)
	}
	googleAccessToken := tokenInfo.AccessToken

	client := g.Config.Client(cxt, tokenInfo)
	service, err := v2.New(client)
	if err != nil {
		utils.CustomLogger(utils.Error, utils.GoogleTokenClientError+":"+err.Error())
		return nil, err
	}

	userInfo, err := service.Tokeninfo().AccessToken(googleAccessToken).Context(cxt).Do()
	if err != nil {
		utils.CustomLogger(utils.Error, err.Error())
		return nil, err
	}

	oAuthInfo = &OAuthInfo{
		Email:        userInfo.Email,
		AccessToken:  googleAccessToken,
		RefreshToken: tokenInfo.RefreshToken,
		Expiry:       tokenInfo.Expiry.Unix(),
	}

	return oAuthInfo, nil
}

func (g *GoogleOAuthManager) RefreshAccessToken(refreshToken string) (*OAuthInfo, error) {
	if refreshToken == "" {
		return nil, errors.New(utils.RefreshTokenIsRequired)
	}
	cxt := context.Background()
	tokenSource := g.Config.TokenSource(cxt, &oauth2.Token{RefreshToken: refreshToken})
	newToken, err := tokenSource.Token()
	if err != nil {
		return nil, errors.New(utils.FailedToRefreshAccessToken + ":" + err.Error())
	}

	return g.createOAuthInfo(cxt, newToken)
}

func (g *GoogleOAuthManager) createOAuthInfo(cxt context.Context, tokenInfo *oauth2.Token) (*OAuthInfo, error) {
	client := g.Config.Client(cxt, tokenInfo)
	service, err := v2.New(client)
	if err != nil {
		utils.CustomLogger(utils.Error, utils.FailedToCreateOAuth2Service+":"+err.Error())
		return nil, errors.New(utils.FailedToCreateOAuth2Service)
	}

	userInfo, err := service.Tokeninfo().AccessToken(tokenInfo.AccessToken).Context(cxt).Do()
	if err != nil {
		utils.CustomLogger(utils.Error, utils.FailedToFetchGoogleUserInfo+":"+err.Error())
		return nil, errors.New(utils.FailedToFetchGoogleUserInfo)
	}

	return &OAuthInfo{
		Email:        userInfo.Email,
		AccessToken:  tokenInfo.AccessToken,
		RefreshToken: tokenInfo.RefreshToken,
		Expiry:       tokenInfo.Expiry.Unix(),
	}, nil
}

type OAuthInfo struct {
	Email        string
	AccessToken  string
	Expiry       int64
	RefreshToken string
}

Discussion

ritouritou

OAuthInfoにユーザー情報を入れたくなるのであれば、メールアドレスだけではなくユーザー識別子も含む方が他の人も汎用的に使えるようになりそうだなと思いました。
さらに、この方法で取得した値はあくまでアクセストークンから引いた情報であること、利用サービスがユーザー認証機能で利用する場合はOIDCの一連のフローで取得した情報を利用すべき、と注釈を入れておいてもらえると誤用のリスクを軽減できそうです。

Kutsu-4ta(yamashita_antapp)Kutsu-4ta(yamashita_antapp)

ご助言感謝いたします。

OAuthInfoにユーザー情報を入れたくなるのであれば、メールアドレスだけではなくユーザー識別子も含む方が他の人も汎用的に使える

たしかに言われてみれば中途半端な切り出し方に見えてきました。。。
結局この後のフローで自身のアプリのユーザーかどうかの識別が必要ですからね。

この方法で取得した値はあくまでアクセストークンから引いた情報であること、利用サービスがユーザー認証機能で利用する場合はOIDCの一連のフローで取得した情報を利用すべき

おっしゃる通りこの注釈は必要ですね。追記しておきます。