🐕

GolangでGoogle OAuthでのアカウント連携をする

2022/04/24に公開2

目的

Googleアカウントとの連携させるためにGoogle OAuthを用いてGoogleアカウントIDを取得する。
今回はGCPでのアプリの設定などは省いています。

大まかな処理の流れの整理

1: ユーザーが連携するリクエストをサービスのAPIに投げかける

2: stateトークンをAPIが発行し、Googleアカウントでのログイン画面のURLにstateトークンとscope、ClientIDを繋げてフロントに返す

3: 返ってきたログインURLにリダイレクトし、同意を求める画面が表示されるので確認したらGoogleの認証サーバーからAuthorization code(以下codeと表示します)が発行されます。

4: stateトークンとGoogleから返されたcodeをAPI側へ送り、以前自身が発行したstateトークンかどうかをAPI側が判断します。

5: 確認できれば、API側からGoogleへアクセスしてcodeをGoogle側が相違ないと判断しユーザー情報を返します。

6: API側が取得したGoogleアカウント情報の一部をDBに保存し、ログイン時にサービスのUserIDと紐付けて連携が完了する。
そして、Googleアカウントでもログインできるようになる。

GoogleアカウントIDを取得する部分のコード

Google.go
package infrastructure

import (
	"context"
	"errors"

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

type Google struct {
	Config *oauth2.Config
}

func NewGoogle(c *Config) *Google {
	return newGoogle(c)
}

func newGoogle(c *Config) *Google {

	google := &Google{
		Config: &oauth2.Config{
			ClientID:     c.Google.ClientID,
			ClientSecret: c.Google.ClientSecret,
			Endpoint:     googleOAuth.Endpoint,
			Scopes:       []string{"openid"},
			RedirectURL:  "http://localhost:8080/auth/callback/google",
		},
	}

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

	return google
}

func (g *Google) GetLoginURL(state string) (clientID string) {
	return g.Config.AuthCodeURL(state)
}

func (g *Google) GetUserID(code string) (googleUserID string, err error) {

	cxt := context.Background()

	httpClient, _ := g.Config.Exchange(cxt, code)
	if httpClient == nil {
		return "", errors.New("接続エラー")
	}

	client := g.Config.Client(cxt, httpClient)

	service, err := v2.New(client)
	if err != nil {
		return "", errors.New("接続エラー")
	}

	userInfo, err := service.Tokeninfo().AccessToken(httpClient.AccessToken).Context(cxt).Do()
	if err != nil {
		return "", errors.New("接続エラー")
	}

	return userInfo.UserId, nil
}

基本的に記述はドキュメントを参考にしています。

userInfo, err := service.Tokeninfo().AccessToken(httpClient.AccessToken).Context(cxt).Do()

この箇所で取得したものには下記の構造体が返されてくる(パッケージ内から抜粋)

google.golang.org/api/oauth2/v2
type Tokeninfo struct {
	Audience string `json:"audience,omitempty"`
	Email string `json:"email,omitempty"`
	ExpiresIn int64 `json:"expires_in,omitempty"`
	IssuedTo string `json:"issued_to,omitempty"`
	Scope string `json:"scope,omitempty"`
	UserId string `json:"user_id,omitempty"`
	VerifiedEmail bool `json:"verified_email,omitempty"`
	googleapi.ServerResponse `json:"-"`
	ForceSendFields []string `json:"-"`
	NullFields []string `json:"-"`
}

今回はこの中からUserIdを取得しサービスのUserIDを紐付けている。

まとめ

・stateトークンーAPI側が発行し、操作したユーザーで相違ないとして不正なアクセスでないことをAPI側が判断する
・Authorization codeー認証サーバー(今回だとGoogle側)から発行され、これもGoogleが当サービスの正当性を確認するために発行されるもの

今回はGoogleアカウントとサービスのUserIDを紐付けることを目的としているのでGoogleアカウントのUserIdではなくアクセストークンのがいいのではないかなどありますが、認可や認証などまとめて書いていこうと思います。

Discussion

ritouritou

今回記事に書いていただいた内容は OAuth 2.0 の仕組みでアクセストークンを取得、それを利用してユーザー情報をAPIにより取得されています。これはよく "OAuth認証" と呼ばれる仕組みでありますが、サーバーサイドアプリケーションであり、state検証をしっかり行い、かつアクセストークン取得後にすぐにそれに紐づくユーザー情報を取りに行っているため一定の安全性は保たれており大きな問題が出ることはないでしょう。

今回はGoogleアカウントとサービスのUserIDを紐付けることを目的としている

こちらの目的であれば、GoogleはOAuth 2.0にユーザー情報などを受け渡しする仕組みを追加したOpenID Connectという仕様に沿った仕組みが提供されています。

参考URL : https://developers.google.com/identity/protocols/oauth2/openid-connect

"OAuth認証" との一番の違いとして

  • Authorization Code から Access Token を取得する際に 認証イベント、ユーザーの属性情報をJWT形式で表現したIDTokenというものを返す

というものがあります。

GolangのライブラリでOIDCをサポートしているものもあるかと思いますので、余裕がありましたら利用方法や今回のライブラリの違いなどを見てもらえたら今後の役に立つかもしれません。

しょうごしょうご

コメントありがとうございます!!
そのような仕様がOAuth認証と別で存在しているんですね。
そして、リンクまで貼っていただきありがとうございます。
今回行った作業との違いを調べてまたまとめたいと思います。