ユーザー認証(ID,パスワードでログイン)の学習

WEBアプリを公開したら、世界中のインターネット利用者が閲覧できる状態になっています。
自分だけのデータを登録したら、IDとパスワードで自分のデータを保護したいですよね。
ここでは、3つのユーザー認証方式を学びます。

  • 対象: HTML / JavaScript / Node.js を学んでいる中学生〜高校生
  • サーバー環境: Ubuntu / nginx 1.24 / Node.js / MySQL

「ユーザー認証」とは?

アプリを利用できる人なのか確かめる仕組みです。
3つの認証方式から自分のアプリ条件に合う方式を選びます。

項目BASIC認証APIアプリ認証認証サービス
実装難易度簡単やや難しい難しいが、電話番号での認証やメール認証のしくみなどさまざまな用途が選べる
どこに設定するかNginxなどミドルウェアの設定ファイルNode.jsなどアプリのコードで実装FirebaseやCogniteなどの認証サービスを利用
IDパスワードの保存場所.htpasswdファイルにハッシュ化して保存.envファイルやOSの環境変数に保存外部の認証サービス側に委託
ログイン画面の制作WEBブラウザが自動表示ログイン画面は、アプリで制作する認証サービス側が提供
用途(本番用/開発用)本番には向かず開発用途本番サービス向き本番サービス向き

BASIC認証とは

ブラウザが自動で表示する 「ID と パスワードを入力してください」 という小さなポップアップです。
nginx がリクエストを受け取ったとき、パスワードファイルと照合して OK なら通してくれます。

いいところ

  • 設定が一番簡単(設定ファイルに記述するだけ)
  • コーディングが不要

イマイチなところ

  • ログイン画面が古い(ブラウザ標準で、アプリ画面に組み込めない)
  • 利用者毎の権限をコントロールできない(読み込み専用や読み書き可などわけられない)
  • 「新規ユーザー追加」や「パスワードリセット」などのフローをアプリに組み込めない
  • ログアウトのしくみがない

設定方法

Step 1. パスワードファイルを作る

Linuxコマンドで実行する

# apache2-utils をインストール(htpasswd コマンドが使えるようになる)
sudo apt install apache2-utils

# user01用のパスワードファイルを作る(初回は -c オプションをつける)
sudo htpasswd -c /etc/nginx/.htpasswd_user01 user01

# パスワードを聞かれるので入力する
# New password: ●●●●●●
# Re-type new password: ●●●●●●
# Adding password for user user01

Step 2. Nginxの設定ファイル

対象サイトのコンフィグを編集する
$ sudo nano /etc/nginx/sites-enabled/sub.example.com

# 現在の設定(認証なし)
location /user01/ {
    alias /home/user01/www/;
    try_files $uri $uri/ =404;
}

下記のように変更する

# BASIC認証あり
location /user01/ {
    alias /home/user01/www/;
    try_files $uri $uri/ =404;

    # ここから認証の設定
    auth_basic "user01のページ";                        # ポップアップに表示されるメッセージ
    auth_basic_user_file /etc/nginx/.htpasswd_user01;   # パスワードファイルのパス
}

Step 3. Nginx を再起動する

# 設定ファイルに間違いがないかチェック
sudo nginx -t

# 問題なければ再起動
sudo systemctl reload nginx

Step 4. 検査する

ブラウザで自分のWEBアプリにアクセスすると、ログイン画面がポップアップ表示します

パスワードを変更するとき

# -c なしで実行するとパスワードを上書きできる
sudo htpasswd /etc/nginx/.htpasswd_user01 user01

APIアプリ認証

制作したログイン画面でID・パスワードを入力してログインに成功した後、トークンというデータを発行してトークンを持っている人だけがバックエンドAPIを使える仕組み
トークン形式の例としてJWT(JSON Web Token)がよく使われる
トークンの有効期限を設定することで毎回ログインするか一定期間経過後に再ログインするか決めることができる

いいところ

  • アプリにあわせて自由にログイン画面を制作できる(ログアウトも可能)
  • ユーザー権限を細かく管理できる(読み込み専用、読み書き可、管理者など)

