Sails.js + PassportでID/Password認証
Passportについて
以前、everyauth を使ってパスワード認証・SNS認証など実装していましたが、なんとなく自分には取っ付き辛く、Passport を見つけてからこちらを採用するようになりました。Passport は一般的な ID/Password認証 の他に各SNSを使ったOAuth認証までサポートしています。
(認証と権限設定のみなので、投稿機能などの実装は SecretTokenをその他のパッケージに譲ることになります)
passportモジュールは、Passportの骨格となるパッケージとなり、その他 passport-github や passport-twitter などがそれぞれの認証機能を提供しています。
今回は、passport-local を使ってログイン処理をする例を紹介します。
- Node.js: v0.10.26
- Sails.js v0.10.0-rc8
記事公開時 v0.10系がrc版 ですが、今のところ基本的にv0.9系もほぼ同じ実装方法で大丈夫だと思います。
- 参考: Using Passport.JS with Sails.JS
- http://jethrokuan.github.io/2013/12/19/Using-Passport-With-Sails-JS.html
Sailsのインストールとフォルダ生成
sails
コマンドを使えるようにします。
$ npm install -g sails@v0.10.0-rc8
ちなみに npm install -g sails@beta
で unstableの最新版のインストールが可能です。
プロジェクトのベースとなるフォルダ類を sails new
コマンドで生成します。
$ sails new sails-passport
$ cd sails-passport
sails lift
もしくは node app.js
でアプリケーションを起動します。
http://localhost:1337 をブラウザで開いて、起動していることを確認します。
デフォルトでは、ルートアクセスで、views/homepage.ejs
を表示しており、 設定は config/routes.js
に定義しています。
これ以降 sails lift
コマンドを利用しても良いのですが、コード書き換えの時に何度も再起動を行わなければならないので、再起動モジュールを導入します。
forever のインストールと実行
npm install -g forever
forever を使うことでコード変更を監視し再起動を行うのですが、監視しなくても良いファイルもあるので、それらを除外する設定を行ないます。
## Create .foreverignore (app.jsと同階層に)
**/.tmp/**
assets/**
views/**/*.ejs
.idea/**
.git/**
forever -w app.js
で、コード変更時に再起動が行われるようになり、sails lift
を何度も実行しなくても良くなります。
ログイン遷移処理とアクセス制限の設定
ログイン承認処理を行う画面と前後の遷移を行えるよう、sailsコマンドで controller と view を作成します。
まず、ログイン画面、ログイン認証、ログアウト処理を実行する controller を作成
$ sails generate controller auth login process logout
info: Created a new controller ("auth") at api/controllers/AuthController.js!
AuthController.js 以下、login/process/logout のプロパティが生成されました。
## View api/controllers/AuthController.js
module.exports = {
/**
* `AuthController.login()`
*/
login: function (req, res) {
return res.json({
todo: 'login() is not implemented yet!'
});
},
/// ...
ログイン画面表示
http://localhost:1337/auth/login にアクセスするとcontrollerで定義された json が画面に表示されます。 (auth/process /auth/logoutも同様)ログインID、パスワード入力画面が必要なので、ログイン画面(view側) を作成します。
## Create views/auth/login.ejs
<form action="/auth/process" method="post">
<div class="login">
login: <input type="text" name="username" placeholder="Input Login ID" value="" /> <br/>
password: <input type="password" name="password" placeholder="Input Login Password" value="" /><br/>
<button type="submit">ログイン</button>
</div>
</form>
/auth/login
(api/controllers/AuthController.login) へのアクセスは view (views/auth/login.ejs)に割り当てます。
Auth(Controller)/login
なので、return res.view()
とすることで、(何も引数を渡さなくても) views/auth/login(.ejs) をview とします。
/auth/process
(api/controllers/AuthController.process) へのアクセスは、ログイン認証が終わったものとして /dashboard
へリダイレクトします。
/auth/logout
(api/controllers/AuthController.logout) へのアクセスは、ログイン認証解除が終わったものとして /
へリダイレクトします。
## Modify api/controllers/AuthController.js
module.exports = {
/**
* `AuthController.login()`
*/
login: function (req, res) {
// ログイン画面表示
return res.view();
},
/**
* `AuthController.process()`
*/
process: function (req, res) {
////
// ログイン承認処理
////
//承認後 /dashboardへ
return res.redirect("/dashboard");
},
/**
* `AuthController.logout()`
*/
logout: function (req, res) {
////
// ログイン承認解除
////
//解除後、topページへ
return res.redirect("/");
}
};
続いてログイン承認後の ダッシュボード(ログイン認証済みonly)画面を作成します。
$ sails generate controller dashboard index
info: Created a new controller ("dashboard") at api/controllers/DashboardController.js!
# Modify api/controllers/DashboardController.js
module.exports = {
/**
* `DashboardController.index()`
*/
index: function (req, res) {
// ログインonly ダッシュボード
return res.view();
}
};
## Create views/dashboard/index.ejs
<div class="hello">
<h1>Sails.js + Passport Dashboard</h1>
<p>Hello!!!</p>
</div>
これでユーザーの状態はさておき、ページのモックとなるものは完成しました。
しかしこのままだと、だれでも /dashboard
へアクセス可能なページとなりますので、だれもアクセス出来ないページに変更します。
ダッシュボードページへのアクセス制限(暫定)
ダッシュボードへのアクセスにフィルターを掛け、すべてのユーザーを /
へリダイレクトする設定を行ないます。
後ほど、ログイン認証機能を実装した時に承認したユーザーのみリダイレクトを行わないように変更するためです。
ログインアクセスポリシーとして loginAuth
という(任意の)名称でポリシーファイルを作成します。
## Create api/policies/loginAuth.js
module.exports = function(req, res, next) {
// All User Redirect
return res.redirect("/");
};
(※ もともとsessionAuth.jsがあるのでそれを利用してもよい)
続いて、ログインポリシーloginAuth
を割り当てるURL(Controller) を設定します。
## Modify config/policies.js
module.exports.policies = {
// Default policy for all controllers and actions
// (`true` allows public access)
'*': true,
DashboardController: {
// index : "loginAuth"
'*': "loginAuth"
}
}
※ dashboardController でも可
これにより、loginAuth
ポリシーが有効となり、 /dashbord/(index)
へのアクセスはすべて /
へリダイレクトします。
ログインポリシーついては passport の実装が終わるまで一旦置いておきます。
ログインユーザデータの設定
ログインユーザー用 User Model と User CRUD を作成します。
$ sails generate api user
info: Created a new controller ("user") at api/controllers/UserController.js!
info: Created a new model ("User") at api/models/User.js!
info: REST API generated @ http://localhost:1337/user
info: and will be available the next time you run `sails lift`.
これにより api/controllers/UserController.js
と api/models/User.js
が生成されました。
http://localhost:1337/user アクセスで User への参照、更新/削除などが行える環境が整いました。
(※ 実際の運用には CSRF や Authenticate Policies の定義が必要になりますが、ここでは省略します)
- CSRF
- /config/csrf.js
- Authenticate Policies
- /config/policies.js 、 /api/policies/adminAuth.js定義など
一度試してみましょう。 http://localhost:1337/user/create?username=hoge&password=fuga へアクセスします。
するとUserデータが登録されます。
{
"username": "hoge",
"password": "fuga",
"createdAt": "yyyy-MM-ddTHH:mm:ss.sssZ",
"updatedAt": "yyyy-MM-ddTHH:mm:ss.sssZ",
"id": 1
}
もう一度同じURLにアクセスすると、同じユーザー情報が登録されてしまいます。
{
"username": "hoge",
"password": "fuga",
"createdAt": "yyyy-MM-ddTHH:mm:ss.sssZ",
"updatedAt": "yyyy-MM-ddTHH:mm:ss.sssZ",
"id": 2
}
http://localhost:1337/user で、確認します。
username の重複防止とパスワードの暗号化を行うために schemeとプロパティ定義を行う必要があります。
(※ development 環境下では、DBデータはローカルファイル(.tmp/localDiskDb.db)に格納されています。)
一旦、登録したユーザーデータを削除します。
http://localhost:1337/user/destroy/1
http://localhost:1337/user/destroy/2
/destroy/:id
となるので http://localhost:1337/user/ へアクセスするとデータが削除されていることが確認出来ます。
Model scheme
では、改めて User model の定義に移ります。modelのプロパティとして attributesに username
と password
を設定。
username
は必須かつユニーク項目、password
は必須項目としています。
## Modify api/models/User.js
module.exports = {
attributes: {
username: {
type: 'string',
required: true,
unique: true
},
password: {
type: 'string',
required: true
}
}
}
この状態で /user/create
へ 必要項目を設定せず登録しようとすると "E_VALIDATION" でエラーが返ります
http://localhost:1337/user/create?test1=a&test2=b
次に必要な項目を設定し http://localhost:1337/user/create?username=hoge&password=fuga へアクセスします。
しっかりデータが登録されます。
{
"username": "hoge",
"password": "fuga",
"createdAt": "yyyy-MM-ddTHH:mm:ss.sssZ",
"updatedAt": "yyyy-MM-ddTHH:mm:ss.sssZ",
"id": 3
}
同じデータを登録しようとすると username
の unique: true
制限で "E_VALIDATION" エラーが返ります。
では、再びデータを削除します。
http://localhost:1337/user/destroy/3
パスワード暗号化
``beforeCreate``, ``afterCreate`` など、Insert時の前後にhookを掛けることができるので、beforeCreate にて、登録時 ``password`` 項目をSalt化します。暗号化モジュールのインストール
$ npm install -S bcrypt
## Modify api/models/User.js
var bcrypt = require('bcrypt');
module.exports = {
attributes: {
username: {
type: 'string',
required: true,
unique: true
},
password: {
type: 'string',
required: true
},
},
beforeCreate: function(user, cb) {
bcrypt.genSalt(10, function(err, salt) {
bcrypt.hash(user.password, salt, function(err, hash) {
if (err) {
console.log(err);
cb(err);
}else{
user.password = hash;
cb(null, user);
}
});
});
}
}
では、Userが登録可能で、hook等が有効かどうかを確認します。
http://localhost:1337/user/create?username=hoge&password=fuga
{
"username": "hoge",
"password": "$2a$10$UXZIwUkCx.1IUETSUXHwcuG22FKC7JbDyD1QLEK68UwqdGI0cHPvW",
"createdAt": "yyyy-MM-ddTHH:mm:ss.sssZ",
"updatedAt": "yyyy-MM-ddTHH:mm:ss.sssZ",
"id": 4
}
その他hook処理
次に例としてオブジェクト取得時に使用されるメソッドの toJson() をオーバーライドします。 http://localhost:1337/user による Userデータを取得(toJson)する際に ``password`` 項目を削除しています。## Modify api/models/User.js
var bcrypt = require('bcrypt');
module.exports = {
attributes: {
toJSON: function() {
console.log("toJson");
var obj = this.toObject();
delete obj.password;
return obj;
},
username: {
type: 'string',
required: true,
unique: true
},
password: {
type: 'string',
required: true
}
},
beforeCreate: function(user, cb) {
bcrypt.genSalt(10, function(err, salt) {
bcrypt.hash(user.password, salt, function(err, hash) {
if (err) {
console.log(err);
cb(err);
}else{
user.password = hash;
cb(null, user);
}
});
});
}
}
Passport 承認処理設定
必要なモジュール類をパッケージをインストールします。
$ npm install -S passport passport-local
- passport
- Passport コアモジュール
- passport-local
- ID/Password認証用サブモジュール
- bcrypt
- 暗号化モジュール
passport側の ログイン認証処理の基本動作を定義します。
## Create config/passport.js
var passport = require('passport'),
LocalStrategy = require('passport-local').Strategy,
bcrypt = require('bcrypt');
//helper functions
function findById(id, fn) {
User.findOne(id).exec(function (err, user) {
if (err) {
return fn(null, null);
} else {
return fn(null, user);
}
});
}
function findByUsername(u, fn) {
User.findOne({
username: u
}).exec(function (err, user) {
// Error handling
if (err) {
return fn(null, null);
// The User was found successfully!
} else {
return fn(null, user);
}
});
}
// ① passport コアモジュール 基本動作定義
passport.serializeUser(function (user, done) {
done(null, user.id);
});
passport.deserializeUser(function (id, done) {
findById(id, function (err, user) {
done(err, user);
});
});
// ② passport-local サブモジュール ID/Password認証定義
passport.use(new LocalStrategy(
function (username, password, done) {
process.nextTick(function () {
findByUsername(username, function (err, user) {
if (err)
return done(null, err);
if (!user) {
return done(null, false, {
message: 'Unknown user ' + username
});
}
bcrypt.compare(password, user.password, function (err, res) {
if (!res)
return done(null, false, {
message: 'Invalid Password'
});
var returnUser = {
username: user.username,
id: user.id
};
return done(null, returnUser, {
message: 'Logged In Successfully'
});
});
})
});
}
));
// ③ express middleware hook
module.exports = {
express: {
customMiddleware: function(app){
// app: express() オブジェクト
console.log("passport module initialize");
app.use(passport.initialize());
app.use(passport.session());
}
}
};
passport コアモジュール 基本動作定義
コアの処理としてデータシリアライズなどの定義をします。 この例では user.id のみをキーとして持ち回りしています。Passport内部でのreq.user への値の出し入れ時に動作します。
passport-local サブモジュール ID/Password認証定義
ログイン処理時(後述)に実行される処理を定義します。 引数として渡された ``userneme``,``password`` を元にUserデータを確認し、必要なデータ(任意のオブジェクト)を、コールバックの引数へ渡します。express middleware hook
expressの middleware へ passport モジュールを使用するための設定です。 ここは express を利用した場合と同じ実装です。Passport 認証設定
ログイン処理
ログイン認証機能を実装します。AuthController.process と config/passport.js の LocalStrategy 定義と割り当てます。
## Modify api/controllers/AuthController.js
var passport = require('passport');
module.exports = {
//ログイン認証処理
process: function(req, res){
passport.authenticate('local', function(err, user, info) {
if ((err) || (!user)) {
return res.send({
message: 'login failed'
});
}
req.logIn(user, function(err) {
if (err) res.send(err);
return res.send({
message: 'login successful'
});
});
})(req, res);
},
};
/process
への postしたログインが正しく機能するかを確認します。
/user
で登録した userneme: hoge & password: foga でログインしてみます。
登録したユーザに対しての正しいパスワード情報で 'Successfully' となれば passport-local の認証機能設定はOKです。
passport を利用すると、@request オブジェクトに login,logout関数が割り当てられるので、login()を実行しダッシュボードへリダイレクトします。
/**
* `AuthController.process()`
*/
process: function(req, res){
passport.authenticate('local', function(err, user, info) {
console.log(info);
if ((err) || (!user)) {
return res.send({
message: 'login failed'
});
}
// req.isAuthenticated() -> false
// req.user -> undefined
req.logIn(user, function(err) {
if (err) res.send(err);
// req.isAuthenticated() -> true
// req.user -> user -> When new LocalStrategy, Callback user Object
return res.redirect("/dashboard");
})(req, res);
},
ログアウト処理
ログアウト/**
* `AuthController.logout()`
*/
logout: function (req, res) {
////
// ログイン状態解除
////
req.logout();
res.redirect('/');
}
ダッシュボードへのアクセス制限
passport を連携した @request.login()、@request.logout()の実行により、@response.isAuthenticated() の取得結果が変わります。この結果を以ってダッシュボードへのアクセス権限である、loginAuthポリシーの処理を変更します
## Modify api/policies/loginAuth.js
module.exports = function(req, res, next) {
if(req.isAuthenticated()) {
// This request is authenticated
return next();
}
// This request is not authenticate...
return res.redirect("/");
};
これにより、ログイン判定処理が行われるようになります。
Routing の変更
好みにも依りますが、URLのRoutingを変更して分かりやすい遷移を行うことが出来ます。
## Modify views/auth/login.ejs
<form action="/login" method="post">
<div class="login">
login: <input type="text" name="username" placeholder="Input Login ID" value="" /> <br/>
password: <input type="password" name="password" placeholder="Input Login Password" value="" /><br/>
<button type="submit">ログイン</button>
</div>
</form>
## Modify config/routes.js
module.exports.routes = {
'/': {
view: 'homepage'
},
'get /login': {
controller: 'auth',
action: 'login'
},
'post /login': {
controller: 'auth',
action: 'process'
},
'/logout': {
controller: 'auth',
action: 'logout'
}
}
http://localhost:1337/login からログインすることが可能になりました。