From 5388f0da8fbbc4e0586fb611fcd9e1b8a723fb70 Mon Sep 17 00:00:00 2001 From: Noste <83548733+Noooste@users.noreply.github.com> Date: Tue, 12 May 2026 17:57:07 +0200 Subject: [PATCH] feat: add support for TLS InsecureSkipVerify in OIDC configuration (#42) Signed-off-by: Noste <83548733+Noooste@users.noreply.github.com> --- backend/internal/auth/auth.go | 26 ++++++++-- backend/internal/auth/auth_test.go | 80 ++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/backend/internal/auth/auth.go b/backend/internal/auth/auth.go index 6b01f8e..ee268b1 100644 --- a/backend/internal/auth/auth.go +++ b/backend/internal/auth/auth.go @@ -4,9 +4,11 @@ import ( "Noooste/garage-ui/pkg/logger" "context" "crypto/subtle" + "crypto/tls" "encoding/base64" "encoding/json" "fmt" + "net/http" "strings" "Noooste/garage-ui/internal/config" @@ -22,6 +24,7 @@ type Service struct { oidcProvider *oidc.Provider oidcVerifier *oidc.IDTokenVerifier oauth2Config *oauth2.Config + oidcClient *http.Client jwtService *JWTService } @@ -58,7 +61,15 @@ func NewAuthService(authCfg *config.AuthConfig, serverCfg *config.ServerConfig) // initOIDC initializes the OIDC provider and configuration func (a *Service) initOIDC() error { - ctx := context.Background() + if a.authConfig.OIDC.TLSSkipVerify { + a.oidcClient = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + } + } + + ctx := a.oidcContext(context.Background()) // Create OIDC provider provider, err := oidc.NewProvider(ctx, a.authConfig.OIDC.IssuerURL) @@ -92,6 +103,13 @@ func (a *Service) initOIDC() error { return nil } +func (a *Service) oidcContext(ctx context.Context) context.Context { + if a.oidcClient == nil { + return ctx + } + return oidc.ClientContext(ctx, a.oidcClient) +} + // ValidateBasicAuth validates basic authentication credentials func (a *Service) ValidateBasicAuth(username, password string) bool { // Use constant-time comparison to prevent timing attacks @@ -123,7 +141,7 @@ func (a *Service) ExchangeCode(ctx context.Context, code string) (*oauth2.Token, return nil, fmt.Errorf("OIDC not initialized") } - token, err := a.oauth2Config.Exchange(ctx, code) + token, err := a.oauth2Config.Exchange(a.oidcContext(ctx), code) if err != nil { return nil, fmt.Errorf("failed to exchange code: %w", err) } @@ -138,7 +156,7 @@ func (a *Service) VerifyIDToken(ctx context.Context, rawIDToken string) (*UserIn } // Verify the ID token - idToken, err := a.oidcVerifier.Verify(ctx, rawIDToken) + idToken, err := a.oidcVerifier.Verify(a.oidcContext(ctx), rawIDToken) if err != nil { return nil, fmt.Errorf("failed to verify ID token: %w", err) } @@ -170,6 +188,8 @@ func (a *Service) GetUserInfo(ctx context.Context, token *oauth2.Token) (*UserIn return nil, fmt.Errorf("OIDC not initialized") } + ctx = a.oidcContext(ctx) + // Create OAuth2 token source tokenSource := a.oauth2Config.TokenSource(ctx, token) diff --git a/backend/internal/auth/auth_test.go b/backend/internal/auth/auth_test.go index a923eca..84041e2 100644 --- a/backend/internal/auth/auth_test.go +++ b/backend/internal/auth/auth_test.go @@ -268,6 +268,86 @@ func TestNewAuthService_OIDCEnabled_DiscoversProvider(t *testing.T) { } } +// newTLSDiscoveryServer is the same as newDiscoveryServer but serves the OIDC +// discovery document over HTTPS using httptest's self-signed certificate. The +// cert is not signed by any system-trusted CA, so any HTTP client without +// InsecureSkipVerify (or the cert pinned) will fail to connect. +func newTLSDiscoveryServer(t *testing.T) *httptest.Server { + t.Helper() + mux := http.NewServeMux() + var srv *httptest.Server + mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) { + doc := map[string]any{ + "issuer": srv.URL, + "authorization_endpoint": srv.URL + "/auth", + "token_endpoint": srv.URL + "/token", + "jwks_uri": srv.URL + "/jwks", + "userinfo_endpoint": srv.URL + "/userinfo", + "id_token_signing_alg_values_supported": []string{"RS256", "EdDSA"}, + "response_types_supported": []string{"code"}, + "subject_types_supported": []string{"public"}, + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(doc) + }) + mux.HandleFunc("/jwks", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"keys":[]}`)) + }) + srv = httptest.NewTLSServer(mux) + t.Cleanup(srv.Close) + return srv +} + +func TestNewAuthService_OIDCEnabled_SelfSignedIssuer_FailsWithoutTLSSkipVerify(t *testing.T) { + disco := newTLSDiscoveryServer(t) + + authCfg := &config.AuthConfig{ + OIDC: config.OIDCConfig{ + Enabled: true, + ClientID: "test-client", + IssuerURL: disco.URL, + Scopes: []string{"openid"}, + TLSSkipVerify: false, + }, + } + srvCfg := &config.ServerConfig{RootURL: "https://garage-ui.example"} + + _, err := NewAuthService(authCfg, srvCfg) + if err == nil { + t.Fatal("expected TLS verification error from self-signed issuer, got nil") + } + if !strings.Contains(err.Error(), "failed to initialize OIDC") { + t.Errorf("expected wrapping error, got %v", err) + } +} + +func TestNewAuthService_OIDCEnabled_SelfSignedIssuer_SucceedsWithTLSSkipVerify(t *testing.T) { + disco := newTLSDiscoveryServer(t) + + authCfg := &config.AuthConfig{ + OIDC: config.OIDCConfig{ + Enabled: true, + ClientID: "test-client", + IssuerURL: disco.URL, + Scopes: []string{"openid"}, + TLSSkipVerify: true, + }, + } + srvCfg := &config.ServerConfig{RootURL: "https://garage-ui.example"} + + svc, err := NewAuthService(authCfg, srvCfg) + if err != nil { + t.Fatalf("NewAuthService with tls_skip_verify=true should succeed: %v", err) + } + if svc.oidcProvider == nil { + t.Fatal("oidcProvider not initialized") + } + if svc.oidcClient == nil { + t.Fatal("oidcClient should be set when tls_skip_verify=true") + } +} + func TestNewAuthService_OIDCEnabled_BadIssuerURLReturnsError(t *testing.T) { authCfg := &config.AuthConfig{ OIDC: config.OIDCConfig{