【Backend入門】Express.js+Typescriptでのウェブサーバ(jwt, passport)

TypeScript Expressjs

概要

背景やモジュールなどに関しての説明は省いて実装の課程を記述してみたいと思います。

各種バージョン

  • Nodejs 18.20.0
  • Expressjs 4.19.1
  • Typescript 5.4.3
  • ts-node 10.9.2
  • passport 0.7.0
  • passport-jwt 4.0.1
  • passport-local 1.0.0

導入

jwt認証モジュールとTypescript志向ようのモジュールをインストールします。

$ npm install passport passport-jwt passport-local
$ npm install --save-dev @types/passport @types/passport-local @types/passport-jwt

認証storategyを用意します。認証成功ユーザーは「hoge」で固定します。次回などえ拡張してDBなどと連携してみたいと思います。

$ cat strategy.ts
...
passport.use(
  new LocalStrategy(
    {
      usernameField: 'username',
      passwordField: 'password',
    },
    (username: string, password: string, done: any) => {
      if (username === 'hoge' && password === 'fuga') {
        return done(null, username);
      } else {
        return done(null, false, {
          message: 'username or password is unknown or mismatch.',
        });
      }
    }
  )
);
...

認証トークンを検証のためのstrategyを追加します。

$ cat strategy.ts
...
passport.use(
  new LocalStrategy(
...
const options: StrategyOptions = {
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
  secretOrKey: process.env.JWT_SECRET || '',
};

passport.use(
  new JWTStrategy(
    options,
    (jwt_payload: any, done: any) => {
      done(null, jwt_payload);
    }
  )
)
...

認証トークン取得や認証トークンでのアクセス

プロセスでstrategyを利用

$ cat index.ts
...
# token, jwt関連処理をstrategy.tsファイルに実装してあるとする
import passport from './strategy';
...
# strategy.tsで設定した処理を初期化
app.use(passport.initialize());

app.use('/signup', signRouter); # <- こちらのendpointで認証用トークンを取得
app.use('/users', userRouter);

# 以下のendpointは認証キーを必要とする。
app.use('/photos', passport.authenticate('jwt', {session: false}),  photoRouter);
...

全体

index.ts

以下の機能について実装ずみのコードに認証機能の追加

$ cat index.ts
import cors, { CorsOptions } from 'cors';
import express from 'express';
import dotenv from 'dotenv';
import { userRouter } from './user_router';
import { photoRouter } from './photo_router';
import log4js from 'log4js';
import passport from './strategy';
import { signRouter } from './login_router';

dotenv.config();

const app = express();
const port = process.env.PORT || 3000;

const logger = log4js.configure('src/config/log.config.json').getLogger();

const corsOptions: CorsOptions = {
  origin: ['http://localhost:3000'],
  methods: [ 'GET', 'POST', 'PUT', 'DELETE' ],
  allowedHeaders: [ 'Content-Type', 'X-Requested-With',  'Authorization' ],
}

app.use(cors(corsOptions));

// ミドルウェア設定
app.use(express.json());

app.use(passport.initialize());

app.use('/signup', signRouter);
app.use('/users', userRouter);
app.use('/photos', passport.authenticate('jwt', {session: false}),  photoRouter);

app.listen(port, () => {
  console.log(`Server is running on port: ${port}`);
  logger.info(`Server is running on port: ${port}`);
});

strategy.ts

import dotenv from 'dotenv';
import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';
import { Strategy as JWTStrategy, ExtractJwt, StrategyOptions } from 'passport-jwt';

dotenv.config()

passport.use(
  new LocalStrategy(
    {
      usernameField: 'username',
      passwordField: 'password',
    },
    (username: string, password: string, done: any) => {
      if (username === 'hoge' && password === 'fuga') {
        return done(null, username);
      } else {
        return done(null, false, {
          message: 'username or password is unknown or mismatch.',
        });
      }
    }
  )
);

const options: StrategyOptions = {
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
  secretOrKey: process.env.JWT_SECRET || '',
};

passport.use(
  new JWTStrategy(
    options,
    (jwt_payload: any, done: any) => {
      done(null, jwt_payload);
    }
  )
)

export default passport;

photo_router.ts

認証に成功したリクエストのみ応答します。

import express from 'express';

const photos = [
  { id: 0, filename: 'http://path.to/images/image-01.jpg', created_at: 2021 },
  { id: 1, filename: 'http://path.to/images/image-02.jpg', created_at: 2022 },
  { id: 2, filename: 'http://path.to/images/image-02.jpg', created_at: 2023 }
];

export const photoRouter = express.Router();

// ルーティング
photoRouter.get('/', (req, res) => {
  res.json(photos);
});

login_router.ts

import express from 'express';
import passport from 'passport';
import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';

dotenv.config()

export const signRouter = express.Router();

signRouter.get('/login',(req, res) => {

  res.send('login. get');
})

// ルーティング
signRouter.post(
  '/login', 
  passport.authenticate('local', {session: false}),
  (req, res, next) => {
    const user = req.user;
    const payload = { user: req.user };
    const token = jwt.sign(payload, process.env.JWT_SECRET || '', {
      expiresIn: '1m',
    });
    res.json({ user, token })
  }
);

実行してみる

プロセス起動

$ npx ts-node src/index.ts
Server is running on port: 3000
[2024-04-03T18:27:09.437] [INFO] default - Server is running on port: 3000

postmanなどを使用してのテストも可能ですがvscodeの拡張機能を利用してのテストしてみたいと思います。

vscodeのREST Clientをインストールしてテストします。

テストのためのファイルを作成します。

# ログインしてトークンを取得します。
POST http://localhost:3000/signup/login
Content-Type: application/json

{
  "username": "hoge",
  "password": "fuga"
}
###
### 取得したトーケンを利用してAPIを呼び出す。

GET http://localhost:3000/photos
Authorization: Bearer {取得したトークン}

vscodeで上記の内容を持つfilename.httpを作成しますとPOST, GETの上に「Send Request」が現れます。

結果

POSTの上の「Send Request」を押下したら

HTTP/1.1 200 OK
X-Powered-By: Express
Vary: Origin
Content-Type: application/json; charset=utf-8
Content-Length: 173
ETag: W/"ad-88gzLj/+gTudQaPUqBPQLUlRU5c"
Date: ******
Connection: close

{
  "user": "hoge",
  "token": トークン
}

取得したトークンを利用してGETの上の「Send Request」を押下したら

HTTP/1.1 200 OK
X-Powered-By: Express
Vary: Origin
Content-Type: application/json; charset=utf-8
Content-Length: 226
ETag: W/"e2-fa3+HldGaxJMTLyoBI69g7ICRv0"
Date: ******
Connection: close

[
  {
    "id": 0,
    "filename": "http://path.to/images/image-01.jpg",
    "created_at": 2021
  },
  {
    "id": 1,
    "filename": "http://path.to/images/image-02.jpg",
    "created_at": 2022
  },
  {
    "id": 2,
    "filename": "http://path.to/images/image-02.jpg",
    "created_at": 2023
  }
]
タイトルとURLをコピーしました