@@ -151,7 +151,7 @@ func setupBackend(t *testing.T, cfg testConfig) (closeableBackend, logical.Stora
151151 return cb , storage
152152}
153153
154- func getTestJWT (t * testing.T , privKey string , cl sqjwt. Claims , privateCl interface {}) (string , * ecdsa.PrivateKey ) {
154+ func getTestJWT (t * testing.T , privKey string , cl interface {} , privateCl interface {}) (string , * ecdsa.PrivateKey ) {
155155 t .Helper ()
156156 var key * ecdsa.PrivateKey
157157 block , _ := pem .Decode ([]byte (privKey ))
@@ -208,6 +208,223 @@ func getTestOIDC(t *testing.T) string {
208208 return out .AccessToken
209209}
210210
211+ // TestLoginBoundAudiences tests that the login JWT's aud claim is ignored if
212+ // it is a single string. This is a case that is fixed in later versions of
213+ // the plugin.
214+ // See https://github.com/hashicorp/vault-plugin-auth-jwt/pull/308
215+ func TestLoginBoundAudiences (t * testing.T ) {
216+ testCases := []struct {
217+ name string
218+ roleData map [string ]interface {}
219+ jwtData map [string ]interface {}
220+ expectErr bool
221+ }{
222+ {
223+ name : "jwt with string aud and no bound_audiences" ,
224+ roleData : map [string ]interface {}{
225+ "role_type" : "jwt" ,
226+ "bound_subject" : "subject" ,
227+ "user_claim" : "https://vault/user" ,
228+ "policies" : "test" ,
229+ "period" : "3s" ,
230+ "ttl" : "1s" ,
231+ "num_uses" : 12 ,
232+ "max_ttl" : "5s" ,
233+ },
234+ jwtData : map [string ]interface {}{
235+ "sub" : "subject" ,
236+ "iss" : "https://team-vault.auth0.com/" ,
237+ "aud" : "https://vault.plugin.auth.jwt.test" ,
238+ "nbf" : sqjwt .NewNumericDate (time .Now ().Add (- 5 * time .Second )),
239+ },
240+ expectErr : true ,
241+ },
242+ {
243+ name : "jwt with array aud and no bound_audiences" ,
244+ // bound_audiences is unset
245+ roleData : map [string ]interface {}{
246+ "role_type" : "jwt" ,
247+ "bound_subject" : "subject" ,
248+ "user_claim" : "https://vault/user" ,
249+ "policies" : "test" ,
250+ "period" : "3s" ,
251+ "ttl" : "1s" ,
252+ "num_uses" : 12 ,
253+ "max_ttl" : "5s" ,
254+ },
255+ jwtData : map [string ]interface {}{
256+ "sub" : "subject" ,
257+ "iss" : "https://team-vault.auth0.com/" ,
258+ "aud" : []string {"https://vault.plugin.auth.jwt.test" },
259+ "nbf" : sqjwt .NewNumericDate (time .Now ().Add (- 5 * time .Second )),
260+ },
261+ expectErr : true ,
262+ },
263+ {
264+ name : "jwt with array aud and role with bound_audiences" ,
265+ roleData : map [string ]interface {}{
266+ "role_type" : "jwt" ,
267+ "bound_audiences" : []string {"https://vault.plugin.auth.jwt.test" , "another_audience" },
268+ "bound_subject" : "subject" ,
269+ "user_claim" : "https://vault/user" ,
270+ "policies" : "test" ,
271+ "period" : "3s" ,
272+ "ttl" : "1s" ,
273+ "num_uses" : 12 ,
274+ "max_ttl" : "5s" ,
275+ },
276+ // aud is an array
277+ jwtData : map [string ]interface {}{
278+ "sub" : "subject" ,
279+ "iss" : "https://team-vault.auth0.com/" ,
280+ "aud" : []string {"https://vault.plugin.auth.jwt.test" },
281+ "nbf" : sqjwt .NewNumericDate (time .Now ().Add (- 5 * time .Second )),
282+ },
283+ expectErr : false ,
284+ },
285+ {
286+ name : "error jwt with string aud and role with no match of bound_audiences" ,
287+ roleData : map [string ]interface {}{
288+ "role_type" : "jwt" ,
289+ "bound_audiences" : []string {"https://vault.plugin.auth.jwt.test" , "another_audience" },
290+ "bound_subject" : "subject" ,
291+ "user_claim" : "https://vault/user" ,
292+ "policies" : "test" ,
293+ "period" : "3s" ,
294+ "ttl" : "1s" ,
295+ "num_uses" : 12 ,
296+ "max_ttl" : "5s" ,
297+ },
298+ // aud is not contained in role's bound_audiences
299+ jwtData : map [string ]interface {}{
300+ "sub" : "subject" ,
301+ "iss" : "https://team-vault.auth0.com/" ,
302+ "aud" : "https://foo.com" ,
303+ "nbf" : sqjwt .NewNumericDate (time .Now ().Add (- 5 * time .Second )),
304+ },
305+ expectErr : true ,
306+ },
307+ {
308+ name : "jwt with string aud and role with bound_audiences" ,
309+ roleData : map [string ]interface {}{
310+ "role_type" : "jwt" ,
311+ "bound_audiences" : []string {"https://vault.plugin.auth.jwt.test" , "another_audience" },
312+ "bound_subject" : "subject" ,
313+ "user_claim" : "https://vault/user" ,
314+ "policies" : "test" ,
315+ "period" : "3s" ,
316+ "ttl" : "1s" ,
317+ "num_uses" : 12 ,
318+ "max_ttl" : "5s" ,
319+ },
320+ jwtData : map [string ]interface {}{
321+ "sub" : "subject" ,
322+ "iss" : "https://team-vault.auth0.com/" ,
323+ "aud" : "https://vault.plugin.auth.jwt.test" ,
324+ "nbf" : sqjwt .NewNumericDate (time .Now ().Add (- 5 * time .Second )),
325+ },
326+ expectErr : false ,
327+ },
328+ {
329+ name : "jwt with no aud and role with no bound_audiences" ,
330+ roleData : map [string ]interface {}{
331+ "role_type" : "jwt" ,
332+ "bound_subject" : "subject" ,
333+ "user_claim" : "https://vault/user" ,
334+ "policies" : "test" ,
335+ "period" : "3s" ,
336+ "ttl" : "1s" ,
337+ "num_uses" : 12 ,
338+ "max_ttl" : "5s" ,
339+ },
340+ // aud is not set
341+ jwtData : map [string ]interface {}{
342+ "sub" : "subject" ,
343+ "iss" : "https://team-vault.auth0.com/" ,
344+ "nbf" : sqjwt .NewNumericDate (time .Now ().Add (- 5 * time .Second )),
345+ },
346+ expectErr : false ,
347+ },
348+ }
349+
350+ for _ , tt := range testCases {
351+ t .Run (tt .name , func (t * testing.T ) {
352+ b , storage := getBackend (t )
353+
354+ configData := map [string ]interface {}{
355+ "bound_issuer" : "https://team-vault.auth0.com/" ,
356+ "jwt_validation_pubkeys" : ecdsaPubKey ,
357+ }
358+
359+ req := & logical.Request {
360+ Operation : logical .UpdateOperation ,
361+ Path : configPath ,
362+ Storage : storage ,
363+ Data : configData ,
364+ }
365+
366+ resp , err := b .HandleRequest (context .Background (), req )
367+ if err != nil || (resp != nil && resp .IsError ()) {
368+ t .Fatalf ("err:%s resp:%#v\n " , err , resp )
369+ }
370+
371+ req = & logical.Request {
372+ Operation : logical .CreateOperation ,
373+ Path : "role/plugin-test" ,
374+ Storage : storage ,
375+ Data : tt .roleData ,
376+ }
377+
378+ resp , err = b .HandleRequest (context .Background (), req )
379+ if err != nil || (resp != nil && resp .IsError ()) {
380+ t .Fatalf ("err:%s resp:%#v\n " , err , resp )
381+ }
382+
383+ privateCl := struct {
384+ User string `json:"https://vault/user"`
385+ Groups []string `json:"https://vault/groups"`
386+ }{
387+ "jeff" ,
388+ []string {"foo" , "bar" },
389+ }
390+
391+ jwtData , _ := getTestJWT (t , ecdsaPrivKey , tt .jwtData , privateCl )
392+
393+ loginData := map [string ]interface {}{
394+ "role" : "plugin-test" ,
395+ "jwt" : jwtData ,
396+ }
397+
398+ req = & logical.Request {
399+ Operation : logical .UpdateOperation ,
400+ Path : "login" ,
401+ Storage : storage ,
402+ Data : loginData ,
403+ Connection : & logical.Connection {
404+ RemoteAddr : "127.0.0.1" ,
405+ },
406+ }
407+
408+ resp , err = b .HandleRequest (context .Background (), req )
409+ if err != nil {
410+ t .Fatal (err )
411+ }
412+ if resp == nil {
413+ t .Fatal ("got nil response" )
414+ }
415+ if tt .expectErr {
416+ if ! resp .IsError () {
417+ t .Fatal ("expected error" )
418+ }
419+ }
420+
421+ if ! tt .expectErr && resp .IsError () {
422+ t .Fatalf ("unexpected error: %q" , resp .Error ().Error ())
423+ }
424+ })
425+ }
426+ }
427+
211428func TestLogin_JWT (t * testing.T ) {
212429 testLogin_JWT (t , false )
213430 testLogin_JWT (t , true )
0 commit comments