From 52af34e8941cb728eff2c3161c53d6b309d2a907 Mon Sep 17 00:00:00 2001 From: John-Michael Faircloth Date: Thu, 13 Jun 2024 15:14:52 -0500 Subject: [PATCH] Prepare for v0.21.0 release (#309) * Prepare for v0.21.0 release * add test * update test --- CHANGELOG.md | 4 + path_login_test.go | 219 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 222 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84194495..340306af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased +## v0.21.0 + +NO CHANGES + ## v0.20.3 BUG FIXES: diff --git a/path_login_test.go b/path_login_test.go index 84ecd83c..d949a8d7 100644 --- a/path_login_test.go +++ b/path_login_test.go @@ -151,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)) @@ -208,6 +208,223 @@ 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 and no bound_audiences", + 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: true, + }, + { + name: "jwt with array aud and no bound_audiences", + // 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 and role with 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: "error jwt with string aud and role with no match of 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 not contained in role's bound_audiences + 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: true, + }, + { + name: "jwt with string aud and role with 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", + }, + 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 no aud and role with no bound_audiences", + 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", + }, + // aud is not set + jwtData: map[string]interface{}{ + "sub": "subject", + "iss": "https://team-vault.auth0.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 !tt.expectErr && resp.IsError() { + t.Fatalf("unexpected error: %q", resp.Error().Error()) + } + }) + } +} + func TestLogin_JWT(t *testing.T) { testLogin_JWT(t, false) testLogin_JWT(t, true)