This file provides guidance to Claude Code (claude.ai/code) when working with code in this
repository. It is the single guide for the whole mini- family: the umbrella conventions
first, then a folded-in section per shipping service (mini-kms, mini-idp, mini-directory, mini-oidc,
mini-gateway, mini-ca). There are no per-module CLAUDE.md files — this is it.
Read first.
docs/DIRECTION.mdis the canonical map of the family — the vision, the component catalog, the architecture, the runtime relationships, the roadmap, and themini-commonextraction candidates. Read it before touching code, then read the relevant service section below and the module's ownREADME.md.
mini-auth is the monorepo for a family of small, single-responsibility auth/identity services and libraries, built in the spirit of mini-kms and mini-idp: educational, but homelab-functional. The code is meant to be read — heavily commented, JDK-first, real-but- un-audited crypto via vetted libraries. mini-auth itself is two things:
- One aggregator build —
./gradlew buildfrom the root compiles and tests the whole family (vendored services included), under one wrapper, one version catalog, and one set of convention plugins. - The shared direction doc (
docs/DIRECTION.md) — the map the individual modules can't carry.
mini-kms and mini-idp were two formerly-independent Gradle builds, pulled in and unified here:
one root wrapper, one settings.gradle.kts, one gradle/libs.versions.toml, one build-logic
convention-plugin set, and one CI workflow. mini-auth does not re-implement what the shipping
services already do — it composes them and adds the connective tissue (mini-token,
mini-policy) and the new front doors (mini-oidc, mini-gateway, mini-directory).
- Small, single-responsibility, readable. A "mini" is either a library (focused machinery) or a service (a thin deployable front door that wires libraries together and adds a transport). Clarity and correct security reasoning matter more than features; comments carry teaching weight.
- Real but un-audited crypto via vetted libraries. Never hand-roll crypto you can get from pk-auth or the existing token plane (mini-idp / the future mini-token). Where the family does hand-roll a format (mini-idp's JWS, mini-kms's envelopes), it is deliberate, isolated, and heavily commented — match that bar, don't add new hand-rolled crypto casually.
corestays I/O-free. Crypto and domain logic never import a transport (no sockets, HTTP, or CLI parsing incore). The composition root lives in theserver/applicationmodule.- Scaffolds say so. A "scaffolded" module compiles and passes a trivial test, with the real protocol/crypto left as clearly-marked TODOs at the seams. Do not turn a scaffold into a half-built service that looks finished — preserve the honest seams.
- No secrets in logs. Tokens, passphrases, private keys, and request/response bodies are never logged. Access logs record method/path/status only.
- No oracles. Auth/crypto failures collapse to one generic error (mini-idp's single
invalid_client; mini-kms's singleDecryptionFailed) — never distinguish unknown-principal from wrong-secret, or leak why a decrypt failed. - Constant-time secret comparison (
MessageDigest.isEqual); handle passphrases aschar[]and zero them. - Secrets via env/file, never argv. Follow the existing
resolveAdminToken/resolveToken/readPassphrasepatterns. - At-rest stores are atomic +
0600(temp-file →ATOMIC_MOVE→0600; mini-idp'sJsonStore, mini-kms'sKeystore). Integrity-protect metadata where the siblings do. - Loopback bind by default. Exposing anything beyond loopback is an explicit operator decision.
- Use
System.Logger, not a third-party logging dependency.
JDK 21+ on PATH (the Gradle toolchain is pinned to 21; foojay can auto-download it). There is
one root wrapper — run every Gradle command from the repo root.
./gradlew build # compile + run ALL tests across the whole family — this IS the CI gate
./gradlew test # tests only, all modules
./gradlew :services:mini-idp:core:test # one module
./gradlew :services:mini-kms:core:test --tests "*LocalKeyringTest" # one class
./gradlew :services:mini-oidc:installDist # runnable launcherThere is no separate linter/formatter; ./gradlew build is the full gate. Tests are JUnit 5.
Gradle's configuration cache is on (org.gradle.configuration-cache=true), so build-script changes
(including edits to build-logic) may need --no-configuration-cache while iterating.
After any change, ./gradlew build from the repo root must be green and all pre-existing tests
must still pass.
Modules are grouped by role under services/ and libs/; the Gradle project path follows the
directory.
mini-auth/
├── settings.gradle.kts # one build: includes every module + pluginManagement.includeBuild(build-logic)
├── build.gradle.kts # just the `base` plugin — conventions live in build-logic
├── gradle/libs.versions.toml # one catalog for the whole family (Jackson 3.x)
├── build-logic/ # included build: the shared convention plugins
│ └── src/main/kotlin/
│ ├── miniauth.java-conventions.gradle.kts # toolchain 21, JUnit 5, -parameters, test deps
│ ├── miniauth.library-conventions.gradle.kts # java-conventions + java-library
│ └── miniauth.application-conventions.gradle.kts # java-conventions + application
├── docs/DIRECTION.md
├── services/ # deployable front doors
│ ├── mini-kms/ {core, server, client} (shipping)
│ ├── mini-idp/ {core, server} (shipping)
│ ├── mini-oidc/ (shipping; human SSO / OpenID Provider, embeds pk-auth)
│ ├── mini-gateway/ (shipping; forward-auth for a reverse proxy)
│ ├── mini-directory/ (shipping; identity source of truth; mini-idp reads service accounts from it)
│ ├── mini-ca/ (shipping; internal CA, CA key wrapped under mini-kms)
│ └── mini-console/ (shipping; optional unified admin console + exercise harness over the family)
└── libs/ # shared libraries (no transport)
├── mini-token/ (shipping; token plane + shared SSO session store)
├── mini-policy/ (shipping; shared decision engine)
├── mini-client-common/ (shipping; shared HTTP/token/JSON plumbing + no-oracle collapse for the client libs)
├── mini-directory-client/ (shipping; HTTP client consumed by mini-console)
├── mini-idp-client/ (shipping; HTTP client consumed by mini-console)
├── mini-oidc-client/ (shipping; HTTP client consumed by mini-console)
├── mini-ca-client/ (shipping; HTTP client consumed by mini-console)
└── mini-gateway-client/ (shipping; forward-auth /verify client — maps status→outcome, no body oracle)
| Module | Project path | Type | Status |
|---|---|---|---|
| mini-kms | :services:mini-kms:core/server/client |
service | shipping (§ below) |
| mini-idp | :services:mini-idp:core/server |
service | shipping (§ below) |
| mini-oidc | :services:mini-oidc |
service (application) | shipping (§ below) — embeds pk-auth |
| mini-gateway | :services:mini-gateway |
service (application) | shipping (§ below) |
| mini-directory | :services:mini-directory |
service (application) | shipping (§ below) |
| mini-ca | :services:mini-ca |
service (application) | shipping (§ below) |
| mini-console | :services:mini-console |
service (application) | shipping (§ below) — optional admin console + exercise harness |
| mini-token | :libs:mini-token |
library | shipping (token plane extracted from mini-idp) |
| mini-policy | :libs:mini-policy |
library | shipping (shared decision engine; consumed by directory/oidc/gateway/kms) |
| mini-client-common | :libs:mini-client-common |
library | shipping (HTTP/token/JSON plumbing + no-oracle collapse for the client libs) |
| mini-directory-client | :libs:mini-directory-client |
library | shipping (consumed by mini-console) |
| mini-idp-client | :libs:mini-idp-client |
library | shipping (consumed by mini-console) |
| mini-oidc-client | :libs:mini-oidc-client |
library | shipping (consumed by mini-console) |
| mini-ca-client | :libs:mini-ca-client |
library | shipping (consumed by mini-console) |
| mini-gateway-client | :libs:mini-gateway-client |
library | shipping (consumed by mini-console) |
- Convention plugins, not
subprojects {}. The shared Java conventions (JDK 21 toolchain, Maven Central, JUnit 5 + the common test stack, the-parametersflag) live in thebuild-logicincluded build and are applied per-module by id:id("miniauth.library-conventions")for libraries and the I/O-freecores,id("miniauth.application-conventions")for runnable services. The empty grouping projects (:services,:libs) stay inert. - Editing
build-logic: keep its plugin descriptors valid by building the whole repo. One sharp edge: do not put backticks in a comment that precedes aplugins {}block in any*.gradle.kts— Gradle's lightweight prescan of that block can be derailed by backtick-quoted text in the comment, and the plugin silently fails to apply. - Base package is
com.codeheadsystems.<mini>(e.g.com.codeheadsystems.minitoken). The package names are independent of the directory grouping — they were not renamed when modules moved underservices//libs/. - Use the shared version catalog (
gradle/libs.versions.toml) — never pin a version inline. -parametersis required family-wide (set inminiauth.java-conventions): Jackson binds records (protocol/keystore/store/claim DTOs) by constructor parameter name.- Jackson 3.x (
tools.jackson.*) everywhere. Mappers are immutable — build withJsonMapper.builder()…build()(no instanceenable/configure); read/write throw the uncheckedtools.jackson.core.JacksonException. Onlyjackson-annotationsstays oncom.fasterxml.jackson.annotation, so@JsonProperty/@JsonInclude/@JsonCreatorimports are unchanged. YAML usestools.jackson.dataformat.yaml.YAMLMapper. - pk-auth is external, not vendored —
com.codeheadsystems:pk-auth-corefrom Maven Central (mini-oidc's passkey credential layer).
- Read
docs/DIRECTION.mdand the relevant existing module(s) before writing code. The runtime relationships (both issuers go through mini-token; everything decides through mini-policy; mini-token's signing keys are eventually wrapped by mini-kms; mini-directory is the identity source of truth) shape where new code belongs. - Refactors are behavior-preserving — structure only, no functional change; the test suite must still pass unchanged.
- The token-claim contract aligns across the family. mini-idp's
grantsclaim maps onto mini-kms's authorization model (sub → Principal.id,grants.control → Principal.admin,grants.groups[]→ a per-key-groupPolicyEnginedecision (mini-policy));auth/KeyOperationstring values are the contract — don't rename them. Preserve this mapping in mini-token / mini-policy work. Note this is a designed contract, not a wired runtime path:KmsRequestHandlerstill authenticates with a shared per-plane token + fixed principals and shipsAllowAllPolicyEngine— it does not yet parse a JWT or consumegrants. (GrantsClaim.toAuthorization()now does have a production caller, but it is mini-gateway'sBearerAuthenticatormapping a machine token'sgrantsinto forward-auth scopes — not mini-kms.) The token → mini-kms authorization step is a future seam; see DIRECTION.md's "Wired vs. designed" note. - Don't duplicate foundation code. Argon2 hashing, the atomic-
0600JSON store, base64url, constant-time compare, and theServerConfigenv/file token pattern exist in both shipping services today. They are catalogued asmini-commonextraction candidates indocs/DIRECTION.md— when adding similar machinery, prefer extending one of those (and note the duplication) over writing a third copy. The extraction itself is a planned, separate step.
mini-kms is an educational single-machine Key Management Service in Java: envelope encryption with rotatable keys served to local processes over sockets. Heavily commented on purpose — the code is meant to be read to learn how a KMS works. Real, sound crypto (Argon2id, AES-256-GCM/AEAD), explicitly not production-audited.
Run it locally. Server reads the passphrase with no echo (Console.readPassword), or
MINIKMS_PASSPHRASE when there's no TTY. Both tokens come from env vars or files, never CLI args.
export MINIKMS_API_TOKEN="$(openssl rand -hex 32)" # data plane
export MINIKMS_ADMIN_TOKEN="$(openssl rand -hex 32)" # control plane
./gradlew :services:mini-kms:server:installDist :services:mini-kms:client:installDist
services/mini-kms/server/build/install/server/bin/server --tcp-port 9123 --keystore ~/.mini-kms/keystore.json
services/mini-kms/client/build/install/client/bin/client --tcp 127.0.0.1:9123 health # data-plane CLI
services/mini-kms/client/build/install/client/bin/kms-admin --tcp 127.0.0.1:9123 list-keys # control-plane CLIArchitecture. Three modules under base package com.codeheadsystems.minikms (paths
:services:mini-kms:core/server/client):
-
core— all crypto + key management, no socket/transport/CLI code. Owns the wire-protocol DTOs (shared by server + client), the envelope formats, the keyring, and the request handler. This I/O-free separation is load-bearing. -
server— the socket daemon (ServerMain); depends oncore. -
client—KmsClientlibrary plus two CLIs (ClientMain= data plane,KeyringAdminMain= control plane); depends oncore. Theclientbuild registers a second launcher (kms-admin) in the same distribution — preserve that when touching its build file. -
client/KmsSigningKeyStore— the recursive integration: aDocumentStore<SigningKeys>(mini-token's key-at-rest SPI) that envelope-wraps each Ed25519 signing key under a mini-kms key group viaKmsClient.encrypt/decrypt(andreEncryptfor KEK rotation), so the auth services' signing keys never sit plaintext on disk. It lives here (not in mini-token) to keep the dependency acyclic —clientgained amini-tokendependency; mini-token knows nothing of mini-kms. mini-idp and mini-oidc opt in via--kms-tcp/--kms-key-group. Note the leaf-name jar fix: bothmini-kms:coreandmini-idp:coreset a uniquebase.archivesName, because a service bundling both would otherwise collide oncore.jar. -
Two planes, two tokens. Every
RequestTypeis taggedDATAorCONTROL(RequestPlane).KmsRequestHandler(incore) routes by plane and validates the matching token — the API token for data ops, a separate admin token for control ops. Data-plane ops also pass through a per-key-groupPolicyEnginedecision (mini-policy); the shippedAllowAllPolicyEngineis the documented seam for per-client authz later (the thingmini-policygeneralizes). -
The two seams.
KmsRequestHandlerdepends only onMasterKeyProvider(data:wrap/unwrap/encrypt/decrypt/keyIdOf) andKeyringManager(control:create/rotate/list/disable/enable/destroy), both implemented byLocalKeyring. Each ciphertext carries its ownkek_idinside the opaque blob, so a remote/HSM provider can drop in later. Preserve this boundary — don't make the handler reach past these interfaces. -
Key hierarchy.
passphrase --Argon2id+salt--> root key --wraps--> KEK versions --wrap--> DEKs --AES-GCM--> data. Root key and KEKs exist only asbyte[]and are zeroed on shutdown (LocalKeyring.close()).DestroyVersionis intentionally irreversible (crypto-shredding). -
Crypto & formats.
crypto/AesGcmis the only place raw symmetric crypto happens (nonces always fresh-random, never caller-supplied). Three nested binary formats:EnvelopeFormat,KekEnvelope(client-facing blob), client-onlyFileEnvelope(MKE1). The keystore (keystore.json,0600) holds an HMAC over all metadata (KeystoreIntegrity); the MAC is required on load (tampering is rejected; pre-MAC keystores fail to load). -
Transport. Loopback TCP + a Unix domain socket (
0600in a0700dir); each connection on a virtual thread. Newline-delimited JSON, bounded per frame; aSemaphoreconnection cap + an idle-timeout watchdog.ConnectionHandleris transport-agnostic. -
Conventions.
corestays I/O-free (no sockets/files beyondKeystore/CLI). Any keyring/AEAD failure flattens to oneDecryptionFailed. Secrets viaresolveToken/readPassphrase(passphrases aschar[], zeroed). -
Docs.
services/mini-kms/README.md(architecture, formats, glossary) andservices/mini-kms/docs/security/(review findings: connection exhaustion, keystore integrity; one open item: loopback-TCP local exposure).
mini-idp is an educational identity provider in Java. It issues short-lived Ed25519-signed JWT
access tokens via the OAuth 2.0 client-credentials grant and publishes its public signing keys
(JWKS) so a verifier can validate tokens offline. It is a pure token issuer: service accounts
(the OAuth clients) live in mini-directory, and mini-idp resolves a client's credentials and grants
from there at token issuance (over the ServiceAccountDirectory SPI). Real crypto (Ed25519/EdDSA),
not production-audited. Optionally wraps its signing keys under mini-kms (the recursive integration);
otherwise no code-level dependency on mini-kms.
Run it locally. The bootstrap admin token comes from an env var or a file, never a CLI arg, and is never logged.
export MINIIDP_ADMIN_TOKEN="$(openssl rand -hex 32)"
./gradlew :services:mini-idp:server:installDist
services/mini-idp/server/build/install/server/bin/server --port 8455 --data-dir ~/.mini-idp
# Register a client (admin), then exchange credentials for a token; browse the API at /docs.Architecture. Two modules under base package com.codeheadsystems.miniidp (paths
:services:mini-idp:core/server):
-
core— now just the atomic-0600store/JsonStore(which implements mini-token'sDocumentStoreSPI), backing the signing-key / revocation / audit documents. The token plane was extracted to:libs:mini-token; the client registry + Argon2 hashing moved to mini-directory. Both are consumed as dependencies. -
server— the HTTP daemon (ServerMain, JDKcom.sun.net.httpserver.HttpServeron loopback), router, handlers, config, thedirectory/package (theServiceAccountDirectorySPI +HttpServiceAccountDirectory/InMemoryServiceAccountDirectory+ grant reassembly), and the OpenAPI spec + vendored Swagger UI. Depends oncore, mini-token, and mini-kms:client. -
Service accounts come from mini-directory. At
/oauth/token, mini-idp resolves the client via theServiceAccountDirectorySPI — productionHttpServiceAccountDirectoryPOSTs to mini-directory's/admin/service-accounts/authenticate(verification happens there; the secret hash never leaves the directory), tests useInMemoryServiceAccountDirectory. The resolved grants are reassembled into the per-key-groupgrantsclaim, so the token is identical to the old registry's output.--directory-url+ a directory admin token are required. -
The token plane lives in
mini-token. The Ed25519 keys, the hand-rolled JWS/JWT, the JWKS model, thegrantsclaim, the auth model the claim maps onto, and the signing-key / revocation / audit services are all in:libs:mini-token(com.codeheadsystems.minitoken.*). mini-idp wires them inserver/IdpServer; the issuer takes a(subject, Authorization). -
The token contract.
server/src/main/resources/openapi.yaml(served at/openapi.yaml,/openapi.json,/docs) is authoritative. Thegrantsclaim (mini-token'stoken/GrantsClaimoverauth/Authorization) maps to mini-kms:sub → Principal.id,grants.control → Principal.admin,grants.groups[]→ a per-key-groupPolicyEnginedecision (mini-policy).auth/KeyOperationis a deliberate mirror of mini-kms's enum — the string values are the contract, do not rename them. Acnfclaim is reserved (RFC 7800) but not enforced yet. -
Crypto & formats (hand-rolled bits, now in mini-token). Ed25519 via the JDK only (
crypto/Ed25519Keys). The JWS is hand-rolled intoken/Jws(not a JOSE lib):base64url(header).base64url(payload).base64url(sig);service/TokenVerifieris the reference offline verifier (signature first, theniss/aud/time/revocation). Client secrets — which stay in mini-idp — are hashed with Argon2id (secret/Argon2SecretHasher), verified in constant time. -
Persistence & rotation. All state is JSON via mini-idp's
store/JsonStore(atomic temp-file +ATOMIC_MOVE+0600), passed to the token services through theDocumentStoreSPI. Private signing keys insigning-keys.json(0600) by default — or, with--kms-tcp/--kms-key-group, envelope-wrapped under mini-kms so no plaintext key touches disk (the recursive integration; see the mini-kms section anddocs/DIRECTION.md). Rotation (mini-token'sservice/SigningKeyService) keeps retired keys in the JWKS (retiredKeyRetention, = 2× token TTL) so in-flight tokens verify. -
Conventions.
corestays HTTP-free (composition root isserver/IdpServer). The token endpoint returns one genericinvalid_clientfor any auth failure. Admin token viaresolveAdminToken. The OpenAPI spec is the contract —OpenApiContractTestfails if a documented path/method doesn't resolve on the live server, so keepopenapi.yamland the routes inApiHandlersin sync. -
Docs.
services/mini-idp/README.md— endpoint list, token claim schema, JWKS/discovery URLs.
mini-directory is the single identity source of truth for the family: it owns humans,
service accounts, groups, and roles, and the grant mappings between them. Its defining
job is resolution — turning any stored account into a mini-policy Principal plus a
fully-expanded, de-duplicated set of (action, resource) grants (roles expand to grants; group
memberships are inherited), which is exactly what a mini-policy decision function consumes. It is
shipping and wired in: mini-idp reads its service accounts from here at every /oauth/token
(required — --directory-url; mini-idp keeps no local client registry), and mini-oidc resolves
humans from here when --directory-url is set (optional, in-memory fallback otherwise). See the
resolved client-registry decision in docs/DIRECTION.md.
Run it locally. The bootstrap admin token comes from an env var or a file, never a CLI arg, and is never logged. Loopback by default.
export MINIDIR_ADMIN_TOKEN="$(openssl rand -hex 32)"
./gradlew :services:mini-directory:installDist
services/mini-directory/build/install/mini-directory/bin/mini-directory --port 8466 --data-dir ~/.mini-directory
# Create a role/group/human (admin), then GET /admin/principals/{id}/resolution. Browse /docs.Architecture. One application module under base package com.codeheadsystems.minidirectory:
-
model— the records:Account(aHUMANorSERVICE_ACCOUNT, the resolvable identity),Group,Role, the flatGrantSpec({action, resource}, the JSON-friendly mirror of a mini-policyGrant), andResolvedPrincipal(a mini-policyPrincipal+ expandedGrants). -
service/DirectoryService— the I/O-free heart: CRUD for accounts/groups/roles, assignment,resolve(id)(the role/group → grant expansion),authenticate(id, secret)(no-oracle, constant-time, dummy-hash for unknown — the family's credential-check pattern), andimportServiceAccount(...)(the migration entry point). All methodssynchronized; persisted on every mutation. mini-idp now reads service accounts from here (via the authenticate endpoint). -
secret—Argon2SecretHasher/SecretHash/Argon2Settings: the family's Argon2id pattern (amini-commoncandidate), replicated here so the service stays standalone. Only service accounts carry a hash; humans carry none. -
store—JsonStore(atomic temp-file →ATOMIC_MOVE→0600, the replicated family store) holding oneDirectoryDocument(directory.json: accounts + groups + roles in one atomic file). -
server—ServerMain/DirectoryServer(composition root),ServerConfig,ApiHandlers,AdminAuthenticator, the OpenAPI/Swagger serving, and the reusedhttp/router. JDKHttpServeron loopback, one virtual thread per request. -
Reuse over reinvention. The
server/httprouter,AdminAuthenticator,OpenApiDocument,SwaggerUiPage, the Argon2 hasher, andJsonStoreare deliberate copies of mini-idp's (the documentedmini-commoncandidates) so the service is self-contained until that library exists. The decision model is not copied — it depends on:libs:mini-policyand resolves into itsPrincipal/Grant/GrantBasedPolicyEnginetypes directly. -
Conventions. Admin API guarded by the bootstrap bearer token (
MINIDIR_ADMIN_TOKEN). Service-account secrets returned exactly once at creation, hashed at rest, never logged. Loopback bind by default. Id collisions → 409, dangling role/group references → 400. The OpenAPI spec is the contract —OpenApiContractTestfails if a documented path/method doesn't resolve on the live server, so keepopenapi.yamland the routes inApiHandlersin sync. -
Docs.
services/mini-directory/README.md— the record model, the resolution rule, and the endpoint list.
mini-oidc is the human SSO / OpenID Provider: the OAuth 2.0 authorization-code flow with PKCE, authenticating people with passkeys (WebAuthn) and issuing ID + access tokens (plus rotating refresh tokens) that verify offline against the shared JWKS. Where mini-idp authenticates machines, mini-oidc authenticates people. Shipping, and composition over reinvention: it embeds pk-auth, mints through mini-token, decides through mini-policy, and resolves identities from mini-directory.
Run it locally. Loopback by default; the bootstrap admin token (for client registration) comes from env/file, never argv, never logged.
export MINIOIDC_ADMIN_TOKEN="$(openssl rand -hex 32)"
./gradlew :services:mini-oidc:installDist
services/mini-oidc/build/install/mini-oidc/bin/mini-oidc \
--port 8477 --issuer https://oidc.example --rp-id oidc.example --rp-origin https://oidc.example \
--directory-url http://127.0.0.1:8466 --secure-cookies
# Discovery at /.well-known/openid-configuration, JWKS at /jwks.json, docs at /docs.Architecture. One application module under base package com.codeheadsystems.minioidc:
-
auth— the human-authentication seam.HumanAuthenticator(passkey ceremony) is implemented byPkAuthHumanAuthenticatorover pk-auth'sPasskeyAuthenticationService;RecoveryAuthenticatorwraps pk-auth'sBackupCodeServicefor fallback login.PasskeyStackassembles the embedded pk-auth stack over its in-memory SPIs (the documented swap point for persistent credential storage; reading the authenticatedUserHandleoff the verified assertion, never pk-auth's JWT). -
directory— theUserDirectorySPI that resolves an authenticated human to a mini-policyPrincipal+ grants + profile claims.HttpUserDirectoryis the production path (calls mini-directory's/admin/principals/{id}/resolution);InMemoryUserDirectorybacks tests/dev. -
service—OidcTokensmints ID/access tokens via mini-token's keys +Jws(see below);OidcTokenVerifieris the offline reference verifier;ScopeAuthorizerauthorizes scopes through mini-policy; plus the flow stores (PendingAuthorizationStore,AuthorizationCodeStorewith replay→family-revoke,SessionService,RefreshTokenServicerotating with replay defense,ClientService). -
server—ServerMain/OidcServer(composition root),ServerConfig,OidcHandlers(every endpoint),LoginPages(minimal login/consent UI),Cookies,AdminAuthenticator, the reusedhttp/router, and the OpenAPI/Swagger serving. -
The token plane is REUSED, not re-implemented. ID/access tokens are signed with mini-token's
SigningKeyServicekeys and published through its JWKS + rotation. The only addition to mini-token was an additive genericJws.sign(JwsHeader, Map, PrivateKey)overload, so OIDC claim sets (nonce,auth_time,scope, profile/email) sign with the same format and keys — mini-idp's typed path is untouched. -
Browser-flow security. Secure (configurable), HttpOnly, SameSite=Lax session cookie; the SSO session lifetime is distinct from the token TTLs; per-pending-authorization CSRF token on every state-changing browser POST; PKCE mandatory for every code request; redirect only to pre-registered URIs. Loopback by default — any LAN exposure MUST be behind a TLS reverse proxy with
--secure-cookies. -
Conventions. No oracle (one
invalid_grantfor every code/refresh failure, oneinvalid_clientfor client-auth failure, oneinvalid_tokenat userinfo), constant-time secret comparison, no secrets in logs. The OpenAPI spec is the contract —OpenApiContractTestkeepsopenapi.yamland the routes in sync. -
Not yet wired by default.
--directory-urlis optional; without it an empty in-memory directory is used (nobody resolves). Passkey enrolment (/register/passkey/**) is currently unauthenticated self-enrolment — a real deployment gates it. -
Docs.
services/mini-oidc/README.md— the flow, the endpoint list, and the security model.
mini-gateway is the forward-auth endpoint a reverse proxy (Traefik ForwardAuth, Caddy forward_auth, nginx auth_request) calls before forwarding a request, to gate upstreams that have no auth of their own. Shipping, and pure composition: it reuses mini-token (session store + JWS verification) and mini-policy (the decision), inventing nothing.
Run it. No secrets of its own; it validates the family's sessions/tokens.
./gradlew :services:mini-gateway:installDist
services/mini-gateway/build/install/mini-gateway/bin/mini-gateway \
--port 8488 --sessions-file ~/.mini-oidc/sessions.json --routes-file ./routes.json \
--login-url "https://example.com/authorize?response_type=code&client_id=gateway&redirect_uri=https://example.com/&scope=openid&code_challenge=…&code_challenge_method=S256" \
--jwks-url https://example.com/jwks.json --issuer https://example.com --audience https://example.com/userinfoArchitecture. One application module under base package com.codeheadsystems.minigateway:
-
server/GatewayHandlers— theGET /verifyendpoint (registered for all methods). It reads the original method/URI fromX-Forwarded-*/X-Original-*and the client'sCookie/Authorization, authenticates, evaluates the route, and answers 200 (+X-Auth-Subject/X-Auth-Scope/X-Auth-Source), 302-to-login (unauthenticated browser), 401 (API), or 403 (forbidden). -
auth—SessionAuthenticator(the shared mini-tokenSessionServiceover the samesessions.jsonmini-oidc writes),BearerAuthenticator(mini-tokenJwsClaimsVerifieragainst the OP JWKS), and theJwksProviderSPI (HttpJwksProviderin production; injectable in tests). -
service/RoutePolicy+model— config-driven route rules (routes.json: ordered prefix rules,PUBLIC/AUTHENTICATED/SCOPE);SCOPEdecisions defer to a mini-policyGrantBasedPolicyEnginebuilt from the caller's scopes. Deny by default for unmatched paths. -
server—ServerMain/GatewayServer(composition root),ServerConfig, the reusedhttp/router andstore/JsonStore. -
Shared session, not a second one. The SSO session mechanism lives in mini-token (
session/SessionService+BrowserSession+Sessions, over theDocumentStoreSPI). mini-oidc is the sole writer; mini-gateway is a reader of the same file. The session cookie name is the sharedSessionService.DEFAULT_COOKIE_NAME. For the cookie to reach a gated app, mini-oidc and the app share a hostname (host-only cookie,Path=/) — see the README's proxy snippets. -
Conventions. No oracle (failures collapse to 401/403/302; bearer verification fails closed), no secrets in logs, deny-by-default, loopback bind (the proxy reaches it over the loopback/Docker network; never exposed to clients directly).
-
Docs.
services/mini-gateway/README.md— the decision flow, the routes config, and runnable Traefik / Caddy / nginx snippets;services/mini-gateway/examples/—routes.json, a Traefikdocker-compose, and aCaddyfilethat gate a no-authwhoamibehind a mini-oidc login.
mini-ca is a small internal certificate authority for the homelab: it issues and renews short-lived X.509 leaf certs (mTLS between the minis + homelab services) from PKCS#10 CSRs, keeps an issuance log + revocation list, and wraps its own CA private key under mini-kms (the recursive integration). Shipping, and deliberately not a full PKI — see the README's non-goals (one self-signed root, JSON revocation list rather than a signed DER CRL, no intermediates/ACME/OCSP).
Run it. Loopback by default; admin token from env/file (never argv, never logged).
export MINICA_ADMIN_TOKEN="$(openssl rand -hex 32)"
./gradlew :services:mini-ca:installDist
services/mini-ca/build/install/mini-ca/bin/mini-ca --port 8499 --data-dir ~/.mini-ca \
--kms-tcp 127.0.0.1:9123 --kms-key-group ca-key # --kms-* optional (else plaintext 0600)Architecture. One application module under base package com.codeheadsystems.minica:
-
ca— the crypto:CaKeys(EC P-256 keygen + self-signed root via BouncyCastle PKIX),CertificateAuthority(issues an mTLS leaf from a CSR — verifies proof-of-possession, sets the extension set, random serial, short TTL), and hand-rolledPem. -
service/CaService— bootstraps (mint a fresh CA on first run, else load), issues/renews/ revokes, and maintains the issuance log + revocation list. The CA private key is persisted as a one-record mini-tokenSigningKeysdocument through the injectedDocumentStore— so it reusesKmsSigningKeyStore(KMS-wrapped) or a plaintextJsonStore, exactly like the token signing keys. The root cert / log / revocations are plaintext (public). -
server—ServerMain/CaServer(composition root),ServerConfig,ApiHandlers,AdminAuthenticator, the reusedhttp/router +store/JsonStore, and the OpenAPI/Swagger serving. -
CA key under mini-kms. Reuses Prompt 6's
KmsSigningKeyStoreverbatim (the CA key is just a PKCS#8 in aSigningKeysrecord); with--kms-*set,ca-key.jsonholds only akms1:envelope, unwrapped in memory at bootstrap. Bootstrap ordering / failure modes are the token-key ones indocs/DIRECTION.md. -
Conventions.
/ca(trust anchor) +/revocations+/healthare public; issuance/renewal/ revocation/log are admin-guarded. EC P-256 /SHA256withECDSA; short leaf TTLs (clamped); CSR PoP verified (the CA never sees a requester's private key); one generic400for a bad CSR (no oracle); no secrets logged; loopback bind. -
Docs.
services/mini-ca/README.md— scope, the explicit non-goals, the endpoint list, and the mini-kms key-wrapping wiring.
mini-console is the optional unified admin console + exercise harness over the whole family. Two faces in one loopback process: an operator console (server-rendered HTML that browses and mutates the surfaces the family already exposes) and an exercise harness (scripted end-to-end flows that drive the family and verify the result offline). It is shipping, and pure composition: it reaches each service through a thin client library, mints/verifies nothing itself, and adds no new authority — every mutation equals an operator curling a downstream admin API with the same token.
Run it. Loopback by default; the console login token + each downstream admin token come from env/file (never argv, never logged). Wire only the services you have.
export MINICONSOLE_ADMIN_TOKEN="$(openssl rand -hex 32)"
export MINICONSOLE_DIRECTORY_TOKEN="$MINIDIR_ADMIN_TOKEN" # held copies of each downstream admin token
./gradlew :services:mini-console:installDist
services/mini-console/build/install/mini-console/bin/mini-console --port 8500 --data-dir ~/.mini-console \
--directory-url http://127.0.0.1:8466 --idp-url http://127.0.0.1:8455 --oidc-url http://127.0.0.1:8477 \
--ca-url http://127.0.0.1:8499 --kms-tcp 127.0.0.1:9123 --gateway-url http://127.0.0.1:8488
# Sign in at /login; API docs at /docs.Architecture. One application module under base package com.codeheadsystems.miniconsole, plus
five HTTP client libraries under libs/ (mini-directory-client, mini-idp-client,
mini-oidc-client, mini-ca-client, mini-gateway-client) sharing libs/mini-client-common.
-
server—ServerMain/ConsoleServer(composition root; JDKHttpServeron loopback, one vthread per request),ConsoleConfig(per-downstream URL/token resolution,MINICONSOLE_*names),AdminAuthenticator(constant-time console token),ConsoleSession(mini-tokenSessionServiceover its OWNJsonStore, a distinct cookie name — not the SSO cookie),Cookies/Csrf, the reusedhttp/router, and the/apiOpenAPI serving (OpenApiDocument/SwaggerUiPage). -
pages— hand-rolled HTML (mini-oidcLoginPagesstyle) for the Dashboard, Identities, Clients, Keys, Certificates, Audit, and Harness screens;Layoutescapes everything and never renders a secret (the once-only service-account / client secrets are shown exactly once at creation). -
harness— the I/O-free exercise engine:Exercise/ExerciseResult/ExerciseRegistryand the flows (M2mTokenFlow,KeyRotationFlow,CertLifecycleFlow,OidcCodePkceFlow,GatewayVerifyFlow). Each verifies offline (JWS-vs-JWKS via mini-token, cert chain to the CA root, the gateway's allow/deny decision); a step it cannot drive (a headless passkey login, a branch with no supplied credential) reports SKIP, never a misleading PASS. Results are secret-free by contract. -
The client libraries. Each copies only the wire records it needs into its own
…client.modelpackage and depends on no service module (dependency direction is one-way: console → *-client → mini-client-common → catalog; acyclic). All collapse failures to oneClientException(no oracle). mini-kms is the exception: it reuses the existing:services:mini-kms:client(socket-era) as-is. mini-gateway-client is the other exception:/verify's whole job is to answer a reverse proxy with distinct statuses, so it deliberately maps 200/302/401/403 to aVerifyOutcomeinstead of collapsing — but it reads no response body, so there is still no body oracle. -
The
/apisurface. A small read-only JSON twin of the Dashboard (GET /api/health,GET /api/harness), guarded by the console bearer token, withopenapi.yaml+OpenApiContractTest(the family contract-test pattern) and a vendored offline Swagger UI at/docs. -
Security. No new authority; stores no identity/grants/keys (only login-session/CSRF state in an atomic-
0600console-sessions.json). The one new secret concentration: it holds up to six downstream tokens (env/file, never argv, never logged) — a deliberate operator trade-off, mitigated by loopback default. CSRF on every browser POST; PKCE S256 in the OIDC flow; no secrets in logs. -
Docs.
services/mini-console/README.md(the two faces, the screen/route list, the harness flows and what "pass" means, the security model) anddocs/design/mini-console.md(the full design + slice history).