// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package vault

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"sort"
	"strings"
	"testing"
	"time"

	"github.com/go-test/deep"
	"github.com/hashicorp/go-secure-stdlib/parseutil"
	"github.com/hashicorp/vault/helper/namespace"
	"github.com/hashicorp/vault/sdk/framework"
	"github.com/hashicorp/vault/sdk/logical"
	"github.com/stretchr/testify/require"
)

/*
 Tests for the Vault OIDC provider configuration and OIDC-related
 endpoints. Additional tests for the Vault OIDC provider exist
 in: vault/external_tests/identity/oidc_provider_test.go
*/

const (
	authCodeRegex = "[a-zA-Z0-9]{32}"
)

// Tests that an authorization code issued by one provider cannot be exchanged
// for a token using a different provider that the client is allowed to use.
func TestOIDC_Path_OIDC_Cross_Provider_Exchange(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	s := new(logical.InmemStorage)

	// Create the common OIDC configuration
	entityID, _, _, clientID, clientSecret := setupOIDCCommon(t, c, s)

	// Create a second provider
	providerPath := "oidc/provider/test-provider-2"
	req := testProviderReq(s, clientID)
	req.Path = providerPath
	resp, err := c.identityStore.HandleRequest(ctx, req)
	require.NoError(t, err)

	// Obtain an authorization code from the first provider
	var authRes struct {
		Code  string `json:"code"`
		State string `json:"state"`
	}
	req = testAuthorizeReq(s, clientID)
	req.EntityID = entityID
	resp, err = c.identityStore.HandleRequest(ctx, req)
	require.NoError(t, err)
	require.NoError(t, json.Unmarshal(resp.Data["http_raw_body"].([]byte), &authRes))
	require.Regexp(t, authCodeRegex, authRes.Code)
	require.Equal(t, req.Data["state"], authRes.State)

	// Assert that the authorization code cannot be exchanged using the second provider
	var tokenRes struct {
		Error            string `json:"error"`
		ErrorDescription string `json:"error_description"`
	}
	req = testTokenReq(s, authRes.Code, clientID, clientSecret)
	req.Path = providerPath + "/token"
	resp, err = c.identityStore.HandleRequest(ctx, req)
	require.NoError(t, err)
	require.NoError(t, json.Unmarshal(resp.Data["http_raw_body"].([]byte), &tokenRes))
	require.Equal(t, ErrTokenInvalidGrant, tokenRes.Error)
	require.Equal(t, "authorization code was not issued by the provider", tokenRes.ErrorDescription)
}

func TestOIDC_Path_OIDC_Token(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	s := new(logical.InmemStorage)

	entityID, groupID, _, clientID, clientSecret := setupOIDCCommon(t, c, s)

	type args struct {
		clientReq              *logical.Request
		providerReq            *logical.Request
		assignmentReq          *logical.Request
		authorizeReq           *logical.Request
		tokenReq               *logical.Request
		vaultTokenCreationTime func() time.Time
	}
	tests := []struct {
		name    string
		args    args
		wantErr string
	}{
		{
			name: "invalid token request with provider not found",
			args: args{
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq:  testAuthorizeReq(s, clientID),
				tokenReq: func() *logical.Request {
					req := testTokenReq(s, "", clientID, clientSecret)
					req.Path = "oidc/provider/non-existent-provider/token"
					return req
				}(),
			},
			wantErr: ErrTokenInvalidRequest,
		},
		{
			name: "invalid token request with missing basic auth header",
			args: args{
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq:  testAuthorizeReq(s, clientID),
				tokenReq: func() *logical.Request {
					req := testTokenReq(s, "", clientID, clientSecret)
					req.Headers = nil
					return req
				}(),
			},
			wantErr: ErrTokenInvalidRequest,
		},
		{
			name: "invalid token request with client ID not found",
			args: args{
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq:  testAuthorizeReq(s, clientID),
				tokenReq:      testTokenReq(s, "", "non-existent-client-id", clientSecret),
			},
			wantErr: ErrTokenInvalidClient,
		},
		{
			name: "invalid token request with client secret mismatch",
			args: args{
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq:  testAuthorizeReq(s, clientID),
				tokenReq:      testTokenReq(s, "", clientID, "wrong-client-secret"),
			},
			wantErr: ErrTokenInvalidClient,
		},
		{
			name: "invalid token request with client_id not allowed by provider",
			args: args{
				clientReq: testClientReq(s),
				providerReq: func() *logical.Request {
					req := testProviderReq(s, clientID)
					req.Data["allowed_client_ids"] = []string{"not-client-id"}
					return req
				}(),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq:  testAuthorizeReq(s, clientID),
				tokenReq:      testTokenReq(s, "", clientID, clientSecret),
			},
			wantErr: ErrTokenInvalidClient,
		},
		{
			name: "invalid token request with empty grant_type",
			args: args{
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq:  testAuthorizeReq(s, clientID),
				tokenReq: func() *logical.Request {
					req := testTokenReq(s, "", clientID, clientSecret)
					req.Data["grant_type"] = ""
					return req
				}(),
			},
			wantErr: ErrTokenInvalidRequest,
		},
		{
			name: "invalid token request with unsupported grant_type",
			args: args{
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq:  testAuthorizeReq(s, clientID),
				tokenReq: func() *logical.Request {
					req := testTokenReq(s, "", clientID, clientSecret)
					req.Data["grant_type"] = "not-supported-grant-type"
					return req
				}(),
			},
			wantErr: ErrTokenUnsupportedGrantType,
		},
		{
			name: "invalid token request with invalid code",
			args: args{
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq:  testAuthorizeReq(s, clientID),
				tokenReq: func() *logical.Request {
					req := testTokenReq(s, "", clientID, clientSecret)
					req.Data["code"] = "invalid-code"
					return req
				}(),
			},
			wantErr: ErrTokenInvalidGrant,
		},
		{
			name: "invalid token request with missing redirect_uri",
			args: args{
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq:  testAuthorizeReq(s, clientID),
				tokenReq: func() *logical.Request {
					req := testTokenReq(s, "", clientID, clientSecret)
					req.Data["redirect_uri"] = ""
					return req
				}(),
			},
			wantErr: ErrTokenInvalidRequest,
		},
		{
			name: "invalid token request with entity not found in client assignment",
			args: args{
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, "not-entity-id", ""),
				authorizeReq:  testAuthorizeReq(s, clientID),
				tokenReq:      testTokenReq(s, "", clientID, clientSecret),
			},
			wantErr: ErrTokenInvalidRequest,
		},
		{
			name: "invalid token request with redirect_uri mismatch",
			args: args{
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq:  testAuthorizeReq(s, clientID),
				tokenReq: func() *logical.Request {
					req := testTokenReq(s, "", clientID, clientSecret)
					req.Data["redirect_uri"] = "https://not.original.redirect.uri:8251/callback"
					return req
				}(),
			},
			wantErr: ErrTokenInvalidGrant,
		},
		{
			name: "invalid token request with group not found in client assignment",
			args: args{
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, "", "not-group-id"),
				authorizeReq:  testAuthorizeReq(s, clientID),
				tokenReq:      testTokenReq(s, "", clientID, clientSecret),
			},
			wantErr: ErrTokenInvalidRequest,
		},
		{
			name: "invalid token request with scopes claim conflict",
			args: args{
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["scope"] = "openid test-scope conflict"
					return req
				}(),
				tokenReq: testTokenReq(s, "", clientID, clientSecret),
			},
			wantErr: ErrTokenInvalidRequest,
		},
		{
			name: "invalid token request with empty code_verifier",
			args: args{
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["code_challenge_method"] = "plain"
					req.Data["code_challenge"] = "43_char_min_abcdefghijklmnopqrstuvwxyzabcde"
					return req
				}(),
				tokenReq: func() *logical.Request {
					req := testTokenReq(s, "", clientID, clientSecret)
					req.Data["code_verifier"] = ""
					return req
				}(),
			},
			wantErr: ErrTokenInvalidRequest,
		},
		{
			name: "invalid token request with code_verifier provided for non-PKCE flow",
			args: args{
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq:  testAuthorizeReq(s, clientID),
				tokenReq: func() *logical.Request {
					req := testTokenReq(s, "", clientID, clientSecret)
					req.Data["code_verifier"] = "pkce_not_used_in_authorize_request"
					return req
				}(),
			},
			wantErr: ErrTokenInvalidRequest,
		},
		{
			name: "invalid token request with incorrect plain code_verifier",
			args: args{
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["code_challenge_method"] = "plain"
					req.Data["code_challenge"] = "43_char_min_abcdefghijklmnopqrstuvwxyzabcde"
					return req
				}(),
				tokenReq: func() *logical.Request {
					req := testTokenReq(s, "", clientID, clientSecret)
					req.Data["code_verifier"] = "wont_match_challenge"
					return req
				}(),
			},
			wantErr: ErrTokenInvalidGrant,
		},
		{
			name: "invalid token request with incorrect S256 code_verifier",
			args: args{
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["code_challenge_method"] = "S256"
					req.Data["code_challenge"] = "43_char_min_abcdefghijklmnopqrstuvwxyzabcde"
					return req
				}(),
				tokenReq: func() *logical.Request {
					req := testTokenReq(s, "", clientID, clientSecret)
					req.Data["code_verifier"] = "wont_hash_to_challenge"
					return req
				}(),
			},
			wantErr: ErrTokenInvalidGrant,
		},
		{
			name: "valid token request with plain code_challenge_method",
			args: args{
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["code_challenge_method"] = "plain"
					req.Data["code_challenge"] = "43_char_min_abcdefghijklmnopqrstuvwxyzabcde"
					return req
				}(),
				tokenReq: func() *logical.Request {
					req := testTokenReq(s, "", clientID, clientSecret)
					req.Data["code_verifier"] = "43_char_min_abcdefghijklmnopqrstuvwxyzabcde"
					return req
				}(),
			},
		},
		{
			name: "valid token request with default plain code_challenge_method",
			args: args{
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					// code_challenge_method intentionally not provided
					req := testAuthorizeReq(s, clientID)
					req.Data["code_challenge"] = "43_char_min_abcdefghijklmnopqrstuvwxyzabcde"
					return req
				}(),
				tokenReq: func() *logical.Request {
					req := testTokenReq(s, "", clientID, clientSecret)
					req.Data["code_verifier"] = "43_char_min_abcdefghijklmnopqrstuvwxyzabcde"
					return req
				}(),
			},
		},
		{
			name: "valid token request with S256 code_challenge_method",
			args: args{
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["code_challenge_method"] = "S256"
					req.Data["code_challenge"] = "hMn-5TBH-t3uN00FEaGsQtYPhyC4Otbx-9vDcPTYHmc"
					return req
				}(),
				tokenReq: func() *logical.Request {
					req := testTokenReq(s, "", clientID, clientSecret)
					req.Data["code_verifier"] = "43_char_min_abcdefghijklmnopqrstuvwxyzabcde"
					return req
				}(),
			},
		},
		{
			name: "valid token request with max_age and auth_time claim",
			args: args{
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["max_age"] = "30"
					return req
				}(),
				tokenReq: testTokenReq(s, "", clientID, clientSecret),
				vaultTokenCreationTime: func() time.Time {
					return time.Now()
				},
			},
		},
		{
			name: "valid token request with empty nonce in authorize request",
			args: args{
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					delete(req.Data, "nonce")
					return req
				}(),
				tokenReq: testTokenReq(s, "", clientID, clientSecret),
			},
		},
		{
			name: "valid token request with client_secret_post client authentication method",
			args: args{
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq:  testAuthorizeReq(s, clientID),
				tokenReq: func() *logical.Request {
					req := testTokenReq(s, "", clientID, clientSecret)
					req.Headers = nil
					req.Data["client_id"] = clientID
					req.Data["client_secret"] = clientSecret
					return req
				}(),
			},
		},
		{
			name: "valid token request",
			args: args{
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq:  testAuthorizeReq(s, clientID),
				tokenReq:      testTokenReq(s, "", clientID, clientSecret),
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create a token entry to associate with the authorize request
			creationTime := time.Now()
			if tt.args.vaultTokenCreationTime != nil {
				creationTime = tt.args.vaultTokenCreationTime()
			}
			te := &logical.TokenEntry{
				Path:         "test",
				Policies:     []string{"default"},
				TTL:          time.Hour * 24,
				CreationTime: creationTime.Unix(),
			}
			testMakeTokenDirectly(t, c.tokenStore, te)
			require.NotEmpty(t, te.ID)

			// Reset any configuration modifications
			resetCommonOIDCConfig(t, s, c, entityID, groupID, clientID)

			// Send the request to the OIDC authorize endpoint
			tt.args.authorizeReq.EntityID = entityID
			tt.args.authorizeReq.ClientToken = te.ID
			resp, err := c.identityStore.HandleRequest(ctx, tt.args.authorizeReq)
			expectSuccess(t, resp, err)

			// Parse the authorize response
			var authRes struct {
				Code  string `json:"code"`
				State string `json:"state"`
			}
			require.NoError(t, json.Unmarshal(resp.Data["http_raw_body"].([]byte), &authRes))
			require.Regexp(t, authCodeRegex, authRes.Code)
			require.Equal(t, tt.args.authorizeReq.Data["state"], authRes.State)

			// Update the assignment
			tt.args.assignmentReq.Operation = logical.UpdateOperation
			resp, err = c.identityStore.HandleRequest(ctx, tt.args.assignmentReq)
			expectSuccess(t, resp, err)

			// Update the client
			tt.args.clientReq.Operation = logical.UpdateOperation
			resp, err = c.identityStore.HandleRequest(ctx, tt.args.clientReq)
			expectSuccess(t, resp, err)

			// Update the provider
			tt.args.providerReq.Operation = logical.UpdateOperation
			resp, err = c.identityStore.HandleRequest(ctx, tt.args.providerReq)
			expectSuccess(t, resp, err)

			// Update the code if provided by test arguments
			authCode := authRes.Code
			if tt.args.tokenReq.Data["code"] != "" {
				authCode = tt.args.tokenReq.Data["code"].(string)
			}

			// Send the request to the OIDC token endpoint
			tt.args.tokenReq.Data["code"] = authCode
			resp, err = c.identityStore.HandleRequest(ctx, tt.args.tokenReq)
			expectSuccess(t, resp, err)

			// Parse the token response
			var tokenRes struct {
				TokenType        string `json:"token_type"`
				AccessToken      string `json:"access_token"`
				IDToken          string `json:"id_token"`
				ExpiresIn        int64  `json:"expires_in"`
				Error            string `json:"error"`
				ErrorDescription string `json:"error_description"`
			}
			require.NotNil(t, resp)
			require.NotNil(t, resp.Data[logical.HTTPRawBody])
			require.NotNil(t, resp.Data[logical.HTTPStatusCode])
			require.NotNil(t, resp.Data[logical.HTTPContentType])
			require.NotNil(t, resp.Data[logical.HTTPPragmaHeader])
			require.NotNil(t, resp.Data[logical.HTTPCacheControlHeader])
			require.Equal(t, "no-cache", resp.Data[logical.HTTPPragmaHeader])
			require.Equal(t, "no-store", resp.Data[logical.HTTPCacheControlHeader])
			require.Equal(t, "application/json", resp.Data[logical.HTTPContentType].(string))
			require.NoError(t, json.Unmarshal(resp.Data["http_raw_body"].([]byte), &tokenRes))

			if tt.wantErr != "" {
				// Assert that we receive the expected error code and description
				require.Equal(t, tt.wantErr, tokenRes.Error)
				require.NotEmpty(t, tokenRes.ErrorDescription)

				// Assert that we receive the expected status code
				statusCode := resp.Data[logical.HTTPStatusCode].(int)
				switch tokenRes.Error {
				case ErrTokenInvalidClient:
					require.Equal(t, http.StatusUnauthorized, statusCode)
					require.Equal(t, "Basic", resp.Data[logical.HTTPWWWAuthenticateHeader])
				case ErrTokenServerError:
					require.Equal(t, http.StatusInternalServerError, statusCode)
				default:
					require.Equal(t, http.StatusBadRequest, statusCode)
				}
				return
			}

			// Assert that we receive the expected token response
			expectSuccess(t, resp, err)
			require.Equal(t, http.StatusOK, resp.Data[logical.HTTPStatusCode].(int))
			require.Equal(t, "Bearer", tokenRes.TokenType)
			require.NotEmpty(t, tokenRes.AccessToken)
			require.NotEmpty(t, tokenRes.IDToken)
			require.Equal(t, int64(86400), tokenRes.ExpiresIn)
			require.Empty(t, tokenRes.Error)
			require.Empty(t, tokenRes.ErrorDescription)

			// Parse the claims from the ID token payload
			parts := strings.Split(tokenRes.IDToken, ".")
			require.Equal(t, 3, len(parts))
			payload, err := base64.RawURLEncoding.DecodeString(parts[1])
			require.NoError(t, err)
			claims := make(map[string]interface{})
			require.NoError(t, json.Unmarshal(payload, &claims))

			// Assert that reserved claims are present in the ID token.
			// Optional reserved claims are asserted on conditionally.
			for _, c := range reservedClaims {
				switch c {
				case "nonce":
					// nonce must equal the nonce provided in the authorize request (including empty)
					require.EqualValues(t, tt.args.authorizeReq.Data[c], claims[c])

				case "auth_time":
					// auth_time must exist if max_age provided in the authorize request
					if _, ok := tt.args.authorizeReq.Data["max_age"]; ok {
						require.EqualValues(t, creationTime.Unix(), claims[c])
					} else {
						require.Empty(t, claims[c])
					}

				default:
					// other reserved claims must be present in all cases
					require.NotEmpty(t, claims[c])
				}
			}
		})
	}
}

