@@ -17,11 +17,14 @@ package kms
1717
1818import (
1919 "context"
20+ "encoding/json"
2021 "net"
2122 "net/http"
2223 "net/http/httptest"
2324 "os"
25+ "sync"
2426 "testing"
27+ "time"
2528
2629 "github.com/stretchr/testify/assert"
2730 "github.com/tektoncd/chains/pkg/config"
@@ -126,6 +129,216 @@ func TestValidVaultAddressConnection(t *testing.T) {
126129 })
127130}
128131
132+ // TestOIDCTokenEndToEnd proves the full flow: JWT file → rpcAuth.OIDC.Token
133+ // → ApplyRPCAuthOpts → oidcLogin → Vault HTTP request.
134+ // A mock Vault server captures the login request and verifies the JWT, role,
135+ // and auth path arrive exactly as configured.
136+ func TestOIDCTokenEndToEnd (t * testing.T ) {
137+ var mu sync.Mutex
138+ var loginCalled bool
139+ var receivedJWT , receivedRole , receivedPath string
140+
141+ server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
142+ mu .Lock ()
143+ defer mu .Unlock ()
144+
145+ switch {
146+ case r .URL .Path == "/v1/auth/jwt/login" && r .Method == http .MethodPut :
147+ loginCalled = true
148+ receivedPath = "jwt"
149+
150+ var body map [string ]interface {}
151+ if err := json .NewDecoder (r .Body ).Decode (& body ); err == nil {
152+ if v , ok := body ["jwt" ].(string ); ok {
153+ receivedJWT = v
154+ }
155+ if v , ok := body ["role" ].(string ); ok {
156+ receivedRole = v
157+ }
158+ }
159+
160+ w .Header ().Set ("Content-Type" , "application/json" )
161+ resp := `{"auth":{"client_token":"hvs.mock-vault-token","policies":["default"],"lease_duration":3600,"renewable":true}}`
162+ w .Write ([]byte (resp ))
163+ default :
164+ w .WriteHeader (http .StatusNotFound )
165+ }
166+ }))
167+ defer server .Close ()
168+
169+ tokenFile , err := os .CreateTemp ("" , "jwt-token" )
170+ if err != nil {
171+ t .Fatalf ("creating temp file: %v" , err )
172+ }
173+ defer os .Remove (tokenFile .Name ())
174+
175+ err = os .WriteFile (tokenFile .Name (), []byte ("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.test-payload\n " ), 0644 )
176+ if err != nil {
177+ t .Fatalf ("writing temp file: %v" , err )
178+ }
179+
180+ cfg := config.KMSSigner {
181+ KMSRef : "hashivault://supply-chain" ,
182+ Auth : config.KMSAuth {
183+ Address : server .URL ,
184+ OIDC : config.KMSAuthOIDC {
185+ Path : "jwt" ,
186+ Role : "tekton-chains" ,
187+ TokenPath : tokenFile .Name (),
188+ },
189+ },
190+ }
191+
192+ signer , err := NewSigner (context .Background (), cfg )
193+
194+ // The signer should be created successfully — oidcLogin exchanges the
195+ // JWT for a Vault token, newHashivaultClient stores it, no transit API
196+ // call happens during construction.
197+ if err != nil {
198+ t .Fatalf ("NewSigner should succeed when OIDC login returns a valid token, got: %v" , err )
199+ }
200+ if signer == nil {
201+ t .Fatal ("signer must not be nil" )
202+ }
203+
204+ mu .Lock ()
205+ defer mu .Unlock ()
206+ assert .True (t , loginCalled , "Vault auth/jwt/login endpoint must have been called" )
207+ assert .Equal (t , "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.test-payload" , receivedJWT ,
208+ "JWT sent to Vault must match the trimmed file contents" )
209+ assert .Equal (t , "tekton-chains" , receivedRole ,
210+ "role sent to Vault must match the configured OIDC role" )
211+ assert .Equal (t , "jwt" , receivedPath ,
212+ "auth path must match the configured OIDC path" )
213+ }
214+
215+ // TestOIDCTokenFallbackToDefaultPath proves that when oidc.path/role are set
216+ // but no token-path is given, the code tries the default K8s SA token path.
217+ func TestOIDCTokenFallbackToDefaultPath (t * testing.T ) {
218+ cfg := config.KMSSigner {
219+ Auth : config.KMSAuth {
220+ OIDC : config.KMSAuthOIDC {
221+ Path : "jwt" ,
222+ Role : "tekton-chains" ,
223+ },
224+ },
225+ }
226+
227+ _ , err := NewSigner (context .Background (), cfg )
228+
229+ if err == nil {
230+ t .Fatal ("expected error when default SA token path does not exist" )
231+ }
232+ assert .Contains (t , err .Error (), "reading OIDC token" )
233+ assert .Contains (t , err .Error (), defaultOIDCTokenPath ,
234+ "error must reference the default K8s SA token path" )
235+ }
236+
237+ // TestOIDCTokenSkippedWhenNotConfigured proves the OIDC block is not entered
238+ // when neither oidc.path nor oidc.role are set.
239+ func TestOIDCTokenSkippedWhenNotConfigured (t * testing.T ) {
240+ cfg := config.KMSSigner {}
241+
242+ _ , err := NewSigner (context .Background (), cfg )
243+
244+ if err == nil {
245+ t .Fatal ("expected error when no KMS config is set" )
246+ }
247+ assert .NotContains (t , err .Error (), "reading OIDC token" ,
248+ "OIDC reading must not be attempted when OIDC is not configured" )
249+ assert .NotContains (t , err .Error (), "OIDC token file" ,
250+ "OIDC empty-file check must not be reached when OIDC is not configured" )
251+ }
252+
253+ // TestOIDCTokenSkippedWhenStaticTokenSet proves that the file-based OIDC
254+ // token reading is skipped when a static Vault token is already set, even if
255+ // oidc.path/oidc.role are configured. This prevents breaking existing users
256+ // who have both a static token and leftover OIDC config.
257+ func TestOIDCTokenSkippedWhenStaticTokenSet (t * testing.T ) {
258+ cfg := config.KMSSigner {
259+ Auth : config.KMSAuth {
260+ Token : "my-static-vault-token" ,
261+ OIDC : config.KMSAuthOIDC {
262+ Path : "jwt" ,
263+ Role : "tekton-chains" ,
264+ },
265+ },
266+ }
267+
268+ _ , err := NewSigner (context .Background (), cfg )
269+
270+ if err == nil {
271+ t .Fatal ("expected error (no KMSRef), but should NOT be an OIDC reading error" )
272+ }
273+ assert .NotContains (t , err .Error (), "reading OIDC token" ,
274+ "OIDC file reading must be skipped when a static token is set" )
275+ assert .NotContains (t , err .Error (), "OIDC token file" ,
276+ "OIDC empty check must be skipped when a static token is set" )
277+ }
278+
279+ // TestOIDCTokenEmptyFileErrors proves that an empty token file produces a
280+ // clear error rather than silently breaking OIDC.
281+ func TestOIDCTokenEmptyFileErrors (t * testing.T ) {
282+ tokenFile , err := os .CreateTemp ("" , "empty-jwt" )
283+ if err != nil {
284+ t .Fatalf ("creating temp file: %v" , err )
285+ }
286+ defer os .Remove (tokenFile .Name ())
287+
288+ cfg := config.KMSSigner {
289+ Auth : config.KMSAuth {
290+ OIDC : config.KMSAuthOIDC {
291+ Path : "jwt" ,
292+ Role : "tekton-chains" ,
293+ TokenPath : tokenFile .Name (),
294+ },
295+ },
296+ }
297+
298+ _ , err = NewSigner (context .Background (), cfg )
299+
300+ if err == nil {
301+ t .Fatal ("expected error for empty OIDC token file" )
302+ }
303+ assert .Contains (t , err .Error (), "OIDC token file" )
304+ assert .Contains (t , err .Error (), "is empty" )
305+ }
306+
307+ // TestOIDCTokenNotReadWhenSpireConfigured proves that the file-based OIDC
308+ // token reading is skipped when Spire is configured (Spire takes precedence).
309+ // The Spire gRPC client retries indefinitely, so we use a short-lived context
310+ // to make it fail quickly and verify the error is about Spire, not file-based
311+ // OIDC.
312+ func TestOIDCTokenNotReadWhenSpireConfigured (t * testing.T ) {
313+ ctx , cancel := context .WithTimeout (context .Background (), 3 * time .Second )
314+ defer cancel ()
315+
316+ cfg := config.KMSSigner {
317+ Auth : config.KMSAuth {
318+ OIDC : config.KMSAuthOIDC {
319+ Path : "jwt" ,
320+ Role : "tekton-chains" ,
321+ },
322+ Spire : config.KMSAuthSpire {
323+ Sock : "unix:///tmp/nonexistent-spire.sock" ,
324+ Audience : "test" ,
325+ },
326+ },
327+ }
328+
329+ _ , err := NewSigner (ctx , cfg )
330+
331+ if err == nil {
332+ t .Fatal ("expected error when Spire socket does not exist" )
333+ }
334+ // The error should be about Spire/context, NOT about reading an OIDC
335+ // token file — proving the Spire block runs and the file block is skipped.
336+ assert .NotContains (t , err .Error (), "reading OIDC token" ,
337+ "file-based OIDC reading must be skipped when Spire is configured" )
338+ assert .NotContains (t , err .Error (), "OIDC token file" ,
339+ "file-based OIDC empty check must be skipped when Spire is configured" )
340+ }
341+
129342// Test for getKMSAuthToken with non-directory path
130343func TestGetKMSAuthToken_NotADirectory (t * testing.T ) {
131344 tempFile , err := os .CreateTemp ("" , "not-a-dir" )
0 commit comments