独自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。