package server import ( "fmt" "net/http" "strings" "git.wntrmute.dev/mc/mcr/internal/auth" ) // TokenValidator abstracts token validation so the middleware can work // with the real MCIAS client or a test fake. type TokenValidator interface { ValidateToken(token string) (*auth.Claims, error) } // RequireAuth returns middleware that validates Bearer tokens via the // given TokenValidator. On success the authenticated Claims are injected // into the request context. On failure a 401 with an OCI-format error // body and a WWW-Authenticate header is returned. func RequireAuth(validator TokenValidator, serviceName string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Build the WWW-Authenticate header with an absolute realm URL // derived from the request Host, per OCI Distribution Spec. scheme := "https" realm := fmt.Sprintf("%s://%s/v2/token", scheme, r.Host) wwwAuth := fmt.Sprintf(`Bearer realm="%s",service="%s"`, realm, serviceName) token := extractBearerToken(r) if token == "" { w.Header().Set("WWW-Authenticate", wwwAuth) writeOCIError(w, "UNAUTHORIZED", http.StatusUnauthorized, "authentication required") return } claims, err := validator.ValidateToken(token) if err != nil { w.Header().Set("WWW-Authenticate", wwwAuth) writeOCIError(w, "UNAUTHORIZED", http.StatusUnauthorized, "authentication required") return } ctx := auth.ContextWithClaims(r.Context(), claims) next.ServeHTTP(w, r.WithContext(ctx)) }) } } // extractBearerToken parses a "Bearer " value from the // Authorization header. It returns an empty string if the header is // missing or malformed. func extractBearerToken(r *http.Request) string { h := r.Header.Get("Authorization") if h == "" { return "" } const prefix = "Bearer " if !strings.HasPrefix(h, prefix) { return "" } return strings.TrimSpace(h[len(prefix):]) }