誰を通すか
アカウントを作れるのは、同じ世帯の人だけにしてください。 denpa は自分の世帯の中で私的に観るための
ものです。OIDC のグループ (OIDC_GROUP) に入れる人、ペアリングするテレビ、TRUSTED_NETWORKS に入れる
ネットワークも、同じ世帯の人と機器に限ります。考え方は Discussion #760 にあります。
エンドポイントによって守り方が違います。
| エンドポイント | 守り方 |
|---|---|
/api/live/socket |
使い捨てのチケット (下記)。ここだけ SvelteKit に届きません |
| それ以外 (画面・録画のファイル・外から使う API) | OIDC (設定してあれば) |
| どのエンドポイントも | TRUSTED_NETWORKS に当たれば素通し (下記) |
| どのエンドポイントも | アクセストークン (Authorization: Bearer denpa_…) があれば、どこから来ても通す (下記) |
/login* /logout /api/health /manifest.webmanifest /api/device/code /api/device/token |
素通し (auth.ts の OPEN_PATHS) |
OIDC も TRUSTED_NETWORKS も設定していなければ、認証なしで通すエンドポイントのほかは全部断ります (403、
理由を本文に書いて返す。下記)。全部開けたいなら
TRUSTED_NETWORKS=0.0.0.0/0 です (公開時の注意)。
/api/live/socket だけは入口の server.js が SvelteKit の手前で受けます。 SvelteKit の
仕組みでは WebSocket の握手を扱えないためで、hooks.server.ts に届かず、ここに書いた守り方は効きません。
ブラウザは WebSocket の握手に Authorization を付けてくれないので、画面が先に
POST /api/live/ticket で使い捨てのチケットを取り、URL に付けて繋ぎます。チケットの発行は
普通の HTTP なので、OIDC も信頼したネットワークもそのまま効きます。チケットは1回使ったら
消し、寿命は30秒 (src/lib/server/tickets.ts、詳しくは stream.md)。
/api/health を守ると Pod が再起動を繰り返します (Kubernetes の livenessProbe が呼ぶため)。
出しているのは「生きているか・動いている版と新しい版・局が何件か・録画が何本か」だけです。
/manifest.webmanifest を守るとホーム画面の名前が付きません。 ブラウザは
マニフェストを資格情報を付けずに取得するので、401 で落ちます。
中身は表示名・色・アイコンの場所だけです (watch.md)。
録画のファイル (/api/recordings/<id>/file) も、ほかの API と同じ守り方です。 観る画面の <video> も
端末に保存もログインの控え (Cookie) を、テレビのアプリはアクセストークンを付けます。ログインせずに開ける URL を
渡す仕組み (前の「再生リンク」) はやめました (config.md)。
ログイン方法が無ければ、全部断る
Section titled “ログイン方法が無ければ、全部断る”何も設定しないまま立てると、認証なしで通すエンドポイント (上の OPEN_PATHS) のほかは 403 で断ります (fail-closed)。
起動ログに1度だけ案内を出します。
[boot] ログイン方法が設定されていないため、すべてのアクセスを断ります。 OIDC (docs/auth.md) か TRUSTED_NETWORKS (CIDR のカンマ区切り) を設定してください。 すべて開けるなら TRUSTED_NETWORKS=0.0.0.0/0 (公開時の注意は docs/auth.md)前の版のベーシック認証 (起動時に自動生成) は廃止しました。自動で掛かる方式は「設定を忘れた」と区別が付かないためです。
OIDC でのログイン
Section titled “OIDC でのログイン”3つ揃ったときだけ有効になります。揃っていなければ、ログイン方法は
TRUSTED_NETWORKS だけです。
| 変数 | |
|---|---|
OIDC_ISSUER |
例 https://login.microsoftonline.com/<tenant>/v2.0 |
OIDC_CLIENT_ID |
アプリ登録のアプリケーションID |
OIDC_CLIENT_SECRET |
クライアントシークレット |
OIDC_GROUP |
このグループにいるか、このロールを持つ人だけ通す。空なら入れた人は全員 |
OIDC_SESSION_TTL |
ログインの有効期間(ms)。既定30日 |
秘密を含むので環境変数だけから読み、設定画面には出しません。
ライブラリを入れていません
Section titled “ライブラリを入れていません”使うエンドポイントは discovery・authorize・token・jwks の4つだけで、どれも素の HTTP と
WebCrypto で足ります。認証まわりで「中で何をしているか分からない」を抱えるより、
200行書くほうを選びました (src/lib/server/oidc.ts)。
OIDC の標準しか使っていないので、Keycloak でも Google でも同じはずです (実機で当てたのは Entra ID だけ)。
- 認可コードフロー + PKCE (S256)。
stateとnonceも使います - ID トークンは署名まで見ます。 認可コードフローでは TLS 越しに相手から直に 受け取るので仕様上は省けますが (OIDC Core 3.1.3.7)、省くと安全が 「トークンをどこから受け取ったか」に乗ります。WebCrypto で数行です
- 受ける署名方式は RS256 だけ。
alg: noneを受けると署名を見ない抜け道ができます - 発行元・宛先・期限・合言葉 (
nonce) を確かめます。時計のずれは60秒まで許します
グループかロールで決める
Section titled “グループかロールで決める”誰がログインしたかでは決めません。 人が増えても denpa 側を触らずに済みます。
見るのは groups と roles の両方で、どちらかに OIDC_GROUP があれば通します。
グループを載せると ID トークンが大きくなる (前段の oauth2-proxy などの Cookie が壊れることがある) ので、
アプリロールに移す人もいます。両方受ければ、どちらの形式でも、切り替えの途中でも入れます。
Entra ID でグループを載せるには、アプリ登録の「トークン構成」で
groupMembershipClaims を有効にしてください。既定で載るのは
グループのオブジェクトID (GUID) なので、OIDC_GROUP にもそれを書きます。
OIDC_GROUP=6f1b2c3d-4e5f-6789-abcd-ef0123456789ロールで決めるなら、アプリ登録のアプリロールを作って人に割り当て、OIDC_GROUP
にはそのロールの値を書きます。
断るときは理由を画面に出します。黙って弾くと「なぜか自分だけ入れない」になるためです。
| 出るもの | 意味 |
|---|---|
... のグループにもロールにも入っていません |
そのとおり |
ID トークンに groups も roles もありません |
groupMembershipClaims がまだ無効で、アプリロールも割り当てていない |
グループが多すぎて ID トークンに載っていません |
Entra が groups の代わりに _claim_names を返し、roles も無かった。アプリロールに移すか、グループを減らすか、OIDC_GROUP を空にして Entra 側のアプリ割り当てで決める |
アプリのペアリング
Section titled “アプリのペアリング”テレビのアプリ (danything/denpa-tv) は、QR で
ペアリングしてアクセストークンを持ちます (device-auth.ts)。家の外から OIDC 越しに使うためで、
信頼するネットワークの中ならペアリングは要りません (アプリはアクセストークンなしで API を呼び、
401 の JSON が返ったときだけペアリングに進む。api.md)。
方式は OAuth のデバイス認可 (RFC 8628) と同じです (API の形式は api.md)。
テレビがコードを貰って device?code=… を QR に出し、スマホで開くと、いつものログイン方法
(信頼するネットワーク、無ければ OIDC のログイン) を通ったうえでそのまま済む。テレビは待っていたアクセストークンを1度だけ受け取り、
以後は Authorization: Bearer denpa_… でどのエンドポイントにも入る (/device で別の端末を済ませることだけは断る。下記)。
「許す / 断る」は聞きません。 家で使う前提で、QR を読めるのはテレビの前にいる人だからです。 その代わり:
- コードは10分で切れ、1度しか使えない。 アクセストークンを渡したらコードは使い終わり (
consumed_at) - コードは画面に出さない。 QR を読んだ時点でそのテレビと結び付いていて、コードは URL に入って自動で渡る。人が打つことも見比べることも無いので、出しても読む人がいない
- 開いただけでは済ませない。
/deviceは開くと自分でフォームを送る (POST。CSRF の守りが効く)。 GET のまま済ませると、ブラウザの先読みやリンクの下見で黙って済んでしまう。 読み直してもアクセストークンは2つにならない (作るのはテレビが受け取るとき) - 有効なコードは20まで (
MAX_PENDING)。コードを発行するエンドポイントは誰でも呼べるので溜め込ませない。 埋められても10分でペアリングできるようになり、入れるようにはならない - 秘密は DB に置かない。
deviceCodeもアクセストークンも SHA-256 だけ持つ (device_codes/api_tokens)。 アクセストークンは許した瞬間ではなく渡すときに作るので、生のアクセストークンを預かっておく場所が要らない - 無効にした・知らないアクセストークンは 401 (
{"error":"invalid_token"})。信頼するネットワークの中でも、 ほかの認証へは回さない — 黙って通すと、アプリは取り消されたことに気付けず、外へ出た途端に 使えなくなる。denpa_で始まらない Bearer (前段の SSO などが付けるもの) は見ずに、いつもの認証へ回す - 取り消しは設定の「連携」の「テレビのアプリ」から (
/settings/integrations#tv。行は消さずにrevoked_at。いつ・誰が許したかを残す)。 アプリの「サーバーから外す」はPOST /api/device/logoutで自分のアクセストークンを無効にする - 最後に使った時刻は1分に1度だけ書く (映像は Range ごとに来るので、毎回書くと DB に書き続けることになる)
- アクセストークンで入った相手は
/deviceで済ませられない (403。approveが断る)。盗まれたアクセストークンで 別のアクセストークンを作れると、元のものを取り消しても作ったものが生き残るため
確かめずに済ませることで残る危うさ (承知のうえ)。 コードを発行するエンドポイントは誰でも呼べるので、
悪意のある人が自分の端末でコードを作り、その device?code=… を、denpa に入れる人に開かせると
(メッセージのリンク・QR を差し替えるなど)、開いた瞬間に相手の端末へアクセストークンが渡ります
(デバイス認可のフィッシング)。家で使う前提で、確かめの手順を挟まないことを選んでいます。
コードが10分で切れて1度しか使えない (上記) ほかに:
- 完了の画面に端末の名前を出す。テレビの前にいないのにこの画面が出たら、それはおかしい (名前は端末の自己申告で、だれでも「テレビ」と名乗れる)
- 渡したアクセストークンは「テレビのアプリ」の節に名前・ペアリングした時刻・最後に使った時刻つきで並び、 その場で取り消せる。覚えのない端末が無いか、ときどき見てください
- 人から送られてきた
device?code=のリンクは開かない (QR はテレビに出ているものだけ読む)
ネットワークの中なら、何も聞かない
Section titled “ネットワークの中なら、何も聞かない”TRUSTED_NETWORKS に当たると、OIDC も掛かりません。
LAN のテレビのアプリ (denpa-tv) に、ログインもペアリングもなしで使わせるためのものです。
TRUSTED_NETWORKS=10.10.0.0/16# いくつでも並べられるTRUSTED_NETWORKS=10.10.0.0/16,10.20.0.0/16,192.168.1.5# 全部開ける (家の中だけで完結する構成向け。公開時の注意は下)TRUSTED_NETWORKS=0.0.0.0/00.0.0.0/0 はインターネットに向けて開くのと同じです。 録画も設定も誰でも触れます。
家の外に出す構成では使わず、OIDC にします。
見るのはアドレスだけで、どの名前で来たかは問いません。 外向きと LAN 用の2つの名前で出していても、 LAN から外向きの名前を開けば通ります (install.md)。
前段の後ろに置くとき (TRUSTED_PROXIES)
Section titled “前段の後ろに置くとき (TRUSTED_PROXIES)”前段 (Gateway やリバースプロキシ) があるなら、前段のアドレスを TRUSTED_PROXIES に書くこと。
書かないと接続元が前段のアドレスになり、TRUSTED_NETWORKS が誰にも当たりません (=全員が認証を
求められます。開いてしまう向きには倒れません)。前段が無い (ブラウザが直に denpa へ来る) なら
何も要りません。
# Kubernetes の Gateway (Cilium の Envoy など hostNetwork で動くもの) は Pod のネットワークから繋いでくるTRUSTED_PROXIES=10.42.0.0/16,fd42::/64# 同じ compose のネットワークにいるリバースプロキシTRUSTED_PROXIES=172.16.0.0/12決め方は nginx の set_real_ip_from + real_ip_recursive on と同じです
(src/lib/server/address.ts の resolveClient。入口の server.js が使う)。
- 直の相手が
TRUSTED_PROXIESに当たらなければ、相手そのものが接続元。X-Forwarded-Forは 誰でも付けられるので読みません。denpa のポートへ前段を通らずに届く経路があっても、ヘッダでTRUSTED_NETWORKSを名乗ることはできません - 当たれば
X-Forwarded-Forを右から辿り、前段のアドレスを飛ばして、最初に出てきた 前段でないアドレスを接続元にします。前段を何段重ねても、全部TRUSTED_PROXIESに入っていれば 段数を数える必要はありません。全部が前段なら左端を使います - 読めない項目に当たったら、そこで止めます (その先は信じない)
::ffff:10.0.0.1のような IPv4 を写した IPv6 は IPv4 として扱います- 前段の網を広く書くと、その網の中の誰もが前段を名乗れます。 Pod の網を入れると、
クラスタの中の Pod は
X-Forwarded-Forを付けて denpa に直に来れば好きな接続元を名乗れます。 クラスタの中を信じられないなら、前段のアドレスだけに絞ってください
決めたアドレスは adapter-bun へ x-denpa-remote で渡します (外から同じ名前で付けて
来ても上書きで消えます)。アドレスを読めなかったときは通しません。
X-Forwarded-Proto は前段かどうかを問わず読みます。 付けられて変わるのは、付けた本人に
返る origin の見た目だけです (CSRF の判定はブラウザが付ける Origin と比べるので、他人の
ブラウザからは付けられない)。前段だけに絞ると、TRUSTED_PROXIES を書き忘れた https の構成で
POST が全部断られるようになるだけで、守れるものがありません。
前の版の
ADDRESS_HEADER=x-forwarded-forは廃止しました (読みません)。上げるときは docs/config.md。
ネットワークの書き方は CIDR (10.10.0.0/16・fd00::/8) かアドレスそのまま。IPv4 と IPv6 の
どちらも書けます。
ログインの控え
Section titled “ログインの控え”DBに持ちます (sessions)。署名した Cookie に中身を入れる手もありますが、それだと
「この端末だけ切る」ができません。Cookie に入るのは推測できない32バイトだけです。
httpOnly/SameSite=Lax/ https のときはSecure- 切れたものは
RECONCILE_INTERVALごとに片付けます (読む側は先に無視しています) - ログアウトはこちらの控えを消すだけで、Entra 側からは出しません。
end_session_endpointへ送ると、同じ Entra で入っている他のものまで巻き添えで 切れるためです - ログアウトは設定の「その他」の「ログイン」から (
/settings/general#login。POST /logout)。ヘッダーには置いていません - 同じ「その他」の**「ほかの端末からもログアウト」**は、いま使っている控えのほかを誰のものでも全部消します
(2回押し。
session.destroyOthers)。なくした端末・手放した端末を止めるためのものです。 消した数を画面に出し、消された端末は次に開いたときにログインし直しになります。 テレビのアプリのアクセストークン (api_tokens) はここでは止めません (「テレビのアプリ」の節で1台ずつ取り消します) - 素通し (
TRUSTED_NETWORKS) で入っているときは控えが無いので、「その他」にはその旨の1行だけ出します
Entra ID 側の用意
Section titled “Entra ID 側の用意”- アプリの登録を作る (シングルテナントでよい)
- リダイレクト URI を「Web」で登録する。denpa の名前ごとに1つずつ要ります
(
https://<denpa の名前>/login/callback) - クライアントシークレットを作る
- トークン構成で
groupMembershipClaimsを有効にする (グループで絞るなら)。 ロールで絞るなら、代わりにアプリロールを作って割り当てる
Helm では、chart の外で作った Secret (キーは issuer / client-id / client-secret / group) の名前を
denpa.oidcSecretName に渡します。denpa は Secret を環境変数で読むので、値を変えたら Pod を入れ替えます。
前段の認証 (oauth2-proxy の forward-auth など) は要りません。 前段で掛けていたものを外すときは 「denpa 側を設定 → 実機で入れることを確かめる → 前段から外す」の順にします。逆にすると、 OIDC の設定を間違えていたときに、誰も入れないのではなく誰でも入れる状態になります。