独自WebサービスにGoogleアカウントを使った認証を実装する
結構ハマったので記録に残しておく。Webアプリを想定し、コードはNode.jsでのものの抜粋。
基本的にはGoogle DevelopersのOpenID Connectページに書いてある通りで、OAuth 2.0を使って認証を行える。
事前準備としては、Google Developers Consoleでプロジェクトを作成しておく。認証だけであればAPIはすべて無効でOK。認証情報では使用する「承認済みのリダイレクトURI」を適切に登録しておく。
コード的には、一般的なOAuth 2.0での認証と同じ。流れ的には次のような感じ。
1. 適当なランダム文字列を生成する
下記のコードではstateToke変数がそれ。今回はランダム文字列と日付と適当な文字列(salt)を組み合わせてsha256ハッシュを取って使用。これは認証が試みられるたびに異なるものを作る必要がある。
var salt = 'hogehoge'; var hash = crypto.createHash('sha256'); hash.update(salt + Math.random() + Date()) var stateToken = hash.digest('hex');
2. 認証URLを作成
クライアントIDとランダム文字列とリダイレクトURLをパラメータとして指定する。
var authUrl = 'https://accounts.google.com/o/oauth2/v2/auth?' + 'response_type=code' + '&client_id=' + config.googleToken.clientID + '&redirect_uri=' + <リダイレクトURL> + '&scope=profile%20email' + '&state=' + stateToken;
3. ランダム文字列をセッションストアに保存したうえで認証URLにリダイレクトする
この辺はフレームワークによって異なるのでコードは割愛。リダイレクトURLがDevelopers Consoleで指定したものと異なるとエラーになるので注意。
4. リダイレクトURL経由でトークンを取得
認証に成功すると、Googleの認証ページから指定したリダイレクトURLにリダイレクトされる。このとき、URLには「?state=<token>&code=<code>」というパラメータが付加されるので、URLをパースして取得。
var params = url.parse(req.url, true); var code = params.query.code; var state = params.query.state;
ここで、stateには最初に作成したランダム文字列が入るので、セッションストアに保存されていた値と比較する、同じクライアントでアクセスしていれば一致するはず。一致しなければ認証失敗を返す。
if (state !== session.stateToken) { // 認証に失敗。エラーを返すコードを書く
5. 取得したcodeを使ってアクセストークンを取得する
ここは面倒臭いのでモジュールの利用を推奨。リダイレクトURLは最初に指定したものと同じものを指定する。
var oauth2 = new oauth.OAuth2( config.googleToken.clientID, //cliendId config.googleToken.clientSecret, //clientSecret 'https://accounts.google.com/o/', //baseSite null, //authorizePath 'oauth2/token', //accessTokenPath null //customHeaders ); var param = { grant_type: 'authorization_code', redirect_uri: <リダイレクトURL> }; oauth2.getOAuthAccessToken(code, param, tokenCallback);
6. 受け取ったトークンを検証する
無事認証に成功するとアクセストークンとリフレッシュトークン、認証用データが入ったオブジェクトがコールバック関数に渡される(下記のtoken、refresh、resultsがそれ)。
var jwtToken = require('./jwt-token'); function tokenCallback(err, token, refresh, results) { if (err) { callback(err, null); return; } jwtToken.verify(results.id_token, function (err, token) { if (err || !token) { callback(err, null); return; } callback(null, token); }); };
ここで、ユーザー情報はresultsオブジェクトのid_tokenプロパティにJSON Web Tokens(JWT)という形式で署名済みの形で格納されているので、それをデコードして検証しなければならない。
JWTを扱うモジュールとしてjwt-simpleモジュールがあるので、今回はこれを使用。また、検証に使う公開鍵はそれなりの頻度で変更されるとのことなので、検証のたびにダウンロードして使用する。この処理をラップしたモジュール(jwt-token.js)が下記。
var jwt = require('jwt-simple'); var https = require('https'); var certsUrl = 'https://www.googleapis.com/oauth2/v1/certs'; function decodeBase64(strings) { var buf = new Buffer(strings, 'base64'); return buf.toString('utf8'); } function verifyJwtToken(token, callback) { var segments = token.split('.'); var envelope = JSON.parse(decodeBase64(segments[0])); var payload = JSON.parse(decodeBase64(segments[1])); var data = ''; var req = https.get(certsUrl, function (res) { res.on('data', function (chunk) { data += chunk; }); res.on('end', function () { var certs = JSON.parse(data); var key = certs[envelope.kid]; var result = jwt.decode(token, key); callback(null, result); }); }); req.on('error', function (err) { callback(err); }); } exports.verify = verifyJwtToken;
トークンをデコードすると、中にemailというプロパティがあるのでそこからユーザーのメールアドレスを取得できるので、これを自分のサービス側に登録されているメールアドレスと比較してユーザーと対応付けるなり、新たにユーザーを作るなりOpenID関連の情報を使って認証するなりすればOK。