func TestOIDC_Path_OIDC_Authorize(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	s := new(logical.InmemStorage)

	entityID, groupID, parentGroupID, clientID, _ := setupOIDCCommon(t, c, s)

	type args struct {
		entityID               string
		clientReq              *logical.Request
		providerReq            *logical.Request
		assignmentReq          *logical.Request
		authorizeReq           *logical.Request
		vaultTokenCreationTime func() time.Time
	}
	tests := []struct {
		name    string
		args    args
		wantErr string
	}{
		{
			name: "invalid authorize request with provider not found",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Path = "oidc/provider/non-existent-provider/authorize"
					return req
				}(),
			},
			wantErr: ErrAuthInvalidRequest,
		},
		{
			name: "invalid authorize request with empty scope",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["scope"] = ""
					return req
				}(),
			},
			wantErr: ErrAuthInvalidRequest,
		},
		{
			name: "invalid authorize request with missing openid scope",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["scope"] = "groups email profile"
					return req
				}(),
			},
			wantErr: ErrAuthInvalidRequest,
		},
		{
			name: "invalid authorize request with missing response_type",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["response_type"] = ""
					return req
				}(),
			},
			wantErr: ErrAuthInvalidRequest,
		},
		{
			name: "invalid authorize request with unsupported response_type",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["response_type"] = "id_token"
					return req
				}(),
			},
			wantErr: ErrAuthUnsupportedResponseType,
		},
		{
			name: "invalid authorize request with client_id not found",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					return testAuthorizeReq(s, "non-existent-client-id")
				}(),
			},
			wantErr: ErrAuthInvalidClientID,
		},
		{
			name: "invalid authorize request with client_id not allowed by provider",
			args: args{
				entityID:  entityID,
				clientReq: testClientReq(s),
				providerReq: func() *logical.Request {
					req := testProviderReq(s, clientID)
					req.Data["allowed_client_ids"] = []string{"not-client-id"}
					return req
				}(),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq:  testAuthorizeReq(s, clientID),
			},
			wantErr: ErrAuthUnauthorizedClient,
		},
		{
			name: "invalid authorize request with missing redirect_uri",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["redirect_uri"] = ""
					return req
				}(),
			},
			wantErr: ErrAuthInvalidRequest,
		},
		{
			name: "invalid authorize request with redirect_uri not allowed by client",
			args: args{
				entityID: entityID,
				clientReq: func() *logical.Request {
					req := testClientReq(s)
					req.Data["redirect_uris"] = []string{"https://not.redirect.uri:8251/callback"}
					return req
				}(),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq:  testAuthorizeReq(s, clientID),
			},
			wantErr: ErrAuthInvalidRedirectURI,
		},
		{
			name: "invalid authorize request with request parameter provided",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["request"] = "header.payload.signature"
					return req
				}(),
			},
			wantErr: ErrAuthRequestNotSupported,
		},
		{
			name: "invalid authorize request with request_uri parameter provided",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["request_uri"] = "https://client.example.org/request.jwt"
					return req
				}(),
			},
			wantErr: ErrAuthRequestURINotSupported,
		},
		{
			name: "invalid authorize request with identity entity not associated with the request",
			args: args{
				entityID:      "",
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq:  testAuthorizeReq(s, clientID),
			},
			wantErr: ErrAuthAccessDenied,
		},
		{
			name: "invalid authorize request with identity entity ID not found",
			args: args{
				entityID:      "non-existent-entity",
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq:  testAuthorizeReq(s, clientID),
			},
			wantErr: ErrAuthAccessDenied,
		},
		{
			name: "invalid authorize request with entity not found in client assignment",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, "not-entity-id", ""),
				authorizeReq:  testAuthorizeReq(s, clientID),
			},
			wantErr: ErrAuthAccessDenied,
		},
		{
			name: "invalid authorize request with group not found in client assignment",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, "", "not-group-id"),
				authorizeReq:  testAuthorizeReq(s, clientID),
			},
			wantErr: ErrAuthAccessDenied,
		},
		{
			name: "invalid authorize request with negative max_age",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["max_age"] = "-1"
					return req
				}(),
			},
			wantErr: ErrAuthInvalidRequest,
		},
		{
			name: "invalid authorize request with invalid code_challenge_method",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["code_challenge_method"] = "S512"
					req.Data["code_challenge"] = "43_char_min_abcdefghijklmnopqrstuvwxyzabcde"
					return req
				}(),
			},
			wantErr: ErrAuthInvalidRequest,
		},
		{
			name: "invalid authorize request with code_challenge length < 43 characters",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["code_challenge_method"] = "S256"
					req.Data["code_challenge"] = ""
					return req
				}(),
			},
			wantErr: ErrAuthInvalidRequest,
		},
		{
			name: "invalid authorize request with code_challenge length > 128 characters",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["code_challenge_method"] = "S256"
					req.Data["code_challenge"] = `
					129_char_abcdefghijklmnopqrstuvwxyzabcd
					129_char_abcdefghijklmnopqrstuvwxyzabcd
					129_char_abcdefghijklmnopqrstuvwxyzabcd
					`
					return req
				}(),
			},
			wantErr: ErrAuthInvalidRequest,
		},
		{
			name: "valid authorize request with empty nonce",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					delete(req.Data, "nonce")
					return req
				}(),
			},
		},
		{
			name: "valid authorize request with empty state",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["state"] = ""
					return req
				}(),
			},
		},
		{
			name: "active re-authentication required with token creation time exceeding max_age requirement",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["max_age"] = "30"
					return req
				}(),
				vaultTokenCreationTime: func() time.Time {
					return time.Now().Add(-time.Minute)
				},
			},
			wantErr: ErrAuthMaxAgeReAuthenticate,
		},
		{
			name: "valid authorize request with token creation time within max_age requirement",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["max_age"] = "30"
					return req
				}(),
				vaultTokenCreationTime: func() time.Time {
					return time.Now()
				},
			},
		},
		{
			name: "valid authorize request using update operation (HTTP POST)",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq:  testAuthorizeReq(s, clientID),
			},
		},
		{
			name: "valid authorize request using read operation (HTTP GET)",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Operation = logical.ReadOperation
					return req
				}(),
			},
		},
		{
			name: "valid authorize request using client assignment with only entity membership",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, ""),
				authorizeReq:  testAuthorizeReq(s, clientID),
			},
		},
		{
			name: "valid authorize request using client assignment with only group membership",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, "", groupID),
				authorizeReq:  testAuthorizeReq(s, clientID),
			},
		},
		{
			name: "valid authorize request using client assignment with inherited group membership",
			args: args{
				entityID:      entityID,
				clientReq:     testClientReq(s),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, "", parentGroupID),
				authorizeReq:  testAuthorizeReq(s, clientID),
			},
		},
		{
			name: "valid authorize request with port-agnostic loopback redirect_uri 127.0.0.1",
			args: args{
				entityID: entityID,
				clientReq: func() *logical.Request {
					req := testClientReq(s)
					req.Data["redirect_uris"] = []string{"http://127.0.0.1/callback"}
					return req
				}(),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["redirect_uri"] = "http://127.0.0.1:51004/callback"
					return req
				}(),
			},
		},
		{
			name: "valid authorize request with port-agnostic loopback redirect_uri 127.0.0.1 with port",
			args: args{
				entityID: entityID,
				clientReq: func() *logical.Request {
					req := testClientReq(s)
					req.Data["redirect_uris"] = []string{"http://127.0.0.1:8251/callback"}
					return req
				}(),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["redirect_uri"] = "http://127.0.0.1:51005/callback"
					return req
				}(),
			},
		},
		{
			name: "valid authorize request with port-agnostic loopback redirect_uri localhost",
			args: args{
				entityID: entityID,
				clientReq: func() *logical.Request {
					req := testClientReq(s)
					req.Data["redirect_uris"] = []string{"http://localhost:8251/callback"}
					return req
				}(),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["redirect_uri"] = "http://localhost:51006/callback"
					return req
				}(),
			},
		},
		{
			name: "valid authorize request with port-agnostic loopback redirect_uri [::1]",
			args: args{
				entityID: entityID,
				clientReq: func() *logical.Request {
					req := testClientReq(s)
					req.Data["redirect_uris"] = []string{"http://[::1]:8251/callback"}
					return req
				}(),
				providerReq:   testProviderReq(s, clientID),
				assignmentReq: testAssignmentReq(s, entityID, groupID),
				authorizeReq: func() *logical.Request {
					req := testAuthorizeReq(s, clientID)
					req.Data["redirect_uri"] = "http://[::1]:51007/callback"
					return req
				}(),
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create a token entry to associate with the authorize request
			creationTime := time.Now()
			if tt.args.vaultTokenCreationTime != nil {
				creationTime = tt.args.vaultTokenCreationTime()
			}
			te := &logical.TokenEntry{
				Path:         "test",
				Policies:     []string{"default"},
				TTL:          time.Hour * 24,
				CreationTime: creationTime.Unix(),
			}
			testMakeTokenDirectly(t, c.tokenStore, te)
			require.NotEmpty(t, te.ID)

			// Update the assignment
			tt.args.assignmentReq.Operation = logical.UpdateOperation
			resp, err := c.identityStore.HandleRequest(ctx, tt.args.assignmentReq)
			expectSuccess(t, resp, err)

			// Update the client
			tt.args.clientReq.Operation = logical.UpdateOperation
			resp, err = c.identityStore.HandleRequest(ctx, tt.args.clientReq)
			expectSuccess(t, resp, err)

			// Update the provider
			tt.args.providerReq.Operation = logical.UpdateOperation
			resp, err = c.identityStore.HandleRequest(ctx, tt.args.providerReq)
			expectSuccess(t, resp, err)

			// Send the request to the OIDC authorize endpoint
			tt.args.authorizeReq.EntityID = tt.args.entityID
			tt.args.authorizeReq.ClientToken = te.ID
			resp, err = c.identityStore.HandleRequest(ctx, tt.args.authorizeReq)

			// Parse the response
			var authRes struct {
				Code             string `json:"code"`
				State            string `json:"state"`
				Error            string `json:"error"`
				ErrorDescription string `json:"error_description"`
			}
			require.NotNil(t, resp)
			require.NotNil(t, resp.Data[logical.HTTPRawBody])
			require.NotNil(t, resp.Data[logical.HTTPStatusCode])
			require.NotNil(t, resp.Data[logical.HTTPContentType])
			require.Equal(t, "application/json", resp.Data[logical.HTTPContentType].(string))
			require.NoError(t, json.Unmarshal(resp.Data["http_raw_body"].([]byte), &authRes))

			if tt.wantErr != "" {
				// Assert that we receive the expected error code and description
				require.Equal(t, tt.wantErr, authRes.Error)
				require.NotEmpty(t, authRes.ErrorDescription)

				// Assert that we receive the expected status code
				statusCode := resp.Data[logical.HTTPStatusCode].(int)
				switch authRes.Error {
				case ErrAuthServerError:
					require.Equal(t, http.StatusInternalServerError, statusCode)
				default:
					require.Equal(t, http.StatusBadRequest, statusCode)
				}
				return
			}

			// Assert that we receive an authorization code (base62) and state
			expectSuccess(t, resp, err)
			require.Equal(t, http.StatusOK, resp.Data[logical.HTTPStatusCode].(int))
			require.Regexp(t, authCodeRegex, authRes.Code)
			require.Equal(t, tt.args.authorizeReq.Data["state"], authRes.State)
			require.Empty(t, authRes.Error)
			require.Empty(t, authRes.ErrorDescription)
		})
	}
}

