年末休みにnodeでAPIサーバを勉強した復習 最終回〜part5 ログイン認証をPassportで

#今までの話
2018年の年末休みに、メンバーの教育、
nodeの勉強、Typescriptの勉強でAPIサーバの開発を行った

##Part1　設計編
https://qiita.com/YukiMiyatake/items/2a443d78308b51aa2344
仮想作るものをソーシャルゲームのAPIサーバにした
言語FWは流行りなどで色々と悩んだが、非同期を教えたかったのと、フロントするにもFaaSするにも
JavaScript、nodeは覚えておくべきなのでnode+Expressにした
DBはMongoでdocker-composeを使い、ソースはGitlabで行うことにした
DBやAPIなどとりあえず設計した。
マイクロサービス化もふまえ、テーブル（コレクション）は機能単位でこまかく分割した

##Part2 単純なAPI作成
https://qiita.com/YukiMiyatake/items/30b9a6b8735d8675b0e8
HelloWorld的なものを作る
Typescriptの文法覚えたり、Express、mongooseの設定など
Joliによるバリデーションが非常に便利だ
一度Joliは深く掘り下げたい

##Part3 Typescript化の説明
https://qiita.com/YukiMiyatake/items/43a56b6b4d804de0bd66
JavaScriptをTypescript化するには少しコツがあったけど
覚えてしまえばどうってことない

##Part4 async/awaitの説明
https://qiita.com/YukiMiyatake/items/3cdaff234ca2d49cbd9c
うちのメンバーには、とりあえずここだけ理解してもらえればいいや
ってぐらい大事
非同期プログラムを普通にかくとCallback Hellになるが、それを解決する方法は用意されている
コルーチン、promise/future等
nodeの場合は、async/awaitを使うと大抵の場合、同期とあまり変わらず便利にコードを書くことができる
ただしExpressは現状のバージョンではPromise/futureに対応していないので、例外処理などもふまえ
Expressでの非同期の書き方の注意を調べた

#今回
最低限のログインと実装まで行う事ができた
トークンの有効期限とワンタイムのランダムSaltを実装し、HTTPS通信にすれば大抵の用途には十分と思われる

##認証方針
https://qiita.com/YukiMiyatake/items/4c2162f85fe3c9c203a7
ここでまとめた通り、私の調査では、今回の案件では下記の認証方針で良さそうだ

通信はHTTPSで行なうため、TLSレイヤーで暗号化される

初回登録時はサーバからユーザIDと ランダムパスワードが送られるのでローカルに保存する
パスワードは元々ランダムなので特にハッシュ化は必要ない

まずログインを行なう。ユーザIDとパスワードをサーバに送り、JWTトークンを受け取る
トークンには有効時間とSaltが入っている。
サーバでは通信ごとにランダムなSalt値を生成しDBに保存する
クライアントは前回受け取ったトークンをHTTPヘッダに付与する
サーバでトークンのSaltを確認すれば、トークンは1回しか使えない

##一意なユーザIDの作成
色々な方法はあるが、管理やシャーディングなどを考え、連番をつけることにした
MongoにはAuto incrementフィールドは存在しないが、おそらくロックフリーで実装された安全なインクリメントがあるのでそれを使う

https://qiita.com/nwtgck/items/c3828aa4c7ca6b34edab

```typescript:counter.model.ts
export interface ICounterDocument extends mongoose.Document {
    id: String
    seq: Number
}
export const schema = new mongoose.Schema({
    id: {
        type: String,
        required: true
    },
    seq: {
        type: Number,
        required: true
    }
});


//export const Counter = mongoose.model("Counter", schema);
export const Counter = mongoose.model<ICounterDocument>("Counter", schema);

//require('crypto').randomBytes(256).toString('hex')
export async function generateUserId() {
    let doc  = await Counter.findOneAndUpdate({id: "user_id"}, { $inc: { seq: 1 } }).exec();
    if(doc==null) throw("can not generate user_id");
    let counter = doc as ICounterDocument;
    return (counter.seq);
}
```
findAndModifyを使いたいがmongooseにはないため
mongooseではfindOneAndUpdateを使う

https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndUpdate/

generateUserId（）で、連番の一意のuser_idが生成できる

##登録
登録は簡単だ
先程の generateUserId()を使いユーザIDを作り
今回はTODOだが、ランダムパスワードを作り
DBを作り、ユーザIDとパスワードを返せばいい

実際には、device_idを確認してすでに登録しているユーザーか？を確認する必要がある

