技術記事に戻る

DB不要!Node.js CryptoモジュールとHttpOnly Cookieで実現する安全な簡易認証システム

Node.jsSecurityExpress

DB不要!Node.js CryptoモジュールとHttpOnly Cookieで実現する安全な簡易認証システム

ソースコード: https://github.com/hayate-hu6/local-media-viewer.git

はじめに

ローカルネットワークのアクセス制限だけでは、同じネットワーク利用者の同居人に見せたくない写真や動画まで見られてしまう可能性があります。そこで、認証機能の実装を検討しました。 プライベートな写真や動画を扱う以上、認証機能は欲しいところです。しかし、MySQLなどのデータベースを用意してユーザー登録機能を実装するのは大掛かりすぎます。そこで、サーバー起動時にランダムなパスワードを生成し、コンソールに表示する」という非常にシンプルなアプローチを採用しています。今回はその実装の詳細を解説します。

ランダムパスワードの生成

サーバーが起動するたびに、Node.jsの標準モジュールである crypto を使って安全なランダムパスワードを生成しています。

const crypto = require('crypto');

// Generate Session Password
const SESSION_PASSWORD = crypto.randomBytes(4).toString('hex'); // 8文字 (4バイト)

crypto.randomBytes(4) は暗号学的に安全な擬似乱数を4バイト生成します。これを .toString('hex') で16進数文字列に変換することで、a1b2c3d4 のような8文字の文字列が得られます。

このパスワードはサーバーのメモリ上にのみ存在し、ファイルには保存されません。サーバーを再起動するとパスワードが変わるため、万が一パスワードが漏れても、再起動すれば無効化できるというセキュリティ上のメリットもあります(いわゆるワンタイムパスワードに近い運用です)。

console.log('\n==================================================');
console.log(` [SECURE] Session Password: ${SESSION_PASSWORD}`);
console.log('==================================================\n');

起動時にこのようにコンソールに大きく表示されるので、ユーザーはこれをコピーしてログイン画面に入力します。 Local Media Viewerのログイン画面

ログインAPIの実装

クライアント(フロントエンド)から送られてきたパスワードを検証するAPIです。

app.post('/api/login', (req, res) => {
    const { password } = req.body;
    if (password === SESSION_PASSWORD) {
        // 認証成功: クッキーをセット
        res.cookie(SESSION_COOKIE_NAME, SESSION_PASSWORD, {
            httpOnly: true, 
            sameSite: 'strict',
            maxAge: 24 * 60 * 60 * 1000 // 1日
        });
        res.json({ success: true });
    } else {
        res.status(401).json({ error: 'Invalid password' });
    }
});

セキュリティの要: HttpOnly Cookie

認証に成功した場合、res.cookie でブラウザに認証情報を保存させます。ここで重要なオプションがいくつかあります。

  1. httpOnly: true: これが最も重要です。これを true にすると、ブラウザ上のJavaScriptからこのクッキーにアクセスできなくなりますdocument.cookie で見えなくなる)。これにより、万が一XSS(クロスサイトスクリプティング)脆弱性があっても、攻撃者にセッションID(この場合はパスワード)を盗み出されるリスクを大幅に低減できます。

  2. sameSite: 'strict': 他のサイトからのリクエストではこのクッキーを送信しないようにします。CSRF(クロスサイトリクエストフォージェリ)攻撃を防ぐための強力な設定です。

  3. maxAge: クッキーの有効期限です。ここでは24時間(1日)に設定しています。

本来であれば、パスワードそのものをクッキーに保存するのではなく、セッションIDを発行してサーバー側で管理するのがベストプラクティスですが、このアプリは「シングルユーザー・再起動でリセット」という前提があるため、実装の簡素化のためにパスワードをそのままトークンとして利用しています。ローカル利用においては許容範囲といえるでしょう。

認証ミドルウェア (requireAuth)

APIや画像を保護するためのミドルウェアです。

const SESSION_COOKIE_NAME = 'media_viewer_auth';

const requireAuth = (req, res, next) => {
    const authCookie = req.cookies[SESSION_COOKIE_NAME];

    if (authCookie === SESSION_PASSWORD) {
        next(); // 認証OK、次の処理へ
    } else {
        res.status(401).json({ error: 'Unauthorized' });
    }
};

ブラウザから送られてきたクッキーの値を取り出し、サーバーが記憶している SESSION_PASSWORD と一致するか確認します。 前述の httpOnly 設定により、ブラウザの通信機能が自動的にこのクッキーをヘッダーに付与して送ってくれるため、フロントエンド側で特別なトークン管理をする必要がありません。

まとめ

Local Media Viewerの認証システムは、複雑なDB構成を避けつつ、cryptoHttpOnly Cookie というWeb標準のセキュリティ機能を活用することで、必要十分な安全性を確保しています。

  • DB不要: サーバー再起動ごとの使い捨てパスワード
  • XSS対策: HttpOnly Cookie
  • CSRF対策: SameSite属性

これらの要素は、フルスタックな認証ライブラリを使わずとも、基本原理を理解していれば数行のコードで実装できる良い例です。

次回は、サーバーサイド編の締めくくりとして、ディレクトリ内のファイルを高速にスキャンし、フィルタリングする「ファイルシステム操作」のアルゴリズムについて解説します。

次回: Node.jsで数千ファイル規模のディレクトリツリーを高速に解析する再帰アルゴリズムの実装


同シリーズ記事