mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-01-08 15:21:55 +00:00
Allow dynamic clients to be used in downstream OIDC flows
This is only a first commit towards making this feature work. - Hook dynamic clients into fosite by returning them from the storage interface (after finding and validating them) - In the auth endpoint, prevent the use of the username and password headers for dynamic clients to force them to use the browser-based login flows for all the upstream types - Add happy path integration tests in supervisor_login_test.go - Add lots of comments (and some small refactors) in supervisor_login_test.go to make it much easier to understand - Add lots of unit tests for the auth endpoint regarding dynamic clients (more unit tests to be added for other endpoints in follow-up commits) - Enhance crud.go to make lifetime=0 mean never garbage collect, since we want client secret storage Secrets to last forever - Move the OIDCClient validation code to a package where it can be shared between the controller and the fosite storage interface - Make shared test helpers for tests that need to create OIDC client secret storage Secrets - Create a public const for "pinniped-cli" now that we are using that string in several places in the production code
This commit is contained in:
@@ -156,29 +156,90 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
return ldapIDP, secret
|
||||
}
|
||||
|
||||
// These tests attempt to exercise the entire login and refresh flow of the Supervisor for various cases.
|
||||
// They do not use the Pinniped CLI as the client, which allows them to exercise the Supervisor as an
|
||||
// OIDC provider in ways that the CLI might not use. Similar tests exist using the CLI in e2e_test.go.
|
||||
//
|
||||
// Each of these tests perform the following flow:
|
||||
// 1. Create a FederationDomain with TLS configured and wait for its JWKS endpoint to be available.
|
||||
// 2. Configure an IDP CR.
|
||||
// 3. Call the authorization endpoint and log in as a specific user.
|
||||
// Note that these tests do not use form_post response type (which is tested by e2e_test.go).
|
||||
// 4. Listen on a local callback server for the authorization redirect, and assert that it was success or failure.
|
||||
// 5. Call the token endpoint to exchange the authcode.
|
||||
// 6. Call the token endpoint to perform the RFC8693 token exchange for the cluster-scoped ID token.
|
||||
// 7. Potentially edit the refresh session data or IDP settings before the refresh.
|
||||
// 8. Call the token endpoint to perform a refresh, and expect it to succeed.
|
||||
// 9. Call the token endpoint again to perform another RFC8693 token exchange for the cluster-scoped ID token,
|
||||
// this time using the recently refreshed tokens when submitting the request.
|
||||
// 10. Potentially edit the refresh session data or IDP settings again, this time in such a way that the next
|
||||
// refresh should fail. If done, then perform one more refresh and expect failure.
|
||||
tests := []struct {
|
||||
name string
|
||||
maybeSkip func(t *testing.T)
|
||||
createTestUser func(t *testing.T) (string, string)
|
||||
deleteTestUser func(t *testing.T, username string)
|
||||
requestAuthorization func(t *testing.T, downstreamIssuer, downstreamAuthorizeURL, downstreamCallbackURL, username, password string, httpClient *http.Client)
|
||||
createIDP func(t *testing.T) string
|
||||
requestTokenExchangeAud string
|
||||
downstreamScopes []string
|
||||
wantLocalhostCallbackToNeverHappen bool
|
||||
wantDownstreamIDTokenSubjectToMatch string
|
||||
wantDownstreamIDTokenUsernameToMatch func(username string) string
|
||||
wantDownstreamIDTokenGroups []string
|
||||
wantErrorDescription string
|
||||
wantErrorType string
|
||||
wantTokenExchangeResponse func(t *testing.T, status int, body string)
|
||||
name string
|
||||
|
||||
// Either revoke the user's session on the upstream provider, or manipulate the user's session
|
||||
// This required function might choose to skip the test case, for example if the LDAP server is not
|
||||
// available for an LDAP test.
|
||||
maybeSkip func(t *testing.T)
|
||||
|
||||
// This required function should configure an IDP CR. It should also wait for it to be ready and schedule
|
||||
// its cleanup. Return the name of the IDP CR.
|
||||
createIDP func(t *testing.T) string
|
||||
|
||||
// Optionally create an OIDCClient CR for the test to use. Return the client ID and client secret for the
|
||||
// test to use. When not set, the test will default to using the "pinniped-cli" static client with no secret.
|
||||
// When a client secret is returned, it will be used for authcode exchange, refresh requests, and RFC8693
|
||||
// token exchanges for cluster-scoped tokens (client secrets are not needed in authorization requests).
|
||||
createOIDCClient func(t *testing.T, callbackURL string) (string, string)
|
||||
|
||||
// Optionally return the username and password for the test to use when logging in. This username/password
|
||||
// will be passed to requestAuthorization(), or empty strings will be passed to indicate that the defaults
|
||||
// should be used. If there is any cleanup required, then this function should also schedule that cleanup.
|
||||
testUser func(t *testing.T) (string, string)
|
||||
|
||||
// This required function should call the authorization endpoint using the given URL and also perform whatever
|
||||
// interactions are needed to log in as the user.
|
||||
requestAuthorization func(t *testing.T, downstreamIssuer, downstreamAuthorizeURL, downstreamCallbackURL, username, password string, httpClient *http.Client)
|
||||
|
||||
// This string will be used as the requested audience in the RFC8693 token exchange for
|
||||
// the cluster-scoped ID token. When it is not specified, a default string will be used.
|
||||
requestTokenExchangeAud string
|
||||
|
||||
// The scopes to request from the authorization endpoint. Defaults will be used when not specified.
|
||||
downstreamScopes []string
|
||||
|
||||
// When we want the localhost callback to have never happened, then the flow will stop there. The login was
|
||||
// unable to finish so there is nothing to assert about what should have happened with the callback, and there
|
||||
// won't be any error sent to the callback either. This would happen, for example, when the user fails to log
|
||||
// in at the LDAP/AD login page, because then they would be redirected back to that page again, instead of
|
||||
// getting a callback success/error redirect.
|
||||
wantLocalhostCallbackToNeverHappen bool
|
||||
|
||||
// The expected ID token subject claim value as a regexp, for the original ID token and the refreshed ID token.
|
||||
wantDownstreamIDTokenSubjectToMatch string
|
||||
// The expected ID token username claim value as a regexp, for the original ID token and the refreshed ID token.
|
||||
wantDownstreamIDTokenUsernameToMatch func(username string) string
|
||||
// The expected ID token groups claim value, for the original ID token and the refreshed ID token.
|
||||
wantDownstreamIDTokenGroups []string
|
||||
|
||||
// Want the authorization endpoint to redirect to the callback with this error type.
|
||||
// The rest of the flow will be skipped since the initial authorization failed.
|
||||
wantErrorType string
|
||||
// Want the authorization endpoint to redirect to the callback with this error description.
|
||||
// Should be used with wantErrorType.
|
||||
wantErrorDescription string
|
||||
|
||||
// Optionally make all required assertions about the response of the RFC8693 token exchange for
|
||||
// the cluster-scoped ID token, given the http response status and response body from the token endpoint.
|
||||
// When this is not specified then the appropriate default assertions for a successful exchange are made.
|
||||
// Even if this expects failures, the rest of the flow will continue.
|
||||
wantTokenExchangeResponse func(t *testing.T, status int, body string)
|
||||
|
||||
// Optionally edit the refresh session data between the initial login and the first refresh,
|
||||
// which is still expected to succeed after these edits.
|
||||
editRefreshSessionDataWithoutBreaking func(t *testing.T, sessionData *psession.PinnipedSession, idpName, username string) []string
|
||||
// Optionally either revoke the user's session on the upstream provider, or manipulate the user's session
|
||||
// data in such a way that it should cause the next upstream refresh attempt to fail.
|
||||
breakRefreshSessionData func(t *testing.T, sessionData *psession.PinnipedSession, idpName, username string)
|
||||
// Edit the refresh session data between the initial login and the refresh, which is expected to
|
||||
// succeed.
|
||||
editRefreshSessionDataWithoutBreaking func(t *testing.T, sessionData *psession.PinnipedSession, idpName, username string) []string
|
||||
}{
|
||||
{
|
||||
name: "oidc with default username and groups claim settings",
|
||||
@@ -389,7 +450,7 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
idp, _ := createLDAPIdentityProvider(t, nil)
|
||||
return idp.Name
|
||||
},
|
||||
createTestUser: func(t *testing.T) (string, string) {
|
||||
testUser: func(t *testing.T) (string, string) {
|
||||
// return the username and password of the existing user that we want to use for this test
|
||||
return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||
env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login
|
||||
@@ -414,7 +475,7 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
idp, _ := createLDAPIdentityProvider(t, nil)
|
||||
return idp.Name
|
||||
},
|
||||
createTestUser: func(t *testing.T) (string, string) {
|
||||
testUser: func(t *testing.T) (string, string) {
|
||||
// return the username and password of the existing user that we want to use for this test
|
||||
return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||
"this is the wrong password" // password to present to server during login
|
||||
@@ -429,7 +490,7 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
idp, _ := createLDAPIdentityProvider(t, nil)
|
||||
return idp.Name
|
||||
},
|
||||
createTestUser: func(t *testing.T) (string, string) {
|
||||
testUser: func(t *testing.T) (string, string) {
|
||||
// return the username and password of the existing user that we want to use for this test
|
||||
return "this is the wrong username", // username to present to server during login
|
||||
env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login
|
||||
@@ -444,7 +505,7 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
idp, _ := createLDAPIdentityProvider(t, nil)
|
||||
return idp.Name
|
||||
},
|
||||
createTestUser: func(t *testing.T) (string, string) {
|
||||
testUser: func(t *testing.T) (string, string) {
|
||||
// return the username and password of the existing user that we want to use for this test
|
||||
return env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||
env.SupervisorUpstreamLDAP.TestUserPassword // password to present to server during login
|
||||
@@ -964,12 +1025,9 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
idp, _ := createActiveDirectoryIdentityProvider(t, nil)
|
||||
return idp.Name
|
||||
},
|
||||
createTestUser: func(t *testing.T) (string, string) {
|
||||
testUser: func(t *testing.T) (string, string) {
|
||||
return testlib.CreateFreshADTestUser(t, env)
|
||||
},
|
||||
deleteTestUser: func(t *testing.T, username string) {
|
||||
testlib.DeleteTestADUser(t, env, username)
|
||||
},
|
||||
requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, testUserName, testUserPassword string, httpClient *http.Client) {
|
||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||
downstreamAuthorizeURL,
|
||||
@@ -997,12 +1055,9 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
idp, _ := createActiveDirectoryIdentityProvider(t, nil)
|
||||
return idp.Name
|
||||
},
|
||||
createTestUser: func(t *testing.T) (string, string) {
|
||||
testUser: func(t *testing.T) (string, string) {
|
||||
return testlib.CreateFreshADTestUser(t, env)
|
||||
},
|
||||
deleteTestUser: func(t *testing.T, username string) {
|
||||
testlib.DeleteTestADUser(t, env, username)
|
||||
},
|
||||
requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, testUserName, testUserPassword string, httpClient *http.Client) {
|
||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||
downstreamAuthorizeURL,
|
||||
@@ -1030,12 +1085,9 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
idp, _ := createActiveDirectoryIdentityProvider(t, nil)
|
||||
return idp.Name
|
||||
},
|
||||
createTestUser: func(t *testing.T) (string, string) {
|
||||
testUser: func(t *testing.T) (string, string) {
|
||||
return testlib.CreateFreshADTestUser(t, env)
|
||||
},
|
||||
deleteTestUser: func(t *testing.T, username string) {
|
||||
testlib.DeleteTestADUser(t, env, username)
|
||||
},
|
||||
requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, testUserName, testUserPassword string, httpClient *http.Client) {
|
||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||
downstreamAuthorizeURL,
|
||||
@@ -1226,7 +1278,62 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
body)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "oidc upstream with downstream dynamic client happy path",
|
||||
maybeSkip: skipNever,
|
||||
createIDP: func(t *testing.T) string {
|
||||
return testlib.CreateTestOIDCIdentityProvider(t, basicOIDCIdentityProviderSpec(), idpv1alpha1.PhaseReady).Name
|
||||
},
|
||||
createOIDCClient: func(t *testing.T, callbackURL string) (string, string) {
|
||||
return testlib.CreateOIDCClient(t, configv1alpha1.OIDCClientSpec{
|
||||
AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)},
|
||||
AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"},
|
||||
AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "groups"},
|
||||
}, configv1alpha1.PhaseReady)
|
||||
},
|
||||
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowOIDC,
|
||||
// the ID token Subject should include the upstream user ID after the upstream issuer name
|
||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
||||
// the ID token Username should include the upstream user ID after the upstream issuer name
|
||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string { return "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+" },
|
||||
},
|
||||
{
|
||||
name: "ldap upstream with downstream dynamic client happy path",
|
||||
maybeSkip: skipLDAPTests,
|
||||
createIDP: func(t *testing.T) string {
|
||||
idp, _ := createLDAPIdentityProvider(t, nil)
|
||||
return idp.Name
|
||||
},
|
||||
createOIDCClient: func(t *testing.T, callbackURL string) (string, string) {
|
||||
return testlib.CreateOIDCClient(t, configv1alpha1.OIDCClientSpec{
|
||||
AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(callbackURL)},
|
||||
AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"},
|
||||
AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "groups"},
|
||||
}, configv1alpha1.PhaseReady)
|
||||
},
|
||||
requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, _, _, _ string, httpClient *http.Client) {
|
||||
requestAuthorizationUsingCLIPasswordFlow(t,
|
||||
downstreamAuthorizeURL,
|
||||
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login
|
||||
env.SupervisorUpstreamLDAP.TestUserPassword, // password to present to server during login
|
||||
httpClient,
|
||||
false,
|
||||
)
|
||||
},
|
||||
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
|
||||
wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(
|
||||
"ldaps://"+env.SupervisorUpstreamLDAP.Host+
|
||||
"?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+
|
||||
"&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
|
||||
) + "$",
|
||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$"
|
||||
},
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
tt := test
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -1237,8 +1344,8 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
tt.requestAuthorization,
|
||||
tt.editRefreshSessionDataWithoutBreaking,
|
||||
tt.breakRefreshSessionData,
|
||||
tt.createTestUser,
|
||||
tt.deleteTestUser,
|
||||
tt.testUser,
|
||||
tt.createOIDCClient,
|
||||
tt.downstreamScopes,
|
||||
tt.requestTokenExchangeAud,
|
||||
tt.wantLocalhostCallbackToNeverHappen,
|
||||
@@ -1377,8 +1484,8 @@ func testSupervisorLogin(
|
||||
requestAuthorization func(t *testing.T, downstreamIssuer string, downstreamAuthorizeURL string, downstreamCallbackURL string, username string, password string, httpClient *http.Client),
|
||||
editRefreshSessionDataWithoutBreaking func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName, username string) []string,
|
||||
breakRefreshSessionData func(t *testing.T, pinnipedSession *psession.PinnipedSession, idpName, username string),
|
||||
createTestUser func(t *testing.T) (string, string),
|
||||
deleteTestUser func(t *testing.T, username string),
|
||||
testUser func(t *testing.T) (string, string),
|
||||
createOIDCClient func(t *testing.T, callbackURL string) (string, string),
|
||||
downstreamScopes []string,
|
||||
requestTokenExchangeAud string,
|
||||
wantLocalhostCallbackToNeverHappen bool,
|
||||
@@ -1475,12 +1582,20 @@ func testSupervisorLogin(
|
||||
// Create upstream IDP and wait for it to become ready.
|
||||
idpName := createIDP(t)
|
||||
|
||||
// Start a callback server on localhost.
|
||||
localCallbackServer := startLocalCallbackServer(t)
|
||||
|
||||
// Optionally create an OIDCClient. Default to using the hardcoded public client that the Supervisor supports.
|
||||
clientID, clientSecret := "pinniped-cli", "" //nolint:gosec // empty credential is not a hardcoded credential
|
||||
if createOIDCClient != nil {
|
||||
clientID, clientSecret = createOIDCClient(t, localCallbackServer.URL)
|
||||
}
|
||||
|
||||
// Optionally override which user to use for the test, or choose zero values to mean use the default for
|
||||
// the test's IDP.
|
||||
username, password := "", ""
|
||||
if createTestUser != nil {
|
||||
username, password = createTestUser(t)
|
||||
if deleteTestUser != nil {
|
||||
defer deleteTestUser(t, username)
|
||||
}
|
||||
if testUser != nil {
|
||||
username, password = testUser(t)
|
||||
}
|
||||
|
||||
// Perform OIDC discovery for our downstream.
|
||||
@@ -1491,23 +1606,27 @@ func testSupervisorLogin(
|
||||
requireEventually.NoError(err)
|
||||
}, 30*time.Second, 200*time.Millisecond)
|
||||
|
||||
// Start a callback server on localhost.
|
||||
localCallbackServer := startLocalCallbackServer(t)
|
||||
|
||||
if downstreamScopes == nil {
|
||||
downstreamScopes = []string{"openid", "pinniped:request-audience", "offline_access", "groups"}
|
||||
}
|
||||
|
||||
// Form the OAuth2 configuration corresponding to our CLI client.
|
||||
// Create the OAuth2 configuration.
|
||||
// Note that this is not using response_type=form_post, so the Supervisor will redirect to the callback endpoint
|
||||
// directly, without using the Javascript form_post HTML page to POST back to the callback endpoint. The e2e
|
||||
// tests which use the Pinniped CLI are testing the form_post part of the flow, so that is covered elsewhere.
|
||||
// When ClientSecret is set here, it will be used for all token endpoint requests, but not for the authorization
|
||||
// request, where it is not needed.
|
||||
endpoint := discovery.Endpoint()
|
||||
if clientSecret != "" {
|
||||
// We only support basic auth for dynamic clients, so use basic auth in these tests.
|
||||
endpoint.AuthStyle = oauth2.AuthStyleInHeader
|
||||
}
|
||||
downstreamOAuth2Config := oauth2.Config{
|
||||
// This is the hardcoded public client that the supervisor supports.
|
||||
ClientID: "pinniped-cli",
|
||||
Endpoint: discovery.Endpoint(),
|
||||
RedirectURL: localCallbackServer.URL,
|
||||
Scopes: downstreamScopes,
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
Endpoint: endpoint,
|
||||
RedirectURL: localCallbackServer.URL,
|
||||
Scopes: downstreamScopes,
|
||||
}
|
||||
|
||||
// Build a valid downstream authorize URL for the supervisor.
|
||||
@@ -1573,9 +1692,9 @@ func testSupervisorLogin(
|
||||
signatureOfLatestRefreshToken := getFositeDataSignature(t, latestRefreshToken)
|
||||
|
||||
// First use the latest downstream refresh token to look up the corresponding session in the Supervisor's storage.
|
||||
kubeClient := testlib.NewKubernetesClientset(t)
|
||||
supervisorSecretsClient := kubeClient.CoreV1().Secrets(env.SupervisorNamespace)
|
||||
oauthStore := oidc.NewKubeStorage(supervisorSecretsClient, oidc.DefaultOIDCTimeoutsConfiguration())
|
||||
supervisorSecretsClient := testlib.NewKubernetesClientset(t).CoreV1().Secrets(env.SupervisorNamespace)
|
||||
supervisorOIDCClientsClient := testlib.NewSupervisorClientset(t).ConfigV1alpha1().OIDCClients(env.SupervisorNamespace)
|
||||
oauthStore := oidc.NewKubeStorage(supervisorSecretsClient, supervisorOIDCClientsClient, oidc.DefaultOIDCTimeoutsConfiguration())
|
||||
storedRefreshSession, err := oauthStore.GetRefreshTokenSession(ctx, signatureOfLatestRefreshToken, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -1618,9 +1737,9 @@ func testSupervisorLogin(
|
||||
signatureOfLatestRefreshToken := getFositeDataSignature(t, latestRefreshToken)
|
||||
|
||||
// First use the latest downstream refresh token to look up the corresponding session in the Supervisor's storage.
|
||||
kubeClient := testlib.NewKubernetesClientset(t)
|
||||
supervisorSecretsClient := kubeClient.CoreV1().Secrets(env.SupervisorNamespace)
|
||||
oauthStore := oidc.NewKubeStorage(supervisorSecretsClient, oidc.DefaultOIDCTimeoutsConfiguration())
|
||||
supervisorSecretsClient := testlib.NewKubernetesClientset(t).CoreV1().Secrets(env.SupervisorNamespace)
|
||||
supervisorOIDCClientsClient := testlib.NewSupervisorClientset(t).ConfigV1alpha1().OIDCClients(env.SupervisorNamespace)
|
||||
oauthStore := oidc.NewKubeStorage(supervisorSecretsClient, supervisorOIDCClientsClient, oidc.DefaultOIDCTimeoutsConfiguration())
|
||||
storedRefreshSession, err := oauthStore.GetRefreshTokenSession(ctx, signatureOfLatestRefreshToken, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -1922,6 +2041,10 @@ func doTokenExchange(
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, config.Endpoint.TokenURL, reqBody)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("content-type", "application/x-www-form-urlencoded")
|
||||
if config.ClientSecret != "" {
|
||||
// We only support basic auth for dynamic clients, so use basic auth in these tests.
|
||||
req.SetBasicAuth(config.ClientID, config.ClientSecret)
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -528,16 +528,7 @@ func TestOIDCClientControllerValidations_Parallel(t *testing.T) {
|
||||
AllowedScopes: []supervisorconfigv1alpha1.Scope{"openid"},
|
||||
},
|
||||
},
|
||||
secret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"storage.pinniped.dev/type": "oidc-client-secret"},
|
||||
},
|
||||
Type: "storage.pinniped.dev/oidc-client-secret",
|
||||
Data: map[string][]byte{
|
||||
"pinniped-storage-data": []byte(`{"version":"1","hashes":[]}`),
|
||||
"pinniped-storage-version": []byte("1"),
|
||||
},
|
||||
},
|
||||
secret: testutil.OIDCClientSecretStorageSecretWithoutName(t, env.SupervisorNamespace, []string{}),
|
||||
wantPhase: "Error",
|
||||
wantConditions: []supervisorconfigv1alpha1.Condition{
|
||||
{
|
||||
@@ -572,16 +563,7 @@ func TestOIDCClientControllerValidations_Parallel(t *testing.T) {
|
||||
AllowedScopes: []supervisorconfigv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"},
|
||||
},
|
||||
},
|
||||
secret: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"storage.pinniped.dev/type": "oidc-client-secret"},
|
||||
},
|
||||
Type: "storage.pinniped.dev/oidc-client-secret",
|
||||
Data: map[string][]byte{
|
||||
"pinniped-storage-data": []byte(`{"version":"1","hashes":["$2y$15$Kh7cRj0ScSD5QelE3ZNSl.nF04JDv7zb3SgGN.tSfLIX.4kt3UX7m"]}`),
|
||||
"pinniped-storage-version": []byte("1"),
|
||||
},
|
||||
},
|
||||
secret: testutil.OIDCClientSecretStorageSecretWithoutName(t, env.SupervisorNamespace, []string{"$2y$15$Kh7cRj0ScSD5QelE3ZNSl.nF04JDv7zb3SgGN.tSfLIX.4kt3UX7m"}),
|
||||
wantPhase: "Ready",
|
||||
wantConditions: []supervisorconfigv1alpha1.Condition{
|
||||
{
|
||||
|
||||
@@ -186,9 +186,10 @@ func TestSupervisorWarnings_Browser(t *testing.T) {
|
||||
|
||||
// using the refresh token signature contained in the cache, get the refresh token session
|
||||
// out of kube secret storage.
|
||||
kubeClient := testlib.NewKubernetesClientset(t).CoreV1()
|
||||
supervisorSecretsClient := testlib.NewKubernetesClientset(t).CoreV1().Secrets(env.SupervisorNamespace)
|
||||
supervisorOIDCClientsClient := testlib.NewSupervisorClientset(t).ConfigV1alpha1().OIDCClients(env.SupervisorNamespace)
|
||||
oauthStore := oidc.NewKubeStorage(supervisorSecretsClient, supervisorOIDCClientsClient, oidc.DefaultOIDCTimeoutsConfiguration())
|
||||
refreshTokenSignature := strings.Split(token.RefreshToken.Token, ".")[1]
|
||||
oauthStore := oidc.NewKubeStorage(kubeClient.Secrets(env.SupervisorNamespace), oidc.DefaultOIDCTimeoutsConfiguration())
|
||||
storedRefreshSession, err := oauthStore.GetRefreshTokenSession(ctx, refreshTokenSignature, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -246,9 +247,6 @@ func TestSupervisorWarnings_Browser(t *testing.T) {
|
||||
testlib.SkipTestWhenActiveDirectoryIsUnavailable(t, env)
|
||||
|
||||
expectedUsername, password := testlib.CreateFreshADTestUser(t, env)
|
||||
t.Cleanup(func() {
|
||||
testlib.DeleteTestADUser(t, env, expectedUsername)
|
||||
})
|
||||
|
||||
sAMAccountName := expectedUsername + "@" + env.SupervisorUpstreamActiveDirectory.Domain
|
||||
setupClusterForEndToEndActiveDirectoryTest(t, sAMAccountName, env)
|
||||
@@ -308,9 +306,6 @@ func TestSupervisorWarnings_Browser(t *testing.T) {
|
||||
|
||||
// create an active directory group, and add our user to it.
|
||||
groupName := testlib.CreateFreshADTestGroup(t, env)
|
||||
t.Cleanup(func() {
|
||||
testlib.DeleteTestADUser(t, env, groupName)
|
||||
})
|
||||
testlib.AddTestUserToGroup(t, env, groupName, expectedUsername)
|
||||
|
||||
// remove the credential cache, which includes the cached cert, so it won't be reused and the refresh flow will be triggered.
|
||||
@@ -499,9 +494,10 @@ func TestSupervisorWarnings_Browser(t *testing.T) {
|
||||
|
||||
// using the refresh token signature contained in the cache, get the refresh token session
|
||||
// out of kube secret storage.
|
||||
kubeClient := testlib.NewKubernetesClientset(t).CoreV1()
|
||||
supervisorSecretsClient := testlib.NewKubernetesClientset(t).CoreV1().Secrets(env.SupervisorNamespace)
|
||||
supervisorOIDCClientsClient := testlib.NewSupervisorClientset(t).ConfigV1alpha1().OIDCClients(env.SupervisorNamespace)
|
||||
oauthStore := oidc.NewKubeStorage(supervisorSecretsClient, supervisorOIDCClientsClient, oidc.DefaultOIDCTimeoutsConfiguration())
|
||||
refreshTokenSignature := strings.Split(token.RefreshToken.Token, ".")[1]
|
||||
oauthStore := oidc.NewKubeStorage(kubeClient.Secrets(env.SupervisorNamespace), oidc.DefaultOIDCTimeoutsConfiguration())
|
||||
storedRefreshSession, err := oauthStore.GetRefreshTokenSession(ctx, refreshTokenSignature, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
||||
@@ -42,6 +42,11 @@ func CreateFreshADTestUser(t *testing.T, env *TestEnv) (string, string) {
|
||||
err = conn.Add(a)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Now that it has been created, schedule it for cleanup.
|
||||
t.Cleanup(func() {
|
||||
deleteTestADUser(t, env, testUserName)
|
||||
})
|
||||
|
||||
// modify password and enable account
|
||||
testUserPassword := createRandomASCIIString(t, 20)
|
||||
enc := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder()
|
||||
@@ -83,6 +88,11 @@ func CreateFreshADTestGroup(t *testing.T, env *TestEnv) string {
|
||||
err = conn.Add(a)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Now that it has been created, schedule it for cleanup.
|
||||
t.Cleanup(func() {
|
||||
deleteTestADUser(t, env, testGroupName)
|
||||
})
|
||||
|
||||
time.Sleep(20 * time.Second) // intrasite domain controller replication can take up to 15 seconds, so wait to ensure the change has propogated.
|
||||
return testGroupName
|
||||
}
|
||||
@@ -164,8 +174,8 @@ func ChangeADTestUserPassword(t *testing.T, env *TestEnv, testUserName string) {
|
||||
// don't bother to return the new password... we won't be using it, just checking that it's changed.
|
||||
}
|
||||
|
||||
// DeleteTestADUser deletes the test user created for this test.
|
||||
func DeleteTestADUser(t *testing.T, env *TestEnv, testUserName string) {
|
||||
// deleteTestADUser deletes the test user created for this test.
|
||||
func deleteTestADUser(t *testing.T, env *TestEnv, testUserName string) {
|
||||
t.Helper()
|
||||
conn := dialTLS(t, env)
|
||||
// bind
|
||||
|
||||
@@ -16,9 +16,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
authorizationv1 "k8s.io/api/authorization/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
@@ -26,8 +28,6 @@ import (
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
|
||||
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
|
||||
|
||||
auth1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
|
||||
"go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1"
|
||||
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
supervisorclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
|
||||
"go.pinniped.dev/internal/groupsuffix"
|
||||
"go.pinniped.dev/internal/kubeclient"
|
||||
"go.pinniped.dev/internal/oidcclientsecretstorage"
|
||||
|
||||
// Import to initialize client auth plugins - the kubeconfig that we use for
|
||||
// testing may use gcloud, az, oidc, etc.
|
||||
@@ -378,6 +379,89 @@ func CreateClientCredsSecret(t *testing.T, clientID string, clientSecret string)
|
||||
)
|
||||
}
|
||||
|
||||
func CreateOIDCClient(t *testing.T, spec configv1alpha1.OIDCClientSpec, expectedPhase configv1alpha1.OIDCClientPhase) (string, string) {
|
||||
t.Helper()
|
||||
env := IntegrationEnv(t)
|
||||
client := NewSupervisorClientset(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
oidcClientClient := client.ConfigV1alpha1().OIDCClients(env.SupervisorNamespace)
|
||||
|
||||
// Create the OIDCClient using GenerateName to get a random name.
|
||||
created, err := oidcClientClient.Create(ctx, &configv1alpha1.OIDCClient{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "client.oauth.pinniped.dev-test-", // use the required name prefix
|
||||
Labels: map[string]string{"pinniped.dev/test": ""},
|
||||
Annotations: map[string]string{"pinniped.dev/testName": t.Name()},
|
||||
},
|
||||
Spec: spec,
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Always clean this up after this point.
|
||||
t.Cleanup(func() {
|
||||
t.Logf("cleaning up test OIDCClient %s/%s", created.Namespace, created.Name)
|
||||
err := oidcClientClient.Delete(context.Background(), created.Name, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
t.Logf("created test OIDCClient %s", created.Name)
|
||||
|
||||
// Create a client secret for the new OIDCClient.
|
||||
clientSecret := createOIDCClientSecret(t, created)
|
||||
|
||||
// Wait for the OIDCClient to enter the expected phase (or time out).
|
||||
var result *configv1alpha1.OIDCClient
|
||||
RequireEventuallyf(t, func(requireEventually *require.Assertions) {
|
||||
var err error
|
||||
result, err = oidcClientClient.Get(ctx, created.Name, metav1.GetOptions{})
|
||||
requireEventually.NoErrorf(err, "error while getting OIDCClient %s/%s", created.Namespace, created.Name)
|
||||
requireEventually.Equal(expectedPhase, result.Status.Phase)
|
||||
}, 60*time.Second, 1*time.Second, "expected the OIDCClient to go into phase %s, OIDCClient was: %s", expectedPhase, Sdump(result))
|
||||
|
||||
return created.Name, clientSecret
|
||||
}
|
||||
|
||||
func createOIDCClientSecret(t *testing.T, forOIDCClient *configv1alpha1.OIDCClient) string {
|
||||
// TODO Replace this with a call to the real Supervisor API for creating client secrets after that gets implemented.
|
||||
// For now, just manually create a Secret with the right format so the tests can work.
|
||||
t.Helper()
|
||||
env := IntegrationEnv(t)
|
||||
kubeClient := NewKubernetesClientset(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
var buf [32]byte
|
||||
_, err := io.ReadFull(rand.Reader, buf[:])
|
||||
require.NoError(t, err)
|
||||
randomSecret := hex.EncodeToString(buf[:])
|
||||
hashedRandomSecret, err := bcrypt.GenerateFromPassword([]byte(randomSecret), 15)
|
||||
require.NoError(t, err)
|
||||
|
||||
created, err := kubeClient.CoreV1().Secrets(env.SupervisorNamespace).Create(ctx, &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: oidcclientsecretstorage.New(nil, nil).GetName(forOIDCClient.UID), // use the required name
|
||||
Labels: map[string]string{"storage.pinniped.dev/type": "oidc-client-secret", "pinniped.dev/test": ""},
|
||||
Annotations: map[string]string{"pinniped.dev/testName": t.Name()},
|
||||
},
|
||||
Type: "storage.pinniped.dev/oidc-client-secret",
|
||||
Data: map[string][]byte{
|
||||
"pinniped-storage-data": []byte(`{"version":"1","hashes":["` + string(hashedRandomSecret) + `"]}`),
|
||||
"pinniped-storage-version": []byte("1"),
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
t.Logf("cleaning up test Secret %s/%s", created.Namespace, created.Name)
|
||||
err := kubeClient.CoreV1().Secrets(env.SupervisorNamespace).Delete(context.Background(), created.Name, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Logf("created test Secret %s", created.Name)
|
||||
return randomSecret
|
||||
}
|
||||
|
||||
func CreateTestOIDCIdentityProvider(t *testing.T, spec idpv1alpha1.OIDCIdentityProviderSpec, expectedPhase idpv1alpha1.OIDCIdentityProviderPhase) *idpv1alpha1.OIDCIdentityProvider {
|
||||
t.Helper()
|
||||
env := IntegrationEnv(t)
|
||||
@@ -385,9 +469,9 @@ func CreateTestOIDCIdentityProvider(t *testing.T, spec idpv1alpha1.OIDCIdentityP
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Create the OIDCIdentityProvider using GenerateName to get a random name.
|
||||
upstreams := client.IDPV1alpha1().OIDCIdentityProviders(env.SupervisorNamespace)
|
||||
|
||||
// Create the OIDCIdentityProvider using GenerateName to get a random name.
|
||||
created, err := upstreams.Create(ctx, &idpv1alpha1.OIDCIdentityProvider{
|
||||
ObjectMeta: testObjectMeta(t, "upstream-oidc-idp"),
|
||||
Spec: spec,
|
||||
@@ -420,9 +504,9 @@ func CreateTestLDAPIdentityProvider(t *testing.T, spec idpv1alpha1.LDAPIdentityP
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Create the LDAPIdentityProvider using GenerateName to get a random name.
|
||||
upstreams := client.IDPV1alpha1().LDAPIdentityProviders(env.SupervisorNamespace)
|
||||
|
||||
// Create the LDAPIdentityProvider using GenerateName to get a random name.
|
||||
created, err := upstreams.Create(ctx, &idpv1alpha1.LDAPIdentityProvider{
|
||||
ObjectMeta: testObjectMeta(t, "upstream-ldap-idp"),
|
||||
Spec: spec,
|
||||
@@ -461,9 +545,9 @@ func CreateTestActiveDirectoryIdentityProvider(t *testing.T, spec idpv1alpha1.Ac
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Create the ActiveDirectoryIdentityProvider using GenerateName to get a random name.
|
||||
upstreams := client.IDPV1alpha1().ActiveDirectoryIdentityProviders(env.SupervisorNamespace)
|
||||
|
||||
// Create the ActiveDirectoryIdentityProvider using GenerateName to get a random name.
|
||||
created, err := upstreams.Create(ctx, &idpv1alpha1.ActiveDirectoryIdentityProvider{
|
||||
ObjectMeta: testObjectMeta(t, "upstream-ad-idp"),
|
||||
Spec: spec,
|
||||
@@ -501,9 +585,9 @@ func CreateTestClusterRoleBinding(t *testing.T, subject rbacv1.Subject, roleRef
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Create the ClusterRoleBinding using GenerateName to get a random name.
|
||||
clusterRoles := client.RbacV1().ClusterRoleBindings()
|
||||
|
||||
// Create the ClusterRoleBinding using GenerateName to get a random name.
|
||||
created, err := clusterRoles.Create(ctx, &rbacv1.ClusterRoleBinding{
|
||||
ObjectMeta: testObjectMeta(t, "cluster-role"),
|
||||
Subjects: []rbacv1.Subject{subject},
|
||||
|
||||
Reference in New Issue
Block a user