mirror of https://github.com/dexidp/dex.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
223 lines
6.0 KiB
223 lines
6.0 KiB
package authproxy |
|
|
|
import ( |
|
"encoding/json" |
|
"fmt" |
|
"io" |
|
"log/slog" |
|
"net" |
|
"net/http" |
|
"os" |
|
"sync" |
|
"testing" |
|
"time" |
|
|
|
"github.com/dexidp/dex/connector" |
|
) |
|
|
|
// The fixed port on which the Go test backend listens. |
|
// nginx is configured to proxy_pass to host.docker.internal:18562. |
|
const testBackendPort = "18562" |
|
|
|
const testAuthProxyURLEnv = "DEX_AUTHPROXY_URL" |
|
|
|
// identityResult holds the result of HandleCallback invoked on the proxied request. |
|
type identityResult struct { |
|
Identity connector.Identity `json:"identity"` |
|
Error string `json:"error,omitempty"` |
|
} |
|
|
|
// startTestBackend starts an HTTP server that receives proxied requests from nginx, |
|
// invokes HandleCallback on each request, and returns the identity as JSON. |
|
// The caller must call the returned cleanup function to shut down the server. |
|
func startTestBackend(t *testing.T, conn *callback) (cleanup func()) { |
|
t.Helper() |
|
|
|
mux := http.NewServeMux() |
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { |
|
ident, err := conn.HandleCallback(connector.Scopes{Groups: true}, nil, r) |
|
result := identityResult{Identity: ident} |
|
if err != nil { |
|
result.Error = err.Error() |
|
} |
|
|
|
w.Header().Set("Content-Type", "application/json") |
|
json.NewEncoder(w).Encode(result) |
|
}) |
|
|
|
listener, err := net.Listen("tcp", ":"+testBackendPort) |
|
if err != nil { |
|
t.Fatalf("failed to listen on port %s: %v", testBackendPort, err) |
|
} |
|
|
|
server := &http.Server{Handler: mux} |
|
|
|
var wg sync.WaitGroup |
|
wg.Add(1) |
|
go func() { |
|
defer wg.Done() |
|
if err := server.Serve(listener); err != nil && err != http.ErrServerClosed { |
|
t.Errorf("backend server error: %v", err) |
|
} |
|
}() |
|
|
|
return func() { |
|
server.Close() |
|
wg.Wait() |
|
} |
|
} |
|
|
|
// subtest describes a single integration test case for the authproxy connector. |
|
type integrationSubtest struct { |
|
name string |
|
config Config |
|
// path is the nginx location path to hit (e.g., "/default", "/minimal"). |
|
path string |
|
|
|
wantErr bool |
|
want connector.Identity |
|
} |
|
|
|
func TestIntegrationAuthProxy(t *testing.T) { |
|
proxyURL := os.Getenv(testAuthProxyURLEnv) |
|
if proxyURL == "" { |
|
t.Skipf("test environment variable %q not set, skipping authproxy integration tests", testAuthProxyURLEnv) |
|
} |
|
|
|
tests := []integrationSubtest{ |
|
{ |
|
name: "all headers set", |
|
config: Config{}, |
|
path: "/default", |
|
want: connector.Identity{ |
|
UserID: "uid-12345", |
|
Username: "testuser", |
|
PreferredUsername: "Test User", |
|
Email: "testuser@example.com", |
|
EmailVerified: true, |
|
Groups: []string{"group1", "group2", "group3"}, |
|
}, |
|
}, |
|
{ |
|
name: "minimal headers - fallback to X-Remote-User", |
|
config: Config{}, |
|
path: "/minimal", |
|
want: connector.Identity{ |
|
UserID: "janedoe", |
|
Username: "janedoe", |
|
PreferredUsername: "janedoe", |
|
Email: "janedoe", |
|
EmailVerified: true, |
|
}, |
|
}, |
|
{ |
|
name: "no auth headers - expect error", |
|
config: Config{}, |
|
path: "/no-headers", |
|
wantErr: true, |
|
}, |
|
{ |
|
name: "custom group separator", |
|
config: Config{ |
|
GroupHeaderSeparator: ";", |
|
}, |
|
path: "/custom-separator", |
|
want: connector.Identity{ |
|
UserID: "uid-99999", |
|
Username: "johndoe", |
|
PreferredUsername: "John Doe", |
|
Email: "johndoe@example.com", |
|
EmailVerified: true, |
|
Groups: []string{"admins", "developers", "ops"}, |
|
}, |
|
}, |
|
{ |
|
name: "all headers set with static groups", |
|
config: Config{ |
|
Groups: []string{"static-group1", "static-group2"}, |
|
}, |
|
path: "/default", |
|
want: connector.Identity{ |
|
UserID: "uid-12345", |
|
Username: "testuser", |
|
PreferredUsername: "Test User", |
|
Email: "testuser@example.com", |
|
EmailVerified: true, |
|
Groups: []string{"group1", "group2", "group3", "static-group1", "static-group2"}, |
|
}, |
|
}, |
|
} |
|
|
|
for _, tt := range tests { |
|
t.Run(tt.name, func(t *testing.T) { |
|
l := slog.New(slog.DiscardHandler) |
|
c, err := tt.config.Open("test-authproxy", l) |
|
if err != nil { |
|
t.Fatalf("failed to open connector: %v", err) |
|
} |
|
|
|
cb := c.(*callback) |
|
cleanup := startTestBackend(t, cb) |
|
defer cleanup() |
|
|
|
// Give the backend a moment to start. |
|
time.Sleep(50 * time.Millisecond) |
|
|
|
// Make a request through the nginx proxy. |
|
reqURL := fmt.Sprintf("%s%s", proxyURL, tt.path) |
|
|
|
resp, err := http.Get(reqURL) |
|
if err != nil { |
|
t.Fatalf("failed to make request through proxy: %v", err) |
|
} |
|
defer resp.Body.Close() |
|
|
|
body, err := io.ReadAll(resp.Body) |
|
if err != nil { |
|
t.Fatalf("failed to read response body: %v", err) |
|
} |
|
|
|
var result identityResult |
|
if err := json.Unmarshal(body, &result); err != nil { |
|
t.Fatalf("failed to decode response: %v\nbody: %s", err, string(body)) |
|
} |
|
|
|
if tt.wantErr { |
|
if result.Error == "" { |
|
t.Fatal("expected error from HandleCallback, got none") |
|
} |
|
return |
|
} |
|
|
|
if result.Error != "" { |
|
t.Fatalf("unexpected error from HandleCallback: %s", result.Error) |
|
} |
|
|
|
got := result.Identity |
|
if got.UserID != tt.want.UserID { |
|
t.Errorf("UserID: got %q, want %q", got.UserID, tt.want.UserID) |
|
} |
|
if got.Username != tt.want.Username { |
|
t.Errorf("Username: got %q, want %q", got.Username, tt.want.Username) |
|
} |
|
if got.PreferredUsername != tt.want.PreferredUsername { |
|
t.Errorf("PreferredUsername: got %q, want %q", got.PreferredUsername, tt.want.PreferredUsername) |
|
} |
|
if got.Email != tt.want.Email { |
|
t.Errorf("Email: got %q, want %q", got.Email, tt.want.Email) |
|
} |
|
if got.EmailVerified != tt.want.EmailVerified { |
|
t.Errorf("EmailVerified: got %v, want %v", got.EmailVerified, tt.want.EmailVerified) |
|
} |
|
if len(got.Groups) != len(tt.want.Groups) { |
|
t.Errorf("Groups length: got %d, want %d (got: %v, want: %v)", len(got.Groups), len(tt.want.Groups), got.Groups, tt.want.Groups) |
|
} else { |
|
for i := range tt.want.Groups { |
|
if got.Groups[i] != tt.want.Groups[i] { |
|
t.Errorf("Groups[%d]: got %q, want %q", i, got.Groups[i], tt.want.Groups[i]) |
|
} |
|
} |
|
} |
|
}) |
|
} |
|
}
|
|
|