Skip to content

fix(api): raise the OAuth code length cap so Microsoft sign-in works; guard wrong-cluster deploys; stop scan-containers passing silently - #628

Merged
ericfitz merged 1 commit into
mainfrom
fix/oauth-code-length-and-deploy-guards
Jul 30, 2026
Merged

fix(api): raise the OAuth code length cap so Microsoft sign-in works; guard wrong-cluster deploys; stop scan-containers passing silently#628
ericfitz merged 1 commit into
mainfrom
fix/oauth-code-length-and-deploy-guards

Conversation

@ericfitz

Copy link
Copy Markdown
Owner

Four changes, all traced to the same shape: tooling that reported success while doing the wrong thing, or nothing.

1. Microsoft sign-in (the user-facing bug)

Entra sign-in failed on AWS with a 400 from request validation, before the handler ran:

POST /oauth2/token -> 400: doesn't match schema: Error at "/code": maximum string length is 512

Entra v2.0 codes routinely exceed 700 chars (the failing bodies were 872 bytes) and can pass 2000 with extra scopes/claims. Google's are ~100, so only Microsoft was affected and the cap looked fine for a year.

maxLength8192 (raised, not removed — the input stays bounded), with the reason documented on the property so it isn't tightened again.

The two media types also disagreed with each other:

content type before after
application/json pattern + maxLength: 512 pattern + maxLength: 8192
application/x-www-form-urlencoded neither pattern + maxLength: 8192

Same field, same endpoint, different validation depending on how you encoded it.

Verified before/after on a live cluster:

code length before after
800 (Entra-sized) 400 maxLength reaches handler → invalid_grant
8000 reaches handler ✅
9000 400 maxLength — bound still real ✅

Versions → 1.6.0 (.version + OpenAPI info.version). api.go regenerated with oapi-codegen v2.7.1; confirmed the 2859-line diff is the embedded spec blob plus two doc comments, no structural change.

2. make dev-* require an explicit CLUSTER

CLUSTER defaulted to docker-desktop. Both of these happened today: make dev-up against a k3s-hosted server rebuilt and rolled a cluster nobody was watching while the real target stayed stale, and make dev-status reported on a different cluster than the one just deployed.

No default now — exit 2 with a copy-pasteable hint. The hint prints your current kube context but says explicitly that it does not select the target, since that exact ambiguity is what made the mistake plausible.

Applied to the whole dev-* family, not just dev-up: dev-nuke and dev-down make the same mistake unrecoverable.

3. deploy-aws.sh can no longer act on the wrong cluster

It called bare kubectl, resolving against the global current-context. Two harms: a concurrent make dev-up could retarget the AWS workload apply at a local cluster, and aws eks update-kubeconfig mutated the operator's kubeconfig as a side effect — silently leaving your shell pointed at production after every deploy.

  • Isolated KUBECONFIG (throwaway temp file, --kubeconfig passed explicitly): children can only ever see EKS, and your kubeconfig is never touched.
  • Identity assertion before each mutating apply, comparing the API server URL from the AWS API against the resolved kubeconfig — not the context name, which is an arbitrary local label that proves nothing about which cluster answers.