// setupOIDCCommon creates all of the resources needed to test a Vault OIDC provider.
// Returns the entity ID, group ID, client ID, client secret to be used in tests.
func setupOIDCCommon(t *testing.T, c *Core, s logical.Storage) (string, string, string, string, string) {
	t.Helper()
	ctx := namespace.RootContext(nil)

	// Create a key
	resp, err := c.identityStore.HandleRequest(ctx, testKeyReq(s, []string{"*"}, "RS256"))
	expectSuccess(t, resp, err)

	// Create an entity
	resp, err = c.identityStore.HandleRequest(ctx, testEntityReq(s))
	expectSuccess(t, resp, err)
	require.NotNil(t, resp.Data["id"])
	entityID := resp.Data["id"].(string)

	// Create a group
	resp, err = c.identityStore.HandleRequest(ctx, testGroupReq(s, "test-group",
		[]string{entityID}, nil))
	expectSuccess(t, resp, err)
	require.NotNil(t, resp.Data["id"])
	groupID := resp.Data["id"].(string)

	// Create a parent group
	resp, err = c.identityStore.HandleRequest(ctx, testGroupReq(s, "test-parent-group",
		nil, []string{groupID}))
	expectSuccess(t, resp, err)
	require.NotNil(t, resp.Data["id"])
	parentGroupID := resp.Data["id"].(string)

	// Create an assignment
	resp, err = c.identityStore.HandleRequest(ctx, testAssignmentReq(s, entityID, groupID))
	expectSuccess(t, resp, err)

	// Create a client
	resp, err = c.identityStore.HandleRequest(ctx, testClientReq(s))
	expectSuccess(t, resp, err)

	// Read the client ID and secret
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Storage:   s,
		Path:      "oidc/client/test-client",
		Operation: logical.ReadOperation,
	})
	expectSuccess(t, resp, err)
	require.NotNil(t, resp.Data["client_id"])
	require.NotNil(t, resp.Data["client_secret"])
	clientID := resp.Data["client_id"].(string)
	clientSecret := resp.Data["client_secret"].(string)

	// Create a custom scope
	template := `{
		"name": {{identity.entity.name}},
		"contact": {
			"email": {{identity.entity.metadata.email}},
			"phone_number": {{identity.entity.metadata.phone_number}}
		},
		"groups": {{identity.entity.groups.names}}
	}`
	resp, err = c.identityStore.HandleRequest(ctx, testScopeReq(s, "test-scope", template))
	expectSuccess(t, resp, err)

	// Create a custom scope that has a conflicting claim
	template = `{
		"username": {{identity.entity.name}},
		"contact": {
			"user_email": {{identity.entity.metadata.email}},
			"phone_number": {{identity.entity.metadata.phone_number}}
		}
	}`
	resp, err = c.identityStore.HandleRequest(ctx, testScopeReq(s, "conflict", template))
	expectSuccess(t, resp, err)

	// Create a provider
	resp, err = c.identityStore.HandleRequest(ctx, testProviderReq(s, clientID))
	expectSuccess(t, resp, err)

	return entityID, groupID, parentGroupID, clientID, clientSecret
}

// resetCommonOIDCConfig resets the state of common configuration resources
// (i.e., created by setupOIDCCommon) that are modified during tests. This
// enables the tests to continue operating using the same underlying storage
// throughout many test cases that modify the configuration resources.
func resetCommonOIDCConfig(t *testing.T, s logical.Storage, c *Core, entityID, groupID, clientID string) {
	ctx := namespace.RootContext(nil)

	req := testAssignmentReq(s, entityID, groupID)
	req.Operation = logical.UpdateOperation
	resp, err := c.identityStore.HandleRequest(ctx, req)
	expectSuccess(t, resp, err)

	req = testClientReq(s)
	req.Operation = logical.UpdateOperation
	resp, err = c.identityStore.HandleRequest(ctx, req)
	expectSuccess(t, resp, err)

	req = testProviderReq(s, clientID)
	req.Operation = logical.UpdateOperation
	resp, err = c.identityStore.HandleRequest(ctx, req)
	expectSuccess(t, resp, err)
}

func testTokenReq(s logical.Storage, code, clientID, clientSecret string) *logical.Request {
	return &logical.Request{
		Storage:   s,
		Path:      "oidc/provider/test-provider/token",
		Operation: logical.UpdateOperation,
		Headers: map[string][]string{
			"Authorization": {basicAuthHeader(clientID, clientSecret)},
		},
		Data: map[string]interface{}{
			// The code is unknown until returned from the authorization endpoint
			"code":         code,
			"grant_type":   "authorization_code",
			"redirect_uri": "https://localhost:8251/callback",
		},
	}
}

func testAuthorizeReq(s logical.Storage, clientID string) *logical.Request {
	return &logical.Request{
		Storage:   s,
		Path:      "oidc/provider/test-provider/authorize",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"client_id":     clientID,
			"scope":         "openid",
			"redirect_uri":  "https://localhost:8251/callback",
			"response_type": "code",
			"state":         "abcdefg",
			"nonce":         "hijklmn",
		},
	}
}

func testAssignmentReq(s logical.Storage, entityID, groupID string) *logical.Request {
	return &logical.Request{
		Storage:   s,
		Path:      "oidc/assignment/test-assignment",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"entity_ids": []string{entityID},
			"group_ids":  []string{groupID},
		},
	}
}

