【GO】GoogleOAuthの管理パッケージがいい感じにできた
ソースの流れとOAuthについて
最後に全文載せてるので使ってください。
たぶんほぼそのまま動くと思います。
OAuth 2.0 は、ユーザーが安全に認証情報を提供せずに、サードパーティアプリケーションにアクセスを許可するための標準的な認可プロトコルです。
このパッケージは Google OAuth を利用し、アクセストークンの取得やリフレッシュ、ユーザー情報の取得を管理する機能を提供します。基本的な流れは以下の通り。
-
NewGoogleOAuthManager
で Google OAuth 設定を生成。 -
GetAccessToken
で認可コードを使用してアクセストークンを取得。 -
RefreshAccessToken
でリフレッシュトークンを用いて新しいアクセストークンを取得。 -
createOAuthInfo
でアクセストークンを元にユーザー情報を取得。
メソッドの説明
NewGoogleOAuthManager(redirectURL string) *GoogleOAuthManager
環境変数から ClientID
と ClientSecret
を取得し、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 ライブラリを使用しています。
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
OAuthInfoにユーザー情報を入れたくなるのであれば、メールアドレスだけではなくユーザー識別子も含む方が他の人も汎用的に使えるようになりそうだなと思いました。
さらに、この方法で取得した値はあくまでアクセストークンから引いた情報であること、利用サービスがユーザー認証機能で利用する場合はOIDCの一連のフローで取得した情報を利用すべき、と注釈を入れておいてもらえると誤用のリスクを軽減できそうです。
ご助言感謝いたします。
たしかに言われてみれば中途半端な切り出し方に見えてきました。。。
結局この後のフローで自身のアプリのユーザーかどうかの識別が必要ですからね。
おっしゃる通りこの注釈は必要ですね。追記しておきます。