GoogleのOAuth 2.0で受け取ったトークンをrevokeする
はじめに
GoogleのOAuth 2.0を使ったアプリケーションで、認証によって得たユーザの情報を用いて、条件に合ったユーザにのみ、サービスの利用を許可したい、という場合があります。
- 例) 学生用メールアドレス(~ac.jp)を持つユーザにのみサービスを提供したい
このブログでは、この例の場合を想定して、話を進めていきます。
トークンを取り消すこと
Google OAuth 2.0では、認証された際に、アクセストークンが発行されます。このトークンを取り消す方法には、ユーザが自分で行う方法と、アプリケーション側で行う方法があります。
- ユーザがAccount Settingsにアクセスして、認証を取り消す
- アプリケーション側でリクエストを送信し、認証を取り消す
トークンをrevokeすることは、実際のところ、実装の必要性は低いかもしれません。今回は、以下の理由から、revokeを行います。
- サービスの利用を許可しないアカウントに対して、トークン*1を残しておく理由はない
- 認証後に、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を呼び出す、というような利用もできます。
たとえ、認証後にログインページにリダイレクトするようなコードを書いてしまったとしても、無限ループにはならないと思います(そういう実装は良くないと思いますが……)。
ありがとうございました。