イマイチなところ

  • コーディングが複雑になる
  • セキュリティ知識が要求される(パスワードのハッシュ化、間違ってハードコーディングしないこと等)
  • 新規ユーザー登録とパスワードリセットの仕組みをアプリで作ること

認証フロー

【ログイン】

  ブラウザ(フロント)                サーバー(Node.js)
  ─────────────────                ──────────────────────
  ID・パスワードを入力
        │
        │  POST /api/login
        │  { username, password }
        ├─────────────────────────────────►│
        │                                      │ .env の値と照合
        │                                      │
        │                                      │ ❌ 不一致
        │◄─────────────────────────────────┤ 401 エラーを返す
        │  { error: "IDまたはPWが違います" }│
        │                                  │
        │                                  │ ✅ 一致
        │◄─────────────────────────────────┤ JWT トークンを発行して返す
        │  { token: "eyJhbG..." }          │
        │                                  │
  localStorage に
  トークンを保存

【ログイン後 ― 保護されたAPIへのアクセス】

  ブラウザ(フロント)                サーバー(Node.js)
  ─────────────────                ──────────────────────
  localStorage からトークンを取得
        │
        │  GET /api/mypage
        │  Authorization: Bearer eyJhbG...
        ├─────────────────────────────────►│
        │                                  │ requireAuth ミドルウェア
        │                                  │ トークンを検証
        │                                  │
        │                                  │ ❌ トークンなし・期限切れ
        │◄─────────────────────────────────┤ 401 / 403 エラーを返す
        │                                  │
        │                                  │ ✅ トークンOK
        │◄─────────────────────────────────┤ レスポンスを返す
        │  { message: "こんにちは..." }    │

【ログアウト】

  ブラウザ(フロント)                サーバー(Node.js)
  ─────────────────                ──────────────────────
  localStorage.removeItem('token')
        │
        │  (サーバーへの通信なし)
        │
  トークンが消えるので
  次のAPIアクセス時に 401 エラー

設定方法

複数のユーザーを登録し、ユーザー毎に権限を付与する場合はユーザーと権限などの属性をDBに保存する。
ここでは単一ユーザーの認証とする最小コーディングを例にする。

Step 1. モジュールをインストール

cd /home/user01

# package.json を初期化(まだなければ)
npm init -y

# 必要なライブラリをインストール
npm install express jsonwebtoken bcryptjs dotenv

パッケージの役割

  • express … Webサーバーのフレームワーク
  • jsonwebtoken … JWT(トークン)を扱う
  • bcryptjs … パスワードをハッシュ化する(パスワードを平文で.envに記載する場合は不要。本番ではハッシュ化したパスワードを保存すること)
  • dotenv … .envファイルから環境変数を読み取る

Step 2. .env ファイルを作る

秘密にすべきデータを保存する場所
$ sudo nano /home/user01/.env

下記例のような秘密データを保存

# .env ファイルの中身(ユーザーひとりだけの例)
APP_USERNAME=user01
APP_PASSWORD=pass1234     <本番ではハッシュ化したパスワードとする>

# JWT の署名に使う秘密の文字列(長くてランダムなものが安全)
JWT_SECRET=userro3unv34untvotnwotuonetrv5

Step 3. ログインAPI用コードを作る

パスワードは簡易版だが本番ではハッシュ化したパスワードにすること。
GitHubなどにPushする場合は .gitignoreに.envを追加してGit管理の対象外にすること。

// /home/user01/app/server.js

const express = require('express');
const jwt     = require('jsonwebtoken');
const dotenv  = require('dotenv');

dotenv.config(); // .env ファイルを読み込む

const app = express();
app.use(express.json());

// ===== ログイン API =====
// POST /api/login
// body: { username: "user01", password: "pass1234" }
app.post('/api/login', (req, res) => {
    const { username, password } = req.body;

    // .env の値と比較する(DBは使わない)
    if (username !== process.env.APP_USERNAME ||
        password !== process.env.APP_PASSWORD) {
        return res.status(401).json({ error: 'IDまたはパスワードが違います' });
    }

    // 認証OK → JWT トークンを発行(有効期限: 24時間)
    const token = jwt.sign(
        { username },               // トークンに入れる情報
        process.env.JWT_SECRET,     // 署名に使う秘密の文字列
        { expiresIn: '24h' }        // 有効期限
    );

    res.json({ token });
});

