PEANスタックでPassportを使って認証を実装する


PEANスタック(そういう言葉はないが、Postgresql+Express+Angular+Node.jsなのでPEAN)で、ユーザ認証をPassportを使ってやってみます。使うのは、Typescriptです。
実現した事の概要は以下の通りです。

* ユーザIDとパスワードでの認証は、passport-localを使います。
* REST API(ex /login)で認証を行いOKだったら、jsonwebtokenのsignでJWTを作成し返します。
* login以外のAPIは、JWT認証を行いOKだったら、APIを利用する事ができます。
* login以外のAPIは、Graphqlを使います。

# Passportを使うために必要なパッケージのインストール
Passportを使うために必要なパッケージをインストールします。
bcryptモジュールのインストールには、Python2.7が必要ですので、予めインストールしてください。
> npm install bcrypt jsonwebtoken passport passport-jwt --save

Typescriptで実装しますので、以下のものもインストールします。
> npm install @types/bcrypt @types/jsonwebtoken @types/passport @types/passport-jwt --save-dev

# 必要なnpmモジュールのimport
Expressのapp.tsで、必要なnpmモジュールをimportします。

```typescript:app.ts
import passport from 'passport';
import { Strategy as JWTStrategy, ExtractJwt } from 'passport-jwt';
import { Strategy as LocalStrategy } from 'passport-local';
import jwt from 'jsonwebtoken';
```

# passport-localを使った認証の実装
例えば、Eメールアドレスとパスワードが入力可能なログインフォームを作成し、submit(POST)した時の認証ロジックを実装します。
Eメールアドレスはemail、パスワードはpasswordとしてPOSTされます。
認証の結果は、result、message、tokenをJSONで返します。

```typescript:app.ts
// Passportの設定を行う(ユーザID、パスワード認証)
passport.use(new LocalStrategy({
  usernameField: 'email',
  passwordField: 'password',
  session: false
}, ( username, password, done ) => {
  try {
      login(username, password)
        .then((user: any) => {
          const lr: LoginResult = new LoginResult('success');
          if (user === undefined) {
            lr.result = 'failed';
            lr.message = 'Eメールアドレスまたはパスワードが正しくありません。';
          }
          else {
            // payloadを作成する
            const payload: any = {
                email: user.email
            };

            // JWT signするオプションを設定する
            const signopt: any = {
                algorithm: 'RS256',
                expiresIn: '24h',
                subject: user.email
            };

            // 秘密鍵を読み込む
            const privatekey: string = fs.readFileSync(__dirname + '/assets/keys/private.key', { encoding: 'utf8'});

            // JWTトークンを作成する
            lr.token = jwt.sign(payload, privatekey, signopt);
          }

          return done(undefined, lr);
        })
        .catch((err: any) => {
          return done(err, undefined);
        });
  }
  catch ( err ) {
      done(err);
  }
}));
```

# passport-jwtを使った認証
passport-jwtは、JWT認証に関する処理をほとんどやってくれる優れものです。
その実装は以下の通りです。

```typescript:app.ts
// 公開鍵を読み込む
const publickey = fs.readFileSync(__dirname + '/assets/keys/public.key', { encoding: 'utf8'});

// JWTのオプションを設定する
const opts: any = {
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
  secretOrKey: publickey,
  session: false,
  jsonwebtoken: {
    algorithm: 'RS256'
  }
};

// Passportの設定を行う(JWT認証)
passport.use(new JWTStrategy(opts, (jwtPayload: any, done: any) => {
  // JWTペイロードを確認する
  if (jwtPayload === undefined
      || jwtPayload.sub === undefined
      || jwtPayload.email === undefined
      || jwtPayload.role === undefined) {
      return done('Invalid JWT Payload', false);
  }
  else {
      return done(undefined, true);
  }
}));
```

# passportの初期化
passportの初期化を行います。passport認証の結果をsessionで管理する場合は、app.use(passport.session());が必要です。なお、これを呼び出す前に、express-sessionの設定が必要です。
この例ではセッションレスにしたいので、行いません。

```typescript:app.ts
// Passportを初期化する
app.use(passport.initialize());
// app.use(passport.session());
```
# login apiの設定
login APIの実装を行います。
/loginにPOSTされたら、passportの'local'を使って認証します。認証がうまく行くと、req.userにその結果が返ってきますので、それをJSONで返します。

```typescript:app.ts
app.post('/login',
  passport.authenticate('local', {session: false}),
  (req: any, res: any) => {
    const result = JSON.stringify(req.user);
    res.send(result);
  });
```

# その他のAPI認証の実装
その他のAPIは、Graphqlを使って実装します(波が来ているらしいので採用)

```typescript:app.ts
  // JWT認証を行う
  app.use('/graphql',
    passport.authenticate('jwt', {session: false}),
    (req: any, res: any) => {
      console.log(req);
      console.log(res);
    });
```

これで、login APIのみが公開されている状態になり、loginできないとその他のAPIにアクセスできないと言う事が実現できます。