func testClientReq(s logical.Storage) *logical.Request {
	return &logical.Request{
		Storage:   s,
		Path:      "oidc/client/test-client",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"key":              "test-key",
			"redirect_uris":    []string{"https://localhost:8251/callback"},
			"assignments":      []string{"test-assignment"},
			"id_token_ttl":     "24h",
			"access_token_ttl": "24h",
		},
	}
}

func testProviderReq(s logical.Storage, clientID string) *logical.Request {
	return &logical.Request{
		Storage:   s,
		Path:      "oidc/provider/test-provider",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"allowed_client_ids": []string{clientID},
			"scopes_supported":   []string{"test-scope", "conflict"},
		},
	}
}

func testEntityReq(s logical.Storage) *logical.Request {
	return &logical.Request{
		Storage:   s,
		Path:      "entity",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"name": "test-entity",
			"metadata": map[string]string{
				"email":        "test@hashicorp.com",
				"phone_number": "123-456-7890",
			},
		},
	}
}

func testKeyReq(s logical.Storage, allowedClientIDs []string, alg string) *logical.Request {
	return &logical.Request{
		Storage:   s,
		Path:      "oidc/key/test-key",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"allowed_client_ids": allowedClientIDs,
			"algorithm":          alg,
		},
	}
}

func testGroupReq(s logical.Storage, name string, entityIDs, groupIDs []string) *logical.Request {
	return &logical.Request{
		Storage:   s,
		Path:      "group",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"name":              name,
			"member_entity_ids": entityIDs,
			"member_group_ids":  groupIDs,
		},
	}
}

func testScopeReq(s logical.Storage, name, template string) *logical.Request {
	return &logical.Request{
		Storage:   s,
		Path:      fmt.Sprintf("oidc/scope/%s", name),
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"template": template,
		},
	}
}

func basicAuthHeader(username, password string) string {
	auth := fmt.Sprintf("%s:%s", username, password)
	encoded := base64.StdEncoding.EncodeToString([]byte(auth))
	return fmt.Sprintf("Basic %s", encoded)
}

// TestOIDC_Path_OIDC_ProviderReadPublicKey_ProviderDoesNotExist tests that the
// path can handle the read operation when the provider does not exist
func TestOIDC_Path_OIDC_ProviderReadPublicKey_ProviderDoesNotExist(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	// Read "test-provider" .well-known keys
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider/.well-known/keys",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectedResp := &logical.Response{}
	if resp != expectedResp && err != nil {
		t.Fatalf("expected empty response but got success; error:\n%v\nresp: %#v", err, resp)
	}
}

// TestOIDC_Path_OIDC_ProviderReadPublicKey tests the provider .well-known
// keys endpoint read operations
func TestOIDC_Path_OIDC_ProviderReadPublicKey(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	// Create a test key "test-key-1"
	c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/key/test-key-1",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"verification_ttl": "2m",
			"rotation_period":  "2m",
		},
		Storage: storage,
	})

	// Create a test client "test-client-1"
	c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client-1",
		Operation: logical.CreateOperation,
		Storage:   storage,
		Data: map[string]interface{}{
			"key":          "test-key-1",
			"id_token_ttl": "1m",
		},
	})

	// Create a test client "test-client-2" that also uses "test-key-1"
	c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client-2",
		Operation: logical.CreateOperation,
		Storage:   storage,
		Data: map[string]interface{}{
			"key":          "test-key-1",
			"id_token_ttl": "1m",
		},
	})

	// get the clientID for "test-client-1"
	resp, _ := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client-1",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	clientID := resp.Data["client_id"].(string)

	// Create a test provider "test-provider" and allow all client IDs -- should succeed
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider",
		Operation: logical.CreateOperation,
		Storage:   storage,
		Data: map[string]interface{}{
			"issuer":             "https://example.com:8200",
			"allowed_client_ids": []string{"*"},
		},
	})
	expectSuccess(t, resp, err)

	// Read "test-provider" .well-known keys
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider/.well-known/keys",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)

	// at this point only 2 public keys are expected since both clients use
	// the same key "test-key-1"
	assertRespPublicKeyCount(t, resp, 2)

	// Create a test key "test-key-2"
	c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/key/test-key-2",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"verification_ttl": "2m",
			"rotation_period":  "2m",
		},
		Storage: storage,
	})

	// Create a test client "test-client-2"
	c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client-2",
		Operation: logical.CreateOperation,
		Storage:   storage,
		Data: map[string]interface{}{
			"key":          "test-key-2",
			"id_token_ttl": "1m",
		},
	})

	// Read "test-provider" .well-known keys
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider/.well-known/keys",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)

	assertRespPublicKeyCount(t, resp, 4)

	// Update the test provider "test-provider" to only allow test-client-1 -- should succeed
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider",
		Operation: logical.UpdateOperation,
		Storage:   storage,
		Data: map[string]interface{}{
			"allowed_client_ids": []string{clientID},
		},
	})
	expectSuccess(t, resp, err)

	// Read "test-provider" .well-known keys
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider/.well-known/keys",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)

	assertRespPublicKeyCount(t, resp, 2)
}

func TestOIDC_Path_OIDC_Client_Type(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/key/test-key",
		Operation: logical.CreateOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)

	tests := []struct {
		name             string
		createClientType clientType
		updateClientType clientType
		wantCreateErr    bool
		wantUpdateErr    bool
	}{
		{
			name:             "create confidential client and update to public client",
			createClientType: confidential,
			updateClientType: public,
			wantUpdateErr:    true,
		},
		{
			name:             "create confidential client and update to confidential client",
			createClientType: confidential,
			updateClientType: confidential,
		},
		{
			name:             "create public client and update to confidential client",
			createClientType: public,
			updateClientType: confidential,
			wantUpdateErr:    true,
		},
		{
			name:             "create public client and update to public client",
			createClientType: public,
			updateClientType: public,
		},
		{
			name:             "create an invalid client type",
			createClientType: clientType(300),
			wantCreateErr:    true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create a client with the given client type
			resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
				Path:      "oidc/client/test-client",
				Operation: logical.CreateOperation,
				Storage:   storage,
				Data: map[string]interface{}{
					"key":         "test-key",
					"client_type": tt.createClientType.String(),
				},
			})
			if tt.wantCreateErr {
				expectError(t, resp, err)
				return
			}
			expectSuccess(t, resp, err)

			// Read the client
			resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
				Path:      "oidc/client/test-client",
				Operation: logical.ReadOperation,
				Storage:   storage,
			})
			expectSuccess(t, resp, err)

			// Assert that the client type is properly set
			clientType := resp.Data["client_type"].(string)
			require.Equal(t, tt.createClientType.String(), clientType)

			// Assert that all client types have a client ID
			clientID := resp.Data["client_id"].(string)
			require.Len(t, clientID, clientIDLength)

			// Assert that confidential clients have a client secret
			if tt.createClientType == confidential {
				clientSecret := resp.Data["client_secret"].(string)
				require.Contains(t, clientSecret, clientSecretPrefix)
			}

			// Assert that public clients do not have a client secret
			if tt.createClientType == public {
				_, ok := resp.Data["client_secret"]
				require.False(t, ok)
			}

			// Update the client and expect error if the type is different
			resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
				Path:      "oidc/client/test-client",
				Operation: logical.UpdateOperation,
				Storage:   storage,
				Data: map[string]interface{}{
					"key":         "test-key",
					"client_type": tt.updateClientType.String(),
				},
			})
			if tt.wantUpdateErr {
				expectError(t, resp, err)
			} else {
				expectSuccess(t, resp, err)
			}

			// Delete the client
			resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
				Path:      "oidc/client/test-client",
				Operation: logical.DeleteOperation,
				Storage:   storage,
			})
			expectSuccess(t, resp, err)
		})
	}
}

// TestOIDC_Path_OIDC_ProviderClient_DefaultKey tests that a
// client uses the default key if none provided at creation time.
func TestOIDC_Path_OIDC_ProviderClient_DefaultKey(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	require.NoError(t, c.identityStore.storeOIDCDefaultResources(ctx, c.identityStore.view))

	// Create a test client "test-client" without a key param
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client",
		Operation: logical.CreateOperation,
		Storage:   c.identityStore.view,
	})
	expectSuccess(t, resp, err)

	// Read "test-client" to validate it uses the default key
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client",
		Operation: logical.ReadOperation,
		Storage:   c.identityStore.view,
	})
	expectSuccess(t, resp, err)

	// Assert that the client uses the default key
	require.Equal(t, defaultKeyName, resp.Data["key"].(string))
}

// TestOIDC_Path_OIDC_ProviderClient_NilKeyEntry tests that a client cannot be
// created when a key parameter is provided but the key does not exist
func TestOIDC_Path_OIDC_ProviderClient_NilKeyEntry(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	// Create a test client "test-client1" with a non-existent key -- should fail
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client1",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"key": "test-key",
		},
		Storage: storage,
	})
	expectError(t, resp, err)
	// validate error message
	expectedStrings := map[string]interface{}{
		"key \"test-key\" does not exist": true,
	}
	expectStrings(t, []string{resp.Data["error"].(string)}, expectedStrings)
}

// TestOIDC_Path_OIDC_ProviderClient_InvalidTokenTTL tests the TokenTTL validation
func TestOIDC_Path_OIDC_ProviderClient_InvalidTokenTTL(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,
		Data: map[string]interface{}{
			"verification_ttl": int64(60),
		},
		Storage: storage,
	})

	// Create a test client "test-client" with an id_token_ttl longer than the
	// verification_ttl -- should fail with error
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"key":          "test-key",
			"id_token_ttl": int64(3600),
		},
		Storage: storage,
	})
	expectError(t, resp, err)
	// validate error message
	expectedStrings := map[string]interface{}{
		"a client's id_token_ttl cannot be greater than the verification_ttl of the key it references": true,
	}
	expectStrings(t, []string{resp.Data["error"].(string)}, expectedStrings)

	// Read "test-client"
	respReadTestClient, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	// Ensure that "test-client" was not created
	expectSuccess(t, respReadTestClient, err)
	if respReadTestClient != nil {
		t.Fatalf("Expected a nil response but instead got:\n%#v", respReadTestClient)
	}
}

// TestOIDC_Path_OIDC_ProviderClient_UpdateKey tests that a client
// does not allow key modification on Update operations
func TestOIDC_Path_OIDC_ProviderClient_UpdateKey(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	// Create a test key "test-key1"
	c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/key/test-key1",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"verification_ttl": "2m",
			"rotation_period":  "2m",
		},
		Storage: storage,
	})

	// Create a test key "test-key2"
	c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/key/test-key2",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"verification_ttl": "2m",
			"rotation_period":  "2m",
		},
		Storage: storage,
	})

	// Create a test client "test-client" -- should succeed
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client",
		Operation: logical.CreateOperation,
		Storage:   storage,
		Data: map[string]interface{}{
			"key":          "test-key1",
			"id_token_ttl": "1m",
		},
	})
	expectSuccess(t, resp, err)

	// Update the test client "test-client" -- should fail
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client",
		Operation: logical.UpdateOperation,
		Storage:   storage,
		Data: map[string]interface{}{
			"key":          "test-key2",
			"id_token_ttl": "1m",
		},
	})
	expectError(t, resp, err)
	// validate error message
	expectedStrings := map[string]interface{}{
		"key modification is not allowed": true,
	}
	expectStrings(t, []string{resp.Data["error"].(string)}, expectedStrings)
}

