From fa121912e8645c6b138821138fce7cb7db13501e Mon Sep 17 00:00:00 2001 From: JM Faircloth Date: Thu, 13 Jun 2024 13:14:08 -0500 Subject: [PATCH] add test and remove redundant test --- path_login_test.go | 238 ++++++++++++++++++++++++++++++++++----------- path_oidc_test.go | 5 +- 2 files changed, 181 insertions(+), 62 deletions(-) diff --git a/path_login_test.go b/path_login_test.go index da567134..e0bc0445 100644 --- a/path_login_test.go +++ b/path_login_test.go @@ -17,14 +17,13 @@ import ( "testing" "time" + "github.com/go-jose/go-jose/v3" + sqjwt "github.com/go-jose/go-jose/v3/jwt" "github.com/go-test/deep" "github.com/hashicorp/cap/jwt" "github.com/hashicorp/vault/sdk/logical" "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" - - "github.com/go-jose/go-jose/v3" - sqjwt "github.com/go-jose/go-jose/v3/jwt" ) type H map[string]interface{} @@ -152,7 +151,7 @@ func setupBackend(t *testing.T, cfg testConfig) (closeableBackend, logical.Stora return cb, storage } -func getTestJWT(t *testing.T, privKey string, cl sqjwt.Claims, privateCl interface{}) (string, *ecdsa.PrivateKey) { +func getTestJWT(t *testing.T, privKey string, cl interface{}, privateCl interface{}) (string, *ecdsa.PrivateKey) { t.Helper() var key *ecdsa.PrivateKey block, _ := pem.Decode([]byte(privKey)) @@ -209,6 +208,182 @@ func getTestOIDC(t *testing.T) string { return out.AccessToken } +// TestLoginBoundAudiences tests that the login JWT's aud claim is ignored if +// it is a single string. This is a case that is fixed in later versions of +// the plugin. +// See https://github.com/hashicorp/vault-plugin-auth-jwt/pull/308 +func TestLoginBoundAudiences(t *testing.T) { + testCases := []struct { + name string + roleData map[string]interface{} + jwtData map[string]interface{} + expectErr bool + }{ + { + name: "jwt with string aud is ignored", + // bound_audiences is unset + roleData: map[string]interface{}{ + "role_type": "jwt", + "bound_subject": "subject", + "user_claim": "https://vault/user", + "policies": "test", + "period": "3s", + "ttl": "1s", + "num_uses": 12, + "max_ttl": "5s", + }, + jwtData: map[string]interface{}{ + "sub": "subject", + "iss": "https://team-vault.auth0.com/", + "aud": "https://vault.plugin.auth.jwt.test", + "nbf": sqjwt.NewNumericDate(time.Now().Add(-5 * time.Second)), + }, + expectErr: false, + }, + { + name: "jwt with array aud is checked", + // bound_audiences is unset + roleData: map[string]interface{}{ + "role_type": "jwt", + "bound_subject": "subject", + "user_claim": "https://vault/user", + "policies": "test", + "period": "3s", + "ttl": "1s", + "num_uses": 12, + "max_ttl": "5s", + }, + jwtData: map[string]interface{}{ + "sub": "subject", + "iss": "https://team-vault.auth0.com/", + "aud": []string{"https://vault.plugin.auth.jwt.test"}, + "nbf": sqjwt.NewNumericDate(time.Now().Add(-5 * time.Second)), + }, + expectErr: true, + }, + { + name: "jwt with array aud is checked with role bound_audiences", + roleData: map[string]interface{}{ + "role_type": "jwt", + "bound_audiences": []string{"https://vault.plugin.auth.jwt.test", "another_audience"}, + "bound_subject": "subject", + "user_claim": "https://vault/user", + "policies": "test", + "period": "3s", + "ttl": "1s", + "num_uses": 12, + "max_ttl": "5s", + }, + // aud is an array + jwtData: map[string]interface{}{ + "sub": "subject", + "iss": "https://team-vault.auth0.com/", + "aud": []string{"https://vault.plugin.auth.jwt.test"}, + "nbf": sqjwt.NewNumericDate(time.Now().Add(-5 * time.Second)), + }, + expectErr: false, + }, + { + name: "jwt with string aud is ignored with role bound_audiences", + roleData: map[string]interface{}{ + "role_type": "jwt", + "bound_audiences": []string{"https://vault.plugin.auth.jwt.test", "another_audience"}, + "bound_subject": "subject", + "user_claim": "https://vault/user", + "policies": "test", + "period": "3s", + "ttl": "1s", + "num_uses": 12, + "max_ttl": "5s", + }, + // aud is a string + jwtData: map[string]interface{}{ + "sub": "subject", + "iss": "https://team-vault.auth0.com/", + "aud": "https://foo.com", + "nbf": sqjwt.NewNumericDate(time.Now().Add(-5 * time.Second)), + }, + expectErr: false, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + b, storage := getBackend(t) + + configData := map[string]interface{}{ + "bound_issuer": "https://team-vault.auth0.com/", + "jwt_validation_pubkeys": ecdsaPubKey, + } + + req := &logical.Request{ + Operation: logical.UpdateOperation, + Path: configPath, + Storage: storage, + Data: configData, + } + + resp, err := b.HandleRequest(context.Background(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%s resp:%#v\n", err, resp) + } + + req = &logical.Request{ + Operation: logical.CreateOperation, + Path: "role/plugin-test", + Storage: storage, + Data: tt.roleData, + } + + resp, err = b.HandleRequest(context.Background(), req) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%s resp:%#v\n", err, resp) + } + + privateCl := struct { + User string `json:"https://vault/user"` + Groups []string `json:"https://vault/groups"` + }{ + "jeff", + []string{"foo", "bar"}, + } + + jwtData, _ := getTestJWT(t, ecdsaPrivKey, tt.jwtData, privateCl) + + loginData := map[string]interface{}{ + "role": "plugin-test", + "jwt": jwtData, + } + + req = &logical.Request{ + Operation: logical.UpdateOperation, + Path: "login", + Storage: storage, + Data: loginData, + Connection: &logical.Connection{ + RemoteAddr: "127.0.0.1", + }, + } + + resp, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("got nil response") + } + if tt.expectErr { + if !resp.IsError() { + t.Fatal("expected error") + } + if !strings.Contains(resp.Error().Error(), "no audiences bound to the role") { + t.Fatalf("unexpected error: %v", resp.Error()) + } + } + }) + } +} + func TestLogin_JWT(t *testing.T) { testLogin_JWT(t, false) testLogin_JWT(t, true) @@ -271,61 +446,6 @@ func testLogin_JWT(t *testing.T, jwks bool) { } } - // Test missing audience - { - - cfg := testConfig{ - jwks: jwks, - } - b, storage := setupBackend(t, cfg) - - cl := sqjwt.Claims{ - Subject: "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients", - Issuer: "https://team-vault.auth0.com/", - NotBefore: sqjwt.NewNumericDate(time.Now().Add(-5 * time.Second)), - Audience: sqjwt.Audience{"https://vault.plugin.auth.jwt.test"}, - } - - privateCl := struct { - User string `json:"https://vault/user"` - Groups []string `json:"https://vault/groups"` - }{ - "jeff", - []string{"foo", "bar"}, - } - - jwtData, _ := getTestJWT(t, ecdsaPrivKey, cl, privateCl) - - data := map[string]interface{}{ - "role": "plugin-test", - "jwt": jwtData, - } - - req := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "login", - Storage: storage, - Data: data, - Connection: &logical.Connection{ - RemoteAddr: "127.0.0.1", - }, - } - - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("got nil response") - } - if !resp.IsError() { - t.Fatal("expected error") - } - if !strings.Contains(resp.Error().Error(), "no audiences bound to the role") { - t.Fatalf("unexpected error: %v", resp.Error()) - } - } - // test valid inputs { // run test with and without bound_cidrs configured diff --git a/path_oidc_test.go b/path_oidc_test.go index 0edf2520..2b66a1ac 100644 --- a/path_oidc_test.go +++ b/path_oidc_test.go @@ -21,14 +21,13 @@ import ( "testing" "time" + "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v3/jwt" "github.com/hashicorp/cap/oidc" "github.com/hashicorp/go-sockaddr" "github.com/hashicorp/vault/sdk/logical" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/go-jose/go-jose/v3" - "github.com/go-jose/go-jose/v3/jwt" ) func TestOIDC_AuthURL(t *testing.T) {