🔥

Postman / HTTP digest認証機能

に公開

今日はPostmanでAPIを呼び出す際にHTTP digest認証を行う手順を試していきます。

HTTP digest 認証とは

HTTP Digest 認証(Digest Access Authentication)は、HTTPプロトコルで定義された認証方式の1つで、ユーザー名とパスワードをハッシュ化して送信する仕組みです。Basic認証のようにパスワードを平文で送らないため、より安全とされています。

もともとはRFC 2617で定義されMD5アルゴリズムでハッシュ化されますが、RFC 7616にて新たにSHA2(256,512)ハッシュなどに対応しています。
またハッシュには「nonce(使い捨てトークン)」を加えることができ、リプレイ攻撃をある程度防ぐ機能も備わっています。

もともとはユーザ名とパスワードを平文で送らないように開発されています。平文HTTPの時代に最低限パスワードを守る目的で利用されていました。昨今では常時TLS化が進んでいることもあり、以下の様なBearerトークン認証の方が採用されるケースが多いかもしれませんが、まだ多くのAPIで使われている認証方式です。

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...

動作ステップ

1.クライアントによるアクセス
サーバは「認証が必要(401 Unauthorized)」というレスポンスとともに、WWW-Authenticate ヘッダを返します。
2.クライアントはハッシュを計算して再送
パスワードを直接送らず、username, realm, password, nonce, uri, method を組み合わせて MD5 ハッシュを作って再送します。
3.サーバ側で同じ計算をして照合
ハッシュが一致すれば認証成功となります。

つまり同じ認証情報をあらかじめサーバとクライアントが保持しておくことで、同じ計算を行い値が一致すればアクセスが可能になります。この時一つだけ変動するパラメータとして定義されているのがnonceです。

nonce = base64encode( timestamp : MD5(timestamp : realm : secret) )

一番最初のステップでWWW-Authenticateレスポンスの際に計算されたnonceが含まれ、クライアントからハッシュを送付する次のステップのクライアント側でのハッシュ計算時に含められます。ハッシュ値を受け取ったサーバは単純にハッシュ値の突合をおこなうだけではなく、nonceの時間も確認するため、ハッシュ値には有効期限が存在し、MITM攻撃などでハッシュ値が奪取されても、使いまわしを防ぐある程度の防御になるという仕組みです。

ただし、調べてみると有効期限は一般的な時刻同期型OTPより長く数分から数十分ですので近代化された認証機構(JWTやOAuth等)を使えるならそちらを使った方がよいかもしれません。

さっそくやってみる

1.HTTP Diget 認証用ウェブサーバの構築

Ubuntuで簡単にNode環境でウェブサーバを作成します。

mkdir digest-server
cd digest-server
npm init -y
npm install express http-auth

以下のファイルを作成します。

server.js
const express = require("express");
const crypto = require("crypto");

const app = express();
const PORT = 3000;

// ユーザー情報
const USERNAME = "user1";
const PASSWORD = "pass123";
const REALM = "example-area";

// nonceを簡易生成
function generateNonce() {
  return crypto.randomBytes(16).toString("hex");
}

// 401応答を返す関数
function sendAuthRequest(res) {
  const nonce = generateNonce();
  res.setHeader(
    "WWW-Authenticate",
    `Digest realm="${REALM}", qop="auth", nonce="${nonce}", opaque="0000000000000000"`
  );
  res.status(401).send("Authentication required");
}

// 認証チェックミドルウェア
app.use((req, res, next) => {
  const auth = req.headers["authorization"];
  if (!auth || !auth.startsWith("Digest ")) {
    return sendAuthRequest(res);
  }

  // Digestヘッダをパース
  const params = {};
  auth
    .substring(7)
    .split(", ")
    .forEach((part) => {
      const [key, value] = part.split("=");
      params[key] = value?.replace(/"/g, "");
    });

  // 必要パラメータがあるか確認
  if (!params.username || !params.nonce || !params.response) {
    return sendAuthRequest(res);
  }

  // Digest計算 (RFC 7616 MD5)
  const HA1 = crypto
    .createHash("md5")
    .update(`${USERNAME}:${REALM}:${PASSWORD}`)
    .digest("hex");
  const HA2 = crypto
    .createHash("md5")
    .update(`${req.method}:${params.uri}`)
    .digest("hex");
  const validResponse = crypto
    .createHash("md5")
    .update(
      `${HA1}:${params.nonce}:${params.nc}:${params.cnonce}:${params.qop}:${HA2}`
    )
    .digest("hex");

  if (params.username === USERNAME && params.response === validResponse) {
    req.user = USERNAME;
    next();
  } else {
    sendAuthRequest(res);
  }
});

// テスト用ルート
app.get("/", (req, res) => {
  res.send(`認証成功!あなたは ${req.user} です。`);
});

app.listen(PORT, "0.0.0.0", () => {
  console.log(`✅ Digest auth server running at http://0.0.0.0:${PORT}`);
});

2. 認証用ユーザーの作成

create-user.jsでファイルを作成します。

// create-user.js (CommonJS版)
const fs = require("fs");
const crypto = require("crypto");

const realm = "example-area";
const user = "user1";
const password = "pass123";
const file = "./users.htdigest";

const hash = crypto.createHash("md5").update(`${user}:${realm}:${password}`).digest("hex");
const line = `${user}:${realm}:${hash}\n`;

fs.writeFileSync(file, line);
console.log(`${file} にユーザー ${user} を追加しました。`);

node create-user.jsで実行すると以下のレスポンスが戻りユーザーが作成されます。

✅ ./users.htdigest にユーザー user1 を追加しました。

users.htdigestというファイルが以下の内容で作成されています。

user1:example-area:67b8a1c9e7d8db2f5b25a2c3948072ca

3. サーバの起動

node server.jsでサーバを起動します。

4. ブラウザでテスト

http://localhost:3000でアクセスすると認証ダイアログが出てきますので、ユーザ名とパスワードを入力すると無事画面が表示されます。

5. Postmanでテスト

新規のコレクションを作成します。
何も設定を行わずにGETアクセスを行うと規格通りHTTP 401 Unauthorizedが戻ります。

認可タブでダイジェスト認証を選択します。

ユーザー名とパスワードを指定すると無事認証されてアクセスが行われます。

コンソールを開くと2回やりとりされていることがわかります。

1回目のレスポンス
X-Powered-By: Express
WWW-Authenticate: Digest realm="example-area", qauth", nonce="11294231d68d058b81bfe258ed385f44", opaque="0000000000000000"
Content-Type: text/html; charset=utf-8
Content-Length: 23
ETag: W/"17-aCgQ3oG3a2vYjL7XV0dp8drclP4"
Date: Sun, 02 Nov 2025 05:37:45 GMT
Connection: keep-alive
Keep-Alive: timeout=5
2回目のリクエスト
User-Agent: PostmanRuntime/7.48.0
Accept: */*
Postman-Token: 1102d636-ca86-4011-991d-a263658b585c
Host: localhost:3000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Authorization: Digest username="user1", realm="example-area", nonce="11294231d68d058b81bfe258ed385f44", uri="/", algorithm="MD5", qop=auth, nc=00000001, cnonce="VRo7mtvX", response="99366de76a93a921539af98aa466165f", opaque="0000000000000000"

1回目のレスポンスでサーバ側よりWWW-Authenticateが戻ってきています。その値をもとに2回目のリクエストでトークンを生成して提出しています。

Discussion