Support tls server name overrides in EgressSelectorConfiguration (#136640)

add integration test

Use proper test header, change to  etcdMain to recognize test flags

fix goroutine leak in integration test

redo integration test with kubeapiserverapptesting

fix comment capitalization, use existing client libraries

fix comment capitalization, use existing client libraries

consolidate http connect handler logic from odic and tls_server-name into helper

add expected SNI, remove unused test

move oidc helpers.go to right dir, remove copyright year

split helpers.go into descriptive file names

use atomic ptr for SNI, refactor generateTestCerts, remove errors from runTLSEgressProxy, explain jwksHandler in comment

use testify, add back context messages

Clean up tests

Signed-off-by: Monis Khan <mok@microsoft.com>
This commit is contained in:
kennangaibel
2026-03-11 11:23:35 -07:00
committed by GitHub
parent 65d0bc2b76
commit c19439f3f3
14 changed files with 510 additions and 89 deletions

View File

@@ -146,6 +146,10 @@ type TLSConfig struct {
// Must be configured if TCPTransport.URL is prefixed with https://
// +optional
ClientCert string
// tlsServerName is used to check server certificate. If tlsServerName is empty, the hostname used to contact the server is used.
// +optional
TLSServerName string
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

View File

@@ -147,6 +147,10 @@ type TLSConfig struct {
// Must be configured if TCPTransport.URL is prefixed with https://
// +optional
ClientCert string `json:"clientCert,omitempty"`
// tlsServerName is used to check server certificate. If tlsServerName is empty, the hostname used to contact the server is used.
// +optional
TLSServerName string `json:"tlsServerName,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

View File

@@ -817,6 +817,7 @@ func autoConvert_v1alpha1_TLSConfig_To_apiserver_TLSConfig(in *TLSConfig, out *a
out.CABundle = in.CABundle
out.ClientKey = in.ClientKey
out.ClientCert = in.ClientCert
out.TLSServerName = in.TLSServerName
return nil
}
@@ -829,6 +830,7 @@ func autoConvert_apiserver_TLSConfig_To_v1alpha1_TLSConfig(in *apiserver.TLSConf
out.CABundle = in.CABundle
out.ClientKey = in.ClientKey
out.ClientCert = in.ClientCert
out.TLSServerName = in.TLSServerName
return nil
}

View File

@@ -118,6 +118,10 @@ type TLSConfig struct {
// Must be configured if TCPTransport.URL is prefixed with https://
// +optional
ClientCert string `json:"clientCert,omitempty"`
// tlsServerName is used to check server certificate. If tlsServerName is empty, the hostname used to contact the server is used.
// +optional
TLSServerName string `json:"tlsServerName,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

View File

@@ -753,6 +753,7 @@ func autoConvert_v1beta1_TLSConfig_To_apiserver_TLSConfig(in *TLSConfig, out *ap
out.CABundle = in.CABundle
out.ClientKey = in.ClientKey
out.ClientCert = in.ClientCert
out.TLSServerName = in.TLSServerName
return nil
}
@@ -765,6 +766,7 @@ func autoConvert_apiserver_TLSConfig_To_v1beta1_TLSConfig(in *apiserver.TLSConfi
out.CABundle = in.CABundle
out.ClientKey = in.ClientKey
out.ClientCert = in.ClientCert
out.TLSServerName = in.TLSServerName
return nil
}

View File

@@ -302,6 +302,52 @@ spec:
expectedResult: nil,
expectedError: ptr.To("invalid service configuration object \"DaemonSet\""),
},
{
name: "v1beta1 with tlsServerName",
createFile: true,
contents: `
apiVersion: apiserver.k8s.io/v1beta1
kind: EgressSelectorConfiguration
egressSelections:
- name: "cluster"
connection:
proxyProtocol: "HTTPConnect"
transport:
tcp:
url: "https://proxy.kube-system.svc.cluster.local:8443"
tlsConfig:
caBundle: "/etc/srv/kubernetes/pki/konnectivity-server/ca.crt"
clientKey: "/etc/srv/kubernetes/pki/konnectivity-server/client.key"
clientCert: "/etc/srv/kubernetes/pki/konnectivity-server/client.crt"
tlsServerName: "konnectivity-server.example.com"
`,
expectedResult: &apiserver.EgressSelectorConfiguration{
TypeMeta: metav1.TypeMeta{
Kind: "",
APIVersion: "",
},
EgressSelections: []apiserver.EgressSelection{
{
Name: "cluster",
Connection: apiserver.Connection{
ProxyProtocol: "HTTPConnect",
Transport: &apiserver.Transport{
TCP: &apiserver.TCPTransport{
URL: "https://proxy.kube-system.svc.cluster.local:8443",
TLSConfig: &apiserver.TLSConfig{
CABundle: "/etc/srv/kubernetes/pki/konnectivity-server/ca.crt",
ClientKey: "/etc/srv/kubernetes/pki/konnectivity-server/client.key",
ClientCert: "/etc/srv/kubernetes/pki/konnectivity-server/client.crt",
TLSServerName: "konnectivity-server.example.com",
},
},
},
},
},
},
},
expectedError: nil,
},
}
for _, tc := range testcases {

View File

@@ -300,6 +300,7 @@ func getTLSConfig(t *apiserver.TLSConfig) (*tls.Config, error) {
return &tls.Config{
Certificates: []tls.Certificate{clientCerts},
RootCAs: certPool,
ServerName: t.TLSServerName,
}, nil
}

View File

@@ -21,6 +21,8 @@ import (
"errors"
"fmt"
"net"
"os"
"path/filepath"
"strings"
"testing"
"time"
@@ -29,6 +31,7 @@ import (
utilnet "k8s.io/apimachinery/pkg/util/net"
"k8s.io/apiserver/pkg/apis/apiserver"
"k8s.io/apiserver/pkg/server/egressselector/metrics"
certutil "k8s.io/client-go/util/cert"
"k8s.io/component-base/metrics/legacyregistry"
"k8s.io/component-base/metrics/testutil"
testingclock "k8s.io/utils/clock/testing"
@@ -343,3 +346,61 @@ konnectivity_network_proxy_client_client_connections{status="dialing"} 1
})
}
}
func TestGetTLSConfig(t *testing.T) {
tempDir := t.TempDir()
certPEM, keyPEM, err := certutil.GenerateSelfSignedCertKey("localhost", nil, nil)
if err != nil {
t.Fatalf("Failed to generate test certificates: %v", err)
}
certPath := filepath.Join(tempDir, "cert.crt")
keyPath := filepath.Join(tempDir, "cert.key")
if err := os.WriteFile(certPath, certPEM, 0600); err != nil {
t.Fatalf("Failed to write cert file: %v", err)
}
if err := os.WriteFile(keyPath, keyPEM, 0600); err != nil {
t.Fatalf("Failed to write key file: %v", err)
}
testcases := []struct {
name string
tlsConfig *apiserver.TLSConfig
expectedServerName string
}{
{
name: "with TLSServerName set",
tlsConfig: &apiserver.TLSConfig{
CABundle: certPath,
ClientCert: certPath,
ClientKey: keyPath,
TLSServerName: "custom-server.example.com",
},
expectedServerName: "custom-server.example.com",
},
{
name: "without TLSServerName (empty)",
tlsConfig: &apiserver.TLSConfig{
CABundle: certPath,
ClientCert: certPath,
ClientKey: keyPath,
TLSServerName: "",
},
expectedServerName: "",
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
tlsConfig, err := getTLSConfig(tc.tlsConfig)
if err != nil {
t.Fatalf("getTLSConfig returned unexpected error: %v", err)
}
if tlsConfig.ServerName != tc.expectedServerName {
t.Errorf("expected ServerName %q, got %q", tc.expectedServerName, tlsConfig.ServerName)
}
})
}
}