// ===== 認証チェック用ミドルウェア =====
function requireAuth(req, res, next) {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1]; // "Bearer xxxxx" の xxxxx 部分

    if (!token) {
        return res.status(401).json({ error: 'ログインが必要です' });
    }

    try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        req.user = decoded;
        next();
    } catch (err) {
        return res.status(403).json({ error: 'トークンが無効または期限切れです' });
    }
}

// ===== 認証が必要な API の例 =====
// GET /api/mypage
app.get('/api/mypage', requireAuth, (req, res) => {
    res.json({ message: `こんにちは、${req.user.username} さん!` });
});

// ===== サーバー起動 =====
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
    console.log(`サーバー起動: http://localhost:${PORT}`);
});

Step 4. フロントWEB側のコード例


<!-- /home/user01/www/login.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>ログイン</title>
    <style>
        body { font-family: sans-serif; max-width: 400px; margin: 50px auto; }
        input { display: block; width: 100%; padding: 8px; margin: 8px 0; }
        button { padding: 10px 20px; background: #007bff; color: white; border: none; cursor: pointer; }
        #result { margin-top: 20px; color: green; }
        #error  { margin-top: 20px; color: red; }
    </style>
</head>
<body>
    <h2>ログイン</h2>
    <input type="text"     id="username" placeholder="ユーザー名">
    <input type="password" id="password" placeholder="パスワード">
    <button onclick="login()">ログイン</button>

    <div id="result"></div>
    <div id="error"></div>

<script>
const API_BASE = 'https://sub.example.com/api3001';

async function login() {
    const username = document.getElementById('username').value;
    const password = document.getElementById('password').value;

    const res = await fetch(`${API_BASE}/api/login`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password }),
    });

    const data = await res.json();

    if (res.ok) {
        // トークンをブラウザの localStorage に保存
        localStorage.setItem('token', data.token);
        document.getElementById('result').textContent = `ログイン成功!ようこそ ${username} さん`;
        document.getElementById('error').textContent = '';
    } else {
        document.getElementById('error').textContent = `エラー ${data.error}`;
        document.getElementById('result').textContent = '';
    }
}

// 認証が必要なAPIを呼ぶ例
async function getMyPage() {
    const token = localStorage.getItem('token');
    if (!token) { alert('先にログインしてください'); return; }

    const res = await fetch(`${API_BASE}/api/mypage`, {
        headers: {
            'Authorization': `Bearer ${token}`,  // トークンをヘッダーに付けて送る
        },
    });

    const data = await res.json();
    console.log(data);
}

// ログアウト(トークンを削除するだけ)
function logout() {
    localStorage.removeItem('token');
    alert('ログアウトしました');
}
</script>
</body>
</html>

Step 5. バックエンドAPIアプリを再起動する

cd /home/user01/app
node server.js

# バックグラウンドで動かしたい場合(pm2を使う)
pm2 start server.js
pm2 save

外部の認証サービスを使う

ここでは Google Firebase 認証を例にします

いいところ

  • パスワード管理をGoogle側に任せる(セキュリティリスクを転嫁。アプリが原因のリスクを回避する)
  • 利用者の不安払しょく(パスワードをアプリに入力不要。Googleアカウント, 電話番号, メール/パスワード, Facebook, x(旧twitter), GitHub他 でログインできる)
  • 新規ユーザ追加のメール送付、メール記載のURLで承認、パスワードリセットもメールで自動実行できる
  • Firebase管理コンソールでユーザー一覧を閲覧できる
  • 一定量まで無料

イマイチなところ

  • Firebase 専門の学習コストがかかる
  • インターネット接続が必須

認証フロー

【メール/パスワード または Google でログイン】

  ブラウザ(フロント)        Firebase(Google)       サーバー(Node.js)
  ─────────────────        ──────────────────       ──────────────────────
  メール・PW を入力
  または「Googleでログイン」
  ボタンをクリック
        │
        │  認証リクエスト
        ├────────────────────►│
        │                     │ IDとPWを照合
        │                     │ またはGoogleアカウント確認
        │                     │
        │                     │ ❌ 失敗
        │◄────────────────────┤ エラーを返す
        │                     │
        │                     │ ✅ 成功
        │◄────────────────────┤ Firebase IDトークンを返す
        │  idToken: "eyJhbG..."│
        │                     │
  onAuthStateChanged が
  自動的に呼ばれてログイン状態を検知


