mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 02:28:09 +00:00 
			
		
		
		
	Filter identity token keys (#12780)
* filter identity token keys * Update test cases to associate keys with roles * use getOIDCRole helper * add func comment and test assertion * add changelog * remove unnecessary code * build list of keys to return by starting with a list of roles * move comment * update changelog
This commit is contained in:
		 John-Michael Faircloth
					John-Michael Faircloth
				
			
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			 GitHub
						GitHub
					
				
			
						parent
						
							452e47cbf2
						
					
				
				
					commit
					ee40205a62
				
			
							
								
								
									
										3
									
								
								changelog/12780.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								changelog/12780.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ```release-note:improvement | ||||
| identity/token: Only return keys from the `.well-known/keys` endpoint that are being used by roles to sign/verify tokens. | ||||
| ``` | ||||
| @@ -613,6 +613,27 @@ func (i *IdentityStore) pathOIDCReadKey(ctx context.Context, req *logical.Reques | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // keyIDsByName will return a slice of key IDs for the given key name | ||||
| func (i *IdentityStore) keyIDsByName(ctx context.Context, s logical.Storage, name string) ([]string, error) { | ||||
| 	var keyIDs []string | ||||
| 	entry, err := s.Get(ctx, namedKeyConfigPath+name) | ||||
| 	if err != nil { | ||||
| 		return keyIDs, err | ||||
| 	} | ||||
| 	if entry == nil { | ||||
| 		return keyIDs, nil | ||||
| 	} | ||||
|  | ||||
| 	var key namedKey | ||||
| 	if err := entry.DecodeJSON(&key); err != nil { | ||||
| 		return keyIDs, err | ||||
| 	} | ||||
| 	for _, k := range key.KeyRing { | ||||
| 		keyIDs = append(keyIDs, k.KeyID) | ||||
| 	} | ||||
| 	return keyIDs, nil | ||||
| } | ||||
|  | ||||
| // rolesReferencingTargetKeyName returns a map of role names to roles | ||||
| // referencing targetKeyName. | ||||
| // | ||||
| @@ -1538,13 +1559,28 @@ func (i *IdentityStore) generatePublicJWKS(ctx context.Context, s logical.Storag | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	keyIDs, err := listOIDCPublicKeys(ctx, s) | ||||
| 	jwks := &jose.JSONWebKeySet{ | ||||
| 		Keys: make([]jose.JSONWebKey, 0), | ||||
| 	} | ||||
|  | ||||
| 	// only return keys that are associated with a role | ||||
| 	roleNames, err := s.List(ctx, roleConfigPath) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	jwks := &jose.JSONWebKeySet{ | ||||
| 		Keys: make([]jose.JSONWebKey, 0, len(keyIDs)), | ||||
| 	for _, roleName := range roleNames { | ||||
| 		role, err := i.getOIDCRole(ctx, s, roleName) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		if role == nil { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		keyIDs, err := i.keyIDsByName(ctx, s, role.Key) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		for _, keyID := range keyIDs { | ||||
| @@ -1554,6 +1590,7 @@ func (i *IdentityStore) generatePublicJWKS(ctx context.Context, s logical.Storag | ||||
| 			} | ||||
| 			jwks.Keys = append(jwks.Keys, *key) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if err := i.oidcCache.SetDefault(ns, "jwks", jwks); err != nil { | ||||
| 		return nil, err | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| package vault | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/rand" | ||||
| 	"crypto/rsa" | ||||
| 	"encoding/json" | ||||
| @@ -637,6 +638,43 @@ func TestOIDC_Path_OIDCKey_DeleteWithExistingClient(t *testing.T) { | ||||
| 	expectError(t, resp, err) | ||||
| } | ||||
|  | ||||
| // TestOIDC_PublicKeys_NoRole tests that public keys are not returned by the | ||||
| // oidc/.well-known/keys endpoint when they are not associated with a role | ||||
| func TestOIDC_PublicKeys_NoRole(t *testing.T) { | ||||
| 	c, _, _ := TestCoreUnsealed(t) | ||||
| 	ctx := namespace.RootContext(nil) | ||||
| 	s := &logical.InmemStorage{} | ||||
|  | ||||
| 	// Create a test key "test-key" | ||||
| 	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{ | ||||
| 		Path:      "oidc/key/test-key", | ||||
| 		Operation: logical.CreateOperation, | ||||
| 		Storage:   s, | ||||
| 	}) | ||||
| 	expectSuccess(t, resp, err) | ||||
|  | ||||
| 	// .well-known/keys should contain 0 public keys | ||||
| 	assertPublicKeyCount(t, ctx, s, c, 0) | ||||
| } | ||||
|  | ||||
| func assertPublicKeyCount(t *testing.T, ctx context.Context, s logical.Storage, c *Core, keyCount int) { | ||||
| 	t.Helper() | ||||
|  | ||||
| 	// .well-known/keys should contain keyCount public keys | ||||
| 	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{ | ||||
| 		Path:      "oidc/.well-known/keys", | ||||
| 		Operation: logical.ReadOperation, | ||||
| 		Storage:   s, | ||||
| 	}) | ||||
| 	expectSuccess(t, resp, err) | ||||
| 	// parse response | ||||
| 	responseJWKS := &jose.JSONWebKeySet{} | ||||
| 	json.Unmarshal(resp.Data["http_raw_body"].([]byte), responseJWKS) | ||||
| 	if len(responseJWKS.Keys) != keyCount { | ||||
| 		t.Fatalf("expected %d public keys but instead got %d", keyCount, len(responseJWKS.Keys)) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // TestOIDC_PublicKeys tests that public keys are updated by | ||||
| // key creation, rotation, and deletion | ||||
| func TestOIDC_PublicKeys(t *testing.T) { | ||||
| @@ -651,23 +689,22 @@ func TestOIDC_PublicKeys(t *testing.T) { | ||||
| 		Storage:   storage, | ||||
| 	}) | ||||
|  | ||||
| 	// .well-known/keys should contain 2 public keys | ||||
| 	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{ | ||||
| 		Path:      "oidc/.well-known/keys", | ||||
| 		Operation: logical.ReadOperation, | ||||
| 	// Create a test role "test-role" | ||||
| 	c.identityStore.HandleRequest(ctx, &logical.Request{ | ||||
| 		Path:      "oidc/role/test-role", | ||||
| 		Operation: logical.CreateOperation, | ||||
| 		Data: map[string]interface{}{ | ||||
| 			"key": "test-key", | ||||
| 		}, | ||||
| 		Storage: storage, | ||||
| 	}) | ||||
| 	expectSuccess(t, resp, err) | ||||
| 	// parse response | ||||
| 	responseJWKS := &jose.JSONWebKeySet{} | ||||
| 	json.Unmarshal(resp.Data["http_raw_body"].([]byte), responseJWKS) | ||||
| 	if len(responseJWKS.Keys) != 2 { | ||||
| 		t.Fatalf("expected 2 public keys but instead got %d", len(responseJWKS.Keys)) | ||||
| 	} | ||||
|  | ||||
| 	// .well-known/keys should contain 2 public keys | ||||
| 	assertPublicKeyCount(t, ctx, storage, c, 2) | ||||
|  | ||||
| 	// rotate test-key a few times, each rotate should increase the length of public keys returned | ||||
| 	// by the .well-known endpoint | ||||
| 	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{ | ||||
| 	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{ | ||||
| 		Path:      "oidc/key/test-key/rotate", | ||||
| 		Operation: logical.UpdateOperation, | ||||
| 		Storage:   storage, | ||||
| @@ -681,45 +718,47 @@ func TestOIDC_PublicKeys(t *testing.T) { | ||||
| 	expectSuccess(t, resp, err) | ||||
|  | ||||
| 	// .well-known/keys should contain 4 public keys | ||||
| 	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{ | ||||
| 		Path:      "oidc/.well-known/keys", | ||||
| 		Operation: logical.ReadOperation, | ||||
| 		Storage:   storage, | ||||
| 	}) | ||||
| 	expectSuccess(t, resp, err) | ||||
| 	// parse response | ||||
| 	json.Unmarshal(resp.Data["http_raw_body"].([]byte), responseJWKS) | ||||
| 	if len(responseJWKS.Keys) != 4 { | ||||
| 		t.Fatalf("expected 4 public keys but instead got %d", len(responseJWKS.Keys)) | ||||
| 	} | ||||
| 	assertPublicKeyCount(t, ctx, storage, c, 4) | ||||
|  | ||||
| 	// create another named key | ||||
| 	c.identityStore.HandleRequest(ctx, &logical.Request{ | ||||
| 	// create another named key "test-key2" | ||||
| 	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{ | ||||
| 		Path:      "oidc/key/test-key2", | ||||
| 		Operation: logical.CreateOperation, | ||||
| 		Storage:   storage, | ||||
| 	}) | ||||
| 	expectSuccess(t, resp, err) | ||||
|  | ||||
| 	// Create a test role "test-role2" | ||||
| 	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{ | ||||
| 		Path:      "oidc/role/test-role2", | ||||
| 		Operation: logical.CreateOperation, | ||||
| 		Data: map[string]interface{}{ | ||||
| 			"key": "test-key2", | ||||
| 		}, | ||||
| 		Storage: storage, | ||||
| 	}) | ||||
| 	expectSuccess(t, resp, err) | ||||
| 	// .well-known/keys should contain 6 public keys | ||||
| 	assertPublicKeyCount(t, ctx, storage, c, 6) | ||||
|  | ||||
| 	// delete test role that references "test-key" | ||||
| 	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{ | ||||
| 		Path:      "oidc/role/test-role", | ||||
| 		Operation: logical.DeleteOperation, | ||||
| 		Storage:   storage, | ||||
| 	}) | ||||
| 	expectSuccess(t, resp, err) | ||||
| 	// delete test key | ||||
| 	c.identityStore.HandleRequest(ctx, &logical.Request{ | ||||
| 	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{ | ||||
| 		Path:      "oidc/key/test-key", | ||||
| 		Operation: logical.DeleteOperation, | ||||
| 		Storage:   storage, | ||||
| 	}) | ||||
|  | ||||
| 	// .well-known/keys should contain 2 public key, all of the public keys | ||||
| 	// from named key "test-key" should have been deleted | ||||
| 	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{ | ||||
| 		Path:      "oidc/.well-known/keys", | ||||
| 		Operation: logical.ReadOperation, | ||||
| 		Storage:   storage, | ||||
| 	}) | ||||
| 	expectSuccess(t, resp, err) | ||||
| 	// parse response | ||||
| 	json.Unmarshal(resp.Data["http_raw_body"].([]byte), responseJWKS) | ||||
| 	if len(responseJWKS.Keys) != 2 { | ||||
| 		t.Fatalf("expected 2 public keys but instead got %d", len(responseJWKS.Keys)) | ||||
| 	} | ||||
|  | ||||
| 	// .well-known/keys should contain 2 public keys, all of the public keys | ||||
| 	// from named key "test-key" should have been deleted | ||||
| 	assertPublicKeyCount(t, ctx, storage, c, 2) | ||||
| } | ||||
|  | ||||
| // TestOIDC_SignIDToken tests acquiring a signed token and verifying the public portion | ||||
|   | ||||
		Reference in New Issue
	
	Block a user