Verified both directions: aborts against k3s (https://rp2:6443), passes against the real EKS endpoint. The destroy path is covered too, since it deletes an ingress.

4. make scan-containers no longer reports success without scanning

The worst of the four. scan_image() initialized the CVE counts to 0 and only filled them in if returncode == 0, with every grype call on check=False. So an image grype couldn't even resolve — typically one never built locally, where it falls through to Docker Hub and gets UNAUTHORIZED — kept the counts at 0, passed both thresholds, and returned True:

grype tmi/tmi-component-controller:latest → could not determine source
[INFO] Found 0 critical and 0 high severity vulnerabilities
[SUCCESS] All scans completed          ← exit 0, four images never read

A security control that reports success without scanning is worse than one that's switched off, because nobody goes looking for it.

The JSON scan now runs first and is authoritative: nonzero exit, unparseable output, or missing source metadata is a hard failure, and the error says whether the image is absent locally (with the build command) or the scanner itself is broken. SBOM success is claimed only when a non-empty file actually landed, and stale artifacts are deleted first so last build's SBOM can't look current. The redundant first SARIF invocation is gone — it printed nowhere and doubled the error wall.

Now exits nonzero, scans tmi-server for real (0 critical / 0 high, 497KB SBOM written), and reports the other four as unbuilt.

Gates

make validate-openapi 0 errors · make lint 0 issues · make build-server (Version: 1.6.0) · make test-unit 2447 passed / 0 failed · ruff clean on the changed file · bash -n clean

Notes

Also disables the post-commit version hook (an explanatory no-op — it could never fire under PR-only merges) and rewrites CLAUDE.md's versioning section to say versioning is manual until #627, listing the three values that must be bumped together.

Known remaining gap, not addressed here: a grype run against a stale vulnerability DB still reports few findings, which is a weaker version of the same silent pass. During the earlier AWS deploy grype couldn't reach its DB at all. Worth gating on DB age separately.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WnrVyySuyJFqcMxB8q3pNc

Microsoft/Entra sign-in failed on AWS with a 400 from request validation before
the handler ran:

  POST /oauth2/token -> 400: doesn't match schema:
    Error at "/code": maximum string length is 512

Entra v2.0 authorization codes routinely exceed 700 characters (the failing
request bodies were 872 bytes) and can pass 2000 with additional scopes/claims.
Google's are ~100, so only Microsoft was affected and the cap looked fine.

Raise maxLength to 8192 rather than removing it, so the input stays bounded, and
document the reason on the property so it is not tightened again.

Also align the two media types, which disagreed: the application/json variant
carried the pattern and the cap, while application/x-www-form-urlencoded
($ref TokenRequest) had NEITHER. Same field, same endpoint, different
validation. Both now carry both.

Verified before/after on a live cluster: an 800-char code went from the
validation 400 to reaching the handler (invalid_grant, correct for a fake code);
9000 is still rejected, so the bound is real.

Versions bumped to 1.6.0 (.version + OpenAPI info.version); api.go regenerated
with oapi-codegen v2.7.1 — the diff is the embedded spec blob plus two doc
comments, no structural change.

--- make dev-* now require an explicit CLUSTER ---

CLUSTER defaulted to docker-desktop, so `make dev-up` run against a k3s-hosted
server rebuilt and rolled a cluster nobody was watching while the real target
stayed stale, and `make dev-status` reported on a different cluster than the one
just deployed. Both happened today. There is now no default: every dev-* target
exits 2 with a copy-pasteable hint. The hint prints the current kube context but
states explicitly that it does NOT select the target, since that ambiguity is
what made the original mistake plausible. Applied to the whole dev-* family, not
just dev-up, because dev-nuke and dev-down make the same mistake unrecoverable.

--- deploy-aws.sh can no longer act on the wrong cluster ---

The script called bare kubectl, resolving against the GLOBAL current-context.
Anything switching context mid-run (a concurrent make dev-up) could retarget the
AWS workload apply at a local cluster, and `aws eks update-kubeconfig` mutated
the operator's kubeconfig as a side effect, silently leaving their shell pointed
at production after every deploy.

Both fixed: the deploy gets its own throwaway KUBECONFIG (so children can only
ever see EKS, and the operator's file is never touched), plus an identity
assertion before each mutating apply. The assertion compares the API SERVER URL
from the AWS API against the resolved kubeconfig, not the context name — names
are arbitrary local labels and prove nothing about which cluster answers.
Verified both directions: aborts against k3s (https://rp2:6443), passes against
the real EKS endpoint. The destroy path is covered too, since it deletes an
ingress.

--- make scan-containers no longer reports success without scanning ---

scan_image() initialized the CVE counts to 0 and only filled them in when grype
exited 0, with every grype call on check=False. So an image grype could not even
resolve — the common case being one never built locally, where it falls through
to Docker Hub and gets UNAUTHORIZED — kept counts at 0, passed both thresholds,
and returned True. `make scan-containers` printed "Found 0 critical and 0 high"
and "All scans completed", exit 0, for four images it had never read.

The JSON scan now runs first and is authoritative: a nonzero exit, unparseable
output, or missing `source` metadata is a hard failure, with the error naming
whether the image is absent locally (and how to build it) or the scanner itself
is broken. SBOM success is claimed only when a non-empty file actually landed,
and stale artifacts are removed first so last build's SBOM cannot look current.
The redundant first SARIF invocation is gone (it printed to nowhere and doubled
the error wall).

Now correctly exits nonzero, scanning tmi-server for real and reporting the
other four as unbuilt.

Refs #627

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WnrVyySuyJFqcMxB8q3pNc
@ericfitz
ericfitz merged commit 9271429 into main Jul 30, 2026
12 checks passed
@ericfitz
ericfitz deleted the fix/oauth-code-length-and-deploy-guards branch July 30, 2026 14:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant