Skip to content

feat: implement the jotsmith OIDC issuer CLI#14

Merged
MaxAnderson95 merged 2 commits into
mainfrom
feat/initial-cli-implementation
Jun 1, 2026
Merged

feat: implement the jotsmith OIDC issuer CLI#14
MaxAnderson95 merged 2 commits into
mainfrom
feat/initial-cli-implementation

Conversation

@MaxAnderson95

Copy link
Copy Markdown
Owner

What

Initial implementation of the entire jotsmith CLI — a single-user tool that stands up a personal OIDC issuer in Azure (Storage Account static website + Key Vault) and mints JWTs of arbitrary shape against it. This brings the repo from spec-only (PRD/CONTEXT/ADRs) to a complete, tested binary covering all 13 planned slices.

Commands

  • setup — configure an existing Storage Account + Key Vault as an issuer: enable static website hosting, create the RSA signing key, publish the discovery document and JWKS, and write the local config. Idempotent (re-run is a refresh, not a re-create).
  • token mint — assemble standard claims (--sub, --aud, --exp, --iat, --nbf, --jti) and custom claims (--claim, --claim-json, --claims-file), then sign inside Key Vault. stdout is the compact JWT and nothing else.
  • token verify — live discovery + JWKS round-trip, RS256 signature check, and standard-claim validation with 60s clock skew.
  • token decode — inspect a JWT without verifying it.
  • key rotate — snap-cutover rotation (ADR-0005); tokens under the prior key stop verifying immediately.
  • doctor — read-only PASS/WARN/FAIL audit, plus --repair (re-upload docs, re-enable static website) and --json for agents.
  • discovery show / jwks show / config show — print what would be (or is) published.
  • destroy — soft-delete the key and remove published blobs, keeping the Azure resources (--all also removes the local config).
  • completion — emit bash / zsh / fish / powershell completion scripts.

Invariants upheld

  • Private key material never leaves Key Vault — only the SHA-256 digest of the signing input is sent to KV's Sign API.
  • iss is always read from config; there is no --iss flag.
  • kid is the RFC 7638 thumbprint of the public key; RS256 only.
  • The tool never provisions Azure resources (the single exception is enabling static website hosting on the existing account — ADR-0002).

Tech

Go 1.25, urfave/cli/v3, the modular azure-sdk-for-go packages, and DefaultAzureCredential. Unit tests throughout; integration tests behind the integration build tag. Includes CI (SHA-pinned actions), golangci-lint v2 config (0 issues), and a goreleaser config.

Testing

Verified end-to-end against real Azure (StorageV2 + RBAC-mode Key Vault): setup enabled static website hosting and published the documents, doctor reported all PASS, a minted token verified live, key rotate invalidated the prior token, doctor --repair fixed injected drift, and destroy tore the issuer state down. Test resources were removed afterward.

Originally created in OpenCode session ID: ses_17c986ef9ffeHc0S3r1wQcmLDT

Initial implementation of the full command surface from PRD.md:

- setup: configure an existing Storage Account + Key Vault as an OIDC
  issuer (enable static website hosting, create the RSA signing key,
  publish the discovery document and JWKS). Idempotent.
- token mint: assemble standard and custom claims and sign inside Key
  Vault (RS256); stdout is the compact JWT and nothing else.
- token verify: live discovery + JWKS round-trip with RS256 signature
  and standard-claim checks (60s clock skew).
- token decode: inspect a JWT without verifying it.
- key rotate: snap-cutover rotation (ADR-0005).
- doctor: read-only audit with --repair (re-upload docs / re-enable
  static website) and --json output.
- discovery show / jwks show / config show.
- destroy: tear down the issuer state, keeping the Azure resources.
- completion: bash/zsh/fish/powershell scripts.

Private key material never leaves Key Vault; iss always comes from
config; kid is the RFC 7638 thumbprint. Built on urfave/cli v3 and the
modular Azure SDK with DefaultAzureCredential. Includes unit tests
throughout plus integration tests behind the integration build tag, CI,
golangci-lint v2, and goreleaser config.

OpenCode session ID: ses_17c986ef9ffeHc0S3r1wQcmLDT
Centralize discovery and JWKS rendering so setup, rotate, doctor repair, and show commands use the same document path. Enforce the v1 single RS256 JWK invariant during doctor and publishing, remove arbitrary signing-key bit-size plumbing, canonicalize discovery issuer rendering, and make doctor --e2e perform a real mint/verify round trip.\n\nOpenCode session ID: ses_17c2d0269ffeCyZ490A503eFP2
@MaxAnderson95

Copy link
Copy Markdown
Owner Author

Follow-up: thermo-nuclear review fixes

Pushed commit a411afa back to this PR.

What changed:

  • Centralized discovery/JWKS rendering in internal/cli/publish.go so setup, doctor --repair, key rotate, and discovery/jwks show use the same document-generation path.
  • Enforced the ADR-0005 v1 JWKS invariant with jwk.Set.SingleRS256(): exactly one RSA signing JWK, use: sig, alg: RS256, non-empty key material, and kid matching the RFC 7638 thumbprint. doctor now fails multi-key/stale/malformed JWKS instead of accepting the first key.
  • Removed the fake CreateRSAKey(bits) degree of freedom. Azure key creation is now CreateSigningKey(), with RSA 2048 owned inside azurex, matching the RS256-only v1 invariant.
  • Canonicalized issuer rendering in oidc.Render so a trailing slash cannot leak into the discovery document's issuer field.
  • Replaced the visible doctor --e2e stub with a real short-lived mint/verify path using live discovery and JWKS fetches.
  • Added tests covering single-key JWKS validation, multi-key doctor failure, issuer canonicalization, and the doctor --e2e happy path.

Verification:

  • go build ./...
  • go test ./...
  • go vet -tags integration ./...
  • GOTOOLCHAIN=go1.25.5 golangci-lint run

Note: goreleaser check was not run because goreleaser is not installed in this environment.

Originally created in OpenCode session ID: ses_17c2d0269ffeCyZ490A503eFP2

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