```typescript
export async function register(req: Request, res: Response) {
    // TODO: RequestのValidate
    // TODO: deviceチェックを行なう（deviceid & OS_Typeはユニーク）
//    let result = await Account.find({ device_id: req.params.device_id }, {_id:0}).exec();

    // 一意なuser_idを求める
    let user_id = Counter.generateUserId();
    let account = new Account(req.body);
    account.set("user_id", user_id);
    // TODO: Ramdom Password作る
    account.set("password", "pass");
//    req.body["hashed_password"] = bcrypt.hashSync(req.body.password, 10);

//    delete req.body.password;
//    let val = await Joi.validate(req.body, AccountSchema.insertSchema, { abortEarly: true });
    // TODO: statusテーブル等 必要テーブルの作成＆エラー処理
    req.body.account = account;
    return await account.save();
}
```


##Passport-jwt
認証にはPassportを使うと便利そうだった
まだ細かくPassportを追っかけてないので ふわっと

###LocalStrategy
LocalStrategyを作成する
これはパスワード認証を行なうものだと思われる

usernameField、passwordField に、IDとPasswordに実際に使うフィールド名を設定する
APIサーバなので sessionはfalse

promiseに対応しているかわからないので、とりあえずCallbackで書く

ユーザIDをテーブルから引き、Passwordの一致をみる

doneで結果を返す。
done(null, false)  で認証エラー
done(null, hoge)  で認証OKでnext()を呼ぶ。hogeの中身はRequestに追加される
done(error) で一般的なエラーを返せば良いらしい

```typescript
const localLogin  = new LocalStrategy({
    usernameField: 'user_id',
    passwordField: 'password',
    session: false,
}, async (user_id: string, password: string, done: any) => {
    console.log("lcoalLogin");
    let user = await Account.findOne({ "user_id": user_id });
    console.log("user:" + user);
//    if (!user || !bcrypt.compareSync(password, user.get("password"))) {
    if (!user || (password != user.get("password" ))) {
        return done(null, false, { error: 'Your login details could not be verified. Please try again.' });
    }
    user = user.toObject();
    //delete user.get("password");
    console.log("OK");

    done(null, user);
});
```

###認証
実際に、設定したlocalStrategyを実行するには、下記のようにルータに置くとよさそう

```typescript
router.route('/login')
    .get(passport.authenticate('local', { session: false }), login);
```

routerにて、passport.authenticateをまず呼ぶ
localを指定しているので localStrategyが呼ばれ
その認証がOKだった場合に next(この場合はlogin)が呼ばれる

###login
認証OKだったときに呼ばれる

```typescript
export async function login(req: Request, res: Response) {
    let token = generateToken({payloadデータ});
    return await({"token":token});
}

function generateToken(payload: any) {
    return jwt.sign(payload, config.jwtSecret);
}
```

jwt.signにて、JWT(JWS)のトークンを作成することが出来る
実際はPayloadにユーザID、有効期限とSalt等を埋め込む必要がある

JWTトークンを返却する

###トークン確認
認証必要なAPIはトークンの確認（認可）をする必要がある
HTTPのAuthorization bearerにトークンを置くのが、APIサーバでは最も良いはずだ

```typescript
const jwtLogin = new JwtStrategy({
    jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    secretOrKey: config.jwtSecret
    // TODO: any禁止
}, async (payload: any, done: any) => {

//    let user = await User.findOne({"user_id": payload.user_id});
    let user = await Account.findOne({"user_id": payload.user_id});
    if (!user) {
        return done(null, false);
    }
    user = user.toObject();
//    delete user.password;
    done(null, user);
});

passport.use(jwtLogin);

```

ExtractJwt.fromAuthHeaderAsBearerToken()でHTTPヘッダの Authorization bearerからトークンを読み込む
それをBase64デコードし、署名をSecretKeyで確認すれば改ざんを防げる

JWTトークンよりユーザIDを取得している
本来なら、ヘッダの暗号方式、トークンの有効期限やSaltのチェックを行うが今は省略いている

Accountコレクションよりユーザを引いてくる事で、ユーザIDの妥当性とみなしている

データに問題がない場合は doneでnext()を呼ぶ

###認証API
認証が必要なAPIには上記のトークン確認を行う

```typescript
router.route('/status')
    .get(passport.authenticate('jwt', { session: false }), status_read)

```

このように、APIの前に passport.authenticateを呼ぶ
今回は ストラテジにjwtを指定したので、先程のJWT認証が呼ばれる
認証OKのときのみ、APIが呼ばれる