loopbackで作成したプロジェクトでSlackログインできる様にする

f:id:hideack:20171126122221p:plain先日から試行錯誤していた内容のまとめ。

いまloopback.ioを利用してAPIを作ることに取り組んでいるのだけれども、frameworkが提供するユーザ認証の仕組み(メールアドレス及びパスワード)だけではなく、Node.js向けの認証ミドルウェアであるPassportを利用して他の認証機構を利用しようという取り組み。

loopback + passport でfacebooktwitterのアカウントを利用したソーシャルログインの話題は散見するのだけれども、passport-slackを使ってloopbackフレームワークで作成するプロジェクトでのユーザ認証をする話をあまりみかけず、あれこれ試行錯誤した…。

基本的にloopbackプロジェクトが用意しているpassportを利用したユーザ認証のサンプルが以下にあるのでこれに倣えばよい。

github.com

ただサンプルと作り始めているプロジェクトの差分がどこなのだ問題がはっきりせず苦労したので、主に差分を中心にまとめます。

大前提

https://api.slack.com/apps を開いてユーザ認証に使いたいアプリケーションの情報を追加する。

Redirect URIにローカルで開発する用に http://localhost:5000/auth/slack/callback といった形で登録しておく。併せてApp Credentials から Client ID と Client Secret を控えておいて、.env に以下の様な形で書いておく。

SLACK_CLIENT_ID=12345.6789
SLACK_CLIENT_SECRET=xxxyyyzzz

passportでユーザ認証させるために必要なnpmパッケージを導入

先のサンプルで加えられているnpmパッケージと、Slackログインさせるためのpassport-slackをインストールする。

$ npm install --save connect-ensure-login
$ npm install --save cookie-parser
$ npm install --save express-flash
$ npm install --save express-session
$ npm install --save loopback-component-passport
$ npm install --save loopback-passport
$ npm install --save passport-slack

passportが各種情報を収めるモデルを用意する

https://github.com/strongloop/loopback-example-passport で追加されてるモデルを追記する。

上のモデルをloopbackがデフォルトで作成するUserモデルに紐付ける

common/models/user.json

{
"name": "user",
"plural": "users",
"base": "User",
"properties": {},
"validations": [],
"relations": {
"accessTokens": {
"type": "hasMany",
"model": "accessToken",
"foreignKey": "userId"
},
"identities": {
"type": "hasMany",
"model": "userIdentity",
"foreignKey": "userId"
},
"credentials": {
"type": "hasMany",
"model": "userCredential",
"foreignKey": "userId"
}
},
"acls": [],
"methods": {}
}

