NaaN日記

やったこと、覚えたことを発信する場

GoogleのOAuth 2.0で受け取ったトークンをrevokeする

はじめに

GoogleのOAuth 2.0を使ったアプリケーションで、認証によって得たユーザの情報を用いて、条件に合ったユーザにのみ、サービスの利用を許可したい、という場合があります。

  • 例) 学生用メールアドレス(~ac.jp)を持つユーザにのみサービスを提供したい

このブログでは、この例の場合を想定して、話を進めていきます。

トークンを取り消すこと

Google OAuth 2.0では、認証された際に、アクセストークンが発行されます。このトークンを取り消す方法には、ユーザが自分で行う方法と、アプリケーション側で行う方法があります。

  1. ユーザがAccount Settingsにアクセスして、認証を取り消す
  2. アプリケーション側でリクエストを送信し、認証を取り消す

トークンをrevokeすることは、実際のところ、実装の必要性は低いかもしれません。今回は、以下の理由から、revokeを行います。

  1. サービスの利用を許可しないアカウントに対して、トーク*1を残しておく理由はない
  2. 認証後に、OAuth認証用のURL(例 : http://localhost/auth/login/google)へアクセスすると、Googleにログインしているアカウントが一つしかないとき*2、認証済みアカウントに対してトークンが再発行される。

2の場合について、例を思い出します。

例では、OAuth 2.0 を用いた認証後、認証に用いたGoogleのメールアドレスを確認し、条件に合わないユーザであれば、サービスの利用を許可せず、ユーザに通知します。

許可されなかったユーザのうち、学生用メールアドレスを持っているが、異なるアカウントでログインを試みてしまったというようなユーザは、学生アカウントで再認証を行います。

再認証を行うのですが、Googleにログイン中のアカウントが一つだけのとき、アカウントを追加し、切り替えることは少し面倒です。

OAuth 2.0 による認証後に、条件に合っているかどうかを判断しているということは、OAuth 2.0による認証は通っているということです*3

つまり、アプリケーションがサービスの利用を許可していない場合でも、認証を取り消さない間は、期限が切れるまで、認証されている状態になっています。

Google OAuth 2.0の仕様により、ログイン画面にアクセスすると、新しいアカウントを追加する間もなく、以前認証されたものは、自動で認証されます*4

この場合、ユーザが取る行動には、Googleのトップページからアカウントを追加するAccount Settingsから認証を取り消すなどがあります。


また、自動で認証が通るという仕様について、認可画面をスキップしないようにする方法があります。

OAuth 2.0のエンドポイント(https://accounts.google.com/o/oauth2/auth)に対し、
approval_prompt=force のクエリパラメータを付加することです。

しかし、この方法では、全ての(アカウントを一つしか持たない)ユーザが認可画面を毎回見る事になります。そのため、認可画面をスキップするか、しないか、どちらが良いかは状況によると思います。

次に、アプリケーション側でトークンを削除する方法について、書いていきます。

環境

  • Go 1.13.0
  • Echo 3.3.10 (Go web framework)
  • MySQL 8.0.17
  • Docker 19.03.5-ce
  • Docker Compose 1.25.0

認証には、Gomniauthを、データの処理のためにobjxを利用しています。

また、本ブログでは、説明に最低限必要な部分のみ記述しています。認証部分のソースコードは、Go言語によるWebアプリケーション開発を参考にしています。
www.oreilly.co.jp

トークンを取り消す方法

プログラムでトークンを取り消すには、パラメータにトークンを付加した、次の形式のリクエストを送信します。

https://oauth2.googleapis.com/revoke?token={token}

アクセストークンの取得

認証後に呼び出される関数CallbackHandler*5の中で、accessTokenを取得します。

/*
import(
	"net/http"

	"github.com/labstack/echo"
	"github.com/stretchr/gomniauth"
	"github.com/stretchr/objx"
)
*/

// CallbackHandler -- Provider called this handler after login
func (u *userHandler) CallbackHandler(c echo.Context) error {
	provider, err := gomniauth.Provider(c.Param("provider"))
	if err != nil {
		return err
	}

	omap, err := objx.FromURLQuery(c.QueryString())
	if err != nil {
		return err
	}

	creds, err := provider.CompleteAuth(omap)
	if err != nil {
		return err
	}

	accessToken := creds.Get("access_token").Str()

	return c.Redirect(http.StatusTemporaryRedirect, "/")
}

トークンを取り消す関数

取得したアクセストークンを引数に受けとる、以下の関数を作成しました。

/*
import (
	"io"
	"io/ioutil"
	"net/url"
	"net/http"
)
*/

func revokeToken(accessToken string) error {
	const googleRevokeURL = "https://accounts.google.com/o/oauth2/revoke"
	u, err := url.Parse(googleRevokeURL)
	if err != nil {
		return err
	}

	q := u.Query()
	q.Set("token", accessToken)
	u.RawQuery = q.Encode()
	resp, err := http.Get(u.String())
	if err != nil {
		return err
	}

	defer func() {
		io.Copy(ioutil.Discard, resp.Body)
		resp.Body.Close()
	}()

	return err
}

関数CallbackHandler内で取得したaccessTokenを、関数revokeTokenに渡すことで、トークンを取り消すことができました。
これにより、Googleにログインしているアカウントが一つであったとしても、認証用URLにアクセスすると、認可画面が表示されます。
また、条件に合わないユーザが認証してきたら、関数revokeTokenを呼び出す、というような利用もできます。
たとえ、認証後にログインページにリダイレクトするようなコードを書いてしまったとしても、無限ループにはならないと思います(そういう実装は良くないと思いますが……)。


ありがとうございました。

*1:有効期限はあるけれど

*2:アカウントが複数あるとき、認証するアカウントを選択する画面(認可画面)が表示される

*3:この言い回し、いつか話題になった構文のようですね

*4:認可画面をスキップしない方法はある

*5:詳しくは本を読むか、検索をお願いします