【ログイン後 ― 自分のAPIサーバーへのアクセス】

  ブラウザ(フロント)        Firebase(Google)       サーバー(Node.js)
  ─────────────────        ──────────────────       ──────────────────────
  user.getIdToken() で
  Firebase トークンを取得
        │
        │  GET /api/mypage
        │  Authorization: Bearer eyJhbG...
        ├────────────────────────────────────────────►│
        │                                              │ requireFirebaseAuth
        │                                              │ ミドルウェア
        │                     │◄─────────────────────┤ admin.auth()
        │                     │  verifyIdToken()      │ .verifyIdToken(token)
        │                     │
        │                     │ ✅ トークン検証OK
        │                     ├─────────────────────►│ uid・email などを返す
        │                                              │
        │◄─────────────────────────────────────────────┤ レスポンスを返す
        │  { message: "こんにちは..." }                │

【ログアウト】

  ブラウザ(フロント)        Firebase(Google)       サーバー(Node.js)
  ─────────────────        ──────────────────       ──────────────────────
  signOut(auth) を呼ぶ
        │
        │  ログアウトリクエスト
        ├────────────────────►│
        │                     │ セッションを無効化
        │◄────────────────────┤
        │
  onAuthStateChanged が
  自動的に呼ばれてログアウト状態を検知
  (サーバー側トークンも自動的に無効になる)

設定方法

Step 1. Firebase アカウント開設~Firebase プロジェクト を作成

Firebase コンソールで、新規プロジェクトを追加

https://console.firebase.google.com/

Step 2. Firebase Authentication 有効化

[Authentication]メニュー > ログイン方式 > 新しいプロバイダを追加

Step 3. Webアプリを登録して設定コードを取得

Firebase プロジェクトのWebアプリ設定でAPIキーを取得する

// Firebase の設定情報(自分のプロジェクトのものに置き換えてください)
const firebaseConfig = {
    apiKey:            "AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    authDomain:        "user01-project.firebaseapp.com",
    projectId:         "user01-project",
    storageBucket:     "user01-project.appspot.com",
    messagingSenderId: "123456789012",
    appId:             "1:123456789012:web:abcdef1234567890",
};

Step 4. WEBフロントアプリのコード例

Googleアカウントとメールでのログインの場合:

