@o3co/create-auth-policy-verifier 0.8.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/templates/standalone/Dockerfile +1 -1
- package/templates/standalone/README.ja.md +30 -2
- package/templates/standalone/README.md +29 -2
- package/templates/standalone/config/application.conf +34 -0
- package/templates/standalone/docker-compose.yml +39 -0
- package/templates/standalone/package.json +6 -6
- package/templates/standalone/src/__tests__/dockerCompose.test.mts +29 -0
- package/templates/standalone/src/__tests__/loadConfig.test.mts +28 -18
- package/templates/versions.json +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@o3co/create-auth-policy-verifier",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Scaffold a new o3co/auth.policy-verifier project from the standalone template",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"templates"
|
|
29
29
|
],
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@types/node": "^26.
|
|
32
|
-
"vitest": "^
|
|
31
|
+
"@types/node": "^26.5.1",
|
|
32
|
+
"vitest": "^5.0.0"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"build": "tsc",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# rebuild. The digest is the multi-arch index, so amd64 and arm64 both resolve
|
|
4
4
|
# from it. Dependabot's `docker` ecosystem keeps the pair in step — bump the
|
|
5
5
|
# tag and the digest together, never the tag alone.
|
|
6
|
-
FROM node:
|
|
6
|
+
FROM node:26-alpine@sha256:ef24c5053d50fdc3e4e56eb4e7ddb7861874ab0fdc797046ba897581deb8e868 AS node-base
|
|
7
7
|
|
|
8
8
|
ENV HOME=/home/node
|
|
9
9
|
|
|
@@ -43,10 +43,13 @@ OAUTH_JWT_SECRET=$(openssl rand -hex 32) \
|
|
|
43
43
|
| `OAUTH_JWT_JWKS_TIMEOUT_MS` | `5000` | JWKS 取得をこの時間で打ち切る |
|
|
44
44
|
| `OAUTH_JWT_JWKS_COOLDOWN_MS` | `30000` | JWKS 再取得の最小間隔 |
|
|
45
45
|
| `OAUTH_JWT_JWKS_CACHE_MAX_AGE_MS` | `600000` | 取得した JWKS をキャッシュから返す期間 |
|
|
46
|
-
| `OAUTH_JWT_TOKEN_TYPE` | `at+jwt` | 受け入れる `typ` ヘッダ。同じ鍵で署名された id/refresh/logout token
|
|
46
|
+
| `OAUTH_JWT_TOKEN_TYPE` | `at+jwt` | 受け入れる `typ` ヘッダ。同じ鍵で署名された id/refresh/logout token を拒否する。`*` で pin しない(その場合 `audience` を OIDC client id にしてはいけない) |
|
|
47
|
+
| `OAUTH_JWT_AUDIENCE_CLAIM` | `aud` | audience を読む claim。`azp`(Clerk)、`client_id`(Cognito)。`aud` 以外を指定すると `aud` は一切見ない |
|
|
48
|
+
| `OAUTH_AUTHENTICATOR` | `jwt` | subject を確立する token authenticator。他の名前は `main.mts` で module が登録したものでなければならない |
|
|
47
49
|
| `OAUTH_JWT_MODE` | `verify` | `verify` はトークンを完全検証。明示的な `insecure-decode`(テスト専用)は署名検証なしでデコードする — `exp`/`nbf` は引き続き強制される |
|
|
48
50
|
| `RULE_ON_EMPTY_RULE_SET` | `deny` | ルールが 1 つも集まらなかったときの決定(`deny` \| `allow`) |
|
|
49
51
|
| `VERIFY_MAX_BATCH_SIZE` | `50` | `POST /verify/batch` の件数上限 |
|
|
52
|
+
| `VERIFY_CREDENTIAL_TO_COLLECTORS` | `never` | `expose` は生の資格情報を `context.credential` として collector に渡す — subject として下流 API を呼ぶ collector のためだけの設定。既定では collector は検証済みクレームだけを扱う。資格情報はリプレイ可能であり、context がログに出ればそれが漏れる |
|
|
50
53
|
| `LOG_LEVEL` | `info` | 出力する最低レベル: `trace`\|`debug`\|`info`\|`warn`\|`error`\|`fatal`\|`silent`。decision ログのスイッチも兼ねる — [可観測性](#可観測性)を参照 |
|
|
51
54
|
|
|
52
55
|
## HS256 シークレットの強度
|
|
@@ -100,7 +103,7 @@ Authorization: Bearer <jwt>
|
|
|
100
103
|
**このテンプレートは初期状態でまさにその組み合わせです。** `docker-compose.yml` が `HTTP_HOSTNAME=0.0.0.0` を設定する一方、`HTTP_CALLER_AUTH_TOKEN` はどこにも設定されていません。したがって publish したポートがホスト外から到達できる状態で `docker compose up` すると、未認証の判定エンドポイントが公開されます。ローカル開発では問題なく、だからこそ warn に留めています。実トラフィックを流す前に、次のいずれかを行ってください。
|
|
101
104
|
|
|
102
105
|
- **ポートをプライベートネットワークに置く。** セキュリティグループ、プライベートサブネット、Kubernetes の `NetworkPolicy` など、誰が接続してよいかを決める仕組みです。compose の `3000:3000` の publish はこれに該当しません — そのホストに到達できるすべてに到達を許します。
|
|
103
|
-
- **`.env` に `HTTP_CALLER_AUTH_TOKEN` を設定する。** ポートに到達できるだけでは判定を要求できなくなります。攻撃者が既にネットワーク境界の内側にいる場合でも有効なのは、2 つのうちこちらだけです。なお Go の enforcement 層([protobuf.interceptors](https://github.com/o3co/protobuf.interceptors)
|
|
106
|
+
- **`.env` に `HTTP_CALLER_AUTH_TOKEN` を設定する。** ポートに到達できるだけでは判定を要求できなくなります。攻撃者が既にネットワーク境界の内側にいる場合でも有効なのは、2 つのうちこちらだけです。なお Go の enforcement 層([protobuf.interceptors](https://github.com/o3co/protobuf.interceptors))は v0.3.0 以降、`endpoint.WithO3coHeaders` でこのヘッダを送れます。それより前のバージョンで作られた呼び出し元にはネットワーク側の制御が必要です。
|
|
104
107
|
- **あるいは `HTTP_HOSTNAME` を外し、verifier をサイドカーとして動かす。** enforcement 層とネットワーク名前空間を共有すれば、ループバックで足ります。
|
|
105
108
|
|
|
106
109
|
`GET /metrics` も非ゲートです。理由と、ループバック以外に bind したときに何を意味するかは [`/metrics` への到達方法](#metrics-への到達方法)を参照してください。
|
|
@@ -287,6 +290,31 @@ make dev
|
|
|
287
290
|
を読み込みます。資格情報は供給**しません** — `HTTP_CALLER_AUTH_TOKEN` はその
|
|
288
291
|
`.env` に置いてください。
|
|
289
292
|
|
|
293
|
+
### プロセス外の Cedar: `--profile cedar`
|
|
294
|
+
|
|
295
|
+
[Cedar](../../packages/cedar/README.md) のポリシーをプロセス外で評価するデプロイは、
|
|
296
|
+
アプリの隣でエージェントを起動します:
|
|
297
|
+
|
|
298
|
+
```sh
|
|
299
|
+
echo "CEDAR_AUTHENTICATION=$(openssl rand -hex 32)" >> .env
|
|
300
|
+
docker compose --profile cedar up --build
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
`CEDAR_AUTHENTICATION` は必須です: エージェントはこれをトークンとして起動され、
|
|
304
|
+
アプリはすべての呼び出しでこれを送ります。認証されていないエージェントは、
|
|
305
|
+
到達できる何者にもポリシーセットの差し替えを許してしまうからです。未設定の
|
|
306
|
+
ままだとエージェントはすべての呼び出しを拒否し、verifier は起動時にこれを
|
|
307
|
+
設定するよう伝えます。profile を付けない素の `docker compose up` では不要です。
|
|
308
|
+
|
|
309
|
+
`cedar-engine` サービスはアプリコンテナとネットワーク名前空間を共有するので、
|
|
310
|
+
verifier の `http` engine は `http://127.0.0.1:8180` でそれを見つけます — engine
|
|
311
|
+
には既定のアドレスがないため、これは compose ファイルがアプリに設定する
|
|
312
|
+
`CEDAR_ENDPOINT` です。verifier は起動時に `config/policies` をそこへ push し、
|
|
313
|
+
このコンテナの組の外からは誰も到達できません。この profile は、アプリが
|
|
314
|
+
`cedarPolicyModule` を組み込むまで何もしません。パッケージ・モジュール・config
|
|
315
|
+
エントリについては cedar の README を、どういうときにこの形が in-process の形に
|
|
316
|
+
勝るかについてはその sizing 表を参照してください。
|
|
317
|
+
|
|
290
318
|
### `pnpm-lock.yaml` はビルド入力
|
|
291
319
|
|
|
292
320
|
同じソースからの 2 回のビルドが 2 つの異なるイメージを生まないようにするのが
|
|
@@ -43,7 +43,9 @@ Individual values can also be overridden with environment variables.
|
|
|
43
43
|
| `OAUTH_JWT_JWKS_TIMEOUT_MS` | `5000` | Abort a JWKS fetch after this long |
|
|
44
44
|
| `OAUTH_JWT_JWKS_COOLDOWN_MS` | `30000` | Minimum spacing between JWKS fetches |
|
|
45
45
|
| `OAUTH_JWT_JWKS_CACHE_MAX_AGE_MS` | `600000` | How long a fetched JWKS is served from cache |
|
|
46
|
-
| `OAUTH_JWT_TOKEN_TYPE` | `at+jwt` | Accepted `typ` header; rejects id/refresh/logout tokens signed with the same key |
|
|
46
|
+
| `OAUTH_JWT_TOKEN_TYPE` | `at+jwt` | Accepted `typ` header; rejects id/refresh/logout tokens signed with the same key. `*` pins nothing (then `audience` must not be an OIDC client id) |
|
|
47
|
+
| `OAUTH_JWT_AUDIENCE_CLAIM` | `aud` | Claim the audience is read from: `azp` (Clerk), `client_id` (Cognito). Anything but `aud` means `aud` is not consulted at all |
|
|
48
|
+
| `OAUTH_AUTHENTICATOR` | `jwt` | Which token authenticator establishes the subject; another name must be registered by a module in `main.mts` |
|
|
47
49
|
| `OAUTH_JWT_MODE` | `verify` | `verify` fully verifies tokens; the explicit `insecure-decode` (test-only) decodes without signature verification — `exp`/`nbf` are still enforced |
|
|
48
50
|
| `RULE_ON_EMPTY_RULE_SET` | `deny` | Decision when no rule is collected (`deny` \| `allow`) |
|
|
49
51
|
| `VERIFY_MAX_BATCH_SIZE` | `50` | Cap on `POST /verify/batch` entries |
|
|
@@ -101,7 +103,7 @@ Leaving the credential unset is supported and is fine on loopback. On a non-loop
|
|
|
101
103
|
**This template is that combination out of the box.** `HTTP_HOSTNAME=0.0.0.0` is set in `docker-compose.yml` and `HTTP_CALLER_AUTH_TOKEN` is not set anywhere, so a `docker compose up` with the published port reachable from off-host is an unauthenticated decision endpoint. That is fine for local development and is why the warning is a warning. Before it carries traffic, do one of:
|
|
102
104
|
|
|
103
105
|
- **Put the port on a private network.** A security group, a private subnet, a Kubernetes `NetworkPolicy` — something that decides who may connect. Publishing `3000:3000` from compose is not this: it reaches everything that can route to the host.
|
|
104
|
-
- **Set `HTTP_CALLER_AUTH_TOKEN` in `.env`.** Then reaching the port is not enough to ask for a decision, which is the only one of the two that still holds against an attacker already inside the network boundary. Note that the Go enforcement layer ([protobuf.interceptors](https://github.com/o3co/protobuf.interceptors))
|
|
106
|
+
- **Set `HTTP_CALLER_AUTH_TOKEN` in `.env`.** Then reaching the port is not enough to ask for a decision, which is the only one of the two that still holds against an attacker already inside the network boundary. Note that the Go enforcement layer ([protobuf.interceptors](https://github.com/o3co/protobuf.interceptors)) sends this header from v0.3.0 on, through `endpoint.WithO3coHeaders`; a caller built on an earlier version needs the network control instead.
|
|
105
107
|
- **Or drop `HTTP_HOSTNAME` and run the verifier as a sidecar**, sharing a network namespace with the enforcement layer so loopback is enough.
|
|
106
108
|
|
|
107
109
|
`GET /metrics` is not gated either — see [Reaching `/metrics`](#reaching-metrics) for why, and for what that means when the bind is not loopback.
|
|
@@ -291,6 +293,31 @@ make dev
|
|
|
291
293
|
`.env`. It does **not** supply the credential — put `HTTP_CALLER_AUTH_TOKEN` in
|
|
292
294
|
that `.env`.
|
|
293
295
|
|
|
296
|
+
### Out-of-process Cedar: `--profile cedar`
|
|
297
|
+
|
|
298
|
+
A deployment that evaluates [Cedar](../../packages/cedar/README.md) policies
|
|
299
|
+
out of process starts the agent beside the app:
|
|
300
|
+
|
|
301
|
+
```sh
|
|
302
|
+
echo "CEDAR_AUTHENTICATION=$(openssl rand -hex 32)" >> .env
|
|
303
|
+
docker compose --profile cedar up --build
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
`CEDAR_AUTHENTICATION` is required: the agent is started with it as its token
|
|
307
|
+
and the app sends it on every call, because an unauthenticated agent lets
|
|
308
|
+
anything that reaches it replace the policy set. Left unset, the agent refuses
|
|
309
|
+
every call and the verifier's boot says to set it; a plain `docker compose up`
|
|
310
|
+
without the profile does not need it.
|
|
311
|
+
|
|
312
|
+
The `cedar-engine` service shares the app container's network namespace, so
|
|
313
|
+
the verifier's `http` engine finds it at `http://127.0.0.1:8180` — the
|
|
314
|
+
`CEDAR_ENDPOINT` the compose file sets on the app, since the engine has no
|
|
315
|
+
default address — pushes `config/policies` into
|
|
316
|
+
it at boot, and nothing outside the container pair can reach it. The profile
|
|
317
|
+
does nothing until the app composes `cedarPolicyModule`; see cedar's README
|
|
318
|
+
for the packages, the module and the config entry — and its sizing table for
|
|
319
|
+
when this shape beats the in-process one.
|
|
320
|
+
|
|
294
321
|
### `pnpm-lock.yaml` is a build input
|
|
295
322
|
|
|
296
323
|
`pnpm install --frozen-lockfile` is what keeps two builds of the same source
|
|
@@ -35,6 +35,15 @@ http {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
oauth {
|
|
38
|
+
# Which token authenticator establishes the subject of a decision. "jwt"
|
|
39
|
+
# (the default) is the built-in bearer-JWT path the block below configures.
|
|
40
|
+
# A deployment whose IdP issues opaque tokens, or whose gateway already
|
|
41
|
+
# authenticated the caller, registers its own authenticator in main.mts and
|
|
42
|
+
# names it here — see docs/extending.md, "Writing a token authenticator".
|
|
43
|
+
# With another name the `jwt` block must be omitted — that authenticator's
|
|
44
|
+
# keys go under its own sub-block (oauth.<name> { … }).
|
|
45
|
+
authenticator = "jwt"
|
|
46
|
+
authenticator = ${?OAUTH_AUTHENTICATOR}
|
|
38
47
|
jwt {
|
|
39
48
|
algorithm = "HS256"
|
|
40
49
|
algorithm = ${?OAUTH_JWT_ALGORITHM}
|
|
@@ -120,6 +129,19 @@ oauth {
|
|
|
120
129
|
# rejects id_tokens, refresh tokens and logout tokens signed with the same key.
|
|
121
130
|
tokenType = "at+jwt"
|
|
122
131
|
tokenType = ${?OAUTH_JWT_TOKEN_TYPE}
|
|
132
|
+
# `tokenType = "*"` (exactly that) pins nothing — any typ, or none. For
|
|
133
|
+
# issuers whose tokens carry no typ header (Cognito). The audience is then
|
|
134
|
+
# the only thing telling an access token from an id_token signed with the
|
|
135
|
+
# same key, so `audience` must NOT be an OIDC client id under "*": use an
|
|
136
|
+
# API identifier, or an audienceClaim id_tokens lack (Cognito: client_id).
|
|
137
|
+
# The claim the audience is read from. "aud" (the default) is RFC 9068;
|
|
138
|
+
# "azp" binds a Clerk session token, "client_id" a Cognito access token.
|
|
139
|
+
# The check moves, it never goes away — `audience` above stays required.
|
|
140
|
+
# With anything but "aud" here, `aud` is NOT consulted at all: use it only
|
|
141
|
+
# for issuers that emit no aud, and never name a claim the user can write
|
|
142
|
+
# (Clerk unsafe_metadata, Auth0 user_metadata).
|
|
143
|
+
audienceClaim = "aud"
|
|
144
|
+
audienceClaim = ${?OAUTH_JWT_AUDIENCE_CLAIM}
|
|
123
145
|
|
|
124
146
|
# Bounds on a presented token's own lifetime. Both apply in EVERY mode,
|
|
125
147
|
# insecure-decode included. `exp` itself is required and has no setting:
|
|
@@ -176,6 +198,8 @@ attribute {
|
|
|
176
198
|
# can never pass — see the note in the rule block.
|
|
177
199
|
collectors = [
|
|
178
200
|
# The token's `scope` claim, split into the list the scope rule reads.
|
|
201
|
+
# `claim = "scp"` reads Okta's scope array instead; pair it with the same
|
|
202
|
+
# `claim` on ResourceActionScopeRuleCollector below (#219).
|
|
179
203
|
{ collector = "PayloadScopeCollector" }
|
|
180
204
|
# The token's `sub`, which becomes the `subject` on the decision response
|
|
181
205
|
# and the `sub` field on the audit log line.
|
|
@@ -304,6 +328,16 @@ verify {
|
|
|
304
328
|
# the concurrency cap does not spend it.
|
|
305
329
|
collectorTimeoutMs = 2000
|
|
306
330
|
collectorTimeoutMs = ${?VERIFY_COLLECTOR_TIMEOUT_MS}
|
|
331
|
+
# How long one asynchronous rule (an out-of-process policy engine, #225) may
|
|
332
|
+
# take to answer. Same budget and ceiling as one collector; overrun = deny.
|
|
333
|
+
ruleTimeoutMs = 2000
|
|
334
|
+
ruleTimeoutMs = ${?VERIFY_RULE_TIMEOUT_MS}
|
|
335
|
+
# How long all of a decision's asynchronous rules may take together. Rule
|
|
336
|
+
# groups run one after another, so `ruleTimeoutMs` bounds each rule and not
|
|
337
|
+
# the phase; this does, the way `collectorDeadlineMs` does for collectors.
|
|
338
|
+
# An asynchronous rule runs under whichever ends first. Overrun = deny.
|
|
339
|
+
evaluateDeadlineMs = 5000
|
|
340
|
+
evaluateDeadlineMs = ${?VERIFY_EVALUATE_DEADLINE_MS}
|
|
307
341
|
|
|
308
342
|
# `collectorDeadlineMs` is the whole fan-out, per pipeline. The per-collector
|
|
309
343
|
# budget cannot bound a SET: once collectors queue, enough of them each
|
|
@@ -15,6 +15,45 @@ services:
|
|
|
15
15
|
# and it is why HTTP_CALLER_AUTH_TOKEN is worth setting in .env: past the
|
|
16
16
|
# container boundary, anyone who can reach the port can ask for decisions.
|
|
17
17
|
- HTTP_HOSTNAME=0.0.0.0
|
|
18
|
+
# Where the Cedar http engine finds the `cedar-engine` profile below — read
|
|
19
|
+
# only when the app composes cedarPolicyModule without the wasm package.
|
|
20
|
+
# The engine has no default address (a missing one refuses to start,
|
|
21
|
+
# naming this variable and the wasm package), so it is stated here, as an
|
|
22
|
+
# interpolation default: an `environment` entry outranks `env_file`, so a
|
|
23
|
+
# literal value would ignore a CEDAR_ENDPOINT set in .env for another agent.
|
|
24
|
+
- CEDAR_ENDPOINT=${CEDAR_ENDPOINT:-http://127.0.0.1:8180}
|
|
25
|
+
# The token the app sends the agent, from the same interpolation the
|
|
26
|
+
# `cedar-engine` service below starts the agent with — so a value from the
|
|
27
|
+
# shell or `--env-file` reaches both sides, not only the agent.
|
|
28
|
+
- CEDAR_AUTHENTICATION=${CEDAR_AUTHENTICATION:-}
|
|
18
29
|
env_file:
|
|
19
30
|
- path: .env
|
|
20
31
|
required: false
|
|
32
|
+
|
|
33
|
+
# Out-of-process Cedar engine, for deployments that evaluate Cedar policies
|
|
34
|
+
# through @o3co/auth.policy-verifier.cedar without the in-process wasm
|
|
35
|
+
# package (its README, "Running out of process"). Opt in per run:
|
|
36
|
+
#
|
|
37
|
+
# docker compose --profile cedar up --build
|
|
38
|
+
#
|
|
39
|
+
# It shares the app container's network namespace, so the verifier reaches
|
|
40
|
+
# it on loopback at CEDAR_ENDPOINT (http://127.0.0.1:8180, set on the app
|
|
41
|
+
# above), and nothing outside the pair can reach it at all —
|
|
42
|
+
# the same trust boundary the verifier's own bind address draws. The
|
|
43
|
+
# verifier pushes its config/policies into the agent at boot; nothing is
|
|
44
|
+
# mounted here. Pinned by digest for the reason the base image is.
|
|
45
|
+
cedar-engine:
|
|
46
|
+
image: permitio/cedar-agent:0.2.2@sha256:2a7713bce95220eb2ad870ba311f1ae8a4d6e607d13720747d75b96cb0c551ca
|
|
47
|
+
profiles: ["cedar"]
|
|
48
|
+
network_mode: "service:app"
|
|
49
|
+
environment:
|
|
50
|
+
- CEDAR_AGENT_ADDR=127.0.0.1
|
|
51
|
+
- CEDAR_AGENT_PORT=8180
|
|
52
|
+
# Required: an unauthenticated agent is a write oracle over the policy set
|
|
53
|
+
# — anything that reaches its port can PUT a `permit` for everyone. Set
|
|
54
|
+
# CEDAR_AUTHENTICATION in .env; the app reads the same variable and sends
|
|
55
|
+
# it. Unset, the agent starts with an empty token and refuses every call,
|
|
56
|
+
# and the verifier's boot says to set CEDAR_AUTHENTICATION. (Not compose's
|
|
57
|
+
# required-variable form: it is interpolated for every service, profile or
|
|
58
|
+
# not, and would refuse a plain `docker compose up` too.)
|
|
59
|
+
- CEDAR_AGENT_AUTHENTICATION=${CEDAR_AUTHENTICATION:-}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@o3co/auth-policy-verifier-standalone",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"packageManager": "pnpm@10.29.3",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
|
-
"node": ">=22.
|
|
9
|
+
"node": ">=22.12.0"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "tsc",
|
|
@@ -24,12 +24,12 @@
|
|
|
24
24
|
"pino": "^10.3.1"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@types/node": "^26.
|
|
27
|
+
"@types/node": "^26.5.1",
|
|
28
28
|
"@types/supertest": "^7.2.1",
|
|
29
|
-
"jose": "^6.2.
|
|
29
|
+
"jose": "^6.2.12",
|
|
30
30
|
"supertest": "^7.2.2",
|
|
31
|
-
"tsx": "^4.23.
|
|
31
|
+
"tsx": "^4.23.13",
|
|
32
32
|
"typescript": "^5.9.3",
|
|
33
|
-
"vitest": "^
|
|
33
|
+
"vitest": "^5.0.0"
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 1o1 Co. Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
const compose = readFileSync(new URL("../../docker-compose.yml", import.meta.url), "utf8");
|
|
8
|
+
|
|
9
|
+
describe("docker-compose.yml — the cedar profile's agent is authenticated (v0.10.0 audit)", () => {
|
|
10
|
+
it("starts the agent with the token the app sends", () => {
|
|
11
|
+
// An unauthenticated cedar-agent is a write oracle over the policy set:
|
|
12
|
+
// anything that reaches its port can PUT `permit(principal, action,
|
|
13
|
+
// resource);`. The token comes from the same CEDAR_AUTHENTICATION the app
|
|
14
|
+
// reads, so the two cannot disagree.
|
|
15
|
+
expect(compose).toMatch(/- CEDAR_AGENT_AUTHENTICATION=\$\{CEDAR_AUTHENTICATION:-\}/);
|
|
16
|
+
// And the app from the same interpolation, not only through `env_file`:
|
|
17
|
+
// a shell variable or `--env-file` would otherwise reach the agent alone
|
|
18
|
+
// (review).
|
|
19
|
+
expect(compose).toMatch(/- CEDAR_AUTHENTICATION=\$\{CEDAR_AUTHENTICATION:-\}/);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("does not make a plain `docker compose up` depend on it", () => {
|
|
23
|
+
// `${VAR:?}` is interpolated for every service, profile or not, so it
|
|
24
|
+
// would refuse the app-only stack too. Unset, the agent is started with
|
|
25
|
+
// an empty token, refuses every call, and the verifier's boot names
|
|
26
|
+
// CEDAR_AUTHENTICATION — closed, without breaking the default stack.
|
|
27
|
+
expect(compose).not.toMatch(/:\?/);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -55,6 +55,8 @@ const envKeys = [
|
|
|
55
55
|
"VERIFY_COLLECTOR_TIMEOUT_MS",
|
|
56
56
|
"VERIFY_COLLECTOR_DEADLINE_MS",
|
|
57
57
|
"VERIFY_COLLECTOR_CONCURRENCY",
|
|
58
|
+
"VERIFY_RULE_TIMEOUT_MS",
|
|
59
|
+
"VERIFY_EVALUATE_DEADLINE_MS",
|
|
58
60
|
] as const;
|
|
59
61
|
|
|
60
62
|
/** application.conf leaves issuer/audience unset, so every load must supply them. */
|
|
@@ -89,12 +91,12 @@ describe("loadAppConfig", () => {
|
|
|
89
91
|
pathPrefix: "",
|
|
90
92
|
callerAuth: { header: "x-caller-token" },
|
|
91
93
|
});
|
|
92
|
-
expect(config.oauth.jwt
|
|
93
|
-
expect(config.oauth.jwt
|
|
94
|
-
expect(config.oauth.jwt
|
|
95
|
-
expect(config.oauth.jwt
|
|
96
|
-
expect(config.oauth.jwt
|
|
97
|
-
expect(config.oauth.jwt
|
|
94
|
+
expect(config.oauth.jwt?.algorithm).toBe("HS256");
|
|
95
|
+
expect(config.oauth.jwt?.secret).toBe(TEST_SECRET);
|
|
96
|
+
expect(config.oauth.jwt?.mode).toBe("verify");
|
|
97
|
+
expect(config.oauth.jwt?.issuer).toBe("https://issuer.test");
|
|
98
|
+
expect(config.oauth.jwt?.audience).toBe("https://api.test");
|
|
99
|
+
expect(config.oauth.jwt?.tokenType).toBe("at+jwt");
|
|
98
100
|
expect(config.verify.maxBatchSize).toBe(50);
|
|
99
101
|
},
|
|
100
102
|
);
|
|
@@ -170,7 +172,7 @@ describe("loadAppConfig", () => {
|
|
|
170
172
|
|
|
171
173
|
const config = loadAppConfig(configDirPath, "development");
|
|
172
174
|
|
|
173
|
-
expect(config.oauth.jwt
|
|
175
|
+
expect(config.oauth.jwt?.mode).toBe("insecure-decode");
|
|
174
176
|
});
|
|
175
177
|
|
|
176
178
|
it("rejects a boolean-ish OAUTH_JWT_MODE left over from the removed OAUTH_JWT_VALIDATE", () => {
|
|
@@ -237,11 +239,11 @@ describe("loadAppConfig — numeric knobs through the real 3-tier resolution (#1
|
|
|
237
239
|
const config = loadAppConfig(configDirPath, "development");
|
|
238
240
|
|
|
239
241
|
expect(config.http.port).toBe(3000);
|
|
240
|
-
expect(config.oauth.jwt
|
|
241
|
-
expect(config.oauth.jwt
|
|
242
|
-
expect(config.oauth.jwt
|
|
243
|
-
expect(config.oauth.jwt
|
|
244
|
-
expect(config.oauth.jwt
|
|
242
|
+
expect(config.oauth.jwt?.jwksTimeoutMs).toBe(5000);
|
|
243
|
+
expect(config.oauth.jwt?.jwksCooldownMs).toBe(30_000);
|
|
244
|
+
expect(config.oauth.jwt?.jwksCacheMaxAgeMs).toBe(600_000);
|
|
245
|
+
expect(config.oauth.jwt?.maxTokenAgeSeconds).toBe(86_400);
|
|
246
|
+
expect(config.oauth.jwt?.clockToleranceSeconds).toBe(0);
|
|
245
247
|
expect(config.verify.maxBatchSize).toBe(50);
|
|
246
248
|
// The request limits (#118) travel the same path.
|
|
247
249
|
expect(config.verify.maxBodyBytes).toBe(65_536);
|
|
@@ -268,15 +270,17 @@ describe("loadAppConfig — numeric knobs through the real 3-tier resolution (#1
|
|
|
268
270
|
process.env.VERIFY_COLLECTOR_TIMEOUT_MS = "750";
|
|
269
271
|
process.env.VERIFY_COLLECTOR_DEADLINE_MS = "1500";
|
|
270
272
|
process.env.VERIFY_COLLECTOR_CONCURRENCY = "4";
|
|
273
|
+
process.env.VERIFY_RULE_TIMEOUT_MS = "900";
|
|
274
|
+
process.env.VERIFY_EVALUATE_DEADLINE_MS = "2500";
|
|
271
275
|
|
|
272
276
|
const config = loadAppConfig(configDirPath, "development");
|
|
273
277
|
|
|
274
278
|
expect(config.http.port).toBe(8080);
|
|
275
|
-
expect(config.oauth.jwt
|
|
276
|
-
expect(config.oauth.jwt
|
|
277
|
-
expect(config.oauth.jwt
|
|
278
|
-
expect(config.oauth.jwt
|
|
279
|
-
expect(config.oauth.jwt
|
|
279
|
+
expect(config.oauth.jwt?.jwksTimeoutMs).toBe(2500);
|
|
280
|
+
expect(config.oauth.jwt?.jwksCooldownMs).toBe(0);
|
|
281
|
+
expect(config.oauth.jwt?.jwksCacheMaxAgeMs).toBe(120_000);
|
|
282
|
+
expect(config.oauth.jwt?.maxTokenAgeSeconds).toBe(600);
|
|
283
|
+
expect(config.oauth.jwt?.clockToleranceSeconds).toBe(60);
|
|
280
284
|
expect(config.verify.maxBatchSize).toBe(25);
|
|
281
285
|
expect(config.verify.maxBodyBytes).toBe(16_384);
|
|
282
286
|
expect(config.verify.maxResourceLength).toBe(128);
|
|
@@ -286,6 +290,8 @@ describe("loadAppConfig — numeric knobs through the real 3-tier resolution (#1
|
|
|
286
290
|
expect(config.verify.collectorTimeoutMs).toBe(750);
|
|
287
291
|
expect(config.verify.collectorDeadlineMs).toBe(1_500);
|
|
288
292
|
expect(config.verify.collectorConcurrency).toBe(4);
|
|
293
|
+
expect(config.verify.ruleTimeoutMs).toBe(900);
|
|
294
|
+
expect(config.verify.evaluateDeadlineMs).toBe(2_500);
|
|
289
295
|
});
|
|
290
296
|
|
|
291
297
|
it.each([
|
|
@@ -304,6 +310,8 @@ describe("loadAppConfig — numeric knobs through the real 3-tier resolution (#1
|
|
|
304
310
|
["VERIFY_COLLECTOR_TIMEOUT_MS", "0"],
|
|
305
311
|
["VERIFY_COLLECTOR_DEADLINE_MS", "-1"],
|
|
306
312
|
["VERIFY_COLLECTOR_CONCURRENCY", "0"],
|
|
313
|
+
["VERIFY_RULE_TIMEOUT_MS", "0"],
|
|
314
|
+
["VERIFY_EVALUATE_DEADLINE_MS", "-1"],
|
|
307
315
|
])("refuses to boot on %s=%s", (key, value) => {
|
|
308
316
|
setRequiredEnv();
|
|
309
317
|
process.env[key] = value;
|
|
@@ -381,7 +389,7 @@ rule { collectors = [ { collector = ResourceActionScopeRuleCollector } ] }
|
|
|
381
389
|
|
|
382
390
|
const config = loadAppConfig(withoutVerify(), "development");
|
|
383
391
|
|
|
384
|
-
// All
|
|
392
|
+
// All twelve, spelled out. A knob that reached the shape but not the
|
|
385
393
|
// block's `.default()` literal is `undefined` here, and `toEqual` says
|
|
386
394
|
// which one.
|
|
387
395
|
expect(config.verify).toEqual({
|
|
@@ -392,6 +400,8 @@ rule { collectors = [ { collector = ResourceActionScopeRuleCollector } ] }
|
|
|
392
400
|
maxContextEntries: 64,
|
|
393
401
|
maxContextValueLength: 1_024,
|
|
394
402
|
collectorTimeoutMs: 2_000,
|
|
403
|
+
ruleTimeoutMs: 2_000,
|
|
404
|
+
evaluateDeadlineMs: 5_000,
|
|
395
405
|
collectorDeadlineMs: 5_000,
|
|
396
406
|
collectorConcurrency: 8,
|
|
397
407
|
batchConcurrency: 8,
|
package/templates/versions.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"@o3co/auth.policy-verifier.core": "0.
|
|
3
|
-
"@o3co/auth.policy-verifier.builtins": "0.
|
|
4
|
-
"@o3co/auth.policy-verifier.server": "0.
|
|
2
|
+
"@o3co/auth.policy-verifier.core": "0.10.0",
|
|
3
|
+
"@o3co/auth.policy-verifier.builtins": "0.10.0",
|
|
4
|
+
"@o3co/auth.policy-verifier.server": "0.10.0"
|
|
5
5
|
}
|