View File

@@ -0,0 +1,27 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package egressselector
import (
"testing"
"k8s.io/kubernetes/test/integration/framework"
)
func TestMain(m *testing.M) {
framework.EtcdMain(m.Run)
}

View File

@@ -0,0 +1,232 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package egressselector
import (
"context"
"crypto"
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"os"
"path/filepath"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/util/wait"
certutil "k8s.io/client-go/util/cert"
"k8s.io/client-go/util/keyutil"
kubeapiserverapptesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
"k8s.io/kubernetes/test/integration/framework"
testutils "k8s.io/kubernetes/test/utils"
utilsoidc "k8s.io/kubernetes/test/utils/oidc"
)
func generateTestCerts(t *testing.T, tempDir, serverName string) (caCertPath, serverCertPath, serverKeyPath, clientCertPath, clientKeyPath string) {
t.Helper()
caKey, err := testutils.NewPrivateKey()
require.NoError(t, err, "Failed to generate CA key")
caCert, err := certutil.NewSelfSignedCACert(certutil.Config{CommonName: "Test CA"}, caKey)
require.NoError(t, err, "Failed to create CA certificate")
caCertPath = filepath.Join(tempDir, "ca.crt")
require.NoError(t, os.WriteFile(caCertPath, testutils.EncodeCertPEM(caCert), 0600), "Failed to write CA cert")
serverKey, err := testutils.NewPrivateKey()
require.NoError(t, err, "Failed to generate server key")
serverCert, err := testutils.NewSignedCert(&certutil.Config{
CommonName: serverName,
AltNames: certutil.AltNames{DNSNames: []string{serverName}},
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}, serverKey, caCert, caKey)
require.NoError(t, err, "Failed to create server certificate")
serverCertPath, serverKeyPath = writeCertAndKey(t, tempDir, "server", serverCert, serverKey)
clientKey, err := testutils.NewPrivateKey()
require.NoError(t, err, "Failed to generate client key")
clientCert, err := testutils.NewSignedCert(&certutil.Config{
CommonName: "test-client",
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}, clientKey, caCert, caKey)
require.NoError(t, err, "Failed to create client certificate")
clientCertPath, clientKeyPath = writeCertAndKey(t, tempDir, "client", clientCert, clientKey)
return caCertPath, serverCertPath, serverKeyPath, clientCertPath, clientKeyPath
}
func writeCertAndKey(t *testing.T, dir, name string, cert *x509.Certificate, key crypto.Signer) (certPath, keyPath string) {
t.Helper()
certPath = filepath.Join(dir, name+".crt")
keyPath = filepath.Join(dir, name+".key")
require.NoError(t, os.WriteFile(certPath, testutils.EncodeCertPEM(cert), 0600))
keyPEM, err := keyutil.MarshalPrivateKeyToPEM(key)
require.NoError(t, err)
require.NoError(t, os.WriteFile(keyPath, keyPEM, 0600))
return certPath, keyPath
}
// runTLSEgressProxy runs an HTTP CONNECT proxy with TLS.
func runTLSEgressProxy(t *testing.T, serverCertPath, serverKeyPath, caCertPath string, called *atomic.Bool, observedSNI *atomic.Pointer[string]) string {
t.Helper()
serverCert, err := tls.LoadX509KeyPair(serverCertPath, serverKeyPath)
require.NoError(t, err, "Failed to load server cert")
clientCAs, err := certutil.NewPool(caCertPath)
require.NoError(t, err, "Failed to load CA cert pool")
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{serverCert},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: clientCAs,
GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
observedSNI.Store(&hello.ServerName)
return nil, nil
},
}
listener, err := tls.Listen("tcp", "127.0.0.1:0", tlsConfig)
require.NoError(t, err, "Failed to start TLS listener")
proxyAddr := listener.Addr().String()
server := &http.Server{Handler: utilsoidc.NewHTTPConnectProxyHandler(t, called)}
go func() {
if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
t.Logf("TLS egress proxy serve error: %v", err)
}
}()
t.Cleanup(func() {
if err := server.Close(); err != nil {
t.Logf("Failed to close server: %v", err)
}
})
return proxyAddr
}
// TestTLSServerName verifies that the tlsServerName field in the egress selector configuration
// correctly overrides SNI for TLS certificate validation when connecting through a proxy.
func TestTLSServerName(t *testing.T) {
tempDir := t.TempDir()
proxyHostname := "egress-proxy.example.com"
caCertPath, serverCertPath, serverKeyPath, clientCertPath, clientKeyPath := generateTestCerts(t, tempDir, proxyHostname)
testCases := []struct {
name string
tlsServerName string
expectProxyCalled bool
expectedSNI string
description string
}{
{
name: "matching tlsServerName succeeds TLS handshake",
tlsServerName: proxyHostname,
expectProxyCalled: true,
expectedSNI: proxyHostname,
description: "SNI override matches cert SANs, connection succeeds",
},
{
name: "mismatched tlsServerName fails TLS handshake",
tlsServerName: "wrong-hostname.example.com",
expectProxyCalled: false,
expectedSNI: "wrong-hostname.example.com",
description: "SNI override doesn't match cert SANs, connection fails",
},
{
name: "empty tlsServerName uses dial address and fails",
tlsServerName: "",
expectProxyCalled: false,
expectedSNI: "",
description: "Without tlsServerName, uses dial address 127.0.0.1 which fails cert validation",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var proxyCalled atomic.Bool
var observedSNI atomic.Pointer[string]
proxyAddr := runTLSEgressProxy(t, serverCertPath, serverKeyPath, caCertPath, &proxyCalled, &observedSNI)
t.Logf("TLS egress proxy ready at %s (cert issued for: %s)", proxyAddr, proxyHostname)
egressConfig := fmt.Sprintf(`
apiVersion: apiserver.k8s.io/v1beta1
kind: EgressSelectorConfiguration
egressSelections:
- name: cluster
connection:
proxyProtocol: HTTPConnect
transport:
tcp:
url: https://%s
tlsConfig:
caBundle: %s
clientCert: %s
clientKey: %s
tlsServerName: %s
`, proxyAddr, caCertPath, clientCertPath, clientKeyPath, tc.tlsServerName)
const authenticationConfigWithEgress = `
apiVersion: apiserver.config.k8s.io/v1
kind: AuthenticationConfiguration
jwt:
- issuer:
url: https://panda.snorlax
audiences:
- foo
egressSelectorType: cluster
claimMappings:
username:
expression: "'test-' + claims.sub"
`
server := kubeapiserverapptesting.StartTestServerOrDie(
t,
kubeapiserverapptesting.NewDefaultTestServerOptions(),
[]string{
fmt.Sprintf("--egress-selector-config-file=%s", utilsoidc.WriteTempFile(t, egressConfig)),
fmt.Sprintf("--authentication-config=%s", utilsoidc.WriteTempFile(t, authenticationConfigWithEgress)),
},
framework.SharedEtcd(),
)
t.Cleanup(server.TearDownFn)
var sni string
err := wait.PollUntilContextTimeout(t.Context(), time.Second, wait.ForeverTestTimeout, true,
func(ctx context.Context) (done bool, err error) {
if ptr := observedSNI.Load(); ptr != nil {
sni = *ptr
return true, nil
}
return false, nil
},
)
require.NoError(t, err, "SNI not observed")
require.Equal(t, tc.expectedSNI, sni, "SNI mismatch")
require.Equal(t, tc.expectProxyCalled, proxyCalled.Load(), "proxy called mismatch")
})
}
}

