Add more unit tests for dynamic clients and enhance token exchange
- Enhance the token exchange to check that the same client is used compared to the client used during the original authorization and token requests, and also check that the client has the token-exchange grant type allowed in its configuration. - Reduce the minimum required bcrypt cost for OIDCClient secrets because 15 is too slow for real-life use, especially considering that every login and every refresh flow will require two client auths. - In unit tests, use bcrypt hashes with a cost of 4, because bcrypt slows down by 13x when run with the race detector, and we run our tests with the race detector enabled, causing the tests to be unacceptably slow. The production code uses a higher minimum cost. - Centralize all pre-computed bcrypt hashes used by unit tests to a single place. Also extract some other useful test helpers for unit tests related to OIDCClients. - Add tons of unit tests for the token endpoint related to dynamic clients for authcode exchanges, token exchanges, and refreshes.
This commit is contained in:
parent
32ea6090ad
commit
34509e7430
@ -107,7 +107,7 @@ func (c *oidcClientWatcherController) Sync(ctx controllerlib.Context) error {
|
||||
secret = nil
|
||||
}
|
||||
|
||||
_, conditions, clientSecrets := oidcclientvalidator.Validate(oidcClient, secret)
|
||||
_, conditions, clientSecrets := oidcclientvalidator.Validate(oidcClient, secret, oidcclientvalidator.DefaultMinBcryptCost)
|
||||
|
||||
if err := c.updateStatus(ctx.Context, oidcClient, conditions, len(clientSecrets)); err != nil {
|
||||
return fmt.Errorf("cannot update OIDCClient '%s/%s': %w", oidcClient.Namespace, oidcClient.Name, err)
|
||||
|
@ -164,15 +164,6 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
testName = "client.oauth.pinniped.dev-test-name"
|
||||
testNamespace = "test-namespace"
|
||||
testUID = "test-uid-123"
|
||||
|
||||
//nolint:gosec // this is not a credential
|
||||
testBcryptSecret1 = "$2y$15$Kh7cRj0ScSD5QelE3ZNSl.nF04JDv7zb3SgGN.tSfLIX.4kt3UX7m" // bcrypt of "password1" at cost 15
|
||||
//nolint:gosec // this is not a credential
|
||||
testBcryptSecret2 = "$2y$15$Kh7cRj0ScSD5QelE3ZNSl.nF04JDv7zb3SgGN.tSfLIX.4kt3UX7m" // bcrypt of "password2" at cost 15
|
||||
//nolint:gosec // this is not a credential
|
||||
testInvalidBcryptSecretCostTooLow = "$2y$14$njwk1cItiRy6cb6u9aiJLuhtJG83zM9111t.xU6MxvnqqYbkXxzwy" // bcrypt of "password1" at cost 14
|
||||
//nolint:gosec // this is not a credential
|
||||
testInvalidBcryptSecretInvalidFormat = "$2y$14$njwk1cItiRy6cb6u9aiJLuhtJG83zM9111t.xU6MxvnqqYbkXxz" // not enough characters in hash value
|
||||
)
|
||||
|
||||
now := metav1.NewTime(time.Now().UTC())
|
||||
@ -291,7 +282,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testBcryptSecret1})},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword1AtSupervisorMinCost})},
|
||||
wantAPIActions: 1, // one update
|
||||
wantResultingOIDCClients: []configv1alpha1.OIDCClient{
|
||||
{
|
||||
@ -320,7 +311,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
AllowedScopes: []configv1alpha1.Scope{"openid"},
|
||||
},
|
||||
}},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testBcryptSecret1, testBcryptSecret2})},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword1AtSupervisorMinCost, testutil.HashedPassword2AtSupervisorMinCost})},
|
||||
wantAPIActions: 1, // one update
|
||||
wantResultingOIDCClients: []configv1alpha1.OIDCClient{{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
|
||||
@ -353,7 +344,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
TotalClientSecrets: 1,
|
||||
},
|
||||
}},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testBcryptSecret1})},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword1AtSupervisorMinCost})},
|
||||
wantAPIActions: 0, // no updates
|
||||
wantResultingOIDCClients: []configv1alpha1.OIDCClient{{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
|
||||
@ -445,7 +436,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
}},
|
||||
inputSecrets: []runtime.Object{
|
||||
testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID,
|
||||
[]string{testBcryptSecret1, testInvalidBcryptSecretCostTooLow, testInvalidBcryptSecretInvalidFormat}),
|
||||
[]string{testutil.HashedPassword1AtSupervisorMinCost, testutil.HashedPassword1JustBelowSupervisorMinCost, testutil.HashedPassword1InvalidFormat}),
|
||||
},
|
||||
wantAPIActions: 1, // one update
|
||||
wantResultingOIDCClients: []configv1alpha1.OIDCClient{{
|
||||
@ -457,7 +448,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
happyAllowedScopesCondition(now, 1234),
|
||||
sadInvalidClientSecretsCondition(now, 1234,
|
||||
"3 stored client secrets found, but some were invalid, so none will be used: "+
|
||||
"hashed client secret at index 1: bcrypt cost 14 is below the required minimum of 15; "+
|
||||
"hashed client secret at index 1: bcrypt cost 11 is below the required minimum of 12; "+
|
||||
"hashed client secret at index 2: crypto/bcrypt: hashedSecret too short to be a bcrypted password"),
|
||||
},
|
||||
TotalClientSecrets: 0,
|
||||
@ -479,7 +470,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
Spec: configv1alpha1.OIDCClientSpec{},
|
||||
},
|
||||
},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, "uid1", []string{testBcryptSecret1})},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, "uid1", []string{testutil.HashedPassword1AtSupervisorMinCost})},
|
||||
wantAPIActions: 2, // one update for each OIDCClient
|
||||
wantResultingOIDCClients: []configv1alpha1.OIDCClient{
|
||||
{
|
||||
@ -527,7 +518,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
TotalClientSecrets: 1,
|
||||
},
|
||||
}},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testBcryptSecret1})},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword1AtSupervisorMinCost})},
|
||||
wantAPIActions: 1, // one update
|
||||
wantResultingOIDCClients: []configv1alpha1.OIDCClient{{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 4567, UID: testUID},
|
||||
@ -553,7 +544,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
},
|
||||
}},
|
||||
wantAPIActions: 1, // one update
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testBcryptSecret1})},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword1AtSupervisorMinCost})},
|
||||
wantResultingOIDCClients: []configv1alpha1.OIDCClient{{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
|
||||
Status: configv1alpha1.OIDCClientStatus{
|
||||
@ -577,7 +568,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
},
|
||||
}},
|
||||
wantAPIActions: 1, // one update
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testBcryptSecret1})},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword1AtSupervisorMinCost})},
|
||||
wantResultingOIDCClients: []configv1alpha1.OIDCClient{{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
|
||||
Status: configv1alpha1.OIDCClientStatus{
|
||||
@ -606,7 +597,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
},
|
||||
}},
|
||||
wantAPIActions: 1, // one update
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testBcryptSecret1})},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword1AtSupervisorMinCost})},
|
||||
wantResultingOIDCClients: []configv1alpha1.OIDCClient{{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
|
||||
Status: configv1alpha1.OIDCClientStatus{
|
||||
@ -633,7 +624,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
AllowedScopes: []configv1alpha1.Scope{"openid", "pinniped:request-audience", "username", "groups"},
|
||||
},
|
||||
}},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testBcryptSecret1})},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword1AtSupervisorMinCost})},
|
||||
wantAPIActions: 1, // one update
|
||||
wantResultingOIDCClients: []configv1alpha1.OIDCClient{{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
|
||||
@ -657,7 +648,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
AllowedScopes: []configv1alpha1.Scope{"openid"},
|
||||
},
|
||||
}},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testBcryptSecret1})},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword1AtSupervisorMinCost})},
|
||||
wantAPIActions: 1, // one update
|
||||
wantResultingOIDCClients: []configv1alpha1.OIDCClient{{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
|
||||
@ -681,7 +672,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
AllowedScopes: []configv1alpha1.Scope{"openid", "pinniped:request-audience"},
|
||||
},
|
||||
}},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testBcryptSecret1})},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword1AtSupervisorMinCost})},
|
||||
wantAPIActions: 1, // one update
|
||||
wantResultingOIDCClients: []configv1alpha1.OIDCClient{{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
|
||||
@ -705,7 +696,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
AllowedScopes: []configv1alpha1.Scope{"openid", "pinniped:request-audience", "groups"},
|
||||
},
|
||||
}},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testBcryptSecret1})},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword1AtSupervisorMinCost})},
|
||||
wantAPIActions: 1, // one update
|
||||
wantResultingOIDCClients: []configv1alpha1.OIDCClient{{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
|
||||
@ -729,7 +720,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
AllowedScopes: []configv1alpha1.Scope{"openid", "pinniped:request-audience", "username"},
|
||||
},
|
||||
}},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testBcryptSecret1})},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword1AtSupervisorMinCost})},
|
||||
wantAPIActions: 1, // one update
|
||||
wantResultingOIDCClients: []configv1alpha1.OIDCClient{{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
|
||||
@ -753,7 +744,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
AllowedScopes: []configv1alpha1.Scope{"openid"},
|
||||
},
|
||||
}},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testBcryptSecret1})},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword1AtSupervisorMinCost})},
|
||||
wantAPIActions: 1, // one update
|
||||
wantResultingOIDCClients: []configv1alpha1.OIDCClient{{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
|
||||
@ -777,7 +768,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"},
|
||||
},
|
||||
}},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testBcryptSecret1})},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword1AtSupervisorMinCost})},
|
||||
wantAPIActions: 1, // one update
|
||||
wantResultingOIDCClients: []configv1alpha1.OIDCClient{{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
|
||||
@ -801,7 +792,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access"},
|
||||
},
|
||||
}},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testBcryptSecret1})},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword1AtSupervisorMinCost})},
|
||||
wantAPIActions: 1, // one update
|
||||
wantResultingOIDCClients: []configv1alpha1.OIDCClient{{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
|
||||
@ -825,7 +816,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "username"},
|
||||
},
|
||||
}},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testBcryptSecret1})},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword1AtSupervisorMinCost})},
|
||||
wantAPIActions: 1, // one update
|
||||
wantResultingOIDCClients: []configv1alpha1.OIDCClient{{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
|
||||
@ -849,7 +840,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "groups"},
|
||||
},
|
||||
}},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testBcryptSecret1})},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword1AtSupervisorMinCost})},
|
||||
wantAPIActions: 1, // one update
|
||||
wantResultingOIDCClients: []configv1alpha1.OIDCClient{{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
|
||||
@ -873,7 +864,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "username", "groups"},
|
||||
},
|
||||
}},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testBcryptSecret1})},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword1AtSupervisorMinCost})},
|
||||
wantAPIActions: 1, // one update
|
||||
wantResultingOIDCClients: []configv1alpha1.OIDCClient{{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
|
||||
@ -897,7 +888,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
AllowedScopes: []configv1alpha1.Scope{"openid", "username"},
|
||||
},
|
||||
}},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testBcryptSecret1})},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword1AtSupervisorMinCost})},
|
||||
wantAPIActions: 1, // one update
|
||||
wantResultingOIDCClients: []configv1alpha1.OIDCClient{{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
|
||||
@ -921,7 +912,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
AllowedScopes: []configv1alpha1.Scope{"openid", "username"},
|
||||
},
|
||||
}},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testBcryptSecret1})},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword1AtSupervisorMinCost})},
|
||||
wantAPIActions: 1, // one update
|
||||
wantResultingOIDCClients: []configv1alpha1.OIDCClient{{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
|
||||
@ -945,7 +936,7 @@ func TestOIDCClientWatcherControllerSync(t *testing.T) {
|
||||
AllowedScopes: []configv1alpha1.Scope{"openid", "username", "groups"},
|
||||
},
|
||||
}},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testBcryptSecret1})},
|
||||
inputSecrets: []runtime.Object{testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword1AtSupervisorMinCost})},
|
||||
wantAPIActions: 1, // one update
|
||||
wantResultingOIDCClients: []configv1alpha1.OIDCClient{{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID},
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/ory/fosite"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/oauth2"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
@ -27,7 +28,6 @@ import (
|
||||
kubetesting "k8s.io/client-go/testing"
|
||||
"k8s.io/utils/pointer"
|
||||
|
||||
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
||||
supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake"
|
||||
"go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/typed/config/v1alpha1"
|
||||
"go.pinniped.dev/internal/authenticators"
|
||||
@ -79,8 +79,6 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
pinnipedCLIClientID = "pinniped-cli"
|
||||
dynamicClientID = "client.oauth.pinniped.dev-test-name"
|
||||
dynamicClientUID = "fake-client-uid"
|
||||
//nolint:gosec // this is not a credential
|
||||
dynamicClientHashedSecret = "$2y$15$Kh7cRj0ScSD5QelE3ZNSl.nF04JDv7zb3SgGN.tSfLIX.4kt3UX7m" // bcrypt of "password1" at cost 15
|
||||
)
|
||||
|
||||
require.Len(t, happyState, 8, "we expect fosite to allow 8 byte state params, so we want to test that boundary case")
|
||||
@ -237,13 +235,15 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
createOauthHelperWithRealStorage := func(secretsClient v1.SecretInterface, oidcClientsClient v1alpha1.OIDCClientInterface) (fosite.OAuth2Provider, *oidc.KubeStorage) {
|
||||
// Configure fosite the same way that the production code would when using Kube storage.
|
||||
// Inject this into our test subject at the last second so we get a fresh storage for every test.
|
||||
kubeOauthStore := oidc.NewKubeStorage(secretsClient, oidcClientsClient, timeoutsConfiguration)
|
||||
// Use lower minimum required bcrypt cost than we would use in production to keep unit the tests fast.
|
||||
kubeOauthStore := oidc.NewKubeStorage(secretsClient, oidcClientsClient, timeoutsConfiguration, bcrypt.MinCost)
|
||||
return oidc.FositeOauth2Helper(kubeOauthStore, downstreamIssuer, hmacSecretFunc, jwksProviderIsUnused, timeoutsConfiguration), kubeOauthStore
|
||||
}
|
||||
|
||||
createOauthHelperWithNullStorage := func(secretsClient v1.SecretInterface, oidcClientsClient v1alpha1.OIDCClientInterface) (fosite.OAuth2Provider, *oidc.NullStorage) {
|
||||
// Configure fosite the same way that the production code would, using NullStorage to turn off storage.
|
||||
nullOauthStore := oidc.NewNullStorage(secretsClient, oidcClientsClient)
|
||||
// Use lower minimum required bcrypt cost than we would use in production to keep unit the tests fast.
|
||||
nullOauthStore := oidc.NewNullStorage(secretsClient, oidcClientsClient, bcrypt.MinCost)
|
||||
return oidc.FositeOauth2Helper(nullOauthStore, downstreamIssuer, hmacSecretFunc, jwksProviderIsUnused, timeoutsConfiguration), nullOauthStore
|
||||
}
|
||||
|
||||
@ -511,24 +511,11 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
fullyCapableDynamicClient := &configv1alpha1.OIDCClient{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: "some-namespace", Name: dynamicClientID, Generation: 1, UID: dynamicClientUID},
|
||||
Spec: configv1alpha1.OIDCClientSpec{
|
||||
AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"},
|
||||
AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"},
|
||||
AllowedRedirectURIs: []configv1alpha1.RedirectURI{downstreamRedirectURI},
|
||||
},
|
||||
}
|
||||
|
||||
allDynamicClientScopes := "openid offline_access pinniped:request-audience username groups"
|
||||
|
||||
storageSecretWithOneClientSecretForDynamicClient := testutil.OIDCClientSecretStorageSecretForUID(t,
|
||||
"some-namespace", dynamicClientUID, []string{dynamicClientHashedSecret},
|
||||
)
|
||||
|
||||
addFullyCapableDynamicClientAndSecretToKubeResources := func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset) {
|
||||
require.NoError(t, supervisorClient.Tracker().Add(fullyCapableDynamicClient))
|
||||
require.NoError(t, kubeClient.Tracker().Add(storageSecretWithOneClientSecretForDynamicClient))
|
||||
oidcClient, secret := testutil.FullyCapableOIDCClientAndStorageSecret(t,
|
||||
"some-namespace", dynamicClientID, dynamicClientUID, downstreamRedirectURI, []string{testutil.HashedPassword1AtGoMinCost})
|
||||
require.NoError(t, supervisorClient.Tracker().Add(oidcClient))
|
||||
require.NoError(t, kubeClient.Tracker().Add(secret))
|
||||
}
|
||||
|
||||
// Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it
|
||||
@ -611,11 +598,11 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
stateEncoder: happyStateEncoder,
|
||||
cookieEncoder: happyCookieEncoder,
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes}),
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}),
|
||||
wantStatus: http.StatusSeeOther,
|
||||
wantContentType: htmlContentType,
|
||||
wantCSRFValueInCookieHeader: happyCSRF,
|
||||
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes}, "", oidcUpstreamName, "oidc"), nil),
|
||||
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}, "", oidcUpstreamName, "oidc"), nil),
|
||||
wantUpstreamStateParamInLocationHeader: true,
|
||||
wantBodyStringWithLocationInHref: true,
|
||||
},
|
||||
@ -646,11 +633,11 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
stateEncoder: happyStateEncoder,
|
||||
cookieEncoder: happyCookieEncoder,
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes}),
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}),
|
||||
wantStatus: http.StatusSeeOther,
|
||||
wantContentType: htmlContentType,
|
||||
wantCSRFValueInCookieHeader: happyCSRF,
|
||||
wantLocationHeader: urlWithQuery(downstreamIssuer+"/login", map[string]string{"state": expectedUpstreamStateParam(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes}, "", ldapUpstreamName, "ldap")}),
|
||||
wantLocationHeader: urlWithQuery(downstreamIssuer+"/login", map[string]string{"state": expectedUpstreamStateParam(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}, "", ldapUpstreamName, "ldap")}),
|
||||
wantUpstreamStateParamInLocationHeader: true,
|
||||
wantBodyStringWithLocationInHref: true,
|
||||
},
|
||||
@ -681,11 +668,11 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
stateEncoder: happyStateEncoder,
|
||||
cookieEncoder: happyCookieEncoder,
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes}),
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}),
|
||||
wantStatus: http.StatusSeeOther,
|
||||
wantContentType: htmlContentType,
|
||||
wantCSRFValueInCookieHeader: happyCSRF,
|
||||
wantLocationHeader: urlWithQuery(downstreamIssuer+"/login", map[string]string{"state": expectedUpstreamStateParam(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes}, "", activeDirectoryUpstreamName, "activedirectory")}),
|
||||
wantLocationHeader: urlWithQuery(downstreamIssuer+"/login", map[string]string{"state": expectedUpstreamStateParam(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}, "", activeDirectoryUpstreamName, "activedirectory")}),
|
||||
wantUpstreamStateParamInLocationHeader: true,
|
||||
wantBodyStringWithLocationInHref: true,
|
||||
},
|
||||
@ -835,12 +822,12 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
method: http.MethodPost,
|
||||
path: "/some/path",
|
||||
contentType: formContentType,
|
||||
body: encodeQuery(modifiedHappyGetRequestQueryMap(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes})),
|
||||
body: encodeQuery(modifiedHappyGetRequestQueryMap(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep})),
|
||||
wantStatus: http.StatusSeeOther,
|
||||
wantContentType: "",
|
||||
wantBodyString: "",
|
||||
wantCSRFValueInCookieHeader: happyCSRF,
|
||||
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes}, "", oidcUpstreamName, "oidc"), nil),
|
||||
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}, "", oidcUpstreamName, "oidc"), nil),
|
||||
wantUpstreamStateParamInLocationHeader: true,
|
||||
},
|
||||
{
|
||||
@ -874,12 +861,12 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
method: http.MethodPost,
|
||||
path: "/some/path",
|
||||
contentType: formContentType,
|
||||
body: encodeQuery(modifiedHappyGetRequestQueryMap(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes})),
|
||||
body: encodeQuery(modifiedHappyGetRequestQueryMap(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep})),
|
||||
wantStatus: http.StatusSeeOther,
|
||||
wantContentType: "",
|
||||
wantBodyString: "",
|
||||
wantCSRFValueInCookieHeader: happyCSRF,
|
||||
wantLocationHeader: urlWithQuery(downstreamIssuer+"/login", map[string]string{"state": expectedUpstreamStateParam(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes}, "", ldapUpstreamName, "ldap")}),
|
||||
wantLocationHeader: urlWithQuery(downstreamIssuer+"/login", map[string]string{"state": expectedUpstreamStateParam(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}, "", ldapUpstreamName, "ldap")}),
|
||||
wantUpstreamStateParamInLocationHeader: true,
|
||||
},
|
||||
{
|
||||
@ -913,12 +900,12 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
method: http.MethodPost,
|
||||
path: "/some/path",
|
||||
contentType: formContentType,
|
||||
body: encodeQuery(modifiedHappyGetRequestQueryMap(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes})),
|
||||
body: encodeQuery(modifiedHappyGetRequestQueryMap(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep})),
|
||||
wantStatus: http.StatusSeeOther,
|
||||
wantContentType: "",
|
||||
wantBodyString: "",
|
||||
wantCSRFValueInCookieHeader: happyCSRF,
|
||||
wantLocationHeader: urlWithQuery(downstreamIssuer+"/login", map[string]string{"state": expectedUpstreamStateParam(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes}, "", activeDirectoryUpstreamName, "activedirectory")}),
|
||||
wantLocationHeader: urlWithQuery(downstreamIssuer+"/login", map[string]string{"state": expectedUpstreamStateParam(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}, "", activeDirectoryUpstreamName, "activedirectory")}),
|
||||
wantUpstreamStateParamInLocationHeader: true,
|
||||
},
|
||||
{
|
||||
@ -1111,7 +1098,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
path: modifiedHappyGetRequestPath(map[string]string{
|
||||
"redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client
|
||||
"client_id": dynamicClientID,
|
||||
"scope": allDynamicClientScopes,
|
||||
"scope": testutil.AllDynamicClientScopesSpaceSep,
|
||||
}),
|
||||
wantStatus: http.StatusSeeOther,
|
||||
wantContentType: htmlContentType,
|
||||
@ -1119,7 +1106,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{
|
||||
"redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client
|
||||
"client_id": dynamicClientID,
|
||||
"scope": allDynamicClientScopes,
|
||||
"scope": testutil.AllDynamicClientScopesSpaceSep,
|
||||
}, "", oidcUpstreamName, "oidc"), nil),
|
||||
wantUpstreamStateParamInLocationHeader: true,
|
||||
wantBodyStringWithLocationInHref: true,
|
||||
@ -1526,7 +1513,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(passwordGrantUpstreamOIDCIdentityProviderBuilder().Build()),
|
||||
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes}),
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}),
|
||||
customUsernameHeader: pointer.StringPtr(oidcUpstreamUsername),
|
||||
customPasswordHeader: pointer.StringPtr(oidcUpstreamPassword),
|
||||
wantStatus: http.StatusFound,
|
||||
@ -1539,7 +1526,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
||||
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes}),
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}),
|
||||
customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
|
||||
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
|
||||
wantStatus: http.StatusFound,
|
||||
@ -1552,7 +1539,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
|
||||
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes}),
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}),
|
||||
customUsernameHeader: pointer.StringPtr(happyLDAPUsername),
|
||||
customPasswordHeader: pointer.StringPtr(happyLDAPPassword),
|
||||
wantStatus: http.StatusFound,
|
||||
@ -1589,7 +1576,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
path: modifiedHappyGetRequestPath(map[string]string{
|
||||
"redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-dynamic-client",
|
||||
"client_id": dynamicClientID,
|
||||
"scope": allDynamicClientScopes,
|
||||
"scope": testutil.AllDynamicClientScopesSpaceSep,
|
||||
}),
|
||||
wantStatus: http.StatusBadRequest,
|
||||
wantContentType: jsonContentType,
|
||||
@ -1705,7 +1692,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
path: modifiedHappyGetRequestPath(map[string]string{
|
||||
"response_type": "unsupported",
|
||||
"client_id": dynamicClientID,
|
||||
"scope": allDynamicClientScopes,
|
||||
"scope": testutil.AllDynamicClientScopesSpaceSep,
|
||||
}),
|
||||
wantStatus: http.StatusSeeOther,
|
||||
wantContentType: jsonContentType,
|
||||
@ -1754,7 +1741,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
path: modifiedHappyGetRequestPath(map[string]string{
|
||||
"response_type": "unsupported",
|
||||
"client_id": dynamicClientID,
|
||||
"scope": allDynamicClientScopes,
|
||||
"scope": testutil.AllDynamicClientScopesSpaceSep,
|
||||
}),
|
||||
wantStatus: http.StatusSeeOther,
|
||||
wantContentType: jsonContentType,
|
||||
@ -1791,7 +1778,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
path: modifiedHappyGetRequestPath(map[string]string{
|
||||
"response_type": "unsupported",
|
||||
"client_id": dynamicClientID,
|
||||
"scope": allDynamicClientScopes,
|
||||
"scope": testutil.AllDynamicClientScopesSpaceSep,
|
||||
}),
|
||||
wantStatus: http.StatusSeeOther,
|
||||
wantContentType: jsonContentType,
|
||||
@ -1865,7 +1852,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
stateEncoder: happyStateEncoder,
|
||||
cookieEncoder: happyCookieEncoder,
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"response_mode": "form_post", "client_id": dynamicClientID, "scope": allDynamicClientScopes}),
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"response_mode": "form_post", "client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep}),
|
||||
wantStatus: http.StatusOK, // this is weird, but fosite uses a form_post response to tell the client that it is not allowed to use form_post responses
|
||||
wantContentType: htmlContentType,
|
||||
wantBodyRegex: `<input type="hidden" name="encoded_params" value="error=unsupported_response_mode&error_description=The+authorization+server+does+not+support+obtaining+a+response+using+this+response+mode.`,
|
||||
@ -1919,7 +1906,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
stateEncoder: happyStateEncoder,
|
||||
cookieEncoder: happyCookieEncoder,
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes, "response_type": ""}),
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep, "response_type": ""}),
|
||||
wantStatus: http.StatusSeeOther,
|
||||
wantContentType: jsonContentType,
|
||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
|
||||
@ -1964,7 +1951,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider),
|
||||
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes, "response_type": ""}),
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep, "response_type": ""}),
|
||||
wantStatus: http.StatusSeeOther,
|
||||
wantContentType: jsonContentType,
|
||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
|
||||
@ -1997,7 +1984,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamActiveDirectoryIdentityProvider),
|
||||
kubeResources: addFullyCapableDynamicClientAndSecretToKubeResources,
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes, "response_type": ""}),
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep, "response_type": ""}),
|
||||
wantStatus: http.StatusSeeOther,
|
||||
wantContentType: jsonContentType,
|
||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery),
|
||||
@ -2062,7 +2049,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
stateEncoder: happyStateEncoder,
|
||||
cookieEncoder: happyCookieEncoder,
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes, "code_challenge": ""}),
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep, "code_challenge": ""}),
|
||||
wantStatus: http.StatusSeeOther,
|
||||
wantContentType: jsonContentType,
|
||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery),
|
||||
@ -2120,7 +2107,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
stateEncoder: happyStateEncoder,
|
||||
cookieEncoder: happyCookieEncoder,
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes, "code_challenge_method": "this-is-not-a-valid-pkce-alg"}),
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep, "code_challenge_method": "this-is-not-a-valid-pkce-alg"}),
|
||||
wantStatus: http.StatusSeeOther,
|
||||
wantContentType: jsonContentType,
|
||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery),
|
||||
@ -2178,7 +2165,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
stateEncoder: happyStateEncoder,
|
||||
cookieEncoder: happyCookieEncoder,
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes, "code_challenge_method": "plain"}),
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep, "code_challenge_method": "plain"}),
|
||||
wantStatus: http.StatusSeeOther,
|
||||
wantContentType: jsonContentType,
|
||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
|
||||
@ -2236,7 +2223,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
stateEncoder: happyStateEncoder,
|
||||
cookieEncoder: happyCookieEncoder,
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes, "code_challenge_method": ""}),
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep, "code_challenge_method": ""}),
|
||||
wantStatus: http.StatusSeeOther,
|
||||
wantContentType: jsonContentType,
|
||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery),
|
||||
@ -2298,7 +2285,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
stateEncoder: happyStateEncoder,
|
||||
cookieEncoder: happyCookieEncoder,
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes, "prompt": "none login"}),
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep, "prompt": "none login"}),
|
||||
wantStatus: http.StatusSeeOther,
|
||||
wantContentType: jsonContentType,
|
||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery),
|
||||
@ -2865,7 +2852,7 @@ func TestAuthorizationEndpoint(t *testing.T) {
|
||||
stateEncoder: happyStateEncoder,
|
||||
cookieEncoder: happyCookieEncoder,
|
||||
method: http.MethodGet,
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": allDynamicClientScopes, "state": "short"}),
|
||||
path: modifiedHappyGetRequestPath(map[string]string{"client_id": dynamicClientID, "scope": testutil.AllDynamicClientScopesSpaceSep, "state": "short"}),
|
||||
wantStatus: http.StatusSeeOther,
|
||||
wantContentType: jsonContentType,
|
||||
wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidStateErrorQuery),
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
|
||||
@ -139,10 +140,11 @@ func TestCallbackEndpoint(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
idps *oidctestutil.UpstreamIDPListerBuilder
|
||||
method string
|
||||
path string
|
||||
csrfCookie string
|
||||
idps *oidctestutil.UpstreamIDPListerBuilder
|
||||
kubeResources func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset)
|
||||
method string
|
||||
path string
|
||||
csrfCookie string
|
||||
|
||||
wantStatus int
|
||||
wantContentType string
|
||||
@ -1071,14 +1073,20 @@ func TestCallbackEndpoint(t *testing.T) {
|
||||
test := test
|
||||
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
client := fake.NewSimpleClientset()
|
||||
secrets := client.CoreV1().Secrets("some-namespace")
|
||||
oidcClientsClient := supervisorfake.NewSimpleClientset().ConfigV1alpha1().OIDCClients("some-namespace")
|
||||
kubeClient := fake.NewSimpleClientset()
|
||||
supervisorClient := supervisorfake.NewSimpleClientset()
|
||||
secrets := kubeClient.CoreV1().Secrets("some-namespace")
|
||||
oidcClientsClient := supervisorClient.ConfigV1alpha1().OIDCClients("some-namespace")
|
||||
|
||||
if test.kubeResources != nil {
|
||||
test.kubeResources(t, supervisorClient, kubeClient)
|
||||
}
|
||||
|
||||
// Configure fosite the same way that the production code would.
|
||||
// Inject this into our test subject at the last second so we get a fresh storage for every test.
|
||||
timeoutsConfiguration := oidc.DefaultOIDCTimeoutsConfiguration()
|
||||
oauthStore := oidc.NewKubeStorage(secrets, oidcClientsClient, timeoutsConfiguration)
|
||||
// Use lower minimum required bcrypt cost than we would use in production to keep unit the tests fast.
|
||||
oauthStore := oidc.NewKubeStorage(secrets, oidcClientsClient, timeoutsConfiguration, bcrypt.MinCost)
|
||||
hmacSecretFunc := func() []byte { return []byte("some secret - must have at least 32 bytes") }
|
||||
require.GreaterOrEqual(t, len(hmacSecretFunc()), 32, "fosite requires that hmac secrets have at least 32 bytes")
|
||||
jwksProviderIsUnused := jwks.NewDynamicJWKSProvider()
|
||||
@ -1120,7 +1128,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
||||
t,
|
||||
rsp.Body.String(),
|
||||
test.wantBodyFormResponseRegexp,
|
||||
client,
|
||||
kubeClient,
|
||||
secrets,
|
||||
oauthStore,
|
||||
test.wantDownstreamGrantedScopes,
|
||||
@ -1147,7 +1155,7 @@ func TestCallbackEndpoint(t *testing.T) {
|
||||
t,
|
||||
rsp.Header().Get("Location"),
|
||||
test.wantRedirectLocationRegexp,
|
||||
client,
|
||||
kubeClient,
|
||||
secrets,
|
||||
oauthStore,
|
||||
test.wantDownstreamGrantedScopes,
|
||||
|
@ -50,6 +50,7 @@ func (c Client) GetResponseModes() []fosite.ResponseModeType {
|
||||
type ClientManager struct {
|
||||
oidcClientsClient supervisorclient.OIDCClientInterface
|
||||
storage *oidcclientsecretstorage.OIDCClientSecretStorage
|
||||
minBcryptCost int
|
||||
}
|
||||
|
||||
var _ fosite.ClientManager = (*ClientManager)(nil)
|
||||
@ -57,10 +58,12 @@ var _ fosite.ClientManager = (*ClientManager)(nil)
|
||||
func NewClientManager(
|
||||
oidcClientsClient supervisorclient.OIDCClientInterface,
|
||||
storage *oidcclientsecretstorage.OIDCClientSecretStorage,
|
||||
minBcryptCost int,
|
||||
) *ClientManager {
|
||||
return &ClientManager{
|
||||
oidcClientsClient: oidcClientsClient,
|
||||
storage: storage,
|
||||
minBcryptCost: minBcryptCost,
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,7 +98,7 @@ func (m *ClientManager) GetClient(ctx context.Context, id string) (fosite.Client
|
||||
}
|
||||
|
||||
// Check if the OIDCClient and its corresponding Secret are valid.
|
||||
valid, conditions, clientSecrets := oidcclientvalidator.Validate(oidcClient, storageSecret)
|
||||
valid, conditions, clientSecrets := oidcclientvalidator.Validate(oidcClient, storageSecret, m.minBcryptCost)
|
||||
if !valid {
|
||||
// Log the conditions so an admin can see exactly what was invalid at the time of the request.
|
||||
plog.Debug("OIDC client lookup GetClient() found an invalid client", "clientID", id, "conditions", conditions)
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
|
||||
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
||||
supervisorfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake"
|
||||
"go.pinniped.dev/internal/oidc/oidcclientvalidator"
|
||||
"go.pinniped.dev/internal/oidcclientsecretstorage"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
)
|
||||
@ -32,11 +33,6 @@ func TestClientManager(t *testing.T) {
|
||||
testName = "client.oauth.pinniped.dev-test-name"
|
||||
testNamespace = "test-namespace"
|
||||
testUID = "test-uid-123"
|
||||
|
||||
//nolint:gosec // this is not a credential
|
||||
testBcryptSecret1 = "$2y$15$Kh7cRj0ScSD5QelE3ZNSl.nF04JDv7zb3SgGN.tSfLIX.4kt3UX7m" // bcrypt of "password1" at cost 15
|
||||
//nolint:gosec // this is not a credential
|
||||
testBcryptSecret2 = "$2y$15$Kh7cRj0ScSD5QelE3ZNSl.nF04JDv7zb3SgGN.tSfLIX.4kt3UX7m" // bcrypt of "password2" at cost 15
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
@ -121,7 +117,7 @@ func TestClientManager(t *testing.T) {
|
||||
},
|
||||
},
|
||||
secrets: []*corev1.Secret{
|
||||
testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testBcryptSecret1, testBcryptSecret2}),
|
||||
testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword1AtSupervisorMinCost, testutil.HashedPassword2AtSupervisorMinCost}),
|
||||
},
|
||||
run: func(t *testing.T, subject *ClientManager) {
|
||||
got, err := subject.GetClient(ctx, testName)
|
||||
@ -174,7 +170,7 @@ func TestClientManager(t *testing.T) {
|
||||
},
|
||||
},
|
||||
secrets: []*corev1.Secret{
|
||||
testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testBcryptSecret1, testBcryptSecret2}),
|
||||
testutil.OIDCClientSecretStorageSecretForUID(t, testNamespace, testUID, []string{testutil.HashedPassword1AtSupervisorMinCost, testutil.HashedPassword2AtSupervisorMinCost}),
|
||||
},
|
||||
run: func(t *testing.T, subject *ClientManager) {
|
||||
got, err := subject.GetClient(ctx, testName)
|
||||
@ -185,8 +181,8 @@ func TestClientManager(t *testing.T) {
|
||||
require.Equal(t, testName, c.GetID())
|
||||
require.Nil(t, c.GetHashedSecret())
|
||||
require.Len(t, c.GetRotatedHashes(), 2)
|
||||
require.Equal(t, testBcryptSecret1, string(c.GetRotatedHashes()[0]))
|
||||
require.Equal(t, testBcryptSecret2, string(c.GetRotatedHashes()[1]))
|
||||
require.Equal(t, testutil.HashedPassword1AtSupervisorMinCost, string(c.GetRotatedHashes()[0]))
|
||||
require.Equal(t, testutil.HashedPassword2AtSupervisorMinCost, string(c.GetRotatedHashes()[1]))
|
||||
require.Equal(t, []string{"http://localhost:80", "https://foobar.com/callback"}, c.GetRedirectURIs())
|
||||
require.Equal(t, fosite.Arguments{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"}, c.GetGrantTypes())
|
||||
require.Equal(t, fosite.Arguments{"code"}, c.GetResponseTypes())
|
||||
@ -214,7 +210,11 @@ func TestClientManager(t *testing.T) {
|
||||
secrets := kubeClient.CoreV1().Secrets(testNamespace)
|
||||
supervisorClient := supervisorfake.NewSimpleClientset()
|
||||
oidcClientsClient := supervisorClient.ConfigV1alpha1().OIDCClients(testNamespace)
|
||||
subject := NewClientManager(oidcClientsClient, oidcclientsecretstorage.New(secrets, time.Now))
|
||||
subject := NewClientManager(
|
||||
oidcClientsClient,
|
||||
oidcclientsecretstorage.New(secrets, time.Now),
|
||||
oidcclientvalidator.DefaultMinBcryptCost,
|
||||
)
|
||||
|
||||
for _, secret := range test.secrets {
|
||||
require.NoError(t, kubeClient.Tracker().Add(secret))
|
||||
|
@ -35,10 +35,15 @@ type KubeStorage struct {
|
||||
|
||||
var _ fositestoragei.AllFositeStorage = &KubeStorage{}
|
||||
|
||||
func NewKubeStorage(secrets corev1client.SecretInterface, oidcClientsClient v1alpha1.OIDCClientInterface, timeoutsConfiguration TimeoutsConfiguration) *KubeStorage {
|
||||
func NewKubeStorage(
|
||||
secrets corev1client.SecretInterface,
|
||||
oidcClientsClient v1alpha1.OIDCClientInterface,
|
||||
timeoutsConfiguration TimeoutsConfiguration,
|
||||
minBcryptCost int,
|
||||
) *KubeStorage {
|
||||
nowFunc := time.Now
|
||||
return &KubeStorage{
|
||||
clientManager: clientregistry.NewClientManager(oidcClientsClient, oidcclientsecretstorage.New(secrets, nowFunc)),
|
||||
clientManager: clientregistry.NewClientManager(oidcClientsClient, oidcclientsecretstorage.New(secrets, nowFunc), minBcryptCost),
|
||||
authorizationCodeStorage: authorizationcode.New(secrets, nowFunc, timeoutsConfiguration.AuthorizationCodeSessionStorageLifetime),
|
||||
pkceStorage: pkce.New(secrets, nowFunc, timeoutsConfiguration.PKCESessionStorageLifetime),
|
||||
oidcStorage: openidconnect.New(secrets, nowFunc, timeoutsConfiguration.OIDCSessionStorageLifetime),
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
|
||||
@ -232,11 +233,12 @@ func TestPostLoginEndpoint(t *testing.T) {
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
idps *oidctestutil.UpstreamIDPListerBuilder
|
||||
decodedState *oidc.UpstreamStateParamData
|
||||
formParams url.Values
|
||||
reqURIQuery url.Values
|
||||
name string
|
||||
idps *oidctestutil.UpstreamIDPListerBuilder
|
||||
kubeResources func(t *testing.T, supervisorClient *supervisorfake.Clientset, kubeClient *fake.Clientset)
|
||||
decodedState *oidc.UpstreamStateParamData
|
||||
formParams url.Values
|
||||
reqURIQuery url.Values
|
||||
|
||||
wantStatus int
|
||||
wantContentType string
|
||||
@ -671,13 +673,19 @@ func TestPostLoginEndpoint(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
kubeClient := fake.NewSimpleClientset()
|
||||
supervisorClient := supervisorfake.NewSimpleClientset()
|
||||
secretsClient := kubeClient.CoreV1().Secrets("some-namespace")
|
||||
oidcClientsClient := supervisorfake.NewSimpleClientset().ConfigV1alpha1().OIDCClients("some-namespace")
|
||||
oidcClientsClient := supervisorClient.ConfigV1alpha1().OIDCClients("some-namespace")
|
||||
|
||||
if test.kubeResources != nil {
|
||||
test.kubeResources(t, supervisorClient, kubeClient)
|
||||
}
|
||||
|
||||
// Configure fosite the same way that the production code would.
|
||||
// Inject this into our test subject at the last second so we get a fresh storage for every test.
|
||||
timeoutsConfiguration := oidc.DefaultOIDCTimeoutsConfiguration()
|
||||
kubeOauthStore := oidc.NewKubeStorage(secretsClient, oidcClientsClient, timeoutsConfiguration)
|
||||
// Use lower minimum required bcrypt cost than we would use in production to keep unit the tests fast.
|
||||
kubeOauthStore := oidc.NewKubeStorage(secretsClient, oidcClientsClient, timeoutsConfiguration, bcrypt.MinCost)
|
||||
hmacSecretFunc := func() []byte { return []byte("some secret - must have at least 32 bytes") }
|
||||
require.GreaterOrEqual(t, len(hmacSecretFunc()), 32, "fosite requires that hmac secrets have at least 32 bytes")
|
||||
jwksProviderIsUnused := jwks.NewDynamicJWKSProvider()
|
||||
|
@ -26,9 +26,13 @@ type NullStorage struct {
|
||||
|
||||
var _ fositestoragei.AllFositeStorage = &NullStorage{}
|
||||
|
||||
func NewNullStorage(secrets corev1client.SecretInterface, oidcClientsClient v1alpha1.OIDCClientInterface) *NullStorage {
|
||||
func NewNullStorage(
|
||||
secrets corev1client.SecretInterface,
|
||||
oidcClientsClient v1alpha1.OIDCClientInterface,
|
||||
minBcryptCost int,
|
||||
) *NullStorage {
|
||||
return &NullStorage{
|
||||
ClientManager: clientregistry.NewClientManager(oidcClientsClient, oidcclientsecretstorage.New(secrets, time.Now)),
|
||||
ClientManager: clientregistry.NewClientManager(oidcClientsClient, oidcclientsecretstorage.New(secrets, time.Now), minBcryptCost),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultMinBcryptCost = 12
|
||||
|
||||
clientSecretExists = "ClientSecretExists"
|
||||
allowedGrantTypesValid = "AllowedGrantTypesValid"
|
||||
allowedScopesValid = "AllowedScopesValid"
|
||||
@ -37,8 +39,6 @@ const (
|
||||
|
||||
allowedGrantTypesFieldName = "allowedGrantTypes"
|
||||
allowedScopesFieldName = "allowedScopes"
|
||||
|
||||
minimumRequiredBcryptCost = 15
|
||||
)
|
||||
|
||||
// Validate validates the OIDCClient and its corresponding client secret storage Secret.
|
||||
@ -46,10 +46,10 @@ const (
|
||||
// get the validation error for that case. It returns a bool to indicate if the client is valid,
|
||||
// along with a slice of conditions containing more details, and the list of client secrets in the
|
||||
// case that the client was valid.
|
||||
func Validate(oidcClient *v1alpha1.OIDCClient, secret *v1.Secret) (bool, []*v1alpha1.Condition, []string) {
|
||||
func Validate(oidcClient *v1alpha1.OIDCClient, secret *v1.Secret, minBcryptCost int) (bool, []*v1alpha1.Condition, []string) {
|
||||
conds := make([]*v1alpha1.Condition, 0, 3)
|
||||
|
||||
conds, clientSecrets := validateSecret(secret, conds)
|
||||
conds, clientSecrets := validateSecret(secret, conds, minBcryptCost)
|
||||
conds = validateAllowedGrantTypes(oidcClient, conds)
|
||||
conds = validateAllowedScopes(oidcClient, conds)
|
||||
|
||||
@ -141,7 +141,7 @@ func validateAllowedGrantTypes(oidcClient *v1alpha1.OIDCClient, conditions []*v1
|
||||
|
||||
// validateSecret checks if the client secret storage Secret is valid and contains at least one client secret.
|
||||
// It returns the updated conditions slice along with the client secrets found in that case that it is valid.
|
||||
func validateSecret(secret *v1.Secret, conditions []*v1alpha1.Condition) ([]*v1alpha1.Condition, []string) {
|
||||
func validateSecret(secret *v1.Secret, conditions []*v1alpha1.Condition, minBcryptCost int) ([]*v1alpha1.Condition, []string) {
|
||||
emptyList := []string{}
|
||||
|
||||
if secret == nil {
|
||||
@ -188,10 +188,10 @@ func validateSecret(secret *v1.Secret, conditions []*v1alpha1.Condition) ([]*v1a
|
||||
bcryptErrs = append(bcryptErrs, fmt.Sprintf(
|
||||
"hashed client secret at index %d: %s",
|
||||
i, err.Error()))
|
||||
} else if cost < minimumRequiredBcryptCost {
|
||||
} else if cost < minBcryptCost {
|
||||
bcryptErrs = append(bcryptErrs, fmt.Sprintf(
|
||||
"hashed client secret at index %d: bcrypt cost %d is below the required minimum of %d",
|
||||
i, cost, minimumRequiredBcryptCost))
|
||||
i, cost, minBcryptCost))
|
||||
}
|
||||
}
|
||||
if len(bcryptErrs) > 0 {
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"go.pinniped.dev/internal/oidc/idpdiscovery"
|
||||
"go.pinniped.dev/internal/oidc/jwks"
|
||||
"go.pinniped.dev/internal/oidc/login"
|
||||
"go.pinniped.dev/internal/oidc/oidcclientvalidator"
|
||||
"go.pinniped.dev/internal/oidc/provider"
|
||||
"go.pinniped.dev/internal/oidc/token"
|
||||
"go.pinniped.dev/internal/plog"
|
||||
@ -98,7 +99,7 @@ func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIs
|
||||
// Use NullStorage for the authorize endpoint because we do not actually want to store anything until
|
||||
// the upstream callback endpoint is called later.
|
||||
oauthHelperWithNullStorage := oidc.FositeOauth2Helper(
|
||||
oidc.NewNullStorage(m.secretsClient, m.oidcClientsClient),
|
||||
oidc.NewNullStorage(m.secretsClient, m.oidcClientsClient, oidcclientvalidator.DefaultMinBcryptCost),
|
||||
issuer,
|
||||
tokenHMACKeyGetter,
|
||||
nil,
|
||||
@ -107,7 +108,7 @@ func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIs
|
||||
|
||||
// For all the other endpoints, make another oauth helper with exactly the same settings except use real storage.
|
||||
oauthHelperWithKubeStorage := oidc.FositeOauth2Helper(
|
||||
oidc.NewKubeStorage(m.secretsClient, m.oidcClientsClient, timeoutsConfiguration),
|
||||
oidc.NewKubeStorage(m.secretsClient, m.oidcClientsClient, timeoutsConfiguration, oidcclientvalidator.DefaultMinBcryptCost),
|
||||
issuer,
|
||||
tokenHMACKeyGetter,
|
||||
m.dynamicJWKSProvider,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -13,15 +13,17 @@ import (
|
||||
"github.com/ory/fosite/compose"
|
||||
"github.com/ory/fosite/handler/oauth2"
|
||||
"github.com/ory/fosite/handler/openid"
|
||||
"github.com/ory/x/errorsx"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"go.pinniped.dev/internal/oidc/clientregistry"
|
||||
)
|
||||
|
||||
const (
|
||||
tokenTypeAccessToken = "urn:ietf:params:oauth:token-type:access_token" //nolint: gosec
|
||||
tokenTypeJWT = "urn:ietf:params:oauth:token-type:jwt" //nolint: gosec
|
||||
pinnipedTokenExchangeScope = "pinniped:request-audience" //nolint: gosec
|
||||
tokenExchangeGrantType = "urn:ietf:params:oauth:grant-type:token-exchange" //nolint: gosec
|
||||
tokenTypeAccessToken = "urn:ietf:params:oauth:token-type:access_token" //nolint: gosec
|
||||
tokenTypeJWT = "urn:ietf:params:oauth:token-type:jwt" //nolint: gosec
|
||||
pinnipedTokenExchangeScope = "pinniped:request-audience" //nolint: gosec
|
||||
)
|
||||
|
||||
type stsParams struct {
|
||||
@ -70,6 +72,18 @@ func (t *TokenExchangeHandler) PopulateTokenEndpointResponse(ctx context.Context
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
// Check that the currently authenticated client and the client which was originally used to get the access token are the same.
|
||||
if originalRequester.GetClient().GetID() != requester.GetClient().GetID() {
|
||||
// This error message is copied from the similar check in fosite's flow_authorize_code_token.go.
|
||||
return errorsx.WithStack(fosite.ErrInvalidGrant.WithHint("The OAuth 2.0 Client ID from this request does not match the one from the authorize request."))
|
||||
}
|
||||
|
||||
// Check that the client is allowed to perform this grant type.
|
||||
if !requester.GetClient().GetGrantTypes().Has(tokenExchangeGrantType) {
|
||||
// This error message is trying to be similar to the analogous one in fosite's flow_authorize_code_token.go.
|
||||
return errorsx.WithStack(fosite.ErrUnauthorizedClient.WithHintf("The OAuth 2.0 Client is not allowed to use token exchange grant \"%s\".", tokenExchangeGrantType))
|
||||
}
|
||||
|
||||
// Require that the incoming access token has the pinniped:request-audience and OpenID scopes.
|
||||
if !originalRequester.GetGrantedScopes().Has(pinnipedTokenExchangeScope) {
|
||||
return errors.WithStack(fosite.ErrAccessDenied.WithHintf("missing the %q scope", pinnipedTokenExchangeScope))
|
||||
@ -168,5 +182,5 @@ func (t *TokenExchangeHandler) CanSkipClientAuth(_ fosite.AccessRequester) bool
|
||||
}
|
||||
|
||||
func (t *TokenExchangeHandler) CanHandleTokenEndpointRequest(requester fosite.AccessRequester) bool {
|
||||
return requester.GetGrantTypes().ExactOne("urn:ietf:params:oauth:grant-type:token-exchange")
|
||||
return requester.GetGrantTypes().ExactOne(tokenExchangeGrantType)
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
v12 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
)
|
||||
|
||||
@ -54,6 +55,23 @@ func RequireNumberOfSecretsMatchingLabelSelector(t *testing.T, secrets v1.Secret
|
||||
require.Len(t, storedAuthcodeSecrets.Items, expectedNumberOfSecrets)
|
||||
}
|
||||
|
||||
func RequireNumberOfSecretsExcludingLabelSelector(t *testing.T, secrets v1.SecretInterface, labelSet labels.Set, expectedNumberOfSecrets int) {
|
||||
t.Helper()
|
||||
|
||||
selector := labels.Everything()
|
||||
for k, v := range labelSet {
|
||||
requirement, err := labels.NewRequirement(k, selection.NotEquals, []string{v})
|
||||
require.NoError(t, err)
|
||||
selector = selector.Add(*requirement)
|
||||
}
|
||||
|
||||
storedAuthcodeSecrets, err := secrets.List(context.Background(), v12.ListOptions{
|
||||
LabelSelector: selector.String(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, storedAuthcodeSecrets.Items, expectedNumberOfSecrets)
|
||||
}
|
||||
|
||||
func RequireSecurityHeadersWithFormPostPageCSPs(t *testing.T, response *httptest.ResponseRecorder) {
|
||||
// Loosely confirm that the unique CSPs needed for the form_post page were used.
|
||||
cspHeader := response.Header().Get("Content-Security-Policy")
|
||||
|
67
internal/testutil/oidcclient.go
Normal file
67
internal/testutil/oidcclient.go
Normal file
@ -0,0 +1,67 @@
|
||||
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
||||
)
|
||||
|
||||
const (
|
||||
AllDynamicClientScopesSpaceSep = "openid offline_access pinniped:request-audience username groups"
|
||||
|
||||
// PlaintextPassword1 is a fake client secret for use in unit tests, along with several flavors of the bcrypt
|
||||
// hashed version of the password. Do not use for integration tests.
|
||||
PlaintextPassword1 = "password1"
|
||||
HashedPassword1AtGoMinCost = "$2a$04$JfX1ba/ctAt3AGk73E9Zz.Fdki5GiQtj.O/CnPbRRSKQWWfv1svoe" //nolint:gosec // this is not a credential
|
||||
HashedPassword1JustBelowSupervisorMinCost = "$2a$11$w/incy7Z1/ljLYvv2XRg4.WrPgY9oR7phebcgr6rGA3u/5TG9MKOe" //nolint:gosec // this is not a credential
|
||||
HashedPassword1AtSupervisorMinCost = "$2a$12$id4i/yFYxS99txKOFEeboea2kU6DyZY0Nh4ul0eR46sDuoFoNTRV." //nolint:gosec // this is not a credential
|
||||
HashedPassword1InvalidFormat = "$2a$12$id4i/yFYxS99txKOFEeboea2kU6DyZY0Nh4ul0eR46sDuo" //nolint:gosec // this is not a credential
|
||||
|
||||
// PlaintextPassword2 is a second fake client secret for use in unit tests, along with several flavors of the bcrypt
|
||||
// hashed version of the password. Do not use for integration tests.
|
||||
PlaintextPassword2 = "password2"
|
||||
HashedPassword2AtGoMinCost = "$2a$04$VQ5z6kkgU8JPLGSGctg.s.iYyoac3Oisa/SIM3sDK5BxTrVbCkyNm" //nolint:gosec // this is not a credential
|
||||
HashedPassword2AtSupervisorMinCost = "$2a$12$SdUqoJOn4/3yEQfJx616V.q.f76KaXD.ISgJT1oydqFdgfjJpBh6u" //nolint:gosec // this is not a credential
|
||||
)
|
||||
|
||||
// allDynamicClientScopes returns a slice of all scopes that are supported by the Supervisor for dynamic clients.
|
||||
func allDynamicClientScopes() []configv1alpha1.Scope {
|
||||
scopes := []configv1alpha1.Scope{}
|
||||
for _, s := range strings.Split(AllDynamicClientScopesSpaceSep, " ") {
|
||||
scopes = append(scopes, configv1alpha1.Scope(s))
|
||||
}
|
||||
return scopes
|
||||
}
|
||||
|
||||
// fullyCapableOIDCClient returns an OIDC client which is allowed to use all grant types and all scopes that
|
||||
// are supported by the Supervisor for dynamic clients.
|
||||
func fullyCapableOIDCClient(namespace string, clientID string, clientUID string, redirectURI string) *configv1alpha1.OIDCClient {
|
||||
return &configv1alpha1.OIDCClient{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: clientID, Generation: 1, UID: types.UID(clientUID)},
|
||||
Spec: configv1alpha1.OIDCClientSpec{
|
||||
AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"},
|
||||
AllowedScopes: allDynamicClientScopes(),
|
||||
AllowedRedirectURIs: []configv1alpha1.RedirectURI{configv1alpha1.RedirectURI(redirectURI)},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func FullyCapableOIDCClientAndStorageSecret(
|
||||
t *testing.T,
|
||||
namespace string,
|
||||
clientID string,
|
||||
clientUID string,
|
||||
redirectURI string,
|
||||
hashes []string,
|
||||
) (*configv1alpha1.OIDCClient, *corev1.Secret) {
|
||||
return fullyCapableOIDCClient(namespace, clientID, clientUID, redirectURI),
|
||||
OIDCClientSecretStorageSecretForUID(t, namespace, clientUID, hashes)
|
||||
}
|
61
internal/testutil/oidcclient_test.go
Normal file
61
internal/testutil/oidcclient_test.go
Normal file
@ -0,0 +1,61 @@
|
||||
// Copyright 2022 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.pinniped.dev/internal/oidc/oidcclientvalidator"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func TestBcryptConstants(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// It would be helpful to know if upgrading golang changes these constants some day, so test them here for visibility during upgrades.
|
||||
require.Equal(t, 4, bcrypt.MinCost, "golang has changed bcrypt.MinCost: please consider implications to the other tests")
|
||||
require.Equal(t, 10, bcrypt.DefaultCost, "golang has changed bcrypt.DefaultCost: please consider implications to the production code and tests")
|
||||
}
|
||||
|
||||
func TestBcryptHashedPassword1TestHelpers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Can use this to help generate or regenerate the test helper hash constants.
|
||||
// t.Log(generateHash(t, PlaintextPassword1, 12))
|
||||
|
||||
require.NoError(t, bcrypt.CompareHashAndPassword([]byte(HashedPassword1AtGoMinCost), []byte(PlaintextPassword1)))
|
||||
require.NoError(t, bcrypt.CompareHashAndPassword([]byte(HashedPassword1JustBelowSupervisorMinCost), []byte(PlaintextPassword1)))
|
||||
require.NoError(t, bcrypt.CompareHashAndPassword([]byte(HashedPassword1AtSupervisorMinCost), []byte(PlaintextPassword1)))
|
||||
|
||||
requireCost(t, bcrypt.MinCost, HashedPassword1AtGoMinCost)
|
||||
requireCost(t, oidcclientvalidator.DefaultMinBcryptCost-1, HashedPassword1JustBelowSupervisorMinCost)
|
||||
requireCost(t, oidcclientvalidator.DefaultMinBcryptCost, HashedPassword1AtSupervisorMinCost)
|
||||
}
|
||||
|
||||
func TestBcryptHashedPassword2TestHelpers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Can use this to help generate or regenerate the test helper hash constants.
|
||||
// t.Log(generateHash(t, PlaintextPassword2, 12))
|
||||
|
||||
require.NoError(t, bcrypt.CompareHashAndPassword([]byte(HashedPassword2AtGoMinCost), []byte(PlaintextPassword2)))
|
||||
require.NoError(t, bcrypt.CompareHashAndPassword([]byte(HashedPassword2AtSupervisorMinCost), []byte(PlaintextPassword2)))
|
||||
|
||||
requireCost(t, bcrypt.MinCost, HashedPassword2AtGoMinCost)
|
||||
requireCost(t, oidcclientvalidator.DefaultMinBcryptCost, HashedPassword2AtSupervisorMinCost)
|
||||
}
|
||||
|
||||
func generateHash(t *testing.T, password string, cost int) string { //nolint:unused,deadcode // used in comments above
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), cost)
|
||||
require.NoError(t, err)
|
||||
return string(hash)
|
||||
}
|
||||
|
||||
func requireCost(t *testing.T, wantCost int, hash string) {
|
||||
cost, err := bcrypt.Cost([]byte(hash))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, wantCost, cost)
|
||||
}
|
@ -31,6 +31,7 @@ import (
|
||||
idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
|
||||
"go.pinniped.dev/internal/certauthority"
|
||||
"go.pinniped.dev/internal/oidc"
|
||||
"go.pinniped.dev/internal/oidc/oidcclientvalidator"
|
||||
"go.pinniped.dev/internal/psession"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
"go.pinniped.dev/pkg/oidcclient/nonce"
|
||||
@ -1727,7 +1728,7 @@ func testSupervisorLogin(
|
||||
// First use the latest downstream refresh token to look up the corresponding session in the Supervisor's storage.
|
||||
supervisorSecretsClient := testlib.NewKubernetesClientset(t).CoreV1().Secrets(env.SupervisorNamespace)
|
||||
supervisorOIDCClientsClient := testlib.NewSupervisorClientset(t).ConfigV1alpha1().OIDCClients(env.SupervisorNamespace)
|
||||
oauthStore := oidc.NewKubeStorage(supervisorSecretsClient, supervisorOIDCClientsClient, oidc.DefaultOIDCTimeoutsConfiguration())
|
||||
oauthStore := oidc.NewKubeStorage(supervisorSecretsClient, supervisorOIDCClientsClient, oidc.DefaultOIDCTimeoutsConfiguration(), oidcclientvalidator.DefaultMinBcryptCost)
|
||||
storedRefreshSession, err := oauthStore.GetRefreshTokenSession(ctx, signatureOfLatestRefreshToken, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -1772,7 +1773,7 @@ func testSupervisorLogin(
|
||||
// First use the latest downstream refresh token to look up the corresponding session in the Supervisor's storage.
|
||||
supervisorSecretsClient := testlib.NewKubernetesClientset(t).CoreV1().Secrets(env.SupervisorNamespace)
|
||||
supervisorOIDCClientsClient := testlib.NewSupervisorClientset(t).ConfigV1alpha1().OIDCClients(env.SupervisorNamespace)
|
||||
oauthStore := oidc.NewKubeStorage(supervisorSecretsClient, supervisorOIDCClientsClient, oidc.DefaultOIDCTimeoutsConfiguration())
|
||||
oauthStore := oidc.NewKubeStorage(supervisorSecretsClient, supervisorOIDCClientsClient, oidc.DefaultOIDCTimeoutsConfiguration(), oidcclientvalidator.DefaultMinBcryptCost)
|
||||
storedRefreshSession, err := oauthStore.GetRefreshTokenSession(ctx, signatureOfLatestRefreshToken, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -563,7 +563,7 @@ func TestOIDCClientControllerValidations_Parallel(t *testing.T) {
|
||||
AllowedScopes: []supervisorconfigv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"},
|
||||
},
|
||||
},
|
||||
secret: testutil.OIDCClientSecretStorageSecretWithoutName(t, env.SupervisorNamespace, []string{"$2y$15$Kh7cRj0ScSD5QelE3ZNSl.nF04JDv7zb3SgGN.tSfLIX.4kt3UX7m"}),
|
||||
secret: testutil.OIDCClientSecretStorageSecretWithoutName(t, env.SupervisorNamespace, []string{testutil.HashedPassword1AtSupervisorMinCost}),
|
||||
wantPhase: "Ready",
|
||||
wantConditions: []supervisorconfigv1alpha1.Condition{
|
||||
{
|
||||
|
@ -31,6 +31,7 @@ import (
|
||||
idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
|
||||
"go.pinniped.dev/internal/certauthority"
|
||||
"go.pinniped.dev/internal/oidc"
|
||||
"go.pinniped.dev/internal/oidc/oidcclientvalidator"
|
||||
"go.pinniped.dev/internal/psession"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
"go.pinniped.dev/pkg/oidcclient"
|
||||
@ -188,7 +189,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) {
|
||||
// out of kube secret storage.
|
||||
supervisorSecretsClient := testlib.NewKubernetesClientset(t).CoreV1().Secrets(env.SupervisorNamespace)
|
||||
supervisorOIDCClientsClient := testlib.NewSupervisorClientset(t).ConfigV1alpha1().OIDCClients(env.SupervisorNamespace)
|
||||
oauthStore := oidc.NewKubeStorage(supervisorSecretsClient, supervisorOIDCClientsClient, oidc.DefaultOIDCTimeoutsConfiguration())
|
||||
oauthStore := oidc.NewKubeStorage(supervisorSecretsClient, supervisorOIDCClientsClient, oidc.DefaultOIDCTimeoutsConfiguration(), oidcclientvalidator.DefaultMinBcryptCost)
|
||||
refreshTokenSignature := strings.Split(token.RefreshToken.Token, ".")[1]
|
||||
storedRefreshSession, err := oauthStore.GetRefreshTokenSession(ctx, refreshTokenSignature, nil)
|
||||
require.NoError(t, err)
|
||||
@ -496,7 +497,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) {
|
||||
// out of kube secret storage.
|
||||
supervisorSecretsClient := testlib.NewKubernetesClientset(t).CoreV1().Secrets(env.SupervisorNamespace)
|
||||
supervisorOIDCClientsClient := testlib.NewSupervisorClientset(t).ConfigV1alpha1().OIDCClients(env.SupervisorNamespace)
|
||||
oauthStore := oidc.NewKubeStorage(supervisorSecretsClient, supervisorOIDCClientsClient, oidc.DefaultOIDCTimeoutsConfiguration())
|
||||
oauthStore := oidc.NewKubeStorage(supervisorSecretsClient, supervisorOIDCClientsClient, oidc.DefaultOIDCTimeoutsConfiguration(), oidcclientvalidator.DefaultMinBcryptCost)
|
||||
refreshTokenSignature := strings.Split(token.RefreshToken.Token, ".")[1]
|
||||
storedRefreshSession, err := oauthStore.GetRefreshTokenSession(ctx, refreshTokenSignature, nil)
|
||||
require.NoError(t, err)
|
||||
|
Loading…
Reference in New Issue
Block a user