// TestOIDC_Path_OIDC_ProviderClient_AssignmentDoesNotExist tests that a client
// cannot be created with assignments that do not exist
func TestOIDC_Path_OIDC_ProviderClient_AssignmentDoesNotExist(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,
		Data: map[string]interface{}{
			"verification_ttl": "2m",
			"rotation_period":  "2m",
		},
		Storage: storage,
	})

	// Create a test client "test-client" -- should fail
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client",
		Operation: logical.CreateOperation,
		Storage:   storage,
		Data: map[string]interface{}{
			"key":         "test-key",
			"assignments": "my-assignment",
		},
	})
	expectError(t, resp, err)
	// validate error message
	expectedStrings := map[string]interface{}{
		"assignment \"my-assignment\" does not exist": true,
	}
	expectStrings(t, []string{resp.Data["error"].(string)}, expectedStrings)
}

// TestOIDC_Path_OIDC_ProviderClient tests CRUD operations for clients
func TestOIDC_Path_OIDC_ProviderClient(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,
		Data: map[string]interface{}{
			"verification_ttl": "2m",
			"rotation_period":  "2m",
		},
		Storage: storage,
	})

	// Create a test client "test-client" -- should succeed
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client",
		Operation: logical.CreateOperation,
		Storage:   storage,
		Data: map[string]interface{}{
			"key":          "test-key",
			"id_token_ttl": "1m",
		},
	})
	expectSuccess(t, resp, err)

	// Read "test-client" and validate
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)
	expected := map[string]interface{}{
		"redirect_uris":    []string{},
		"assignments":      []string{},
		"key":              "test-key",
		"id_token_ttl":     int64(60),
		"access_token_ttl": int64(86400),
		"client_id":        resp.Data["client_id"],
		"client_secret":    resp.Data["client_secret"],
		"client_type":      confidential.String(),
	}
	if diff := deep.Equal(expected, resp.Data); diff != nil {
		t.Fatal(diff)
	}
	clientID := resp.Data["client_id"].(string)
	if len(clientID) != clientIDLength {
		t.Fatalf("client_id format is incorrect: %#v", clientID)
	}
	clientSecret := resp.Data["client_secret"].(string)
	if !strings.HasPrefix(clientSecret, clientSecretPrefix) {
		t.Fatalf("client_secret format is incorrect: %#v", clientSecret)
	}
	if len(clientSecret) != clientSecretLength+len(clientSecretPrefix) {
		t.Fatalf("client_secret format is incorrect: %#v", clientSecret)
	}

	// Create a test assignment "my-assignment" -- should succeed
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/assignment/my-assignment",
		Operation: logical.CreateOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)

	// Update "test-client" -- should succeed
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"redirect_uris":    "http://localhost:3456/callback",
			"assignments":      "my-assignment",
			"key":              "test-key",
			"id_token_ttl":     "90s",
			"access_token_ttl": "1m",
		},
		Storage: storage,
	})
	expectSuccess(t, resp, err)

	// Read "test-client" again and validate
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)
	expected = map[string]interface{}{
		"redirect_uris":    []string{"http://localhost:3456/callback"},
		"assignments":      []string{"my-assignment"},
		"key":              "test-key",
		"id_token_ttl":     int64(90),
		"access_token_ttl": int64(60),
		"client_id":        resp.Data["client_id"],
		"client_secret":    resp.Data["client_secret"],
		"client_type":      confidential.String(),
	}
	if diff := deep.Equal(expected, resp.Data); diff != nil {
		t.Fatal(diff)
	}

	// Delete test-client -- should succeed
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client",
		Operation: logical.DeleteOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)

	// Read "test-client" again and validate
	resp, _ = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	if resp != nil {
		t.Fatalf("expected nil but got resp: %#v", resp)
	}
}

// TestOIDC_Path_OIDC_ProviderClient_DeDuplication tests that a
// client doesn't have duplicate redirect URIs or Assignments
func TestOIDC_Path_OIDC_ProviderClient_Deduplication(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,
		Data: map[string]interface{}{
			"verification_ttl": "2m",
			"rotation_period":  "2m",
		},
		Storage: storage,
	})

	// Create a test assignment "test-assignment1" -- should succeed
	c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/assignment/test-assignment1",
		Operation: logical.CreateOperation,
		Storage:   storage,
	})

	// Create a test client "test-client" -- should succeed
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client",
		Operation: logical.CreateOperation,
		Storage:   storage,
		Data: map[string]interface{}{
			"key":           "test-key",
			"id_token_ttl":  "1m",
			"assignments":   []string{"test-assignment1", "test-assignment1"},
			"redirect_uris": []string{"http://example.com", "http://notduplicate.com", "http://example.com"},
			"client_type":   public.String(),
		},
	})
	expectSuccess(t, resp, err)

	// Read "test-client" and validate
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)
	expected := map[string]interface{}{
		"redirect_uris":    []string{"http://example.com", "http://notduplicate.com"},
		"assignments":      []string{"test-assignment1"},
		"key":              "test-key",
		"id_token_ttl":     int64(60),
		"access_token_ttl": int64(86400),
		"client_id":        resp.Data["client_id"],
		"client_type":      public.String(),
	}
	if diff := deep.Equal(expected, resp.Data); diff != nil {
		t.Fatal(diff)
	}
}

// TestOIDC_Path_OIDC_ProviderClient_Update tests Update operations for clients
func TestOIDC_Path_OIDC_ProviderClient_Update(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,
		Data: map[string]interface{}{
			"verification_ttl": "2m",
			"rotation_period":  "2m",
		},
		Storage: storage,
	})

	// Create a test assignment "my-assignment" -- should succeed
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/assignment/my-assignment",
		Operation: logical.CreateOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)

	// Create a test client "test-client" -- should succeed
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client",
		Operation: logical.CreateOperation,
		Storage:   storage,
		Data: map[string]interface{}{
			"redirect_uris":    "http://localhost:3456/callback",
			"assignments":      "my-assignment",
			"key":              "test-key",
			"id_token_ttl":     "2m",
			"access_token_ttl": "1h",
		},
	})
	expectSuccess(t, resp, err)

	// Read "test-client" and validate
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)
	expected := map[string]interface{}{
		"redirect_uris":    []string{"http://localhost:3456/callback"},
		"assignments":      []string{"my-assignment"},
		"key":              "test-key",
		"id_token_ttl":     int64(120),
		"access_token_ttl": int64(3600),
		"client_id":        resp.Data["client_id"],
		"client_secret":    resp.Data["client_secret"],
		"client_type":      confidential.String(),
	}
	if diff := deep.Equal(expected, resp.Data); diff != nil {
		t.Fatal(diff)
	}

	// Update "test-client" -- should succeed
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"redirect_uris":    "http://localhost:3456/callback2",
			"id_token_ttl":     "30",
			"access_token_ttl": "1m",
		},
		Storage: storage,
	})
	expectSuccess(t, resp, err)

	// Read "test-client" again and validate
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)
	expected = map[string]interface{}{
		"redirect_uris":    []string{"http://localhost:3456/callback2"},
		"assignments":      []string{"my-assignment"},
		"key":              "test-key",
		"id_token_ttl":     int64(30),
		"access_token_ttl": int64(60),
		"client_id":        resp.Data["client_id"],
		"client_secret":    resp.Data["client_secret"],
		"client_type":      confidential.String(),
	}
	if diff := deep.Equal(expected, resp.Data); diff != nil {
		t.Fatal(diff)
	}
}

// TestOIDC_Path_OIDC_ProviderClient_List tests the List operation for clients
func TestOIDC_Path_OIDC_ProviderClient_List(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	storage := c.identityStore.view

	// Prepare two clients, test-client1 and test-client2
	c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client1",
		Operation: logical.CreateOperation,
		Storage:   storage,
		Data: map[string]interface{}{
			"id_token_ttl": "1m",
		},
	})

	c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client2",
		Operation: logical.CreateOperation,
		Storage:   storage,
		Data: map[string]interface{}{
			"id_token_ttl": "1m",
		},
	})

	// list clients
	respListClients, listErr := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client",
		Operation: logical.ListOperation,
		Storage:   storage,
	})
	expectSuccess(t, respListClients, listErr)

	// validate list response
	expectedStrings := map[string]interface{}{"test-client1": true, "test-client2": true}
	expectStrings(t, respListClients.Data["keys"].([]string), expectedStrings)

	// delete test-client2
	c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client2",
		Operation: logical.DeleteOperation,
		Storage:   storage,
	})

	// list clients again and validate response
	respListClientAfterDelete, listErrAfterDelete := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client",
		Operation: logical.ListOperation,
		Storage:   storage,
	})
	expectSuccess(t, respListClientAfterDelete, listErrAfterDelete)

	// validate list response
	delete(expectedStrings, "test-client2")
	expectStrings(t, respListClientAfterDelete.Data["keys"].([]string), expectedStrings)
}

func TestOIDC_Path_OIDC_Client_List_KeyInfo(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)

	// Create clients with different parameters
	clients := map[string]interface{}{
		"c1": map[string]interface{}{
			"id_token_ttl":     "5m",
			"access_token_ttl": "10m",
			"assignments":      []string{},
			"redirect_uris":    []string{"http://127.0.0.1:8250"},
			"client_type":      "confidential",
			"key":              "default",
		},
		"c2": map[string]interface{}{
			"id_token_ttl":     "24h",
			"access_token_ttl": "5m",
			"assignments":      []string{allowAllAssignmentName},
			"redirect_uris":    []string{"https://localhost:9702/auth/oidc-callback"},
			"client_type":      "public",
			"key":              "default",
		},
	}
	for name, client := range clients {
		input := client.(map[string]interface{})
		resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
			Path:      "oidc/client/" + name,
			Operation: logical.CreateOperation,
			Storage:   c.identityStore.view,
			Data:      input,
		})
		expectSuccess(t, resp, err)
	}

	// List clients
	req := &logical.Request{
		Path:      "oidc/client",
		Operation: logical.ListOperation,
		Storage:   c.identityStore.view,
		Data:      make(map[string]interface{}),
	}
	resp, err := c.identityStore.HandleRequest(ctx, req)
	expectSuccess(t, resp, err)
	require.NotNil(t, resp.Data["key_info"])
	require.NotNil(t, resp.Data["keys"])
	keys := resp.Data["keys"].([]string)
	keyInfo := resp.Data["key_info"].(map[string]interface{})
	require.Equal(t, len(keys), len(keyInfo))

	// Assert the clients returned have additional key info
	for name, details := range keyInfo {
		actual, _ := details.(map[string]interface{})
		require.NotNil(t, clients[name])
		expected := clients[name].(map[string]interface{})
		require.Contains(t, keys, name)

		idTokenTTL, _ := parseutil.ParseDurationSecond(expected["id_token_ttl"].(string))
		accessTokenTTL, _ := parseutil.ParseDurationSecond(expected["access_token_ttl"].(string))
		require.EqualValues(t, idTokenTTL.Seconds(), actual["id_token_ttl"])
		require.EqualValues(t, accessTokenTTL.Seconds(), actual["access_token_ttl"])
		require.Equal(t, expected["redirect_uris"], actual["redirect_uris"])
		require.Equal(t, expected["assignments"], actual["assignments"])
		require.Equal(t, expected["key"], actual["key"])
		require.Equal(t, expected["client_type"], actual["client_type"])
		require.NotEmpty(t, actual["client_id"])
		require.Empty(t, actual["client_secret"])
	}
}

