34509e7430
- 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.
128 lines
4.9 KiB
Go
128 lines
4.9 KiB
Go
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package testutil
|
|
|
|
import (
|
|
"context"
|
|
"mime"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"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"
|
|
)
|
|
|
|
func RequireTimeInDelta(t *testing.T, t1 time.Time, t2 time.Time, delta time.Duration) {
|
|
require.InDeltaf(t,
|
|
float64(t1.UnixNano()),
|
|
float64(t2.UnixNano()),
|
|
float64(delta.Nanoseconds()),
|
|
"expected %s and %s to be < %s apart, but they are %s apart",
|
|
t1.Format(time.RFC3339Nano),
|
|
t2.Format(time.RFC3339Nano),
|
|
delta.String(),
|
|
t1.Sub(t2).String(),
|
|
)
|
|
}
|
|
|
|
func RequireEqualContentType(t *testing.T, actual string, expected string) {
|
|
t.Helper()
|
|
|
|
if expected == "" {
|
|
require.Empty(t, actual)
|
|
return
|
|
}
|
|
|
|
actualContentType, actualContentTypeParams, err := mime.ParseMediaType(expected)
|
|
require.NoError(t, err)
|
|
expectedContentType, expectedContentTypeParams, err := mime.ParseMediaType(expected)
|
|
require.NoError(t, err)
|
|
require.Equal(t, actualContentType, expectedContentType)
|
|
require.Equal(t, actualContentTypeParams, expectedContentTypeParams)
|
|
}
|
|
|
|
func RequireNumberOfSecretsMatchingLabelSelector(t *testing.T, secrets v1.SecretInterface, labelSet labels.Set, expectedNumberOfSecrets int) {
|
|
t.Helper()
|
|
storedAuthcodeSecrets, err := secrets.List(context.Background(), v12.ListOptions{
|
|
LabelSelector: labelSet.String(),
|
|
})
|
|
require.NoError(t, err)
|
|
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")
|
|
require.Contains(t, cspHeader, "script-src '") // loose assertion
|
|
require.Contains(t, cspHeader, "style-src '") // loose assertion
|
|
require.Contains(t, cspHeader, "img-src data:")
|
|
require.Contains(t, cspHeader, "connect-src *")
|
|
|
|
// Also require all the usual security headers.
|
|
requireSecurityHeaders(t, response)
|
|
}
|
|
|
|
func RequireSecurityHeadersWithLoginPageCSPs(t *testing.T, response *httptest.ResponseRecorder) {
|
|
// Loosely confirm that the unique CSPs needed for the login page were used.
|
|
cspHeader := response.Header().Get("Content-Security-Policy")
|
|
require.Contains(t, cspHeader, "style-src '") // loose assertion
|
|
require.NotContains(t, cspHeader, "script-src") // only needed by form_post page
|
|
require.NotContains(t, cspHeader, "img-src data:") // only needed by form_post page
|
|
require.NotContains(t, cspHeader, "connect-src *") // only needed by form_post page
|
|
|
|
// Also require all the usual security headers.
|
|
requireSecurityHeaders(t, response)
|
|
}
|
|
|
|
func RequireSecurityHeadersWithoutCustomCSPs(t *testing.T, response *httptest.ResponseRecorder) {
|
|
// Confirm that the unique CSPs needed for the form_post or login page were NOT used.
|
|
cspHeader := response.Header().Get("Content-Security-Policy")
|
|
require.NotContains(t, cspHeader, "script-src")
|
|
require.NotContains(t, cspHeader, "style-src")
|
|
require.NotContains(t, cspHeader, "img-src data:")
|
|
require.NotContains(t, cspHeader, "connect-src *")
|
|
|
|
// Also require all the usual security headers.
|
|
requireSecurityHeaders(t, response)
|
|
}
|
|
|
|
func requireSecurityHeaders(t *testing.T, response *httptest.ResponseRecorder) {
|
|
// Loosely confirm that the generic default CSPs were used.
|
|
cspHeader := response.Header().Get("Content-Security-Policy")
|
|
require.Contains(t, cspHeader, "default-src 'none'")
|
|
require.Contains(t, cspHeader, "frame-ancestors 'none'")
|
|
|
|
require.Equal(t, "DENY", response.Header().Get("X-Frame-Options"))
|
|
require.Equal(t, "1; mode=block", response.Header().Get("X-XSS-Protection"))
|
|
require.Equal(t, "nosniff", response.Header().Get("X-Content-Type-Options"))
|
|
require.Equal(t, "no-referrer", response.Header().Get("Referrer-Policy"))
|
|
require.Equal(t, "off", response.Header().Get("X-DNS-Prefetch-Control"))
|
|
require.Equal(t, "no-cache", response.Header().Get("Pragma"))
|
|
require.Equal(t, "0", response.Header().Get("Expires"))
|
|
|
|
// This check is more relaxed since Fosite can override the base header we set.
|
|
require.Contains(t, response.Header().Get("Cache-Control"), "no-store")
|
|
}
|