On the dev stack (registry_domains: ["127.0.0.1:5000"]) the /auth/token failure
guidance said `docker login 127.0.0.1`, dropping the port docker needs. The
message exists to tell a stuck developer what to run, so a command that cannot
work is the whole defect. Production is unaffected, both registry domains being
port-free.
The stripping itself is deliberate and stays: NormalizeService removes the port
so JWT audiences match the port-stripped routing host. Removing it would
desynchronize the service key from the routing key.
The unstripped form turned out to survive in only one place. resolveService
returns a key from h.services, which SetServices normalizes, and cfg.Auth.Services
is normalized too by deriveServices, so neither holds the original. Only
cfg.Server.RegistryDomains, straight off the YAML, does, and it was never
reaching the token package.
Adds a display-only map keyed by the same NormalizeService function the other
two key on, so a resolved service always maps back to the entry it came from,
and a multi-domain deployment prints the domain the client is authenticating
against rather than the primary. Collisions take the first configured entry,
matching deriveServices' own first-wins dedupe. A miss falls through to the
normalized name, which is today's behaviour.
Audiences and routing are untouched, proven by a test that mints a real token,
parses the JWT and asserts aud is still the normalized host.
The wiring line landed in the previous commit, both changes having been made in
server.go at the same time.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PDqoCE1j3njokkZ9b1C5n9
Both are pre-existing and were found while working on finding 27.
sendAuthError built its "docker login <host>" line from r.Host. /auth/token is
served on the UI domain as well as on every registry domain, and the
WWW-Authenticate realm points at the UI domain's copy, so a client following
the realm was told to run "docker login seamark.dev" - the one host that
deliberately refuses /v2/* with an OCI UNSUPPORTED error pointing at
seamark.cr. It now uses the service resolved for the token, which is the
registry domain, falling back to the deployment's primary rather than to
r.Host. The single-domain case still prints a host that serves /v2/, and an
unconfigured service supplied by the client cannot steer it.
Separately, the scope parameter was read with .Get, taking the first value
only. The Docker token spec allows scope to be repeated, so a client asking for
two repositories was issued a token covering one and got a 401 on the other.
Both wire forms are now flattened, on the GET query string and on the OAuth2
POST body, which had the same defect via PostFormValue.
Empty and whitespace-only values are dropped. Exact duplicate scope strings
collapse, but two entries naming the same repository with different actions are
left alone: merging them would union the action sets, and every gate downstream
is written only to narrow.
More entries now reach the anonymous gate added in af7522b, which is the
intended effect. Its per-entry verdict is unchanged: public entries survive,
private ones are dropped, and an all-private request still gets the challenge.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PDqoCE1j3njokkZ9b1C5n9