// TestOIDC_pathOIDCClientExistenceCheck tests pathOIDCClientExistenceCheck
func TestOIDC_pathOIDCClientExistenceCheck(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	clientName := "test"

	// Expect nil with empty storage
	exists, err := c.identityStore.pathOIDCClientExistenceCheck(
		ctx,
		&logical.Request{
			Storage: storage,
		},
		&framework.FieldData{
			Raw: map[string]interface{}{"name": clientName},
			Schema: map[string]*framework.FieldSchema{
				"name": {
					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 client
	client := &client{}
	entry, _ := logical.StorageEntryJSON(clientPath+clientName, client)
	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.pathOIDCClientExistenceCheck(
		ctx,
		&logical.Request{
			Storage: storage,
		},
		&framework.FieldData{
			Raw: map[string]interface{}{"name": clientName},
			Schema: map[string]*framework.FieldSchema{
				"name": {
					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_OIDC_ProviderScope_ReservedName tests that the reserved name
// "openid" cannot be used when creating a scope
func TestOIDC_Path_OIDC_ProviderScope_ReservedName(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	// Create a test scope "test-scope" -- should succeed
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/openid",
		Operation: logical.CreateOperation,
		Storage:   storage,
	})
	expectError(t, resp, err)
	// validate error message
	expectedStrings := map[string]interface{}{
		"the \"openid\" scope name is reserved": true,
	}
	expectStrings(t, []string{resp.Data["error"].(string)}, expectedStrings)
}

// TestOIDC_Path_OIDC_ProviderScope_TemplateValidation tests that the template
// validation does not allow restricted claims
func TestOIDC_Path_OIDC_ProviderScope_TemplateValidation(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	testCases := []struct {
		templ         string
		restrictedKey string
	}{
		{
			templ:         `{"aud": "client-12345", "other": "test"}`,
			restrictedKey: "aud",
		},
		{
			templ:         `{"exp": 1311280970, "other": "test"}`,
			restrictedKey: "exp",
		},
		{
			templ:         `{"iat": 1311280970, "other": "test"}`,
			restrictedKey: "iat",
		},
		{
			templ:         `{"iss": "https://openid.c2id.com", "other": "test"}`,
			restrictedKey: "iss",
		},
		{
			templ:         `{"namespace": "n-0S6_WzA2Mj", "other": "test"}`,
			restrictedKey: "namespace",
		},
		{
			templ:         `{"sub": "alice", "other": "test"}`,
			restrictedKey: "sub",
		},
		{
			templ:         `{"auth_time": 123456, "other": "test"}`,
			restrictedKey: "auth_time",
		},
		{
			templ:         `{"at_hash": "abcdefg", "other": "test"}`,
			restrictedKey: "at_hash",
		},
		{
			templ:         `{"c_hash": "hijklmn", "other": "test"}`,
			restrictedKey: "c_hash",
		},
	}
	for _, tc := range testCases {
		encodedTempl := base64.StdEncoding.EncodeToString([]byte(tc.templ))
		// Create a test scope "test-scope" -- should fail
		resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
			Path:      "oidc/scope/test-scope",
			Operation: logical.CreateOperation,
			Storage:   storage,
			Data: map[string]interface{}{
				"template":    encodedTempl,
				"description": "my-description",
			},
		})
		expectError(t, resp, err)
		errString := fmt.Sprintf(
			"top level key %q not allowed. Restricted keys: iat, aud, exp, iss, sub, namespace, nonce, auth_time, at_hash, c_hash",
			tc.restrictedKey,
		)
		// validate error message
		expectedStrings := map[string]interface{}{
			errString: true,
		}
		expectStrings(t, []string{resp.Data["error"].(string)}, expectedStrings)
	}
}

// TestOIDC_Path_OIDC_ProviderScope tests CRUD operations for scopes
func TestOIDC_Path_OIDC_ProviderScope(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	// Create a test scope "test-scope" -- should succeed
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/test-scope",
		Operation: logical.CreateOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)

	// Read "test-scope" and validate
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/test-scope",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)
	expected := map[string]interface{}{
		"template":    "",
		"description": "",
	}
	if diff := deep.Equal(expected, resp.Data); diff != nil {
		t.Fatal(diff)
	}

	templ := `{ "groups": {{identity.entity.groups.names}} }`
	encodedTempl := base64.StdEncoding.EncodeToString([]byte(templ))
	// Update "test-scope" -- should succeed
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/test-scope",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"template":    encodedTempl,
			"description": "my-description",
		},
		Storage: storage,
	})
	expectSuccess(t, resp, err)

	// Read "test-scope" again and validate
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/test-scope",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)
	expected = map[string]interface{}{
		"template":    templ,
		"description": "my-description",
	}
	if diff := deep.Equal(expected, resp.Data); diff != nil {
		t.Fatal(diff)
	}

	// Delete test-scope -- should succeed
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/test-scope",
		Operation: logical.DeleteOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)

	// Read "test-scope" again and validate
	resp, _ = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/test-scope",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	if resp != nil {
		t.Fatalf("expected nil but got resp: %#v", resp)
	}
}

// TestOIDC_Path_OIDC_ProviderScope_Update tests Update operations for scopes
func TestOIDC_Path_OIDC_ProviderScope_Update(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	templ := `{ "groups": {{identity.entity.groups.names}} }`
	encodedTempl := base64.StdEncoding.EncodeToString([]byte(templ))
	// Create a test scope "test-scope" -- should succeed
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/test-scope",
		Operation: logical.CreateOperation,
		Storage:   storage,
		Data: map[string]interface{}{
			"template":    encodedTempl,
			"description": "my-description",
		},
	})
	expectSuccess(t, resp, err)

	// Read "test-scope" and validate
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/test-scope",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)
	expected := map[string]interface{}{
		"template":    templ,
		"description": "my-description",
	}
	if diff := deep.Equal(expected, resp.Data); diff != nil {
		t.Fatal(diff)
	}

	// Update "test-scope" -- should succeed
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/test-scope",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"template":    encodedTempl,
			"description": "my-description-2",
		},
		Storage: storage,
	})
	expectSuccess(t, resp, err)

	// Read "test-scope" again and validate
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/test-scope",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)
	expected = map[string]interface{}{
		"template":    "{ \"groups\": {{identity.entity.groups.names}} }",
		"description": "my-description-2",
	}
	if diff := deep.Equal(expected, resp.Data); diff != nil {
		t.Fatal(diff)
	}
}

// TestOIDC_Path_OIDC_ProviderScope_List tests the List operation for scopes
func TestOIDC_Path_OIDC_ProviderScope_List(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	// Prepare two scopes, test-scope1 and test-scope2
	c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/test-scope1",
		Operation: logical.CreateOperation,
		Storage:   storage,
	})

	c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/test-scope2",
		Operation: logical.CreateOperation,
		Storage:   storage,
	})

	// list scopes
	respListScopes, listErr := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope",
		Operation: logical.ListOperation,
		Storage:   storage,
	})
	expectSuccess(t, respListScopes, listErr)

	// validate list response
	expectedStrings := map[string]interface{}{"test-scope1": true, "test-scope2": true}
	expectStrings(t, respListScopes.Data["keys"].([]string), expectedStrings)

	// delete test-scope2
	c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/test-scope2",
		Operation: logical.DeleteOperation,
		Storage:   storage,
	})

	// list scopes again and validate response
	respListScopeAfterDelete, listErrAfterDelete := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope",
		Operation: logical.ListOperation,
		Storage:   storage,
	})
	expectSuccess(t, respListScopeAfterDelete, listErrAfterDelete)

	// validate list response
	delete(expectedStrings, "test-scope2")
	expectStrings(t, respListScopeAfterDelete.Data["keys"].([]string), expectedStrings)
}

// TestOIDC_pathOIDCScopeExistenceCheck tests pathOIDCScopeExistenceCheck
func TestOIDC_pathOIDCScopeExistenceCheck(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	scopeName := "test"

	// Expect nil with empty storage
	exists, err := c.identityStore.pathOIDCScopeExistenceCheck(
		ctx,
		&logical.Request{
			Storage: storage,
		},
		&framework.FieldData{
			Raw: map[string]interface{}{"name": scopeName},
			Schema: map[string]*framework.FieldSchema{
				"name": {
					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 scope
	scope := &scope{}
	entry, _ := logical.StorageEntryJSON(scopePath+scopeName, scope)
	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.pathOIDCScopeExistenceCheck(
		ctx,
		&logical.Request{
			Storage: storage,
		},
		&framework.FieldData{
			Raw: map[string]interface{}{"name": scopeName},
			Schema: map[string]*framework.FieldSchema{
				"name": {
					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_OIDC_ProviderScope_DeleteWithExistingProvider tests that a
// Scope cannot be deleted when it is referenced by a provider
func TestOIDC_Path_OIDC_ProviderScope_DeleteWithExistingProvider(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	// Create a test scope "test-scope" -- should succeed
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/test-scope",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"template":    `{"groups": "{{identity.entity.groups.names}}"}`,
			"description": "my-description",
		},
		Storage: storage,
	})
	expectSuccess(t, resp, err)

	// Create a test provider "test-provider"
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"scopes_supported": []string{"test-scope"},
		},
		Storage: storage,
	})
	expectSuccess(t, resp, err)

	// Delete test-scope -- should fail
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/test-scope",
		Operation: logical.DeleteOperation,
		Storage:   storage,
	})
	expectError(t, resp, err)
	// validate error message
	expectedStrings := map[string]interface{}{
		"unable to delete scope \"test-scope\" because it is currently referenced by these providers: test-provider": true,
	}
	expectStrings(t, []string{resp.Data["error"].(string)}, expectedStrings)

	// Read "test-scope" again and validate
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/test-scope",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)
}

// TestOIDC_Path_OIDC_ProviderAssignment tests CRUD operations for assignments
func TestOIDC_Path_OIDC_ProviderAssignment(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	// Create a test assignment "test-assignment" -- should succeed
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/assignment/test-assignment",
		Operation: logical.CreateOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)

	// Read "test-assignment" and validate
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/assignment/test-assignment",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)
	expected := map[string]interface{}{
		"group_ids":  []string{},
		"entity_ids": []string{},
	}
	if diff := deep.Equal(expected, resp.Data); diff != nil {
		t.Fatal(diff)
	}

	// Update "test-assignment" -- should succeed
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/assignment/test-assignment",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"group_ids":  "my-group",
			"entity_ids": "my-entity",
		},
		Storage: storage,
	})
	expectSuccess(t, resp, err)

	// Read "test-assignment" again and validate
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/assignment/test-assignment",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)
	expected = map[string]interface{}{
		"group_ids":  []string{"my-group"},
		"entity_ids": []string{"my-entity"},
	}
	if diff := deep.Equal(expected, resp.Data); diff != nil {
		t.Fatal(diff)
	}

	// Delete test-assignment -- should succeed
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/assignment/test-assignment",
		Operation: logical.DeleteOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)

	// Read "test-assignment" again and validate
	resp, _ = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/assignment/test-assignment",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	if resp != nil {
		t.Fatalf("expected nil but got resp: %#v", resp)
	}
}