View File

@@ -19,7 +19,6 @@ package oidc
import (
"context"
"errors"
"io"
"net"
"net/http"
"sync"
@@ -29,6 +28,7 @@ import (
utilnet "k8s.io/apimachinery/pkg/util/net"
"k8s.io/apimachinery/pkg/util/wait"
utilsoidc "k8s.io/kubernetes/test/utils/oidc"
)
func runEgressProxy(t testing.TB, udsName string, ready chan<- struct{}) {
@@ -41,75 +41,7 @@ func runEgressProxy(t testing.TB, udsName string, ready chan<- struct{}) {
}
var called atomic.Bool
httpConnectProxy := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/ready" {
t.Log("egress proxy ready")
w.WriteHeader(http.StatusOK)
return
}
called.Store(true)
if r.Method != http.MethodConnect {
http.Error(w, "this proxy only supports CONNECT passthrough", http.StatusMethodNotAllowed)
return
}
backendConn, err := (&net.Dialer{}).DialContext(r.Context(), "tcp", r.Host)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer func() { _ = backendConn.Close() }()
hijacker, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "hijacking not supported", http.StatusInternalServerError)
return
}
requestHijackedConn, _, err := hijacker.Hijack()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer func() { _ = requestHijackedConn.Close() }()
// use t.Errorf for all errors after this Write since the client may think the connection is good
_, err = requestHijackedConn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
if err != nil {
t.Errorf("unexpected established error: %v", err)
return
}
writerComplete := make(chan struct{})
readerComplete := make(chan struct{})
go func() {
_, err := io.Copy(backendConn, requestHijackedConn)
if err != nil && !utilnet.IsProbableEOF(err) {
t.Errorf("unexpected writer error: %v", err)
}
close(writerComplete)
}()
go func() {
_, err := io.Copy(requestHijackedConn, backendConn)
if err != nil && !utilnet.IsProbableEOF(err) {
t.Errorf("unexpected reader error: %v", err)
}
close(readerComplete)
}()
// Wait for one half the connection to exit. Once it does,
// the defer will clean up the other half of the connection.
select {
case <-writerComplete:
case <-readerComplete:
}
})
server := http.Server{Handler: httpConnectProxy}
server := http.Server{Handler: utilsoidc.NewHTTPConnectProxyHandler(t, &called)}
t.Cleanup(func() {
if !called.Load() {

View File

@@ -2219,7 +2219,7 @@ func startTestAPIServerForOIDC[L utilsoidc.JosePublicKey](t *testing.T, c apiSer
var customFlags []string
if len(c.authenticationConfigYAML) > 0 {
customFlags = []string{fmt.Sprintf("--authentication-config=%s", writeTempFile(t, c.authenticationConfigYAML))}
customFlags = []string{fmt.Sprintf("--authentication-config=%s", utilsoidc.WriteTempFile(t, c.authenticationConfigYAML))}
if c.needsEgressProxyOnStart {
udsName := filepath.Join(t.TempDir(), "uds")
ready := make(chan struct{})
@@ -2241,7 +2241,7 @@ egressSelections:
uds:
udsName: %s
`, udsName)
customFlags = append(customFlags, fmt.Sprintf("--egress-selector-config-file=%s", writeTempFile(t, egressConfig)))
customFlags = append(customFlags, fmt.Sprintf("--egress-selector-config-file=%s", utilsoidc.WriteTempFile(t, egressConfig)))
}
} else {
customFlags = []string{
@@ -2358,23 +2358,6 @@ func generateCert(t *testing.T) (cert, key []byte, certFilePath, keyFilePath str
return cert, key, certFilePath, keyFilePath
}
func writeTempFile(t *testing.T, content string) string {
t.Helper()
file, err := os.CreateTemp("", "oidc-test")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := os.Remove(file.Name()); err != nil {
t.Fatal(err)
}
})
if err := os.WriteFile(file.Name(), []byte(content), 0600); err != nil {
t.Fatal(err)
}
return file.Name()
}
// indentCertificateAuthority indents the certificate authority to match
// the format of the generated authentication config.
func indentCertificateAuthority(caCert string) string {

103
test/utils/oidc/proxy.go Normal file
View File

@@ -0,0 +1,103 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package oidc
import (
"io"
"net"
"net/http"
"sync/atomic"
"testing"
utilnet "k8s.io/apimachinery/pkg/util/net"
)
// NewHTTPConnectProxyHandler returns an http.Handler that implements an HTTP CONNECT proxy.
// When a CONNECT request is received, it dials the target, hijacks the client connection,
// and bidirectionally copies data between them. The called flag is set to true when a
// non-ready request is received.
func NewHTTPConnectProxyHandler(t testing.TB, called *atomic.Bool) http.Handler {
t.Helper()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/ready" {
t.Log("egress proxy ready")
w.WriteHeader(http.StatusOK)
return
}
called.Store(true)
if r.Method != http.MethodConnect {
http.Error(w, "this proxy only supports CONNECT passthrough", http.StatusMethodNotAllowed)
return
}
backendConn, err := (&net.Dialer{}).DialContext(r.Context(), "tcp", r.Host)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer func() { _ = backendConn.Close() }()
hijacker, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "hijacking not supported", http.StatusInternalServerError)
return
}
clientConn, _, err := hijacker.Hijack()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer func() { _ = clientConn.Close() }()
// use t.Errorf for all errors after this Write since the client may think the connection is good
_, err = clientConn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
if err != nil {
t.Errorf("unexpected established error: %v", err)
return
}
writerComplete := make(chan struct{})
readerComplete := make(chan struct{})
go func() {
_, err := io.Copy(backendConn, clientConn)
if err != nil && !utilnet.IsProbableEOF(err) {
t.Logf("writer error: %v", err)
}
close(writerComplete)
}()
go func() {
_, err := io.Copy(clientConn, backendConn)
if err != nil && !utilnet.IsProbableEOF(err) {
t.Logf("reader error: %v", err)
}
close(readerComplete)
}()
// Wait for one half the connection to exit. Once it does,
// the defer will clean up the other half of the connection.
select {
case <-writerComplete:
case <-readerComplete:
}
})
}

View File

@@ -33,6 +33,7 @@ import (
"github.com/stretchr/testify/require"
"gopkg.in/go-jose/go-jose.v2"
"k8s.io/kubernetes/test/utils/oidc/handlers"
)
@@ -255,3 +256,22 @@ func GetSignatureAlgorithm[K JoseKey](key K) jose.SignatureAlgorithm {
panic("unknown key type") // should be impossible
}
}
// WriteTempFile writes content to a temporary file and returns its path.
// The file is automatically cleaned up when the test completes.
func WriteTempFile(t *testing.T, content string) string {
t.Helper()
file, err := os.CreateTemp("", "oidc-test")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := os.Remove(file.Name()); err != nil {
t.Fatal(err)
}
})
if err := os.WriteFile(file.Name(), []byte(content), 0600); err != nil {
t.Fatal(err)
}
return file.Name()
}