はじめに
「ログイン機能なら作ったことがある。でも OAuth とか JWT とか、名前は聞くけど何なのか分からない」——
そんな状態からスタートするための回です。
いきなり OAuth の話をしても混乱するだけなので、この記事ではその手前にある土台、つまり「認証」と「認可」という2つの言葉をしっかり分けるところに集中します。
この記事でわかること
- 「認証」と「認可」がまったく別のものだということ
- ログインできたユーザーに対して、さらに何をチェックしないといけないのか
- 「フロント画面でボタンを隠せば安全」がなぜ間違いなのか
- 権限の決め方には2つの流派(ロール/属性)があること
- 401 と 403 という2つのエラーの使い分け
- なぜ自分でログイン機能を作らず、OAuth のような標準を学ぶのか
この記事の対象読者
- Web アプリを作った(作っている)けれど、認証まわりは見よう見まねという人
- 「認証と認可って違うの?」というレベルから始めたい人
前提知識
ここだけ押さえておけば読めます。
分からなければ、下の用語メモで補ってください。
- ブラウザがサーバに「リクエスト」を送り、サーバが「レスポンス」を返す、というやり取りがあること
- Web アプリには、画面を作る側(フロントエンド)とデータを扱う側(バックエンド/サーバ)があること
用語メモ:リクエストとレスポンスとは?
ブラウザやスマホアプリがサーバに「このデータをください」とお願いするのがリクエスト、サーバが「はい、どうぞ」または「ダメです」と返すのがレスポンスです。
レスポンスには「うまくいったか」を表す3桁の数字(ステータスコード)が付いてきます。
200 = 成功、404 = 見つからない、あたりは見たことがあるかもしれません。
この記事では 401 と 403 という2つの数字が主役になります。
この記事のゴール
「認証」と「認可」を自分の言葉で説明できて、自分のアプリで「どこでチェックすべきか」をなんとなくではなく理由付きで言える状態になること。
本編
まず結論:認証と認可はまったく別物
一番大事な話を先に置きます。
たとえで言うと、認証は建物の入口、認可は各部屋の鍵です。
入口のゲートを通れた(=認証OK)からといって、社長室にも金庫室にも入れるわけではありません。
部屋ごとに別の鍵がかかっている。これが認可です。
英語が似ていて紛らわしいので、実務では AuthN(認証)/AuthZ(認可) と略して書き分けることがあります。
認証 —「あなたは誰か」を確かめる
認証とは、「私は◯◯です」という主張が本物かどうかを確かめることです。
確かめるための材料は、大きく3種類に分けられます。
| 種類 | 中身 | 例 |
|---|---|---|
| 知識情報(something you know) | 本人だけが知っているもの | パスワード・PIN |
| 所持情報(something you have) | 本人だけが持っているもの | スマホ・ハードウェアトークン・SMS が届く電話 |
| 生体情報(something you are) | 本人そのもの | 指紋・顔 |
MFA は「2回聞く」ことではない
ここが初級者のうちにハマりやすいポイントです。
MFA(多要素認証) は「2回チェックする」ことではなく、「上の3種類のうち、種類の違うものから2つチェックする」ことを指します。
用語メモ:MFA(Multi-Factor Authentication / 多要素認証)とは?
種類の違う確認手段を組み合わせて、なりすましを難しくする仕組みのこと。
「パスワード+スマホに届くコード」は、知識情報+所持情報なので MFA です。
では「パスワード+秘密の質問」はどうでしょうか。
これは知識 × 知識なので、MFA になりません。
パスワードも秘密の質問も「頭の中にある情報」なので、フィッシングで騙されたり、漏洩したリストに載ったりすると、同じ攻撃で同時に両方破られます。
種類を変えると攻撃者は「パスワードを盗む」に加えて「スマホも奪う」必要が出てくる。
つまり攻撃者が突破しないといけない経路の数が増える。これが MFA の本質です。
認証に成功すると、サーバは「この人はログイン済みですよ」という証明書のようなもの(セッションまたはトークン)を発行します。
認可 —「何をしてよいか」を決める
認可とは、ログイン済みの人が どのデータに対して、どんな操作をしてよいか を決めることです。
「ログインしているのに見られない」は、おかしなことではなく正常な動作です。
- ログイン済み = 自分のマイページは見られる
- でも、他人の請求書は見られない
この「見られない」を作っているのが認可です。
認可のチェックは、どこで行われている?
「認可」というと if (user.isAdmin) { ... } みたいな1行を想像しがちですが、
実際には役割の違う3つの部品が連携しています。
名前が付いていて、こう呼ばれます。
用語メモ:PEP / PDP / PIP とは?
認可の仕事を3つに分けたときの呼び名です。空港の保安検査にたとえると分かりやすくなります。
- PEP(Policy Enforcement Point / 判定を実行する場所) = ゲートに立っている係員。通す/止めるを実際にやる人
- PDP(Policy Decision Point / 判定を計算する場所) = ルールブックを見て「この人は通してOK」と決める人
- PIP(Policy Information Point / 判定材料を供給する場所) = 名簿や搭乗券のデータベース。判断に必要な情報を渡す人
名前を暗記する必要はありません。「判断する人」と「材料を渡す人」が別にいるという感覚だけ持ち帰ってください。
流れを図にするとこうなります。
ここが本記事で一番大事:材料がない場所では正しく判断できない
上の図で、判定する場所によって手に入る材料が違うというのがポイントです。
| チェックする場所 | そこで分かること | 得意なこと | 気をつけること |
|---|---|---|---|
| 入口(API Gateway など) | 証明書に書いてある情報だけ | 「この機能を使っていい人か」というざっくりした判断 | そのデータが誰のものかは分からない |
| アプリ(プログラム側) | 証明書の情報+データの中身 | 「これはあなたのデータか」という判断 | 所有者チェックを1か所書き忘れると、その API だけ他人のデータを返す |
| データベース(行レベルセキュリティ) | 実際のデータそのもの | アプリ側で「自分のデータだけ」という条件を書き忘れても、DB が他人の行を除外してくれる最後の砦 | ルールが DB 側の設定に書かれるので、アプリのコードを読んでも「なぜ見えないのか」が分からない |
用語メモ:行レベルセキュリティとは?
PostgreSQL などには、「この人が実行したクエリには、アクセス可能な行だけを返すようにDBが制御する」というルールを DB 自身に持たせる仕組みがあります。これを行レベルセキュリティと呼びます。
アプリ側で「自分のデータだけ」という条件を書き忘れても、DB が勝手に絞ってくれます。
頼もしい反面、そのルールはアプリのコードではなく DB の設定側に書かれるため、
「コードを読んだのに、なぜこのデータが見えないのか分からない」ということが起きます。
なぜ入口だけでは足りないのか、具体例で見てみましょう。
ユーザーAが、こんなリクエストを送ったとします。
GET /api/invoices/1043 HTTP/1.1
Host: api.example.com
Authorization: Bearer <ユーザーAの証明書>
- ユーザーAの証明書には「請求書を読んでよい」という権限が書いてあるとします
- 入口の係員が見られるのは「請求書を読んでよい権限があるか」だけ → 通ってしまいます
- でも、請求書1043 が誰のものかは、データベースから実際に読み出すまで分かりません
- つまり「1043 の持ち主 == ユーザーA か?」は、アプリ側でしかチェックできない
このチェック漏れには IDOR という名前が付いています。
用語メモ:IDOR とは?
Insecure Direct Object Reference(安全でない直接オブジェクト参照)の略。
URL の.../invoices/1043のような ID をそのまま使ってデータを取ってきてしまい、「その ID のデータが本当にこの人のものか」を確かめていない実装ミスのことです。
攻撃者は URL の数字を 1043 → 1044 と変えるだけで他人のデータが見られてしまいます。
(OWASP という団体の 2023 年版では BOLA という別名で呼ばれています)
混同すると何が起きるか
事故の起き方には方向が2つあります。どちらも認証は通っているのがポイントです。
つまり原因はログインではなく、その先の認可のチェック漏れです。
| 方向 | どんな事故か | たとえると |
|---|---|---|
| 垂直権限昇格 | 一般ユーザーが管理者の操作をできてしまう(権限の「高さ」を越える) | 平社員が社長室に入ってしまう |
| 水平権限昇格 | 同じ立場の他人のデータが見られてしまう(高さは同じまま「隣」へ広がる) | 隣の席の人の引き出しが開いてしまう |
代表的な事故パターンは3つです。
| # | どう実装していたか | 何が起きるか | 抜けているチェック |
|---|---|---|---|
| 1 | ログインできた = 全部 OK にしていた | 一般ユーザーが管理操作を実行できる(垂直) | 役割・権限の確認 |
| 2 | URL の ID をそのまま使い、持ち主を確認しない(IDOR) | 数字を変えるだけで他人のデータが読める(水平) | 「この人のデータか」の突き合わせ |
| 3 | フロント画面でボタンを隠しただけ | API を直接叩かれるとそのまま素通り | サーバ側のチェックがそもそも存在しない |
3つ目は特に重要なので強調します。
画面でボタンを隠すのは認可ではありません。
ブラウザの開発者ツールや curl を使えば、画面を通さずに API を直接呼べます。
UI で隠すのは「押し間違いを防ぐ親切」であって、セキュリティ対策ではありません。
認可は必ずサーバ側で行います。
事故に気づくためのログの取り方
起きてしまったときに気づけるかどうかは、ログの残し方で決まります。
-
操作した人の ID と 操作されたデータの持ち主の ID を両方残す
- 片方しか無いと、後から「他人のデータを見ていた」ことを追えません
- 401 と 403 をきちんと区別して記録する(次節)
- 同じ人が ID を 1041, 1042, 1043... と連番で叩いていたら、IDOR を試している疑いがあります
401 と 403 の使い分け
似ているようで意味がまったく違う2つのエラーです。
| コード | 意味 | ひとことで言うと |
|---|---|---|
| 401 Unauthorized | 認証されていない(誰か分からない) | 「まず名乗ってきて」 |
| 403 Forbidden | 認証済みだが権限がない | 「あなただと分かったうえで、ダメです」 |
名前が紛らわしいのですが、401 のほうが「認証」の話です(Unauthorized という名前ですが中身は未認証)。
実際のレスポンスを見てみましょう。
# 検証コマンド
curl -s -D - -o /dev/null https://httpbin.org/basic-auth/user/pass | grep -iE "^HTTP|www-authenticate"
# 証明書が無い/期限切れ → 認証の問題
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Fake Realm"
# 検証コマンド
curl -s -D - -o /dev/null https://httpbin.org/status/403 | grep -iE "^HTTP|www-authenticate"
# 証明書は有効。でもこの操作は許されていない → 認可の問題
HTTP/1.1 403 Forbidden
ルールとして決まっていることが3つあります(HTTP の仕様書である RFC 9110 に書かれています)。
-
401 を返すときは
WWW-Authenticateヘッダーを必ず付ける(RFC 9110 Section 15.5.2 の MUST)- このヘッダーは「どういう方式で認証してほしいか」をクライアントに伝えるものです
- 逆に言うと、権限不足なのに 401 を返す設計は、付けようがないヘッダーを要求されることになり、仕様上おかしいと分かります
-
403 に対して、クライアントは同じ証明書で自動リトライすべきではない(RFC 9110 Section 15.5.4 の SHOULD NOT)
- 権限不足を 401 で返してしまうと、クライアントは「再ログインすれば通るかも」と考えて無駄な再ログインを繰り返します
- ログの上でも「認証エラー」と「認可エラー」が混ざり、どちらが起きているのか分からなくなります
-
他人のデータへのアクセスに、あえて 404 を返すのも仕様上アリ(RFC 9110 Section 15.5.4 の MAY)
- 403 を返すと「その ID のデータは存在する」と教えてしまいます
- 存在自体を隠したい場合、403 の代わりに 404(見つかりません)を返してよい、と明記されています
- ただし開発中にデバッグしづらくなるトレードオフはあります
権限の決め方その1:RBAC(役職で決める)
ここからは「誰に何を許すか」をどう管理するか、という話です。代表的な方式が2つあります。
まず RBAC(Role-Based Access Control / ロールベースアクセス制御)。
用語メモ:ロール(Role)とは?
「編集者」「閲覧者」「管理者」のような職務のラベルのこと。
権限をひとまとめにした「役職」だと思ってください。
構造はこうです。
ユーザー → ロール → 権限
(山田さん → editor → 記事の作成・編集ができる)
なぜこんな回り道をするのか。
昔は「ユーザーに直接、権限をひとつずつ付ける」やり方でした。
でもこれだと、山田さんが異動するたびに、全部のデータについて権限を付け直す必要が出てきます。
100個のリソースがあれば100箇所です。運用が破綻しました。
現実の組織では「権限は人ではなく職務にくっついている」ので、
職務(ロール)を間に挟んだのが RBAC です。異動したらロールを付け替えるだけで済みます。
この考え方は ANSI INCITS 359 という標準になっています(現行版は INCITS 359-2012、2022 年に再確認された R2022)。
標準の中でロールは「組織の中での職務(job function)」と定義されています。
| 良いところ | 苦手なところ |
|---|---|
| シンプルで、監査(誰が管理者かの確認)がしやすい | 「自分が作ったデータだけ編集OK」のような、データの中身によって変わる条件が表現しづらい |
苦手なところを無理やり表現しようとすると、ロールがどんどん増えていきます。
これを ロール爆発 と呼びます。
権限の決め方その2:ABAC(データの中身で決める)
もうひとつが ABAC(Attribute-Based Access Control / 属性ベースアクセス制御) です。
用語メモ:属性(Attribute)とは?
人やデータが持っている値そのもののこと。
人の側なら「所属部署」「役職」、データの側なら「作成者の ID」「所属部署」、
さらに「今の時刻」「アクセス元の IP」なども属性です。
判定はこんな形になります。ロールを間に挟まず、その場の値どうしを突き合わせて決めるのが特徴です。
アクセスしてきた人の「部署 = 営業部」
== → 一致するので許可
そのデータの 「部署 = 営業部」
| # | 良いところ | 苦手なところ |
|---|---|---|
| 1 | 細かい条件を書ける(「自分が作ったデータだけ編集OK」も表現できる)。 | ルールが複雑になりテストや監査が難しい。 |
| 2 | ロールが増えないのでロール爆発を避けられる | 「誰が何にアクセスできるか」の一覧を作れない(実際のデータ次第で変わるため) |
つまり 細かく書けることと、全体を見渡せることはトレードオフです。
AWS だと IAM ポリシーの条件キーやタグを使った認可が ABAC 的な仕組みにあたります(本シリーズの範囲外なので、参考リンクだけ示します)。
RBAC と ABAC、どっちを選ぶ?
ここはいまは眺めるだけでOKです。実際に設計する段になったら戻ってきてください。
| 判断のポイント | RBAC 寄り | ABAC 寄り |
|---|---|---|
| 権限の種類の数 | 数個〜十数個で収まる | 条件の組み合わせで増え続ける |
| 判定に必要な情報 | ユーザーの情報だけで決まる | データ側の情報(持ち主・部署・状態)が要る |
| 監査の要件 | 「誰が管理者か」の一覧を出す必要がある | ルールとログで説明できればよい |
| 変更の頻度 | 権限体系が安定している | 組織変更やテナント追加が頻繁 |
| チームの体力 | ルールのテスト工数を割けない | ルールを資産として管理できる |
実務で覚えておくとよいのは、次の3つです。
-
現実には両方使う
- ざっくりした判定は RBAC(この API を呼べるか)、細かい判定は ABAC(このレコードを触れるか)
-
ロール爆発のサインを見逃さない
-
editor-sales-readonlyのように、ロール名に条件が埋め込まれ始めたら ABAC を検討する合図
-
-
迷ったら RBAC から始める
- RBAC → ABAC への移行はできますが、逆(散らばったルールをロールにまとめ直す)はとても大変です
Scope — OAuth が使う「権限の範囲」
ここでようやく OAuth の話に近づきます。
用語メモ:Scope(スコープ)とは?
「このアプリには、ここまでの権限を渡しますよ」という範囲を表す文字列のこと。
openidprofileread:invoicesのような値を、空白で区切って並べます。
大文字と小文字は区別されるのでRead:Invoicesは別物です(RFC 6749 Section 3.3。どんな値を使えるかは認証サービス側が決めます)。
なぜ必要なのか。
OAuth の目的は「パスワードを渡さずに、権限だけを渡す」ことです。
たとえば「この写真アプリに、Google ドライブの写真フォルダだけ読ませたい」といった場面ですね。
このとき「渡す権限の大きさ」を表す入れ物が要ります。
それが無いと、委譲は「全権限を渡す or 何も渡さない」の二択になってしまいます。
Scope とロールは別物
ここは混同しやすいので、はっきり分けておきます。
| 何を表す? | 誰のもの? | |
|---|---|---|
| Scope | アプリに貸し出した権限の範囲 | クライアント(アプリ)側 |
| ロール | ユーザー自身が持っている権限 | ユーザー側 |
そして実際に許される操作は、両方の重なった部分だけです。
実際にできること = アプリに渡した権限 ∩ ユーザー本人の権限
たとえばユーザーが管理者でも、アプリに「読み取りだけ」の Scope しか渡していなければ、そのアプリは書き込めません。
実際の通信ではこう見える
アプリが「この範囲をください」とお願いする URL のサンプルです(読みやすさのため改行しています)。
https://auth.example.com/oauth2/authorize
?response_type=code
&client_id=<クライアントID>
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&scope=openid%20read%3Ainvoices
&state=<ランダム文字列>
%20 は空白、%3A は : を URL 用に変換したものです。つまり scope=openid read:invoices と言っています。
(state は「なりすまし対策のランダム文字列」で、RFC 6749 Section 4.1.1 で RECOMMENDED とされています。)
これに対して認証サービスが返してくる例がこちらです。
{
"access_token": "<アクセストークン>",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid read:invoices"
}
読むポイントは3つあります。
-
お願いした Scope が必ず通るとは限りません。
- 認証サービスは、要求された Scope の一部または全部を無視してよいことになっています(RFC 6749 Section 3.3 の MAY)
- そして要求と違う結果になった場合は、レスポンスに
scopeを含めて「実際に許可した範囲」を伝えなければなりません(MUST) - つまり、アプリ側は「お願いした Scope」ではなく「返ってきた
scope」を基準に動作する必要があります
-
expires_in(有効期限・秒)があることが重要です。- 委譲した権限には期限がある。これがパスワードを直接渡す方式との決定的な違いです(3600 秒 = 1時間)
- ただし
expires_inは RECOMMENDED であって必須ではありません(RFC 6749 Section 5.1。必須なのはaccess_tokenとtoken_type)
- 返ってこない実装も仕様違反ではないので、アプリ側は「必ずある」前提で書かないこと
Scope にも限界がある
read:invoices(請求書を読んでよい)という Scope を持っていても、「どの請求書か」までは Scope では表せません。
そう、この記事の前半で見た IDOR の話に戻ってきます。
持ち主のチェックは、結局アプリ側の仕事なのです。
なぜ OAuth を学ぶ必要があるのか
名前が並んで混乱しがちな3つの技術ですが、それぞれ別の問題を解いています。
| 技術 | ひとことで言うと | 解いている問題 |
|---|---|---|
| OAuth 2.0 | 「権限を貸す」 | パスワードを渡さずに、限定した権限を、期限付きで他のアプリへ渡す |
| OpenID Connect | 「誰かを伝える」 | 「誰がログインしたか」を決まった形式で伝える |
| JWT | 「改ざんに気づける入れ物」 | その結果を、改ざんを検知できる形で持ち運ぶ |
3つの関係は競合ではなく積み重ねです。
OAuth 2.0 という土台の上に OpenID Connect が乗り、その中身の表現形式として JWT が使われます。
すでに標準化された答えがあるので、自分で作り直さない——これが OAuth を学ぶ理由です。
自分で作るとほぼ確実に踏み抜くのが、次の4つです。ここでは「なぜ問題になるのか」だけ押さえてください。
| # | 気をつける点 | なぜ問題になるか |
|---|---|---|
| 1 | トークンをどこに置くか | トークンは「持っている人が本人扱いされる」(Bearer = 持参人)ので、盗まれた時点で突破される。ブラウザの localStorage は XSS で読まれる |
| 2 | リダイレクト先の確認 | ログイン後の戻り先を確認しないと、攻撃者のサイトに認可コードを届けてしまう |
| 3 | トークンの有効期限 | 発行済みトークンをすぐ無効化するのは簡単ではない。だから寿命を短くすることが実質的な対策になる |
| 4 | 署名の確認 | 署名を検証しない JWT は「自己申告の身分証」と同じ。role: admin と名乗られたら通ってしまう |
共通する原則はひとつです。
トークンは秘密情報として扱い、検証してから信じる。
よくある誤解
- 「ログインできたなら、あとは何をしてもいい」 → 別の話です。ログイン(認証)は「誰か」の確認、認可は「何ができるか」の判断。
- 「JWT を使えばセキュアになる」 → JWT はただの入れ物です。中身を検証しなければ何の保証もありません(第4回)。
- 「Scope があればロールは要らない」 → 対象が違います。Scope はアプリに貸した範囲、ロールはユーザー本人の権限。
- 「ABAC のほうが新しくて優れている」 → 表現力と運用の手間のトレードオフです。小規模なら RBAC のほうが楽。
- 「フロントでボタンを隠せば安全」 → API を直接叩かれれば無意味。認可は必ずサーバ側で。
- 「入口(API Gateway)にチェックを付ければ認可は完了」 → 入口で分かるのは「この API を呼べるか」まで。「そのデータが自分のものか」はアプリ側でしか判定できません。
まとめ
- 認証は「誰か」、認可は「何をしてよいか」。 ここを混同するのが権限昇格事故の入口です。
- 認可は入口だけでは完結しません。 「そのデータが自分のものか」は、データの中身が見える場所でしか判断できません。
- MFA は「2回聞く」ことではなく「種類の違う2つで確認する」こと。
- 権限の決め方には RBAC(役職で決める)と ABAC(データの中身で決める)がある。迷ったら RBAC から。
- Scope は「アプリに貸した権限の範囲」で、ユーザーのロールとは別軸。期限(
expires_in)とセットで意味を持ちます。 - 401(未認証)と 403(権限不足)を打ち分ける。 実装の正しさにも、ログの読みやすさにも効きます。
- OAuth / OIDC / JWT は競合ではなく「権限を貸す」「誰かを伝える」「持ち運ぶ」という別々の役割分担です。
次回予告
第2回「【セキュリティ入門】OAuth 2.0とは?認可の仕組みを図解で理解する」。
本記事で触れた「権限を貸す」という話が、実際にどんな登場人物のやり取りで成り立っているのかを見ていきます。
参考リンク
本記事で参照した文書
| 文書 | 本記事で参照した箇所 | リンク |
|---|---|---|
| RFC 9110 — HTTP Semantics | Section 15.5.2 が 401 Unauthorized(WWW-Authenticate は MUST)、Section 15.5.4 が 403 Forbidden。403 の代わりに 404 を返してよい(MAY)旨も 15.5.4 に記載(→「401 と 403 の使い分け」) |
rfc9110 |
| OWASP API Security Top 10 2023 — API1:2023 Broken Object Level Authorization | IDOR に相当。2023 年版本文では BOLA という呼称(→「材料がない場所では正しく判断できない」) | API1:2023 |
| RFC 6749 — The OAuth 2.0 Authorization Framework | Section 3.3 が Scope の定義(大文字小文字を区別/認可サーバは要求を無視してよい MAY)/Section 4.1.1 が認可リクエストのパラメータ(state は RECOMMENDED)/Section 5.1 がトークンレスポンス(expires_in は RECOMMENDED)(→「Scope」)。RFC 8252 / 8996 / 9700 により更新済み(本体は廃止されていない) |
rfc6749 |
| AWS IAM ユーザーガイド — ABAC による許可の定義 | 「AWS の ABAC 的な仕組み」として本文で触れたタグベース認可(→「権限の決め方その2:ABAC」)。 | IAM ABAC |
さらに読むなら(本記事の範囲外)
次回以降で本格的に扱う文書です。いまは「こういうものがある」とだけ知っておけば十分です。
| 文書 | どんな文書か | リンク |
|---|---|---|
| RFC 6750 — OAuth 2.0 Bearer Token Usage | アクセストークンを Authorization: Bearer でどう送り、エラーをどう返すかの仕様。RFC 9700 により更新済み
|
rfc6750 |
| RFC 9700(BCP 240) — Best Current Practice for OAuth 2.0 Security | 2025年発行。RFC 6749 / 6750 / 6819 を更新し、脅威モデルと推奨事項を現行化した BCP。RFC 6749 を読むときは必ずこれと併せて読む | rfc9700 |