// TestOIDC_Path_OIDC_ProviderAssignment_DeleteWithExistingClient tests that an
// assignment cannot be deleted when it is referenced by a client
func TestOIDC_Path_OIDC_ProviderAssignment_DeleteWithExistingClient(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	// Create a test assignment "test-assignment" -- should succeed
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/assignment/test-assignment",
		Operation: logical.CreateOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)

	// Create a test key "test-key"
	c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/key/test-key",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"verification_ttl": "2m",
			"rotation_period":  "2m",
		},
		Storage: storage,
	})

	// Create a test client "test-client" -- should succeed
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/client/test-client",
		Operation: logical.CreateOperation,
		Storage:   storage,
		Data: map[string]interface{}{
			"key":          "test-key",
			"assignments":  []string{"test-assignment"},
			"id_token_ttl": "1m",
		},
	})
	expectSuccess(t, resp, err)

	// Delete test-assignment -- should fail
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/assignment/test-assignment",
		Operation: logical.DeleteOperation,
		Storage:   storage,
	})
	expectError(t, resp, err)
	// validate error message
	expectedStrings := map[string]interface{}{
		"unable to delete assignment \"test-assignment\" because it is currently referenced by these clients: test-client": true,
	}
	expectStrings(t, []string{resp.Data["error"].(string)}, expectedStrings)

	// Read "test-assignment" again and validate
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/assignment/test-assignment",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)
	expected := map[string]interface{}{
		"group_ids":  []string{},
		"entity_ids": []string{},
	}
	if diff := deep.Equal(expected, resp.Data); diff != nil {
		t.Fatal(diff)
	}
}

// TestOIDC_Path_OIDC_ProviderAssignment_Update tests Update operations for assignments
func TestOIDC_Path_OIDC_ProviderAssignment_Update(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	// Create a test assignment "test-assignment" -- should succeed
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/assignment/test-assignment",
		Operation: logical.CreateOperation,
		Storage:   storage,
		Data: map[string]interface{}{
			"group_ids":  "my-group",
			"entity_ids": "my-entity",
		},
	})
	expectSuccess(t, resp, err)

	// Read "test-assignment" and validate
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/assignment/test-assignment",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)
	expected := map[string]interface{}{
		"group_ids":  []string{"my-group"},
		"entity_ids": []string{"my-entity"},
	}
	if diff := deep.Equal(expected, resp.Data); diff != nil {
		t.Fatal(diff)
	}

	// Update "test-assignment" -- should succeed
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/assignment/test-assignment",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"group_ids": "my-group2",
		},
		Storage: storage,
	})
	expectSuccess(t, resp, err)

	// Read "test-assignment" again and validate
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/assignment/test-assignment",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)
	expected = map[string]interface{}{
		"group_ids":  []string{"my-group2"},
		"entity_ids": []string{"my-entity"},
	}
	if diff := deep.Equal(expected, resp.Data); diff != nil {
		t.Fatal(diff)
	}
}

// TestOIDC_Path_OIDC_ProviderAssignment_List tests the List operation for assignments
func TestOIDC_Path_OIDC_ProviderAssignment_List(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	// Prepare two assignments, test-assignment1 and test-assignment2
	c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/assignment/test-assignment1",
		Operation: logical.CreateOperation,
		Storage:   storage,
	})

	c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/assignment/test-assignment2",
		Operation: logical.CreateOperation,
		Storage:   storage,
	})

	// list assignments
	respListAssignments, listErr := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/assignment",
		Operation: logical.ListOperation,
		Storage:   storage,
	})
	expectSuccess(t, respListAssignments, listErr)

	// validate list response
	expectedStrings := map[string]interface{}{"test-assignment1": true, "test-assignment2": true}
	expectStrings(t, respListAssignments.Data["keys"].([]string), expectedStrings)

	// delete test-assignment2
	c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/assignment/test-assignment2",
		Operation: logical.DeleteOperation,
		Storage:   storage,
	})

	// list assignments again and validate response
	respListAssignmentAfterDelete, listErrAfterDelete := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/assignment",
		Operation: logical.ListOperation,
		Storage:   storage,
	})
	expectSuccess(t, respListAssignmentAfterDelete, listErrAfterDelete)

	// validate list response
	delete(expectedStrings, "test-assignment2")
	expectStrings(t, respListAssignmentAfterDelete.Data["keys"].([]string), expectedStrings)
}

// TestOIDC_pathOIDCAssignmentExistenceCheck tests pathOIDCAssignmentExistenceCheck
func TestOIDC_pathOIDCAssignmentExistenceCheck(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	assignmentName := "test"

	// Expect nil with empty storage
	exists, err := c.identityStore.pathOIDCAssignmentExistenceCheck(
		ctx,
		&logical.Request{
			Storage: storage,
		},
		&framework.FieldData{
			Raw: map[string]interface{}{"name": assignmentName},
			Schema: map[string]*framework.FieldSchema{
				"name": {
					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 assignment
	assignment := &assignment{}
	entry, _ := logical.StorageEntryJSON(assignmentPath+assignmentName, assignment)
	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.pathOIDCAssignmentExistenceCheck(
		ctx,
		&logical.Request{
			Storage: storage,
		},
		&framework.FieldData{
			Raw: map[string]interface{}{"name": assignmentName},
			Schema: map[string]*framework.FieldSchema{
				"name": {
					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_OIDCProvider tests CRUD operations for providers
func TestOIDC_Path_OIDCProvider(t *testing.T) {
	redirectAddr := "http://localhost:8200"
	conf := &CoreConfig{
		RedirectAddr: redirectAddr,
	}
	c, _, _ := TestCoreUnsealedWithConfig(t, conf)
	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	// Create a test provider "test-provider" with non-existing scope
	// Should fail
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"scopes_supported": []string{"test-scope"},
		},
		Storage: storage,
	})
	expectError(t, resp, err)
	// validate error message
	expectedStrings := map[string]interface{}{
		"scope \"test-scope\" does not exist": true,
	}
	expectStrings(t, []string{resp.Data["error"].(string)}, expectedStrings)

	// Create a test provider "test-provider" with no scopes -- should succeed
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider",
		Operation: logical.CreateOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)

	// Read "test-provider" and validate
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)
	expected := map[string]interface{}{
		"issuer":             redirectAddr + "/v1/identity/oidc/provider/test-provider",
		"allowed_client_ids": []string{},
		"scopes_supported":   []string{},
	}
	if diff := deep.Equal(expected, resp.Data); diff != nil {
		t.Fatal(diff)
	}

	// Create a test scope "test-scope" -- should succeed
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/test-scope",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"template":    `{"groups": {{identity.entity.groups.names}} }`,
			"description": "my-description",
		},
		Storage: storage,
	})
	expectSuccess(t, resp, err)

	// Update "test-provider" -- should succeed
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"allowed_client_ids": []string{"test-client-id"},
			"scopes_supported":   []string{"test-scope"},
		},
		Storage: storage,
	})
	expectSuccess(t, resp, err)

	// Read "test-provider" again and validate
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)
	expected = map[string]interface{}{
		"issuer":             redirectAddr + "/v1/identity/oidc/provider/test-provider",
		"allowed_client_ids": []string{"test-client-id"},
		"scopes_supported":   []string{"test-scope"},
	}
	if diff := deep.Equal(expected, resp.Data); diff != nil {
		t.Fatal(diff)
	}

	// Update "test-provider" -- should fail issuer validation
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"issuer": "test-issuer",
		},
		Storage: storage,
	})
	expectError(t, resp, err)
	// validate error message
	expectedStrings = map[string]interface{}{
		"invalid issuer, which must include only a scheme, host, and optional port (e.g. https://example.com:8200)": true,
	}
	expectStrings(t, []string{resp.Data["error"].(string)}, expectedStrings)

	// Update "test-provider" -- should succeed
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"issuer": "https://example.com:8200",
		},
		Storage: storage,
	})
	expectSuccess(t, resp, err)

	// Read "test-provider" again and validate
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)
	expected = map[string]interface{}{
		"issuer":             "https://example.com:8200/v1/identity/oidc/provider/test-provider",
		"allowed_client_ids": []string{"test-client-id"},
		"scopes_supported":   []string{"test-scope"},
	}
	if diff := deep.Equal(expected, resp.Data); diff != nil {
		t.Fatal(diff)
	}

	// Delete test-provider -- should succeed
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider",
		Operation: logical.DeleteOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)

	// Read "test-provider" again and validate
	resp, _ = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	if resp != nil {
		t.Fatalf("expected nil but got resp: %#v", resp)
	}
}

// TestOIDC_Path_OIDCProvider_DuplicateTempalteKeys tests that no two
// scopes have the same top-level keys when creating a provider
func TestOIDC_Path_OIDCProvider_DuplicateTemplateKeys(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	// Create a test scope "test-scope1" -- should succeed
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/test-scope1",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"template":    `{"groups": {{identity.entity.groups.names}} }`,
			"description": "desc1",
		},
		Storage: storage,
	})
	expectSuccess(t, resp, err)

	// Create another test scope "test-scope2" -- should succeed
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/test-scope2",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"template":    `{"groups": {{identity.entity.groups.names}} }`,
			"description": "desc2",
		},
		Storage: storage,
	})
	expectSuccess(t, resp, err)

	// Create a test provider "test-provider" with scopes that have same top-level keys
	// Should fail
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"scopes_supported": []string{"test-scope1", "test-scope2"},
		},
		Storage: storage,
	})
	expectSuccess(t, resp, err)
	if resp.Warnings[0] != "Found scope templates with conflicting top-level keys: conflict \"groups\" in scopes \"test-scope2\", \"test-scope1\". This may result in an error if the scopes are requested in an OIDC Authentication Request." {
		t.Fatalf("expected a warning for conflicting keys, got %s", resp.Warnings[0])
	}

	// // Update "test-scope1" -- should succeed
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/test-scope1",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"template": `{"roles": {{identity.entity.groups.names}} }`,
		},
		Storage: storage,
	})
	expectSuccess(t, resp, err)

	// Create a test provider "test-provider" with updated scopes
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"scopes_supported": []string{"test-scope1", "test-scope2"},
		},
		Storage: storage,
	})
	expectSuccess(t, resp, err)
}

// TestOIDC_Path_OIDCProvider_DeDuplication tests that a
// provider doesn't have duplicate scopes or client IDs
func TestOIDC_Path_OIDCProvider_Deduplication(t *testing.T) {
	redirectAddr := "http://localhost:8200"
	conf := &CoreConfig{
		RedirectAddr: redirectAddr,
	}
	c, _, _ := TestCoreUnsealedWithConfig(t, conf)

	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	// Create a test scope "test-scope1" -- should succeed
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/test-scope1",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"template":    `{"groups": {{identity.entity.groups.names}} }`,
			"description": "desc1",
		},
		Storage: storage,
	})
	expectSuccess(t, resp, err)

	// Create a test provider "test-provider" with duplicates
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"scopes_supported":   []string{"test-scope1", "test-scope1"},
			"allowed_client_ids": []string{"test-id1", "test-id2", "test-id1"},
		},
		Storage: storage,
	})
	expectSuccess(t, resp, err)

	// Read "test-provider" again and validate
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)
	expected := map[string]interface{}{
		"issuer":             redirectAddr + "/v1/identity/oidc/provider/test-provider",
		"allowed_client_ids": []string{"test-id1", "test-id2"},
		"scopes_supported":   []string{"test-scope1"},
	}
	if diff := deep.Equal(expected, resp.Data); diff != nil {
		t.Fatal(diff)
	}
}

