mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-19 08:44:14 +00:00
allow dids on docker login
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -141,6 +142,9 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Reconstruct DID usernames that were mangled by BasicAuth's colon split
|
||||
username, password = parseBasicAuthDID(username, password)
|
||||
|
||||
slog.Debug("Got Basic auth credentials", "username", username, "passwordLength", len(password))
|
||||
|
||||
// Parse query parameters
|
||||
@@ -271,3 +275,43 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
render.JSON(w, r, resp)
|
||||
}
|
||||
|
||||
// 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
|
||||
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 2: Raw DID was split by BasicAuth on the first colon
|
||||
// username="did", password="plc:<id>:<real-password>" or "web:<host>:<real-password>"
|
||||
if username != "did" {
|
||||
return username, password
|
||||
}
|
||||
|
||||
if strings.HasPrefix(password, "plc:") {
|
||||
// did:plc:<base32-id> — the ID is a single segment (no colons)
|
||||
// password = "plc:<id>:<real-password>"
|
||||
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> — hostname uses dots not colons
|
||||
// password = "web:<hostname>:<real-password>"
|
||||
rest := strings.TrimPrefix(password, "web:")
|
||||
if idx := strings.Index(rest, ":"); idx > 0 {
|
||||
return "did:web:" + rest[:idx], rest[idx+1:]
|
||||
}
|
||||
}
|
||||
|
||||
return username, password
|
||||
}
|
||||
|
||||
@@ -642,3 +642,107 @@ func TestHandler_ServeHTTP_PullOnlyAccess(t *testing.T) {
|
||||
t.Errorf("Expected status %d for pull-only access, got %d. Body: %s", http.StatusOK, w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseBasicAuthDID(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
username string
|
||||
password string
|
||||
wantUsername string
|
||||
wantPassword string
|
||||
}{
|
||||
{
|
||||
name: "normal handle unchanged",
|
||||
username: "alice.bsky.social",
|
||||
password: "mypassword",
|
||||
wantUsername: "alice.bsky.social",
|
||||
wantPassword: "mypassword",
|
||||
},
|
||||
{
|
||||
name: "URL-encoded did:plc",
|
||||
username: "did%3Aplc%3Aabc123",
|
||||
password: "mypassword",
|
||||
wantUsername: "did:plc:abc123",
|
||||
wantPassword: "mypassword",
|
||||
},
|
||||
{
|
||||
name: "URL-encoded did:web",
|
||||
username: "did%3Aweb%3Aexample.com",
|
||||
password: "mypassword",
|
||||
wantUsername: "did:web:example.com",
|
||||
wantPassword: "mypassword",
|
||||
},
|
||||
{
|
||||
name: "raw did:plc mangled by BasicAuth",
|
||||
username: "did",
|
||||
password: "plc:abc123:mypassword",
|
||||
wantUsername: "did:plc:abc123",
|
||||
wantPassword: "mypassword",
|
||||
},
|
||||
{
|
||||
name: "raw did:web mangled by BasicAuth",
|
||||
username: "did",
|
||||
password: "web:example.com:mypassword",
|
||||
wantUsername: "did:web:example.com",
|
||||
wantPassword: "mypassword",
|
||||
},
|
||||
{
|
||||
name: "raw did:plc with device secret",
|
||||
username: "did",
|
||||
password: "plc:e3kzdezk5gsirzh7eoqplc64:atcr_device_abc123",
|
||||
wantUsername: "did:plc:e3kzdezk5gsirzh7eoqplc64",
|
||||
wantPassword: "atcr_device_abc123",
|
||||
},
|
||||
{
|
||||
name: "username did but not a DID method",
|
||||
username: "did",
|
||||
password: "something:else",
|
||||
wantUsername: "did",
|
||||
wantPassword: "something:else",
|
||||
},
|
||||
{
|
||||
name: "username did with no colon in rest",
|
||||
username: "did",
|
||||
password: "plc:abc123",
|
||||
wantUsername: "did",
|
||||
wantPassword: "plc:abc123",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotUsername, gotPassword := parseBasicAuthDID(tt.username, tt.password)
|
||||
if gotUsername != tt.wantUsername {
|
||||
t.Errorf("username = %q, want %q", gotUsername, tt.wantUsername)
|
||||
}
|
||||
if gotPassword != tt.wantPassword {
|
||||
t.Errorf("password = %q, want %q", gotPassword, tt.wantPassword)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenHandler_DIDBasicAuth(t *testing.T) {
|
||||
// Test that a DID passed as BasicAuth username works through the full handler
|
||||
deviceStore, database := setupTestDeviceStore(t)
|
||||
keyPath := getSharedTestKey(t)
|
||||
issuer, err := NewIssuer(keyPath, "atcr.io", "registry", 15*time.Minute)
|
||||
if err != nil {
|
||||
t.Fatalf("NewIssuer() error = %v", err)
|
||||
}
|
||||
deviceSecret := createTestDevice(t, deviceStore, database, "did:plc:abc123", "alice.bsky.social")
|
||||
|
||||
handler := NewHandler(issuer, deviceStore)
|
||||
|
||||
// Simulate what BasicAuth() does when username is "did:plc:abc123"
|
||||
// It splits on first colon: username="did", password="plc:abc123:<secret>"
|
||||
req := httptest.NewRequest(http.MethodGet, "/auth/token?service=registry&scope=repository:alice.bsky.social/myapp:pull", nil)
|
||||
req.SetBasicAuth("did:plc:abc123", deviceSecret)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status %d for DID BasicAuth, got %d. Body: %s", http.StatusOK, w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user