上の一通りのモデルの設定を server/model-config.json に追記する。

 {
"_meta": {
"sources": [
"loopback/common/models",
"loopback/server/models",
"../common/models",
"./models",
+      "./node_modules/loopback-component-passport/lib/models"
],
"mixins": [
"loopback/common/mixins",
"loopback/server/mixins",
"../common/mixins",
"./mixins"
]
},
"User": {
"dataSource": "db",
"public": false
},
"AccessToken": {
"dataSource": "db",
"public": false
},
+  "userCredential": {
+    "dataSource": "db",
+    "public": false
+  },
+  "userIdentity": {
+    "dataSource": "db",
+    "public": false
+  },

server/server.js に追記

loopbackのcliツールであるlbコマンドで作ったプロジェクトでデフォルトで作成されたファイルをサンプルのものに置き換える。

ここで今回自分は設定ファイルをJSONではなくてJSで取る様にしたので以下の様に修正した。(.envからOAuthのパラメータ取ってもらいたかったので)

var config = {};
try {
config = require('../providers.js');  // JSにしただけ
} catch (err) {
console.trace(err);
process.exit(1); // fatal
}

Slackログイン用の各種情報を書いたproviders.js を置く

.env に記載したClient IDとClient Secretが差し込まれる様にしている。

module.exports = {
"slack-login": {
"provider": "slack",
"module": "passport-slack",
"clientID": process.env.SLACK_CLIENT_ID,
"clientSecret": process.env.SLACK_CLIENT_SECRET,
"authPath": "/auth/slack",
"callbackURL": "/auth/slack/callback",
"callbackPath": "/auth/slack/callback",
"successRedirect": "/",
"failureRedirect": "/signin",
"scope": ["identity.basic", "identity.email", "identity.avatar", "identity.team"],
"failureFlash": true
}
};

ここまでできたらloopbackのプロジェクト立ち上げて /auth/slack にアクセスすると一度Slackへ遷移したのち認証に成功するとコールバックしてくるのが確認できる。

過去作成したloopbackアプリのloopback-component-explorerへの対応

過去に作成したloopbackアプリの場合、slcコマンドでscafoldすると loopback-explorer と呼ばれるloopbackで作成したAPICRUDするUIが含まれた形でアプリが作成されますが、今回この loopback-explorer に大きな変更が加えられて単純に npm update するとアプリの起動が行えなくなるのでその際の対応方法のメモです。

loopback(2.22.0)以降だとslcコマンドでscafoldされた際に新しいAPI explorerに対応しているので問題はないので、既にアプリを作っている人向けの内容です。

package.jsonの修正

これまで利用されていた loopback-explorer を削除。新しく同じ役割をするコンポーネントとして追加された loopback-component-explorer を追加します。

$ npm install loopback-component-explorer --save
$ npm uninstall loopback-explorer --save

修正後のpackage.jsonは以下の様な形になります。

  "dependencies": {
(snip)
"loopback-component-passport": "^1.5.0",
+   "loopback-component-explorer": "^2.1.0",
"loopback-connector-mongodb": "^1.13.0",
(snip)
},
- "optionalDependencies": {
-   "loopback-explorer": "^2.0.1"
- },

component-config.json を追加

loopback-component-explorer をlooopbackアプリにマウントさせてやるために server/component-config.json を新規に追加します。

explorer呼び出すパスをJSON内で記述します。

{
"loopback-component-explorer": {
"mountPath": "/explorer"
}
}

以上の操作で http://localhost:5000/explorer/ へアクセスすることで従来通りのAPI Explorerを利用することができます。

参照

StrongLoop | What’s New in the LoopBack Node.js Framework – July & August 2015


全く蛇足ですが、StrongloopのロゴにIBMのクレジットが入っていた。

loopback-testingを利用してloopbackで作成したAPIのテストを書く

loopbackでAPIのテストを書く際にloopback-testingを利用してAPIのテストを書いてみたのでその際のメモ。

まずは必要なnpmパッケージをloopbackアプリケーションに導入します。loopbackアプリのルートで以下のnpmコマンドを実行。

www.npmjs.com

$ npm install loopback-testing --save-dev
$ npm install chai --save-dev

これで準備完了。加えて npm test でテストを実行できる様に package.json 内のscriptsの定義を以下の様に書き換えます。*1

"scripts": {
"test": "NODE_ENV=\"testing\" ./node_modules/loopback-testing/node_modules/.bin/mocha -R spec --recursive"
}

上記の意図は npm test が実行された際に testing 環境としてnode_modules配下のmochaを利用してテストを実行します。

それにともなってtesting環境でloopbackアプリが実行された際のデータソース(DB接続先)を定義しておきます。Circle CI等のCI環境でもすぐに実行できる様に、memoryを指定します。

~/server/datasources.testing.js というファイルを用意して以下の様にデータの保存先をmemoryに指定します。

module.exports = {
db: {
connector: "memory",
defaultForType: "db"
}
};

これでテストが実行できる様になりました。expressの流儀と同様にプロジェクト直下の test ディレクトリに現在開発しているloopbackアプリケーション(API)のテストを書いてみます。

例えばユーザ登録(メールアドレスとPWをPOSTするとユーザ登録できる)のAPIのテストであれば、

var lt = require('loopback-testing');
var chai = require('chai');
var assert = chai.assert;
var app = require('../../server/server.js');
describe('/api/users', function() {
var newUser = {email: "tester1@remp.jp", password:"tester1remp"};
describe('ユーザ登録', function() {
lt.describe.whenCalledRemotely('POST', '/api/users', {}, function() {
it('ユーザ登録の際にメールアドレス、パスワードのパラメータは必須', function() {
var codes = this.res.body.error.details.codes;
assert.include(codes.password, 'presence');
assert.include(codes.email, 'presence');
assert.include(codes.email, 'format.null');
});
});
lt.describe.whenCalledRemotely('POST', '/api/users', newUser, function() {
it('メールアドレス, パスワードを与えるとユーザ作成できる', function() {
assert.equal(this.res.statusCode, 200);
});
});
});
});

といった具合に記述することができます。

ほぼコードを読んだままなのですが、 lt.describe.whenCalledRemotely の記述で指定のエントリポイント下へ指定のメソッド及びパラメータでアクセスがあった場合のテストを書くことができます。

また、アクセスするユーザにロールや状態をもたせた状態のテストも同様に記述ができ、例えばログインを行ったユーザによるAPIへのアクセスを想定したテストは

  var user = {
id: 2,
email: "tester2@remp.jp",
username: "tester2",
password: "tester2remp"
};
// (snip)
describe('ユーザによるプレイリスト操作', function() {
lt.describe.whenCalledRemotely('GET', '/api/users/2/playlists', function() {
it('未ログインユーザはユーザのプレイリストは取得できない', function() {
assert.equal(this.res.statusCode, 401);
});
});
lt.describe.whenCalledByUser(user, 'GET', '/api/users/2/playlists', function() {
it('ログインしたユーザ自身のプレイリストは取得できる', function() {
assert.equal(this.res.statusCode, 200);
});
});
});

の様に lt.describe.whenCalledByUser という記述で実現することができます。他にも各種ロールを割り当てた上での記述が行える様になっています。

こういった形でテストを用意した上で npm test で実行してみると…

といった具合にmochaによるテストが実行できています。

loopbackで作成したAPIであれば、loopback-testingを用いることでアクセスするユーザの各種ロールを想定したテストが記述できて便利ですというお話でした。

まとめ

  • loopbackアプリケーションでのテストを行うために loopback-testing を導入しました
  • npm test でテストを実行できる様にpackage.jsonの一部を修正しました
  • whenCalledRemotely, whenCalledByUser 等 loopback-testingを利用することで用意に各種ロールを想定したテストが書けました

あわせて読みたい

*1:loopbackのscafold(slcコマンド)で作成された場合は仮のpretestの定義が書いてあります

strong-pmを稼働させてloopbackアプリをデプロイする

http://docs.strongloop.com/display/SLC/Setting+up+a+production+host を参照してloopbackで作ったアプリケーションのデプロイを行ってみました。

いままでREMPチームで作っているプロダクトはminaを利用して、PadrinoやRendrアプリをデプロイしていましたが
loopbackにはデプロイ機構やデプロイしたアプリケーションのプロセスの管理まで用意されているので今回はこの手順で進めてみました。

loopbackアプリのデプロイ及び、アプリケーションのプロセス管理をざっくり整理すると以下の様な図になります。

strong-pm – Process Managerというloopbackを開発しているStrongloopが公開しているNode.jsアプリケーション向けの
プロセスマネージャーをプロダクションのホストに導入することでデプロイの制御や複数アプリケーションを立ち上げた際のプロセス管理まで一元的に行うことができます。

ドキュメントを読みながら色々試行錯誤しながら進めたので、その際の手順をメモしておきます。

手順

アプリケーションを運用するProductionなホストにstrong-pmを導入します。

まずはプロダクション用のサーバ上でnpm install

[production]$ sudo npm install -g strong-pm
(snip)

続いてサービスとして起動できる様にするために以下のコマンドを実行します。

[production]$ sudo sl-pm-install
Service strong-pm installed (/etc/init/strong-pm.conf)

で、サービスを起動します。

[production]$ sudo /sbin/initctl start strong-pm
strong-pm start/running, process 5796

これでproductionを運用するホストにloopbackで作成したアプリケーションがデプロイできる様な状態になりました。

実際にこのproductionのホストに向けてデプロイしてみます。

ビルドサーバでまずビルドします。gitにdeployブランチが作成されます。

[build]$ slc build
Running `git log -1 --pretty=format:"%t" HEAD`
=> 670faf6
Running `git commit-tree -p "refs/heads/deploy" -p "refs/heads/master" -m "Commit tree of
'refs/heads/master' onto 'deploy'" 670faf6`
=> f32106f847861e2f4e008064fa25445cc02ad5f8
Running `git update-ref "refs/heads/deploy" f32106f847861e2f4e008064fa25445cc02ad5f8`
Merged source tree of `refs/heads/master` onto `deploy`
Running `npm install --ignore-scripts`
Running `npm prune --production`
Running `git add --force --all .`
Running `git write-tree`
=> f38b2795a54b584d091dff0d8a61fa7b518fc4b4
Running `git commit-tree -p "refs/heads/deploy" -m "Commit build products" f38b2795a54b584
d091dff0d8a61fa7b518fc4b4`
Running `git update-ref "refs/heads/deploy" e03a0ec552504f3db1744e8add99e03f0a296f67`
Committed build products onto `deploy`

引き続きデプロイ。ビルド時に作成したdeployブランチがプロダクションサーバーにデプロイされます。

[build]$ slc deploy http://10.0.3.xxx:8701
Counting objects: 24, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (24/24), done.
Writing objects: 100% (24/24), 2.81 KiB | 0 bytes/s, done.
Total 24 (delta 12), reused 0 (delta 0)
To http://10.0.3.xxx:8701/api/services/3/deploy/default
acc8711..e03a0ec  deploy -> deploy
Deployed `deploy` as `remp-api` to `http://10.0.3.xxx:8701`

このメッセージでデプロイ完了となります。
実際にデプロイされている先は、productionホストの /var/lib/strong-pm/svc にあって構造は以下の様になります。

[production]$ pwd
/var/lib/strong-pm/svc
[production]$ tree -L 3
.
└── 3
├── repo
│   └── 3
└── work
├── current -> e03a0ec552504f3db1744e8add99e03f0a296f67.1432980055653
└── e03a0ec552504f3db1744e8add99e03f0a296f67.1432980055653

実際に稼働しているかはslcコマンドで確認できます。プロダクションサーバに対して以下のコマンドを実行することで、
稼働させているアプリケーションに設定している環境変数クラスタのサイズ、開けているポート番号等を確認できます。

[build]$ slc ctl -C http://10.0.3.xxx:8701
Service ID: 3
Service Name: remp-api
Environment variables:
Name                Value
NODE_ENV            production
REMP_MONGO_DB_PW    *************
REMP_MONGO_DB_HOST  10.0.3.***
REMP_MONGO_DB_USER  *******
Instances:
Version  Agent version  Cluster size
4.1.1       1.5.1            2
Processes:
ID      PID   WID  Listening Ports  Tracking objects?  CPU profiling?
3.1.22350  22350   0
3.1.22352  22352   1     0.0.0.0:3003
3.1.22354  22354   2     0.0.0.0:3003

これでアプリケーションを稼働させることができる様になりました。今回は3番目に稼働させたアプリケーションなのでService IDとして 3 が振られているので、
該当のアプリケーションは3003番が振られます。*1

この段階でnginx等のリバースプロキシから該当のポートにプロキシしてやることでアプリケーションを公開できる様になります。

メモ

  • strong-pm自体のログは /var/log/upstart/strong-pm.log に保管されます

*1:strong-pmではService ID + 3000がデフォルトのポート番号

loopbackで作成されたAPIのアクセスコントロールを設定する

の様なモデルのリレーションを定義し、APIも自動的に作成され /rempusers/:id/libraries といった形でアクセスできる様に
なったのですが、ログインして得られるアクセストークンを付与しても現状だと401応答が帰って来ます。

$ http http://localhost:5000/api/RempUsers/55486573fecad3d7215ead80/libraries?access_token=*****
HTTP/1.1 401 Unauthorized
Access-Control-Allow-Credentials: true
Connection: keep-alive
Content-Encoding: gzip
Content-Type: application/json; charset=utf-8
Date: Sun, 10 May 2015 01:52:10 GMT
ETag: W/"DNoQruYc4EoovuVyGTv+hg=="
Transfer-Encoding: chunked
Vary: Origin, Accept-Encoding
X-Powered-By: Express
{
"error": {
"code": "AUTHORIZATION_REQUIRED",
"message": "Authorization Required",
"name": "Error",
"status": 401,
"statusCode": 401
}
}

これはloopbackで既定されているUserモデルを継承したモデルにはユーザ自身の情報のみが制御できる様なアクセスコントロールが設定されているため、
そのモデルとリレーションするモデルをAPI経由で呼びだそうとすると制限がかけられています。*1

この問題を解消するためのアクセスコントロールを設定します。

loopbackでアクセスコントロールを設定するためのscafoldは slc loopback:acl というコマンドで実現できます。

今回対象となるAPIのエントリポイントはモデルのメソッド的にはgetに対応するためmethod nameとしては __get__libraries を指定します。

$ slc loopback:acl
? Select the model to apply the ACL entry to: RempUser
? Select the ACL scope: A single method
? Enter the method name: __get__libraries
? Select the role: The user owning the object
? Select the permission to apply: Explicitly grant access

具体的にモデル間のリレーションを組んだ時にどの様なメソッドが自動生成したモデルに備わっているかはドキュメントを参照するとわかりやすいです。

アクセスコントロールを設定した後に再度アクセスしてみると、

$ http http://localhost:5000/api/RempUsers/55486573fecad3d7215ead80/libraries?access_token=****
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Connection: keep-alive
Content-Length: 329
Content-Type: application/json; charset=utf-8
Date: Sun, 10 May 2015 07:27:01 GMT
ETag: W/"149-11634253"
Vary: Origin, Accept-Encoding
X-Powered-By: Express
[]

今度は問題なく取得できています。

なんとなくloopbackの世界が少しだけ見えてきました。

参照

Accessing related models – LoopBack – Documentation

*1:表現が難しい….

loopbackでモデルのrelationを定義する

loopbackでモデルを作成するときslcコマンドで作成できるのだけど、
モデル間のリレーションを同様にscafoldするやり方のメモです。

ここでは1対多となるモデルのリレーションの定義をslcコマンドで行ってみます。
例としてRempUserというユーザが複数のプレイリストを持つケースを想定します。

ユーザのモデルは既に作成されているものとして、プレイリストを管理するモデルを復習がてら作成します。

$ slc loopback:model
? Enter the model name: Playlist
? Select the data-source to attach Playlist to: db (mongodb)
? Select model's base class: PersistedModel
? Expose Playlist via the REST API? Yes
? Custom plural form (used to build REST URL):
Let's add some Playlist properties now.
Enter an empty property name when done.
? Property name: title
invoke   loopback:property
? Property type: string
? Required? Yes
Let's add another Playlist property.
Enter an empty property name when done.
? Property name: published
invoke   loopback:property
? Property type: boolean
? Required? Yes
Let's add another Playlist property.
Enter an empty property name when done.
? Property name:

次に先ほど作成したプレイリストのモデルとユーザモデルの間のリレーションを定義します。

これもscafoldできて slc loopback:relation というコマンドで実現できます。

$ slc loopback:relation
? Select the model to create the relationship from: RempUser
? Relation type: has many
? Choose a model to create a relationship with: playlist
? Enter the property name for the relation: playlists
? Optionally enter a custom foreign key:
? Require a through model? No
$ slc loopback:relation
? Select the model to create the relationship from: playlist
? Relation type: belongs to
? Choose a model to create a relationship with: RempUser
? Enter the property name for the relation: RempUser
? Optionally enter a custom foreign key:

このコマンドで具体的に行われるのは common/models/*.json で保管されている各モデルの定義が記載されているJSONrelations という
要素に定義が追加されます。

実際に該当のJSONファイルを見てみると、例えばユーザモデルであれば、

{
"name": "RempUser",
"base": "User",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {},
"validations": [],
"relations": {
"playlists": {
"type": "hasMany",
"model": "Playlist",
"foreignKey": ""
}
},
"acls": [],
"methods": []
}

といった具合にプレイリストモデルとのリレーションの定義が追加されていることがわかります。

また、loopbackの特徴であるモデル定義がそのままAPIとして反映されているので、loopbackのAPI Explorerを介して該当モデルの
エントリポイントを確認してみると、

といった形でCRUDできるAPIが作られています。便利。

ただ、今回の例の場合、新しく作成されている /RempUsers/:id/playlists といったエントリポイントにアクセスをしても HTTP/1.1 401 Unauthorized となってしまいます。

これは、loopbackで設けられているAPIに対するアクセスコントロールによるものなのですが、この辺りを解決するやり方はまた次回。

参考

StrongLoop | Defining and Mapping Data Relations with LoopBack Connected Models

loopbackでAPIにアクセスした際のACLの適用状況を確認する

loopbackで作成したAPIには、ユーザのロールに応じてアクセスコントロールを指定することができます。

Controlling data access – LoopBack – Documentation

例えば、

  • adminユーザのみアクセス可
  • このロールのユーザのみアクセス可
  • 認証済のユーザであればアクセス可

といった様なアクセスコントロールをslcコマンドでscaffoldすることができます。(やっていることはmodelの定義をしているJSONにアクセス権限の設定を付与している)

この設定を簡単にチェックするためにアプリケーションログとして標準出力するにはloopbackアプリケーション指定時に以下の環境変数を指定することで実現できます。

$ DEBUG=loopback:security:acl slc run

といった形でloopbackアプリを起動するとloobackで作成したAPIにアクセスした際にそのAPIへ適用されているアクセスコントロールの適用状況を確認することができます。

例えば RempUser というモデルがあり、/api/RempUsers/{id} というエントリポイントに対してユーザ情報を取得するためGETリクエストを発行した場合、発行と同時にアクセスコントロールの適用状況がログとして標準出力に出力されます。

$ http http://localhost:5000/api/RempUsers/1?access_token=pIIxAUP6qn2OtXntEGWO6KxAU71qDSS14A1qZamwTTk2xZEOppdtUVRM3agrdoQ8

といった形でリクエストを発行すると、以下の様なログが出力されます。

loopback:security:acl The following ACLs were searched:
loopback:security:acl ---ACL---
loopback:security:acl model RempUser
loopback:security:acl property findById
loopback:security:acl principalType ROLE
loopback:security:acl principalId $owner
loopback:security:acl accessType *
loopback:security:acl permission ALLOW
loopback:security:acl with score: 8016
loopback:security:acl ---ACL---
loopback:security:acl model RempUser
loopback:security:acl property *
loopback:security:acl principalType ROLE
loopback:security:acl principalId $everyone
loopback:security:acl accessType *
loopback:security:acl permission DENY
loopback:security:acl with score: 7495
loopback:security:acl ---Resolved---

この出力を確認することでloopbackが該当のエントリポイントに対してどういった判断が行われてアクセス可/不可の判定を行ったか確認することができます。

loopbackでMongoDBを利用する

loopbackでアプリケーションを作成した直後はデータストレージにmemoryが設定されているため、そのままだとアプリケーションが再起動するたびにmodelの内容が初期化されてしまうため、保管できる様にMongoDB*1を利用できる様にします。

作成したアプリケーションルートでnpmコマンドを叩きます。

$ npm install loopback-connector-mongodb --save

www.npmjs.com

MongoDBに接続するためのコネクタがpackage.jsonに追加されて必要なnpmモジュールがインストールされたのを確認した後、server/datasource.json にある
データベースへの接続設定を更新します。

{
"db": {
"name": "db",
"connector": "mongodb",
"host": "127.0.0.1",
"database": "hogehoge_development",
"username": "",
"password": "",
"port": 27017
}
}

必要な設定はこれだけになります。

先日作成したユーザ登録のAPIを呼び出してみます。

☁  http POST http://0.0.0.0:3000/api/users password=hogehoge email=hideack@remp.jp
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Connection: keep-alive
Content-Length: 59
Content-Type: application/json; charset=utf-8
Date: Mon, 04 May 2015 06:30:46 GMT
ETag: W/"3b-84c0720e"
Vary: Origin, Accept-Encoding
X-Powered-By: Express
{
"email": "hideack@remp.jp",
"id": "554712159967a7b3efce0f0c"
}

MongoDBで永続化されているかを確認してみます。

素朴に mongo コマンドでMongoDBに接続して確認します。

> show collections
User
system.indexes
> db.User.find()
{ "_id" : ObjectId("554712159967a7b3efce0f0c"), "password" : "$2a$10$AzdK8q7nUIHxcoiW5y17Lusb4eag7VOjwQI.e5ukjJRWKMWXSaP6a", "email" : "hideack@remp.jp" }
>

loopbackで作成したアプリケーションで操作されたmodelの内容がMongoDBで永続化される様になりました。

併せて読みたい

過去のloopbackに関する記事

*1:REMPもSTOBOもストレージはMongoDBです

loopbackでアプリケーションを作成してユーザ登録APIを作るまで

StrongLoopが開発したloopbackは、Node.jsで作られたフレームワーク(所謂mBaaSのオープンソース実装)で以下の様な特徴があります。

  • RESTなAPIをモデルと対応させて素早く作れる (モデルを作るとCRUDするAPIが出来上がる)
  • モデルはコネクタを介して様々なDBで永続化できる
  • API自体は application/jsonAPIとして操作でき、各種クライアント向けのSDKを備えている

といった特徴があります。

利用実績としては、GoDaddyでpublic reseller APIに利用されている様です。

StrongLoop | GoDaddy Selects StrongLoop to Power Reseller API Platform

SPAなアプリケーションを2011年からREMPSTOBO
そしてRendrを利用してCastoを作ってきましたが、もしかしたらloopbackを使ってAPI開発をよりスムーズに
解決できるのではないか。という期待を抱きつつ試しに触ってみました。

ひとまずゴールとしては、

loopbackでアプリケーションを作成し、APIを介してユーザを作るところまで

とします。

準備

とにもかくにも strongloop コマンドをインストール。*1

$ npm install -g strongloop

これで slc コマンドが使える様になってscaffoldできます。

アプリケーション作成

実際にアプリケーションを作成してみます。(謎のキャラクターが表示されます)

☁  slc loopback remp-api
_-----_
|       |    .--------------------------.
|--(o)--|    |  Let's create a LoopBack |
`---------´   |       application!       |
( _´U`_ )    '--------------------------'
/___A___\
|  ~  |
__'.___.'__
´   `  |° ´ Y `
? What's the name of your application? remp-api
? Enter name of the directory to contain the project: remp-api
create remp-api/
info change the working directory to remp-api
Generating .yo-rc.json
I'm all done. Running npm install for you to install the required dependencies. If this fails, try running the command yourself.
create .editorconfig
create .jshintignore
create .jshintrc
create README.md
(snip)
Next steps:
Change directory to your app
$ cd remp-api
Create a model in your app
$ slc loopback:model
Optional: Enable StrongOps monitoring
$ slc strongops
Run the app
$ slc run .

ユーザーモデルを作成

続いてユーザーを管理するためのモデルを作成してみます。

loopbackではいくつかのbase classが用意されていてその中にUserというクラスが存在するので今回はこれをこのまま利用します。

基本的なユーザ管理をする場合であればbase classのままでよいのですが、何か新たにプロパティ足したい場合は追加をすることもできます。今回はスキップ。

☁  slc loopback:model
? Enter the model name: user
? Select the data-source to attach user to: db (memory)
? Select model's base class: User
? Expose user via the REST API? Yes
? Custom plural form (used to build REST URL):
Let's add some user properties now.
Enter an empty property name when done.
? Property name:
☁  

上の操作で以下の様な要素を持ったユーザ管理のモデルが作成できます。

要素
realm (string, optional)
username (string, optional)
credentials (object, optional)
challenges (object, optional)
email (string)
emailVerified (boolean, optional)
verificationToken (string, optional)
status (string, optional)
created (string, optional)
lastUpdated (string, optional)
id (number, optional)

APIを呼び出してみる

では、モデルを作成 = API作成 ができたので実際にHTTPクライアントからAPIを呼び出してみます。

loopbackアプリケーションを起動します。

☁  node .
Browse your REST API at http://0.0.0.0:3000/explorer
Web server listening at: http://0.0.0.0:3000/

おもむろにloopbackアプリケーションを呼び出します。

先ほど作成したUserモデルに対応するAPIのエントリポイントは /api/users に対応しますので、そこへメールアドレスとパスワードをPOSTします。

☁  http -v POST http://0.0.0.0:3000/api/users password=hogehoge email=hideack@remp.jp
POST /api/users HTTP/1.1
Accept: application/json
Accept-Encoding: gzip, deflate
Content-Length: 52
Content-Type: application/json; charset=utf-8
Host: 0.0.0.0:3000
User-Agent: HTTPie/0.8.0
{
"email": "hideack@remp.jp",
"password": "hogehoge"
}
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Connection: keep-alive
Content-Length: 34
Content-Type: application/json; charset=utf-8
Date: Sat, 02 May 2015 05:27:48 GMT
ETag: W/"22-3085cb67"
Vary: Origin, Accept-Encoding
X-Powered-By: Express
{
"email": "hideack@remp.jp",
"id": 2
}

ステータスコード200が帰って来てユーザを作成することができました。

ログインしてみる

では、先ほど作成したユーザでログインしてみます。ユーザ作成の際に利用したメールアドレスとパスワードを要素としてPOSTします。

ログイン用のエントリポイントは /api/users/login なので、ここにユーザ登録の際に設定したメールアドレスとパスワードを渡します。

☁  http -v POST http://0.0.0.0:3000/api/users/login email=hideack@remp.jp password=hogehoge
POST /api/users/login HTTP/1.1
Accept: application/json
Accept-Encoding: gzip, deflate
Content-Length: 52
Content-Type: application/json; charset=utf-8
Host: 0.0.0.0:3000
User-Agent: HTTPie/0.8.0
{
"email": "hideack@remp.jp",
"password": "hogehoge"
}
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Connection: keep-alive
Content-Length: 135
Content-Type: application/json; charset=utf-8
Date: Sat, 02 May 2015 05:30:16 GMT
ETag: W/"87-4fc8ab24"
Vary: Origin, Accept-Encoding
X-Powered-By: Express
{
"created": "2015-05-02T05:30:16.742Z",
"id": "r3YFqmceF1INk5FlstmZgO7p2y42TyLYPlzSchPIDIDBlAbuOHQurS5j5gOSH82h",
"ttl": 1209600,
"userId": 2
}

ステータスコード200が帰って来て、API操作用のトークンが戻ってきました。なお、パスワードが誤ると、

HTTP/1.1 401 Unauthorized
{
"error": {
"code": "LOGIN_FAILED",
"message": "login failed",
"name": "Error",
"stack": "Error: login fail...",
"status": 401,
"statusCode": 401
}
}

となって期待通りです。

ログイン時のトークンを利用してユーザ情報を取る

続けて先ほどログイン時に取得したトークンを利用してユーザの情報を取得してみます。

現状だとUserモデルしか存在しないのでできることが少ないですがログインしているユーザ自身の情報を取ってみます。

/api/users/2?access_token=(ログイン時に得たtoken) に対してGETします。

☁  ~  http http://0.0.0.0:3000/api/users/2\?access_token\=r3YFqmceF1INk5FlstmZgO7p2y42TyLYPlzSchPIDIDBlAbuOHQurS5j5gOSH82h
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Connection: keep-alive
Content-Length: 34
Content-Type: application/json; charset=utf-8
Date: Sat, 02 May 2015 05:45:01 GMT
ETag: W/"22-3085cb67"
Vary: Origin, Accept-Encoding
X-Powered-By: Express
{
"email": "hideack@remp.jp",
"id": 2
}

scafoldする際に特にプロパティを追加していないのでidとemailのみ返ります。

API Explorer

ここまでコマンドラインのHTTPクライアントでAPIを操作してみましたが、実はscafoldした時点でStrong Loop API ExplorerというWebアプリが同時に起動しています。

http://0.0.0.0:3000/explorer

へブラウザでアクセスすると以下の様な画面を閲覧できて実際にAPI操作も行うことができます。

まとめ

loopbackを利用して、アプリケーションの作成とユーザを管理するモデルの作成(それと同時にユーザ操作をするAPIの作成)を行ってみました。

気づかれたかもしれませんが、上までの操作だとモデルを保管する先のストレージを何も設定していない状態なのでアプリケーションの再起動で全て消えます…。

データベースの接続はまた次の機会に。

*1:内部的にYeomanを使っているのでYeomanを既に使っていたら npm install -g generator-loopback