5821fe48c7
* Fix identity token crash during invalidation * Check for nil namespace * Fix test * Add nil check test * Check OIDC cache errors
1117 lines
34 KiB
Go
1117 lines
34 KiB
Go
package vault
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"encoding/json"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-test/deep"
|
|
uuid "github.com/hashicorp/go-uuid"
|
|
"github.com/hashicorp/vault/helper/identity"
|
|
"github.com/hashicorp/vault/helper/namespace"
|
|
"github.com/hashicorp/vault/sdk/framework"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
gocache "github.com/patrickmn/go-cache"
|
|
"gopkg.in/square/go-jose.v2"
|
|
"gopkg.in/square/go-jose.v2/jwt"
|
|
)
|
|
|
|
// TestOIDC_Path_OIDCRoleRole tests CRUD operations for roles
|
|
func TestOIDC_Path_OIDCRoleRole(t *testing.T) {
|
|
c, _, _ := TestCoreUnsealed(t)
|
|
ctx := namespace.RootContext(nil)
|
|
storage := &logical.InmemStorage{}
|
|
|
|
// Create a test key "test-key"
|
|
c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/key/test-key",
|
|
Operation: logical.CreateOperation,
|
|
Storage: storage,
|
|
})
|
|
|
|
// Create a test role "test-role1" with a valid key -- should succeed with warning
|
|
resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/role/test-role1",
|
|
Operation: logical.CreateOperation,
|
|
Data: map[string]interface{}{
|
|
"key": "test-key",
|
|
},
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
|
|
// Read "test-role1" and validate
|
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/role/test-role1",
|
|
Operation: logical.ReadOperation,
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
expected := map[string]interface{}{
|
|
"key": "test-key",
|
|
"ttl": int64(86400),
|
|
"template": "",
|
|
"client_id": resp.Data["client_id"],
|
|
}
|
|
if diff := deep.Equal(expected, resp.Data); diff != nil {
|
|
t.Fatal(diff)
|
|
}
|
|
|
|
// Update "test-role1" with valid parameters -- should succeed
|
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/role/test-role1",
|
|
Operation: logical.UpdateOperation,
|
|
Data: map[string]interface{}{
|
|
"template": "{\"some-key\":\"some-value\"}",
|
|
"ttl": "2h",
|
|
},
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
|
|
// Read "test-role1" again and validate
|
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/role/test-role1",
|
|
Operation: logical.ReadOperation,
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
expected = map[string]interface{}{
|
|
"key": "test-key",
|
|
"ttl": int64(7200),
|
|
"template": "{\"some-key\":\"some-value\"}",
|
|
"client_id": resp.Data["client_id"],
|
|
}
|
|
if diff := deep.Equal(expected, resp.Data); diff != nil {
|
|
t.Fatal(diff)
|
|
}
|
|
|
|
// Delete "test-role1"
|
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/role/test-role1",
|
|
Operation: logical.DeleteOperation,
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
|
|
// Read "test-role1"
|
|
respReadTestRole1AfterDelete, err3 := c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/role/test-role1",
|
|
Operation: logical.ReadOperation,
|
|
Storage: storage,
|
|
})
|
|
// Ensure that "test-role1" has been deleted
|
|
expectSuccess(t, respReadTestRole1AfterDelete, err3)
|
|
if respReadTestRole1AfterDelete != nil {
|
|
t.Fatalf("Expected a nil response but instead got:\n%#v", respReadTestRole1AfterDelete)
|
|
}
|
|
if respReadTestRole1AfterDelete != nil {
|
|
t.Fatalf("Expected role to have been deleted but read response was:\n%#v", respReadTestRole1AfterDelete)
|
|
}
|
|
}
|
|
|
|
// TestOIDC_Path_OIDCRole tests the List operation for roles
|
|
func TestOIDC_Path_OIDCRole(t *testing.T) {
|
|
c, _, _ := TestCoreUnsealed(t)
|
|
ctx := namespace.RootContext(nil)
|
|
storage := &logical.InmemStorage{}
|
|
|
|
// Prepare two roles, test-role1 and test-role2
|
|
// Create a test key "test-key"
|
|
c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/key/test-key",
|
|
Operation: logical.CreateOperation,
|
|
Data: map[string]interface{}{
|
|
"allowed_client_ids": "test-role1,test-role2",
|
|
},
|
|
Storage: storage,
|
|
})
|
|
|
|
// Create "test-role1"
|
|
c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/role/test-role1",
|
|
Operation: logical.CreateOperation,
|
|
Data: map[string]interface{}{
|
|
"key": "test-key",
|
|
},
|
|
Storage: storage,
|
|
})
|
|
|
|
// Create "test-role2"
|
|
c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/role/test-role2",
|
|
Operation: logical.CreateOperation,
|
|
Data: map[string]interface{}{
|
|
"key": "test-key",
|
|
},
|
|
Storage: storage,
|
|
})
|
|
|
|
// list roles
|
|
respListRole, listErr := c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/role",
|
|
Operation: logical.ListOperation,
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, respListRole, listErr)
|
|
|
|
// validate list response
|
|
expectedStrings := map[string]interface{}{"test-role1": true, "test-role2": true}
|
|
expectStrings(t, respListRole.Data["keys"].([]string), expectedStrings)
|
|
|
|
// delete test-role2
|
|
c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/role/test-role2",
|
|
Operation: logical.DeleteOperation,
|
|
Storage: storage,
|
|
})
|
|
|
|
// list roles again and validate response
|
|
respListRoleAfterDelete, listErrAfterDelete := c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/role",
|
|
Operation: logical.ListOperation,
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, respListRoleAfterDelete, listErrAfterDelete)
|
|
|
|
// validate list response
|
|
delete(expectedStrings, "test-role2")
|
|
expectStrings(t, respListRoleAfterDelete.Data["keys"].([]string), expectedStrings)
|
|
}
|
|
|
|
// TestOIDC_Path_OIDCKeyKey tests CRUD operations for keys
|
|
func TestOIDC_Path_OIDCKeyKey(t *testing.T) {
|
|
c, _, _ := TestCoreUnsealed(t)
|
|
ctx := namespace.RootContext(nil)
|
|
storage := &logical.InmemStorage{}
|
|
|
|
// Create a test key "test-key" -- should succeed
|
|
resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/key/test-key",
|
|
Operation: logical.CreateOperation,
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
|
|
// Read "test-key" and validate
|
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/key/test-key",
|
|
Operation: logical.ReadOperation,
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
expected := map[string]interface{}{
|
|
"rotation_period": int64(86400),
|
|
"verification_ttl": int64(86400),
|
|
"algorithm": "RS256",
|
|
"allowed_client_ids": []string{},
|
|
}
|
|
if diff := deep.Equal(expected, resp.Data); diff != nil {
|
|
t.Fatal(diff)
|
|
}
|
|
|
|
// Update "test-key" -- should succeed
|
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/key/test-key",
|
|
Operation: logical.UpdateOperation,
|
|
Data: map[string]interface{}{
|
|
"rotation_period": "10m",
|
|
"verification_ttl": "1h",
|
|
"allowed_client_ids": "allowed-test-role",
|
|
},
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
|
|
// Read "test-key" again and validate
|
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/key/test-key",
|
|
Operation: logical.ReadOperation,
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
expected = map[string]interface{}{
|
|
"rotation_period": int64(600),
|
|
"verification_ttl": int64(3600),
|
|
"algorithm": "RS256",
|
|
"allowed_client_ids": []string{"allowed-test-role"},
|
|
}
|
|
if diff := deep.Equal(expected, resp.Data); diff != nil {
|
|
t.Fatal(diff)
|
|
}
|
|
|
|
// Create a role that depends on test key
|
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/role/allowed-test-role",
|
|
Operation: logical.UpdateOperation,
|
|
Data: map[string]interface{}{
|
|
"key": "test-key",
|
|
},
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
fmt.Printf("resp is:\n%#v", resp)
|
|
|
|
// Delete test-key -- should fail because test-role depends on test-key
|
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/key/test-key",
|
|
Operation: logical.DeleteOperation,
|
|
Storage: storage,
|
|
})
|
|
expectError(t, resp, err)
|
|
// validate error message
|
|
expectedStrings := map[string]interface{}{
|
|
"unable to delete key \"test-key\" because it is currently referenced by these roles: allowed-test-role": true,
|
|
}
|
|
expectStrings(t, []string{resp.Data["error"].(string)}, expectedStrings)
|
|
|
|
// Delete allowed-test-role
|
|
c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/role/allowed-test-role",
|
|
Operation: logical.DeleteOperation,
|
|
Storage: storage,
|
|
})
|
|
|
|
// Delete test-key -- should succeed this time because no roles depend on test-key
|
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/key/test-key",
|
|
Operation: logical.DeleteOperation,
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
}
|
|
|
|
// TestOIDC_Path_OIDCKey tests the List operation for keys
|
|
func TestOIDC_Path_OIDCKey(t *testing.T) {
|
|
c, _, _ := TestCoreUnsealed(t)
|
|
ctx := namespace.RootContext(nil)
|
|
storage := &logical.InmemStorage{}
|
|
|
|
// Prepare two keys, test-key1 and test-key2
|
|
c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/key/test-key1",
|
|
Operation: logical.CreateOperation,
|
|
Storage: storage,
|
|
})
|
|
|
|
c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/key/test-key2",
|
|
Operation: logical.CreateOperation,
|
|
Storage: storage,
|
|
})
|
|
|
|
// list keys
|
|
respListKey, listErr := c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/key",
|
|
Operation: logical.ListOperation,
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, respListKey, listErr)
|
|
|
|
// validate list response
|
|
expectedStrings := map[string]interface{}{"test-key1": true, "test-key2": true}
|
|
expectStrings(t, respListKey.Data["keys"].([]string), expectedStrings)
|
|
|
|
// delete test-key2
|
|
c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/key/test-key2",
|
|
Operation: logical.DeleteOperation,
|
|
Storage: storage,
|
|
})
|
|
|
|
// list keyes again and validate response
|
|
respListKeyAfterDelete, listErrAfterDelete := c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/key",
|
|
Operation: logical.ListOperation,
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, respListKeyAfterDelete, listErrAfterDelete)
|
|
|
|
// validate list response
|
|
delete(expectedStrings, "test-key2")
|
|
expectStrings(t, respListKeyAfterDelete.Data["keys"].([]string), expectedStrings)
|
|
}
|
|
|
|
// TestOIDC_PublicKeys tests that public keys are updated by
|
|
// key creation, rotation, and deletion
|
|
func TestOIDC_PublicKeys(t *testing.T) {
|
|
c, _, _ := TestCoreUnsealed(t)
|
|
ctx := namespace.RootContext(nil)
|
|
storage := &logical.InmemStorage{}
|
|
|
|
// Create a test key "test-key"
|
|
c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/key/test-key",
|
|
Operation: logical.CreateOperation,
|
|
Storage: storage,
|
|
})
|
|
|
|
// .well-known/keys should contain 1 public key
|
|
resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/.well-known/keys",
|
|
Operation: logical.ReadOperation,
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
// parse response
|
|
responseJWKS := &jose.JSONWebKeySet{}
|
|
json.Unmarshal(resp.Data["http_raw_body"].([]byte), responseJWKS)
|
|
if len(responseJWKS.Keys) != 1 {
|
|
t.Fatalf("expected 1 public key but instead got %d", len(responseJWKS.Keys))
|
|
}
|
|
|
|
// 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{
|
|
Path: "oidc/key/test-key/rotate",
|
|
Operation: logical.UpdateOperation,
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/key/test-key/rotate",
|
|
Operation: logical.UpdateOperation,
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
|
|
// .well-known/keys should contain 3 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) != 3 {
|
|
t.Fatalf("expected 3 public keys but instead got %d", len(responseJWKS.Keys))
|
|
}
|
|
|
|
// create another named key
|
|
c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/key/test-key2",
|
|
Operation: logical.CreateOperation,
|
|
Storage: storage,
|
|
})
|
|
|
|
// delete test key
|
|
c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/key/test-key",
|
|
Operation: logical.DeleteOperation,
|
|
Storage: storage,
|
|
})
|
|
|
|
// .well-known/keys should contain 1 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) != 1 {
|
|
t.Fatalf("expected 1 public keys but instead got %d", len(responseJWKS.Keys))
|
|
}
|
|
}
|
|
|
|
// TestOIDC_SignIDToken tests acquiring a signed token and verifying the public portion
|
|
// of the signing key
|
|
func TestOIDC_SignIDToken(t *testing.T) {
|
|
c, _, _ := TestCoreUnsealed(t)
|
|
ctx := namespace.RootContext(nil)
|
|
storage := &logical.InmemStorage{}
|
|
|
|
// Create and load an entity, an entity is required to generate an ID token
|
|
testEntity := &identity.Entity{
|
|
Name: "test-entity-name",
|
|
ID: "test-entity-id",
|
|
BucketKey: "test-entity-bucket-key",
|
|
}
|
|
|
|
txn := c.identityStore.db.Txn(true)
|
|
defer txn.Abort()
|
|
err := c.identityStore.upsertEntityInTxn(ctx, txn, testEntity, nil, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
txn.Commit()
|
|
|
|
// Create a test key "test-key"
|
|
c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/key/test-key",
|
|
Operation: logical.CreateOperation,
|
|
Data: map[string]interface{}{
|
|
"allowed_client_ids": "*",
|
|
},
|
|
Storage: storage,
|
|
})
|
|
|
|
// Create a test role "test-role" -- expect no warning
|
|
resp, err := 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)
|
|
if resp != nil {
|
|
t.Fatalf("was expecting a nil response but instead got: %#v", resp)
|
|
}
|
|
|
|
// Determine test-role's client_id
|
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/role/test-role",
|
|
Operation: logical.ReadOperation,
|
|
Data: map[string]interface{}{
|
|
"allowed_client_ids": "",
|
|
},
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
clientID := resp.Data["client_id"].(string)
|
|
|
|
// remove test-role as an allowed role from test-key
|
|
c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/key/test-key",
|
|
Operation: logical.CreateOperation,
|
|
Data: map[string]interface{}{
|
|
"allowed_client_ids": "",
|
|
},
|
|
Storage: storage,
|
|
})
|
|
|
|
// Generate a token against the role "test-role" -- should fail
|
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/token/test-role",
|
|
Operation: logical.ReadOperation,
|
|
Storage: storage,
|
|
EntityID: "test-entity-id",
|
|
})
|
|
expectError(t, resp, err)
|
|
// validate error message
|
|
expectedStrings := map[string]interface{}{
|
|
"the key \"test-key\" does not list the client ID of the role \"test-role\" as an allowed client ID": true,
|
|
}
|
|
expectStrings(t, []string{resp.Data["error"].(string)}, expectedStrings)
|
|
|
|
// add test-role as an allowed role from test-key
|
|
c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/key/test-key",
|
|
Operation: logical.CreateOperation,
|
|
Data: map[string]interface{}{
|
|
"allowed_client_ids": clientID,
|
|
},
|
|
Storage: storage,
|
|
})
|
|
|
|
// Generate a token against the role "test-role" -- should succeed
|
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/token/test-role",
|
|
Operation: logical.ReadOperation,
|
|
Storage: storage,
|
|
EntityID: "test-entity-id",
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
parsedToken, err := jwt.ParseSigned(resp.Data["token"].(string))
|
|
if err != nil {
|
|
t.Fatalf("error parsing token: %s", err.Error())
|
|
}
|
|
|
|
// Acquire the public parts of the key that signed parsedToken
|
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/.well-known/keys",
|
|
Operation: logical.ReadOperation,
|
|
Storage: storage,
|
|
})
|
|
responseJWKS := &jose.JSONWebKeySet{}
|
|
json.Unmarshal(resp.Data["http_raw_body"].([]byte), responseJWKS)
|
|
|
|
// Validate the signature
|
|
claims := &jwt.Claims{}
|
|
if err := parsedToken.Claims(responseJWKS.Keys[0], claims); err != nil {
|
|
t.Fatalf("unable to validate signed token, err:\n%#v", err)
|
|
}
|
|
}
|
|
|
|
// TestOIDC_PeriodicFunc tests timing logic for running key
|
|
// rotations and expiration actions.
|
|
func TestOIDC_PeriodicFunc(t *testing.T) {
|
|
// Prepare a storage to run through periodicFunc
|
|
c, _, _ := TestCoreUnsealed(t)
|
|
ctx := namespace.RootContext(nil)
|
|
|
|
// Prepare a dummy signing key
|
|
key, _ := rsa.GenerateKey(rand.Reader, 2048)
|
|
id, _ := uuid.GenerateUUID()
|
|
jwk := &jose.JSONWebKey{
|
|
Key: key,
|
|
KeyID: id,
|
|
Algorithm: "RS256",
|
|
Use: "sig",
|
|
}
|
|
|
|
cyclePeriod := 2 * time.Second
|
|
|
|
var testSets = []struct {
|
|
namedKey *namedKey
|
|
testCases []struct {
|
|
cycle int
|
|
numKeys int
|
|
numPublicKeys int
|
|
}
|
|
}{
|
|
{
|
|
&namedKey{
|
|
name: "test-key",
|
|
Algorithm: "RS256",
|
|
VerificationTTL: 1 * cyclePeriod,
|
|
RotationPeriod: 1 * cyclePeriod,
|
|
KeyRing: nil,
|
|
SigningKey: jwk,
|
|
NextRotation: time.Now(),
|
|
},
|
|
[]struct {
|
|
cycle int
|
|
numKeys int
|
|
numPublicKeys int
|
|
}{
|
|
{1, 1, 1},
|
|
{2, 2, 2},
|
|
{3, 2, 2},
|
|
{4, 2, 2},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, testSet := range testSets {
|
|
// Store namedKey
|
|
storage := c.router.MatchingStorageByAPIPath(ctx, "identity/oidc")
|
|
entry, _ := logical.StorageEntryJSON(namedKeyConfigPath+testSet.namedKey.name, testSet.namedKey)
|
|
if err := storage.Put(ctx, entry); err != nil {
|
|
t.Fatalf("writing to in mem storage failed")
|
|
}
|
|
|
|
currentCycle := 1
|
|
numCases := len(testSet.testCases)
|
|
lastCycle := testSet.testCases[numCases-1].cycle
|
|
namedKeySamples := make([]*logical.StorageEntry, numCases)
|
|
publicKeysSamples := make([][]string, numCases)
|
|
|
|
i := 0
|
|
// var start time.Time
|
|
for currentCycle <= lastCycle {
|
|
c.identityStore.oidcPeriodicFunc(ctx)
|
|
if currentCycle == testSet.testCases[i].cycle {
|
|
namedKeyEntry, _ := storage.Get(ctx, namedKeyConfigPath+testSet.namedKey.name)
|
|
publicKeysEntry, _ := storage.List(ctx, publicKeysConfigPath)
|
|
namedKeySamples[i] = namedKeyEntry
|
|
publicKeysSamples[i] = publicKeysEntry
|
|
i = i + 1
|
|
}
|
|
currentCycle = currentCycle + 1
|
|
|
|
// sleep until we are in the next cycle - where a next run will happen
|
|
v, _, _ := c.identityStore.oidcCache.Get(noNamespace, "nextRun")
|
|
nextRun := v.(time.Time)
|
|
now := time.Now()
|
|
diff := nextRun.Sub(now)
|
|
if now.Before(nextRun) {
|
|
time.Sleep(diff)
|
|
}
|
|
}
|
|
|
|
// measure collected samples
|
|
for i := range testSet.testCases {
|
|
namedKeySamples[i].DecodeJSON(&testSet.namedKey)
|
|
if len(testSet.namedKey.KeyRing) != testSet.testCases[i].numKeys {
|
|
t.Fatalf("At cycle: %d expected namedKey's KeyRing to be of length %d but was: %d", testSet.testCases[i].cycle, testSet.testCases[i].numKeys, len(testSet.namedKey.KeyRing))
|
|
}
|
|
if len(publicKeysSamples[i]) != testSet.testCases[i].numPublicKeys {
|
|
t.Fatalf("At cycle: %d expected public keys to be of length %d but was: %d", testSet.testCases[i].cycle, testSet.testCases[i].numPublicKeys, len(publicKeysSamples[i]))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestOIDC_Config tests CRUD operations for configuring the OIDC backend
|
|
func TestOIDC_Config(t *testing.T) {
|
|
c, _, _ := TestCoreUnsealed(t)
|
|
ctx := namespace.RootContext(nil)
|
|
storage := &logical.InmemStorage{}
|
|
|
|
testIssuer := "https://example.com:1234"
|
|
|
|
// Read Config - expect defaults
|
|
resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/config",
|
|
Operation: logical.ReadOperation,
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
// issuer should not be set
|
|
if resp.Data["issuer"].(string) != "" {
|
|
t.Fatalf("Expected issuer to not be set but found %q instead", resp.Data["issuer"].(string))
|
|
}
|
|
|
|
// Update Config
|
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/config",
|
|
Operation: logical.UpdateOperation,
|
|
Storage: storage,
|
|
Data: map[string]interface{}{
|
|
"issuer": testIssuer,
|
|
},
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
|
|
// Read Config - expect updated issuer value
|
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/config",
|
|
Operation: logical.ReadOperation,
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
// issuer should be set
|
|
if resp.Data["issuer"].(string) != testIssuer {
|
|
t.Fatalf("Expected issuer to be %q but found %q instead", testIssuer, resp.Data["issuer"].(string))
|
|
}
|
|
|
|
// Test bad issuers
|
|
for _, iss := range []string{"asldfk", "ftp://a.com", "a.com", "http://a.com/", "https://a.com/foo", "http:://a.com"} {
|
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/config",
|
|
Operation: logical.UpdateOperation,
|
|
Storage: storage,
|
|
Data: map[string]interface{}{
|
|
"issuer": iss,
|
|
},
|
|
})
|
|
if resp == nil || !resp.IsError() {
|
|
t.Fatalf("Expected issuer %q to fail but it succeeded.", iss)
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// TestOIDC_pathOIDCKeyExistenceCheck tests pathOIDCKeyExistenceCheck
|
|
func TestOIDC_pathOIDCKeyExistenceCheck(t *testing.T) {
|
|
c, _, _ := TestCoreUnsealed(t)
|
|
ctx := namespace.RootContext(nil)
|
|
storage := &logical.InmemStorage{}
|
|
|
|
keyName := "test"
|
|
|
|
// Expect nil with empty storage
|
|
exists, err := c.identityStore.pathOIDCKeyExistenceCheck(
|
|
ctx,
|
|
&logical.Request{
|
|
Storage: storage,
|
|
},
|
|
&framework.FieldData{
|
|
Raw: map[string]interface{}{"name": keyName},
|
|
Schema: map[string]*framework.FieldSchema{
|
|
"name": &framework.FieldSchema{
|
|
Type: framework.TypeString,
|
|
},
|
|
},
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Error during existence check on an expected nil entry, err:\n%#v", err)
|
|
}
|
|
if exists {
|
|
t.Fatalf("Expected existence check to return false but instead returned: %t", exists)
|
|
}
|
|
|
|
// Populte storage with a namedKey
|
|
namedKey := &namedKey{}
|
|
entry, _ := logical.StorageEntryJSON(namedKeyConfigPath+keyName, namedKey)
|
|
if err := storage.Put(ctx, entry); err != nil {
|
|
t.Fatalf("writing to in mem storage failed")
|
|
}
|
|
|
|
// Expect true with a populated storage
|
|
exists, err = c.identityStore.pathOIDCKeyExistenceCheck(
|
|
ctx,
|
|
&logical.Request{
|
|
Storage: storage,
|
|
},
|
|
&framework.FieldData{
|
|
Raw: map[string]interface{}{"name": keyName},
|
|
Schema: map[string]*framework.FieldSchema{
|
|
"name": &framework.FieldSchema{
|
|
Type: framework.TypeString,
|
|
},
|
|
},
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Error during existence check on an expected nil entry, err:\n%#v", err)
|
|
}
|
|
if !exists {
|
|
t.Fatalf("Expected existence check to return true but instead returned: %t", exists)
|
|
}
|
|
}
|
|
|
|
// TestOIDC_pathOIDCRoleExistenceCheck tests pathOIDCRoleExistenceCheck
|
|
func TestOIDC_pathOIDCRoleExistenceCheck(t *testing.T) {
|
|
c, _, _ := TestCoreUnsealed(t)
|
|
ctx := namespace.RootContext(nil)
|
|
storage := &logical.InmemStorage{}
|
|
|
|
roleName := "test"
|
|
|
|
// Expect nil with empty storage
|
|
exists, err := c.identityStore.pathOIDCRoleExistenceCheck(
|
|
ctx,
|
|
&logical.Request{
|
|
Storage: storage,
|
|
},
|
|
&framework.FieldData{
|
|
Raw: map[string]interface{}{"name": roleName},
|
|
Schema: map[string]*framework.FieldSchema{
|
|
"name": &framework.FieldSchema{
|
|
Type: framework.TypeString,
|
|
},
|
|
},
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Error during existence check on an expected nil entry, err:\n%#v", err)
|
|
}
|
|
if exists {
|
|
t.Fatalf("Expected existence check to return false but instead returned: %t", exists)
|
|
}
|
|
|
|
// Populate storage with a role
|
|
role := &role{}
|
|
entry, _ := logical.StorageEntryJSON(roleConfigPath+roleName, role)
|
|
if err := storage.Put(ctx, entry); err != nil {
|
|
t.Fatalf("writing to in mem storage failed")
|
|
}
|
|
|
|
// Expect true with a populated storage
|
|
exists, err = c.identityStore.pathOIDCRoleExistenceCheck(
|
|
ctx,
|
|
&logical.Request{
|
|
Storage: storage,
|
|
},
|
|
&framework.FieldData{
|
|
Raw: map[string]interface{}{"name": roleName},
|
|
Schema: map[string]*framework.FieldSchema{
|
|
"name": &framework.FieldSchema{
|
|
Type: framework.TypeString,
|
|
},
|
|
},
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Error during existence check on an expected nil entry, err:\n%#v", err)
|
|
}
|
|
if !exists {
|
|
t.Fatalf("Expected existence check to return true but instead returned: %t", exists)
|
|
}
|
|
}
|
|
|
|
// TestOIDC_Path_OpenIDConfig tests read operations for the openid-configuration path
|
|
func TestOIDC_Path_OpenIDConfig(t *testing.T) {
|
|
c, _, _ := TestCoreUnsealed(t)
|
|
ctx := namespace.RootContext(nil)
|
|
storage := &logical.InmemStorage{}
|
|
|
|
// Expect defaults from .well-known/openid-configuration
|
|
resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/.well-known/openid-configuration",
|
|
Operation: logical.ReadOperation,
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
// Validate configurable parts - for now just issuer
|
|
discoveryResp := &discovery{}
|
|
json.Unmarshal(resp.Data["http_raw_body"].([]byte), discoveryResp)
|
|
expected := "/v1/identity/oidc"
|
|
if discoveryResp.Issuer != expected {
|
|
t.Fatalf("Expected Issuer path to be %q but found %q instead", expected, discoveryResp.Issuer)
|
|
}
|
|
|
|
// Update issuer config
|
|
testIssuer := "https://example.com:1234"
|
|
c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/config",
|
|
Operation: logical.UpdateOperation,
|
|
Storage: storage,
|
|
Data: map[string]interface{}{
|
|
"issuer": testIssuer,
|
|
},
|
|
})
|
|
|
|
// Expect updates from .well-known/openid-configuration
|
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/.well-known/openid-configuration",
|
|
Operation: logical.ReadOperation,
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
// Validate configurable parts - for now just issuer
|
|
json.Unmarshal(resp.Data["http_raw_body"].([]byte), discoveryResp)
|
|
expected = "https://example.com:1234/v1/identity/oidc"
|
|
if discoveryResp.Issuer != expected {
|
|
t.Fatalf("Expected Issuer path to be %q but found %q instead", expected, discoveryResp.Issuer)
|
|
}
|
|
}
|
|
|
|
// TestOIDC_Path_Introspect tests update operations on the introspect path
|
|
func TestOIDC_Path_Introspect(t *testing.T) {
|
|
c, _, _ := TestCoreUnsealed(t)
|
|
ctx := namespace.RootContext(nil)
|
|
storage := &logical.InmemStorage{}
|
|
|
|
// Expect active false and an error from a malformed token
|
|
resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/introspect/",
|
|
Operation: logical.UpdateOperation,
|
|
Data: map[string]interface{}{
|
|
"token": "not-a-valid-token",
|
|
},
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
type introspectResponse struct {
|
|
Active bool `json:"active"`
|
|
Error string `json:"error"`
|
|
}
|
|
iresp := &introspectResponse{}
|
|
json.Unmarshal(resp.Data["http_raw_body"].([]byte), iresp)
|
|
if iresp.Active {
|
|
t.Fatalf("expected active state of a malformed token to be false but what was found to be: %t", iresp.Active)
|
|
}
|
|
if iresp.Error == "" {
|
|
t.Fatalf("expected a malformed token to return an error message but instead returned %q", iresp.Error)
|
|
}
|
|
|
|
// Populate backend with a valid token ---
|
|
// Create and load an entity, an entity is required to generate an ID token
|
|
testEntity := &identity.Entity{
|
|
Name: "test-entity-name",
|
|
ID: "test-entity-id",
|
|
BucketKey: "test-entity-bucket-key",
|
|
}
|
|
|
|
txn := c.identityStore.db.Txn(true)
|
|
defer txn.Abort()
|
|
err = c.identityStore.upsertEntityInTxn(ctx, txn, testEntity, nil, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
txn.Commit()
|
|
|
|
for _, alg := range []string{"RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"} {
|
|
key := "test-key-" + alg
|
|
role := "test-role-" + alg
|
|
|
|
// Create a test key "test-key"
|
|
resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/key/" + key,
|
|
Operation: logical.CreateOperation,
|
|
Storage: storage,
|
|
Data: map[string]interface{}{
|
|
"algorithm": alg,
|
|
"allowed_client_ids": "*",
|
|
},
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
|
|
// Create a test role "test-role"
|
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/role/" + role,
|
|
Operation: logical.CreateOperation,
|
|
Data: map[string]interface{}{
|
|
"key": key,
|
|
},
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
|
|
// Generate a token against the role "test-role" -- should succeed
|
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/token/" + role,
|
|
Operation: logical.ReadOperation,
|
|
Storage: storage,
|
|
EntityID: "test-entity-id",
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
|
|
validToken := resp.Data["token"].(string)
|
|
|
|
// Expect active true and no error from a valid token
|
|
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
|
|
Path: "oidc/introspect/",
|
|
Operation: logical.UpdateOperation,
|
|
Data: map[string]interface{}{
|
|
"token": validToken,
|
|
},
|
|
Storage: storage,
|
|
})
|
|
expectSuccess(t, resp, err)
|
|
iresp2 := &introspectResponse{}
|
|
json.Unmarshal(resp.Data["http_raw_body"].([]byte), iresp2)
|
|
if !iresp2.Active {
|
|
t.Fatalf("expected active state of a valid token to be true but what was found to be: %t", iresp2.Active)
|
|
}
|
|
if iresp2.Error != "" {
|
|
t.Fatalf("expected a valid token to return an empty error message but instead got %q", iresp.Error)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOIDC_isTargetNamespacedKey(t *testing.T) {
|
|
tests := []struct {
|
|
nsTargets []string
|
|
nskey string
|
|
expected bool
|
|
}{
|
|
{[]string{"nsid"}, "v0:nsid:key", true},
|
|
{[]string{"nsid"}, "v0:nsid:", true},
|
|
{[]string{"nsid"}, "v0:nsid", false},
|
|
{[]string{"nsid"}, "v0:", false},
|
|
{[]string{"nsid"}, "v0", false},
|
|
{[]string{"nsid"}, "", false},
|
|
{[]string{"nsid1"}, "v0:nsid2:key", false},
|
|
{[]string{"nsid1"}, "nsid1:nsid2:nsid1", false},
|
|
{[]string{"nsid1"}, "nsid1:nsid1:nsid1", true},
|
|
{[]string{"nsid"}, "nsid:nsid:nsid:nsid:nsid:nsid", true},
|
|
{[]string{"nsid"}, ":::", false},
|
|
{[]string{""}, ":::", true}, // "" is a valid key for cache.Set/Get
|
|
{[]string{"nsid1"}, "nsid0:nsid1:nsid0:nsid1:nsid0:nsid1", true},
|
|
{[]string{"nsid0"}, "nsid0:nsid1:nsid0:nsid1:nsid0:nsid1", false},
|
|
{[]string{"nsid0", "nsid1"}, "v0:nsid2:key", false},
|
|
{[]string{"nsid0", "nsid1", "nsid2", "nsid3", "nsid4"}, "v0:nsid3:key", true},
|
|
{[]string{"nsid0", "nsid1", "nsid2", "nsid3", "nsid4"}, "nsid0:nsid1:nsid2:nsid3:nsid4:nsid5", true},
|
|
{[]string{"nsid0", "nsid1", "nsid2", "nsid3", "nsid4"}, "nsid4:nsid5:nsid6:nsid7:nsid8:nsid9", false},
|
|
{[]string{"nsid0", "nsid0", "nsid0", "nsid0", "nsid0"}, "nsid0:nsid0:nsid0:nsid0:nsid0:nsid0", true},
|
|
{[]string{"nsid1", "nsid1", "nsid2", "nsid2"}, "nsid0:nsid0:nsid0:nsid0:nsid0:nsid0", false},
|
|
{[]string{"nsid1", "nsid1", "nsid2", "nsid2"}, "nsid0:nsid0:nsid0:nsid0:nsid0:nsid0", false},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
actual := isTargetNamespacedKey(test.nskey, test.nsTargets)
|
|
if test.expected != actual {
|
|
t.Fatalf("expected %t but got %t for nstargets: %q and nskey: %q", test.expected, actual, test.nsTargets, test.nskey)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOIDC_Flush(t *testing.T) {
|
|
c := newOIDCCache()
|
|
ns := []*namespace.Namespace{
|
|
noNamespace, //ns[0] is nilNamespace
|
|
&namespace.Namespace{ID: "ns1"},
|
|
&namespace.Namespace{ID: "ns2"},
|
|
}
|
|
|
|
// populateNs populates cache by ns with some data
|
|
populateNs := func() {
|
|
for i := range ns {
|
|
for _, val := range []string{"keyA", "keyB", "keyC"} {
|
|
if err := c.SetDefault(ns[i], val, struct{}{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// validate verifies that cache items exist or do not exist based on their namespaced key
|
|
verify := func(items map[string]gocache.Item, expect, doNotExpect []*namespace.Namespace) {
|
|
for _, expectNs := range expect {
|
|
found := false
|
|
for i := range items {
|
|
if isTargetNamespacedKey(i, []string{expectNs.ID}) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("Expected cache to contain an entry with a namespaced key for namespace: %q but did not find one", expectNs.ID)
|
|
}
|
|
}
|
|
|
|
for _, doNotExpectNs := range doNotExpect {
|
|
for i := range items {
|
|
if isTargetNamespacedKey(i, []string{doNotExpectNs.ID}) {
|
|
t.Fatalf("Did not expect cache to contain an entry with a namespaced key for namespace: %q but found the key: %q", doNotExpectNs.ID, i)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// flushing ns1 should flush ns1 and nilNamespace but not ns2
|
|
populateNs()
|
|
if err := c.Flush(ns[1]); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
items := c.c.Items()
|
|
verify(items, []*namespace.Namespace{ns[2]}, []*namespace.Namespace{ns[0], ns[1]})
|
|
|
|
// flushing nilNamespace should flush nilNamespace but not ns1 or ns2
|
|
populateNs()
|
|
if err := c.Flush(ns[0]); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
items = c.c.Items()
|
|
verify(items, []*namespace.Namespace{ns[1], ns[2]}, []*namespace.Namespace{ns[0]})
|
|
}
|
|
|
|
func TestOIDC_CacheNamespaceNilCheck(t *testing.T) {
|
|
cache := newOIDCCache()
|
|
|
|
if _, _, err := cache.Get(nil, "foo"); err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
|
|
if err := cache.SetDefault(nil, "foo", 42); err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
|
|
if err := cache.Flush(nil); err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
}
|
|
|
|
// some helpers
|
|
func expectSuccess(t *testing.T, resp *logical.Response, err error) {
|
|
t.Helper()
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("expected success but got error:\n%v\nresp: %#v", err, resp)
|
|
}
|
|
}
|
|
|
|
func expectError(t *testing.T, resp *logical.Response, err error) {
|
|
if err == nil {
|
|
if resp == nil || !resp.IsError() {
|
|
t.Fatalf("expected error but got success; error:\n%v\nresp: %#v", err, resp)
|
|
}
|
|
}
|
|
}
|
|
|
|
// expectString fails unless every string in actualStrings is also included in expectedStrings and
|
|
// the length of actualStrings and expectedStrings are the same
|
|
func expectStrings(t *testing.T, actualStrings []string, expectedStrings map[string]interface{}) {
|
|
if len(actualStrings) != len(expectedStrings) {
|
|
t.Fatalf("expectStrings mismatch:\nactual strings:\n%#v\nexpected strings:\n%#v\n", actualStrings, expectedStrings)
|
|
}
|
|
for _, actualString := range actualStrings {
|
|
_, ok := expectedStrings[actualString]
|
|
if !ok {
|
|
t.Fatalf("the string %q was not expected", actualString)
|
|
}
|
|
}
|
|
}
|