// TestOIDC_Path_OIDCProvider_Update tests Update operations for providers
func TestOIDC_Path_OIDCProvider_Update(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	// Create a test provider "test-provider" -- should succeed
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider",
		Operation: logical.CreateOperation,
		Storage:   storage,
		Data: map[string]interface{}{
			"issuer":             "https://example.com:8200",
			"allowed_client_ids": []string{"test-client-id"},
		},
	})
	expectSuccess(t, resp, err)

	// Read "test-provider" and validate
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)
	expected := map[string]interface{}{
		"issuer":             "https://example.com:8200/v1/identity/oidc/provider/test-provider",
		"allowed_client_ids": []string{"test-client-id"},
		"scopes_supported":   []string{},
	}
	if diff := deep.Equal(expected, resp.Data); diff != nil {
		t.Fatal(diff)
	}

	// Update "test-provider" -- should succeed
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider",
		Operation: logical.UpdateOperation,
		Data: map[string]interface{}{
			"issuer": "https://changedurl.com",
		},
		Storage: storage,
	})
	expectSuccess(t, resp, err)

	// Read "test-provider" again and validate
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)
	expected = map[string]interface{}{
		"issuer":             "https://changedurl.com/v1/identity/oidc/provider/test-provider",
		"allowed_client_ids": []string{"test-client-id"},
		"scopes_supported":   []string{},
	}
	if diff := deep.Equal(expected, resp.Data); diff != nil {
		t.Fatal(diff)
	}
}

// TestOIDC_Path_OIDC_Provider_List tests the List operation for providers
func TestOIDC_Path_OIDC_Provider_List(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	// Use the identity store's storage view so that the default provider will
	// show up in the test
	storage := c.identityStore.view

	// Prepare two providers, test-provider1 and test-provider2
	c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider1",
		Operation: logical.CreateOperation,
		Storage:   storage,
	})

	c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider2",
		Operation: logical.CreateOperation,
		Storage:   storage,
	})

	// list providers
	respListProviders, listErr := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider",
		Operation: logical.ListOperation,
		Storage:   storage,
	})
	expectSuccess(t, respListProviders, listErr)

	// validate list response
	expectedStrings := map[string]interface{}{"default": true, "test-provider1": true, "test-provider2": true}
	expectStrings(t, respListProviders.Data["keys"].([]string), expectedStrings)

	// delete test-provider2
	c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider2",
		Operation: logical.DeleteOperation,
		Storage:   storage,
	})

	// list providers again and validate response
	respListProvidersAfterDelete, listErrAfterDelete := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider",
		Operation: logical.ListOperation,
		Storage:   storage,
	})
	expectSuccess(t, respListProvidersAfterDelete, listErrAfterDelete)

	// validate list response
	delete(expectedStrings, "test-provider2")
	expectStrings(t, respListProvidersAfterDelete.Data["keys"].([]string), expectedStrings)
}

func TestOIDC_Path_OIDC_Provider_List_KeyInfo(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)

	// Create a custom scope
	template := `{
		"groups": {{identity.entity.groups.names}}
	}`
	resp, err := c.identityStore.HandleRequest(ctx, testScopeReq(c.identityStore.view,
		"groups", template))
	expectSuccess(t, resp, err)

	// Create providers with different parameters
	providers := map[string]interface{}{
		"default": map[string]interface{}{
			"allowed_client_ids": []string{"*"},
			"scopes_supported":   []string{},
			"issuer":             "http://127.0.0.1:8200",
		},
		"p0": map[string]interface{}{
			"allowed_client_ids": []string{"abc", "def"},
			"scopes_supported":   []string{},
			"issuer":             "http://10.0.0.1:8200",
		},
		"p1": map[string]interface{}{
			"allowed_client_ids": []string{"xyz"},
			"scopes_supported":   []string{"groups"},
			"issuer":             "https://myvault.com:8200",
		},
	}
	for name, p := range providers {
		input := p.(map[string]interface{})
		resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
			Path:      "oidc/provider/" + name,
			Operation: logical.CreateOperation,
			Storage:   c.identityStore.view,
			Data:      input,
		})
		expectSuccess(t, resp, err)
	}

	// List providers
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider",
		Operation: logical.ListOperation,
		Storage:   c.identityStore.view,
		Data:      make(map[string]interface{}),
	})
	expectSuccess(t, resp, err)
	require.NotNil(t, resp.Data["key_info"])
	require.NotNil(t, resp.Data["keys"])
	keys := resp.Data["keys"].([]string)
	keyInfo := resp.Data["key_info"].(map[string]interface{})
	require.Equal(t, len(keys), len(keyInfo))

	// Assert the providers returned have additional key info
	for name, details := range keyInfo {
		actual, _ := details.(map[string]interface{})
		require.NotNil(t, providers[name])
		expected := providers[name].(map[string]interface{})
		require.Contains(t, keys, name)

		expectedIssuer := fmt.Sprintf("%s%s%s", expected["issuer"],
			"/v1/identity/oidc/provider/", name)
		require.Equal(t, expectedIssuer, actual["issuer"])
		require.Equal(t, expected["allowed_client_ids"], actual["allowed_client_ids"])
		require.Equal(t, expected["scopes_supported"], actual["scopes_supported"])
	}
}

func TestOIDC_Path_OIDC_Provider_List_Filter(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)

	// Create providers with different allowed_client_ids values
	providers := []struct {
		name             string
		allowedClientIDs []string
	}{
		{name: "p0", allowedClientIDs: []string{"*"}},
		{name: "p1", allowedClientIDs: []string{"abc"}},
		{name: "p2", allowedClientIDs: []string{"abc", "def"}},
		{name: "p3", allowedClientIDs: []string{"abc", "def", "ghi"}},
		{name: "p4", allowedClientIDs: []string{"ghi"}},
		{name: "p5", allowedClientIDs: []string{"jkl"}},
	}
	for _, p := range providers {
		resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
			Path:      "oidc/provider/" + p.name,
			Operation: logical.CreateOperation,
			Storage:   c.identityStore.view,
			Data: map[string]interface{}{
				"allowed_client_ids": p.allowedClientIDs,
			},
		})
		expectSuccess(t, resp, err)
	}

	tests := []struct {
		name              string
		clientIDFilter    string
		expectedProviders []string
	}{
		{
			name:              "list providers with client_id filter subset 1",
			clientIDFilter:    "abc",
			expectedProviders: []string{"default", "p0", "p1", "p2", "p3"},
		},
		{
			name:              "list providers with client_id filter subset 2",
			clientIDFilter:    "def",
			expectedProviders: []string{"default", "p0", "p2", "p3"},
		},
		{
			name:              "list providers with client_id filter subset 3",
			clientIDFilter:    "ghi",
			expectedProviders: []string{"default", "p0", "p3", "p4"},
		},
		{
			name:              "list providers with client_id filter subset 4",
			clientIDFilter:    "jkl",
			expectedProviders: []string{"default", "p0", "p5"},
		},
		{
			name:              "list providers with client_id filter only matching glob",
			clientIDFilter:    "globmatch_only",
			expectedProviders: []string{"default", "p0"},
		},
		{
			name:              "list providers with empty client_id filter returns all",
			clientIDFilter:    "",
			expectedProviders: []string{"default", "p0", "p1", "p2", "p3", "p4", "p5"},
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			// List providers with the allowed_client_id query parameter
			resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
				Path:      "oidc/provider",
				Operation: logical.ListOperation,
				Storage:   c.identityStore.view,
				Data: map[string]interface{}{
					"allowed_client_id": tc.clientIDFilter,
				},
			})
			expectSuccess(t, resp, err)

			// Assert the filtered set of providers is returned
			sort.Strings(tc.expectedProviders)
			sort.Strings(resp.Data["keys"].([]string))
			require.Equal(t, tc.expectedProviders, resp.Data["keys"].([]string))
		})
	}
}

// TestOIDC_Path_OpenIDProviderConfig tests read operations for the
// openid-configuration path
func TestOIDC_Path_OpenIDProviderConfig(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	// Create a test scope "test-scope-1" -- should succeed
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/test-scope-1",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"template":    `{"groups": "{{identity.entity.groups.names}}"}`,
			"description": "my-description",
		},
		Storage: storage,
	})
	expectSuccess(t, resp, err)

	// Create a test provider "test-provider"
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"scopes_supported": []string{"test-scope-1"},
		},
		Storage: storage,
	})
	expectSuccess(t, resp, err)

	// Expect defaults from .well-known/openid-configuration
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider/.well-known/openid-configuration",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)

	basePath := "/v1/identity/oidc/provider/test-provider"
	expected := &providerDiscovery{
		Issuer:                basePath,
		Keys:                  basePath + "/.well-known/keys",
		ResponseTypes:         []string{"code"},
		Scopes:                []string{"test-scope-1", "openid"},
		Claims:                []string{},
		Subjects:              []string{"public"},
		IDTokenAlgs:           supportedAlgs,
		AuthorizationEndpoint: "/ui/vault/identity/oidc/provider/test-provider/authorize",
		TokenEndpoint:         basePath + "/token",
		UserinfoEndpoint:      basePath + "/userinfo",
		GrantTypes:            []string{"authorization_code"},
		AuthMethods:           []string{"none", "client_secret_basic", "client_secret_post"},
		RequestParameter:      false,
		RequestURIParameter:   false,
	}
	discoveryResp := &providerDiscovery{}
	json.Unmarshal(resp.Data["http_raw_body"].([]byte), discoveryResp)
	if diff := deep.Equal(expected, discoveryResp); diff != nil {
		t.Fatal(diff)
	}

	// Create a test scope "test-scope-2" -- should succeed
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/scope/test-scope-2",
		Operation: logical.CreateOperation,
		Data: map[string]interface{}{
			"template":    `{"groups": "{{identity.entity.groups.names}}"}`,
			"description": "my-description",
		},
		Storage: storage,
	})
	expectSuccess(t, resp, err)

	// Update provider issuer config
	testIssuer := "https://example.com:1234"
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider",
		Operation: logical.UpdateOperation,
		Storage:   storage,
		Data: map[string]interface{}{
			"issuer":           testIssuer,
			"scopes_supported": []string{"test-scope-2"},
		},
	})
	expectSuccess(t, resp, err)

	// Expect updates from .well-known/openid-configuration
	resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider/.well-known/openid-configuration",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectSuccess(t, resp, err)
	// Validate
	basePath = testIssuer + basePath
	expected = &providerDiscovery{
		Issuer:                basePath,
		Keys:                  basePath + "/.well-known/keys",
		ResponseTypes:         []string{"code"},
		Scopes:                []string{"test-scope-2", "openid"},
		Claims:                []string{},
		Subjects:              []string{"public"},
		IDTokenAlgs:           supportedAlgs,
		AuthorizationEndpoint: testIssuer + "/ui/vault/identity/oidc/provider/test-provider/authorize",
		TokenEndpoint:         basePath + "/token",
		UserinfoEndpoint:      basePath + "/userinfo",
		GrantTypes:            []string{"authorization_code"},
		AuthMethods:           []string{"none", "client_secret_basic", "client_secret_post"},
		RequestParameter:      false,
		RequestURIParameter:   false,
	}
	discoveryResp = &providerDiscovery{}
	json.Unmarshal(resp.Data["http_raw_body"].([]byte), discoveryResp)
	if diff := deep.Equal(expected, discoveryResp); diff != nil {
		t.Fatal(diff)
	}
}

// TestOIDC_Path_OpenIDProviderConfig_ProviderDoesNotExist tests read
// operations for the openid-configuration path when the provider does not
// exist
func TestOIDC_Path_OpenIDProviderConfig_ProviderDoesNotExist(t *testing.T) {
	c, _, _ := TestCoreUnsealed(t)
	ctx := namespace.RootContext(nil)
	storage := &logical.InmemStorage{}

	// Expect defaults from .well-known/openid-configuration
	// test-provider does not exist
	resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
		Path:      "oidc/provider/test-provider/.well-known/openid-configuration",
		Operation: logical.ReadOperation,
		Storage:   storage,
	})
	expectedResp := &logical.Response{}
	if resp != expectedResp && err != nil {
		t.Fatalf("expected empty response but got success; error:\n%v\nresp: %#v", err, resp)
	}
}
