diff --git a/pkg/appview/middleware/registry.go b/pkg/appview/middleware/registry.go index 4b730ce..9834174 100644 --- a/pkg/appview/middleware/registry.go +++ b/pkg/appview/middleware/registry.go @@ -299,6 +299,13 @@ func (nr *NamespaceResolver) Repository(ctx context.Context, name reference.Name identityStr := parts[0] imageName := parts[1] + // Support hyphen-encoded DIDs in image paths (e.g., did-plc-abc123/repo:tag) + // OCI reference grammar doesn't allow colons in path components, so DIDs must + // be encoded with hyphens instead: did:plc:abc123 → did-plc-abc123 + if decoded, ok := token.DecodeDIDFromHyphens(identityStr); ok { + identityStr = decoded + } + // Resolve identity to DID, handle, and PDS endpoint did, handle, pdsEndpoint, err := atproto.ResolveIdentity(ctx, identityStr) if err != nil { diff --git a/pkg/auth/token/handler.go b/pkg/auth/token/handler.go index 52c15b0..c112236 100644 --- a/pkg/auth/token/handler.go +++ b/pkg/auth/token/handler.go @@ -6,7 +6,6 @@ import ( "fmt" "log/slog" "net/http" - "net/url" "strings" "time" @@ -277,18 +276,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // parseBasicAuthDID fixes DID usernames that are mangled by HTTP Basic Auth. -// Basic Auth splits on the first colon, so "did:plc:abc123" as a username -// produces username="did" and the rest gets prepended to the password. // // This handles two cases: -// 1. URL-encoded DIDs (did%3Aplc%3Aabc123) — decoded back to did:plc:abc123 -// 2. Raw DIDs — reconstructed from the mangled username + password +// 1. Hyphen-encoded DIDs (did-plc-abc123) — converted to did:plc:abc123. +// This is the recommended format for tools like helm that reject colons in usernames. +// 2. Raw DIDs split by BasicAuth — "did:plc:abc123" gets split on the first colon +// into username="did", password="plc:abc123:". Reconstructed here. func parseBasicAuthDID(username, password string) (string, string) { - // Case 1: URL-encoded DID (e.g., did%3Aplc%3Aabc123) - if decoded, err := url.QueryUnescape(username); err == nil && decoded != username { - if strings.HasPrefix(decoded, "did:") { - return decoded, password - } + // Case 1: Hyphen-encoded DID (e.g., did-plc-abc123 or did-web-example.com) + if did, ok := DecodeDIDFromHyphens(username); ok { + return did, password } // Case 2: Raw DID was split by BasicAuth on the first colon @@ -298,15 +295,11 @@ func parseBasicAuthDID(username, password string) (string, string) { } if strings.HasPrefix(password, "plc:") { - // did:plc: — the ID is a single segment (no colons) - // password = "plc::" rest := strings.TrimPrefix(password, "plc:") if idx := strings.Index(rest, ":"); idx > 0 { return "did:plc:" + rest[:idx], rest[idx+1:] } } else if strings.HasPrefix(password, "web:") { - // did:web: — hostname uses dots not colons - // password = "web::" rest := strings.TrimPrefix(password, "web:") if idx := strings.Index(rest, ":"); idx > 0 { return "did:web:" + rest[:idx], rest[idx+1:] @@ -315,3 +308,16 @@ func parseBasicAuthDID(username, password string) (string, string) { return username, password } + +// DecodeDIDFromHyphens converts a hyphen-encoded DID back to colon-separated form. +// "did-plc-abc123" → "did:plc:abc123", "did-web-example.com" → "did:web:example.com" +// Returns the decoded DID and true if the input matched, or ("", false) otherwise. +func DecodeDIDFromHyphens(s string) (string, bool) { + if strings.HasPrefix(s, "did-plc-") { + return "did:plc:" + strings.TrimPrefix(s, "did-plc-"), true + } + if strings.HasPrefix(s, "did-web-") { + return "did:web:" + strings.TrimPrefix(s, "did-web-"), true + } + return "", false +} diff --git a/pkg/auth/token/handler_test.go b/pkg/auth/token/handler_test.go index 643732f..66b1fdf 100644 --- a/pkg/auth/token/handler_test.go +++ b/pkg/auth/token/handler_test.go @@ -659,15 +659,15 @@ func TestParseBasicAuthDID(t *testing.T) { wantPassword: "mypassword", }, { - name: "URL-encoded did:plc", - username: "did%3Aplc%3Aabc123", + name: "hyphen-encoded did:plc", + username: "did-plc-abc123", password: "mypassword", wantUsername: "did:plc:abc123", wantPassword: "mypassword", }, { - name: "URL-encoded did:web", - username: "did%3Aweb%3Aexample.com", + name: "hyphen-encoded did:web", + username: "did-web-example.com", password: "mypassword", wantUsername: "did:web:example.com", wantPassword: "mypassword",