はじめに
ウェブアプリでユーザ認証を実装するのに、Node.js+Passport を使ってみました。
これを通常のウェブアプリでなく、SPA(シングルページアプリ)で使いたいと思いました。
Node.js でウェブ API を用意する
クライアント環境のウェブブラウザで、いわゆるフロントエンドアプリが動きます。
サーバアプリでウェブ API が提供され、クライアントアプリからコールされます。
まず、ウェブ API を提供するサーバアプリを Node.js で用意します。
ワークスペースを作る
まず、Node.js+Express のワークスペースを作ります。
var express = require('express');
var app = express();
var bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({ extended: true }));
var server = app.listen(3000, function(){
console.log("Node.js is listening to PORT " + server.address().port);
});
認証不要なウェブ API を用意する
まず、認証不要でアクセスできる API を作ります。単純な文字列を返すようにします。
app.get('/api/insecure', function(req, res){
res.send("Insecure response.");
});
クライアントアプリからアクセスしてみます。API から返された文字列を表示します。
<button id='insecure'>Insecure Request</button>
<div id='status'></div>
<div id='output'></div>
<script>
document.querySelector('#insecure').addEventListener('click', function(){
fetch('/api/insecure')
.then(function(res){
document.querySelector('#status').innerHTML = res.statusText;
res.text()
.then(function(text){
document.querySelector('#output').innerHTML = text;
});
});
});
</script>
ID とパスワードで都度認証する API を用意する
Passport を使う
ワークスペースに Passport を追加します。
$ npm install --save passport
var passport = require('passport');
app.use(passport.initialize());
ID とパスワードで都度認証する API を用意する
前述の API を ID とパスワードで都度認証するようにします。
var LocalStrategy = require('passport-local').Strategy;
passport.use(new LocalStrategy(function(username, password, done){
if (/* username と password を確認する */) {
return done(null, username);
}
else {
return done(null, false);
}
}));
app.post('/api/secure/local', passport.authenticate('local', { session: false }), function(req, res){
res.send("Secure response from " + JSON.stringify(req.user));
});
クライアントアプリからアクセスします。
<button id='secure_local'>Local Auth Request</button>
<script>
document.querySelector('#secure_local').addEventListener('click', function(){
var data = new FormData();
data.append('username', '◆◆◆◆');
data.append('password', '◆◆◆◆');
fetch('/api/secure/local',{
method: 'POST',
body: data
})
.then(function(res){
document.querySelector('#status').innerHTML = res.statusText;
res.text()
.then(function(text){
document.querySelector('#output').innerHTML = text;
});
});
});
</script>
ID とパスワードの代わりに Google 認証を使う
ID とパスワードの代わりに Google を使って認証するようにします。
前述の API を Google を使って認証するようにしてみましょう。
app.get('/api/secure/google', passport.authenticate('google', { session: false,
scope: ['https://www.googleapis.com/auth/plus.login'] }), function(req, res){
res.send("Secure response from " + JSON.stringify(req.user));
});
/* 以下略 */
でも、API にアクセスするたびに Google のサイトに飛んで認証するとか、ナンセンスですね。
そこで、JWT を使います。
JWT で認証する
JWT とは
JWT(ジョット)とは、JSON Web Token の略で、電子署名付きの JSON で、改ざんをチェックできるようになっているものです。
以下のように使います。
①クライアントアプリは認証情報(ID とパスワードとか)をサーバに送る
②サーバアプリは送られた認証情報を確認する
(①②は OAUTH を使ってもいい。)
③サーバアプリは ID と有効期限を含む情報を暗号化して JSON(これが JWT)にして返す
④クライアントアプリは ID とパスワードの代わりに③で貰った JSON を送るようにする
⑤サーバアプリは送られた JSON の改竄検証した上で認証する
JWT で都度認証する API を用意する
ワークスペースで JWT を使った認証できるようにします。
$ npm install --save passport-jwt
前述の API を JWT で都度認証するようにします。
var JwtStrategy = require('passport-jwt').Strategy;
var ExtractJwt = require('passport-jwt').ExtractJwt;
passport.use(new JwtStrategy({
jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('jwt'),
secretOrKey: '◆◆◆◆',
issuer: '◆◆◆◆',
audience: '◆◆◆◆'
}, function(payload, done){
if (/* payload.sub を確認する */) {
return done(null, user, payload);
}
else {
return done();
}
}));
app.get('/api/secure/jwt', passport.authenticate('jwt', { session: false }), function(req, res){
res.send("Secure response from " + JSON.stringify(req.user));
});
クライアントアプリからアクセスします。
<script>
var accessToken;
document.querySelector('#secure_jwt').addEventListener('click', function(){
var headers = {};
if (accessToken) {
headers['Authorization'] = 'JWT ' + accessToken;
}
fetch('/api/secure/jwt', { headers: headers })
.then(function(res){
document.querySelector('#status').innerHTML = res.statusText;
res.text()
.then(function(text){
document.querySelector('#output').innerHTML = text;
});
});
});
</script>
認証に使うトークンを予め貰っておかないといけませんね。
Google 認証してトークンを返す
Google 認証できるようにする
改めて Google 認証できるようにします。
var GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;
passport.use(new GoogleStrategy({
clientID: "◆◆◆◆◆◆◆◆.apps.googleusercontent.com",
clientSecret: "◆◆◆◆",
callbackURL: "http://◆◆◆◆/auth/google/callback"
}, function(accessToken, refreshToken, profile, done){
if (/* profile.id を確認する */) {
return done(null, user);
}
else {
return done(null, false);
}
}));
app.get('/auth/google', passport.authenticate('google', {
scope: ['https://www.googleapis.com/auth/plus.login']
}));
app.get('/auth/google/callback', passport.authenticate('google', { session: false }), function(req, res){
/* ここでトークンを生成して返す */
});
トークンを生成する
トークンを生成して返すためこうします。
npm install --save jsonwebtoken
var jwt = require('jsonwebtoken');
function generateToken(userId) {
const token = jwt.sign({}, '◆◆◆◆', {
expiresIn: '◆◆◆◆',
audience: '◆◆◆◆',
issuer: '◆◆◆◆',
subject: userId.toString()
});
return token;
}
app.get('/auth/google/callback', passport.authenticate('google', { session: false }), function(req, res){
var token = generateToken(req.user.id);
res.JSON({ token: token });
});
クライアントアプリからアクセスしてみます。
<button id='auth_google'>Google Auth Login</button>
<script>
document.querySelector('#auth_google').addEventListener('click', function(){
fetch('/auth/google')
.then(function(res){
/* 以下略 */
</script>
上手く行きません。Google 認証するにはページ遷移しないといけないようです。
<a href="/auth/google">Google Auth Login</a>
Google 認証できますが、ページ遷移してしまうと、クライアントアプリ自体がいなくなってしまいます。
別ウィンドウを開いて Google 認証する
そこで、クライアントアプリでは別ウィンドウを開いて Google 認証を済ませるようにします。
<button id='auth_google'>Google Auth Login</button>
<script>
document.querySelector('#auth_google').addEventListener('click', function(){
window.open('/auth/google');
});
</script>
クライアントアプリがサーバアプリからトークンを受取る
クライアントアプリがサーバアプリからトークンを受取るには工夫が必要です。
まず、クライアントアプリ側の仕掛け。
window オブジェクトにコールバック関数を仕掛けておきます。
<script>
document.querySelector('#auth_google').addEventListener('click', function(){
window.authenticateCallback = function(token){
accessToken = token;
}
window.open('/auth/google');
});
</script>
次に、サーバアプリ側の仕掛け。
こんなページテンプレートを用意しておきます。
<script>
window.opener.authenticateCallback("{{token}}");
window.close();
</script>
このページが開かれると、事前に仕掛けておいたコールバック関数で、引数に渡したトークンがクライアントアプリに渡ります。
Google 認証からコールバックされトークンを生成したところで、上記のテンプレートをレンダリングします。
今回はレンダリングエンジンに Handlebars を使います。
npm install --save express-handlebars
var handlebars = require('express-handlebars');
app.engine('html', handlebars());
app.set('view engine', 'handlebars');
app.set('views', __dirname + '/●●●●');
app.get('/auth/google/callback', passport.authenticate('google', { session: false }), function(req, res){
var token = generateToken(req.user.id);
res.render('authenticated.html', { token: token });
});
おわりに
自作の SPA で Google 認証できるようにしたいと思い試してみましたが、上手く行きませんでした。
そこで以下の記事を見つけました。
これはいい記事ですが、記事に記載されているコードだけでは、自分のアプリに応用しても動きませんでした。
Github にあるコード全体を読みましたが、自分のスタイルと違っていて、自分のアプリに応用するのはつらかった。
そこで、自分のコードにするため、記事のコードを書換していきました。その経緯を、この記事にしておきました。