<!-- /home/user01/www/login.html -->
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Firebase ログイン</title>
    <style>
        body { font-family: sans-serif; max-width: 500px; margin: 50px auto; }
        input { display: block; width: 100%; padding: 8px; margin: 8px 0; box-sizing: border-box; }
        button { padding: 10px 20px; margin: 5px; cursor: pointer; }
        .google-btn { background: #DB4437; color: white; border: none; }
        #status { margin-top: 20px; padding: 10px; background: #f0f0f0; }
    </style>
</head>
<body>
    <h2>Firebase ログイン</h2>

    <input type="email"    id="email"    placeholder="メールアドレス">
    <input type="password" id="password" placeholder="パスワード">
    <button onclick="registerWithEmail()">新規登録</button>
    <button onclick="loginWithEmail()">ログイン</button>
    <hr>
    <button class="google-btn" onclick="loginWithGoogle()">Googleでログイン</button>
    <button onclick="logout()">ログアウト</button>

    <div id="status">ログアウト中</div>

<!-- Firebase SDK の読み込み(CDN版) -->
<script type="module">
import { initializeApp }                          from 'https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js';
import { getAuth, createUserWithEmailAndPassword,
         signInWithEmailAndPassword,
         signInWithPopup, GoogleAuthProvider,
         signOut, onAuthStateChanged }             from 'https://www.gstatic.com/firebasejs/10.12.0/firebase-auth.js';

// ★ここを自分のプロジェクトの設定に書き換える★
const firebaseConfig = {
    apiKey:            "AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    authDomain:        "user01-project.firebaseapp.com",
    projectId:         "user01-project",
    storageBucket:     "user01-project.appspot.com",
    messagingSenderId: "123456789012",
    appId:             "1:123456789012:web:abcdef1234567890",
};

const app      = initializeApp(firebaseConfig);
const auth     = getAuth(app);
const provider = new GoogleAuthProvider();
const status   = document.getElementById('status');

// ===== ログイン状態を監視 =====
// ページを開いたとき・ログイン・ログアウト時に自動で呼ばれる
onAuthStateChanged(auth, (user) => {
    if (user) {
        status.innerHTML = `
            ログイン<br>
            名前: ${user.displayName || '未設定'}<br>
            メール: ${user.email}<br>
            UID: ${user.uid}
        `;
    } else {
        status.textContent = 'ログアウト中';
    }
});

// ===== メールアドレスで新規登録 =====
window.registerWithEmail = async () => {
    const email    = document.getElementById('email').value;
    const password = document.getElementById('password').value;
    try {
        await createUserWithEmailAndPassword(auth, email, password);
        alert('登録完了!自動でログインされます');
    } catch (err) {
        alert(`エラー: ${err.message}`);
    }
};

// ===== メールアドレスでログイン =====
window.loginWithEmail = async () => {
    const email    = document.getElementById('email').value;
    const password = document.getElementById('password').value;
    try {
        await signInWithEmailAndPassword(auth, email, password);
    } catch (err) {
        alert(`エラー: ${err.message}`);
    }
};

// ===== Google アカウントでログイン =====
window.loginWithGoogle = async () => {
    try {
        await signInWithPopup(auth, provider);
    } catch (err) {
        alert(`エラー: ${err.message}`);
    }
};

// ===== ログアウト =====
window.logout = async () => {
    await signOut(auth);
};
</script>
</body>
</html>

Step 5. Firebase トークンを使って自分のバックエンドAPIアプリを作る

Firebaseでログインに成功したユーザだけが使えるAPIをつくる

Appディレクトリにモジュールをインストール

npm install firebase-admin

Firebase コンソールで取得したJSONファイルをサーバーに保存する。

Firebase コンソール > プロジェクト設定 > サービスアカウント > 新しい秘密鍵を生成 > ダウンロード

保存先例 /home/user01/serviceAccountKey.json

バックエンドAPIアプリのコード例

// server.js に追加する認証ミドルウェア

const admin = require('firebase-admin');
const serviceAccount = require('./serviceAccountKey.json');

admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),
});

// Firebase トークンを検証するミドルウェア
async function requireFirebaseAuth(req, res, next) {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];

    if (!token) {
        return res.status(401).json({ error: 'ログインが必要です' });
    }

    try {
        const decoded = await admin.auth().verifyIdToken(token);
        req.user = decoded; // uid, email などが入っている
        next();
    } catch (err) {
        return res.status(403).json({ error: 'トークンが無効です' });
    }
}

// 認証が必要な API
app.get('/api/mypage', requireFirebaseAuth, (req, res) => {
    res.json({ message: `こんにちは ${req.user.email} さん` });
});

WEBフロントエンド側:Firebaseトークンを取得し、APIに送る

// フロントエンドでトークンを取得してAPIに送る
const user  = auth.currentUser;
const token = await user.getIdToken(); // Firebase のトークンを取得

const res = await fetch('/api3001/api/mypage', {
    headers: {
        'Authorization': `Bearer ${token}`,
    },
});

ハッシュ化を試す

平文のパスワードを入力して、ハッシュ値を出力するコード

// makehash.js
// npm install bcryptjs
const bcrypt = require('bcryptjs');

const plain = process.argv[2]; // コマンドライン引数(例: pass1234)

if (!plain) {
  console.error('使い方: node makehash.js <password>');
  process.exit(1);
}

const saltRounds = 10;

bcrypt.hash(plain, saltRounds)
  .then(hash => {
    console.log('元の文字列:', plain);
    console.log('ハッシュ値  :', hash);
  })
  .catch(err => {
    console.error('エラー:', err);
  });

操作方法

$ node makehash.js pass1234
元の文字列: pass1234
ハッシュ値  : <ここにハッシュ化された文字列が表示される>

コメントを残す