Merge pull request #115122 from r-erema/110782-oidc-test-coverage

add integration tests for OIDC authenticator
This commit is contained in:
Kubernetes Prow Robot 2023-07-10 15:29:10 -07:00 committed by GitHub
commit ad72319ece
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 899 additions and 0 deletions

View File

@ -0,0 +1,27 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package oidc
import (
"testing"
"k8s.io/kubernetes/test/integration/framework"
)
func TestMain(m *testing.M) {
framework.EtcdMain(m.Run)
}

View File

@ -0,0 +1,496 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package oidc
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"net"
"net/http"
"net/url"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd/api"
certutil "k8s.io/client-go/util/cert"
kubeapiserverapptesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
"k8s.io/kubernetes/pkg/apis/rbac"
"k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes"
"k8s.io/kubernetes/test/integration/framework"
utilsoidc "k8s.io/kubernetes/test/utils/oidc"
utilsnet "k8s.io/utils/net"
)
const (
defaultNamespace = "default"
defaultOIDCClientID = "f403b682-603f-4ec9-b3e4-cf111ef36f7c"
defaultOIDCClaimedUsername = "john_doe"
defaultOIDCUsernamePrefix = "k8s-"
defaultRBACRoleName = "developer-role"
defaultRBACRoleBindingName = "developer-role-binding"
defaultStubRefreshToken = "_fake_refresh_token_"
defaultStubAccessToken = "_fake_access_token_"
rsaKeyBitSize = 2048
)
var (
defaultRole = &rbacv1.Role{
TypeMeta: metav1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "Role"},
ObjectMeta: metav1.ObjectMeta{Name: defaultRBACRoleName},
Rules: []rbacv1.PolicyRule{
{
Verbs: []string{"list"},
Resources: []string{"pods"},
APIGroups: []string{""},
ResourceNames: []string{},
},
},
}
defaultRoleBinding = &rbacv1.RoleBinding{
TypeMeta: metav1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "RoleBinding"},
ObjectMeta: metav1.ObjectMeta{Name: defaultRBACRoleBindingName},
Subjects: []rbacv1.Subject{
{
APIGroup: rbac.GroupName,
Kind: rbacv1.UserKind,
Name: defaultOIDCUsernamePrefix + defaultOIDCClaimedUsername,
},
},
RoleRef: rbacv1.RoleRef{
APIGroup: rbac.GroupName,
Kind: "Role",
Name: defaultRBACRoleName,
},
}
)
func TestOIDC(t *testing.T) {
var tests = []struct {
name string
configureInfrastructure func(t *testing.T) (
oidcServer *utilsoidc.TestServer,
apiServer *kubeapiserverapptesting.TestServer,
signingPrivateKey *rsa.PrivateKey,
caCertContent []byte,
caFilePath string,
)
configureOIDCServerBehaviour func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey)
configureClient func(
t *testing.T,
restCfg *rest.Config,
caCert []byte,
certPath,
oidcServerURL,
oidcServerTokenURL string,
) *kubernetes.Clientset
asserErrFn func(t *testing.T, errorToCheck error)
}{
{
name: "ID token is ok",
configureInfrastructure: configureTestInfrastructure,
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
idTokenLifetime := time.Second * 1200
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviourReturningPredefinedJWT(
t,
signingPrivateKey,
oidcServer.URL(),
defaultOIDCClientID,
defaultOIDCClaimedUsername,
defaultStubAccessToken,
defaultStubRefreshToken,
time.Now().Add(idTokenLifetime).Unix(),
))
},
configureClient: configureClientFetchingOIDCCredentials,
asserErrFn: func(t *testing.T, errorToCheck error) {
assert.NoError(t, errorToCheck)
},
},
{
name: "ID token is expired",
configureInfrastructure: configureTestInfrastructure,
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
configureOIDCServerToReturnExpiredIDToken(t, 2, oidcServer, signingPrivateKey)
},
configureClient: configureClientFetchingOIDCCredentials,
asserErrFn: func(t *testing.T, errorToCheck error) {
assert.True(t, apierrors.IsUnauthorized(errorToCheck), errorToCheck)
},
},
{
name: "wrong client ID",
configureInfrastructure: configureTestInfrastructure,
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, _ *rsa.PrivateKey) {
oidcServer.TokenHandler().EXPECT().Token().Times(2).Return(utilsoidc.Token{}, utilsoidc.ErrBadClientID)
},
configureClient: configureClientWithEmptyIDToken,
asserErrFn: func(t *testing.T, errorToCheck error) {
urlError, ok := errorToCheck.(*url.Error)
require.True(t, ok)
assert.Equal(
t,
"failed to refresh token: oauth2: cannot fetch token: 400 Bad Request\nResponse: client ID is bad\n",
urlError.Err.Error(),
)
},
},
{
name: "client has wrong CA",
configureInfrastructure: configureTestInfrastructure,
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, _ *rsa.PrivateKey) {},
configureClient: func(t *testing.T, restCfg *rest.Config, caCert []byte, _, oidcServerURL, oidcServerTokenURL string) *kubernetes.Clientset {
tempDir := t.TempDir()
certFilePath := filepath.Join(tempDir, "localhost_127.0.0.1_.crt")
_, _, wantErr := certutil.GenerateSelfSignedCertKeyWithFixtures("localhost", []net.IP{utilsnet.ParseIPSloppy("127.0.0.1")}, nil, tempDir)
require.NoError(t, wantErr)
return configureClientWithEmptyIDToken(t, restCfg, caCert, certFilePath, oidcServerURL, oidcServerTokenURL)
},
asserErrFn: func(t *testing.T, errorToCheck error) {
expectedErr := new(x509.UnknownAuthorityError)
assert.ErrorAs(t, errorToCheck, expectedErr)
},
},
{
name: "refresh flow does not return ID Token",
configureInfrastructure: configureTestInfrastructure,
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
configureOIDCServerToReturnExpiredIDToken(t, 1, oidcServer, signingPrivateKey)
oidcServer.TokenHandler().EXPECT().Token().Times(1).Return(utilsoidc.Token{
IDToken: "",
AccessToken: defaultStubAccessToken,
RefreshToken: defaultStubRefreshToken,
ExpiresIn: time.Now().Add(time.Second * 1200).Unix(),
}, nil)
},
configureClient: configureClientFetchingOIDCCredentials,
asserErrFn: func(t *testing.T, errorToCheck error) {
expectedError := new(apierrors.StatusError)
assert.ErrorAs(t, errorToCheck, &expectedError)
assert.Equal(
t,
`pods is forbidden: User "system:anonymous" cannot list resource "pods" in API group "" in the namespace "default"`,
errorToCheck.Error(),
)
},
},
{
name: "ID token signature can not be verified due to wrong JWKs",
configureInfrastructure: func(t *testing.T) (
oidcServer *utilsoidc.TestServer,
apiServer *kubeapiserverapptesting.TestServer,
signingPrivateKey *rsa.PrivateKey,
caCertContent []byte,
caFilePath string,
) {
caCertContent, _, caFilePath, caKeyFilePath := generateCert(t)
signingPrivateKey, wantErr := rsa.GenerateKey(rand.Reader, rsaKeyBitSize)
require.NoError(t, wantErr)
oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath)
apiServer = startTestAPIServerForOIDC(t, oidcServer.URL(), defaultOIDCClientID, caFilePath)
adminClient := kubernetes.NewForConfigOrDie(apiServer.ClientConfig)
configureRBAC(t, adminClient, defaultRole, defaultRoleBinding)
anotherSigningPrivateKey, wantErr := rsa.GenerateKey(rand.Reader, rsaKeyBitSize)
require.NoError(t, wantErr)
oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehaviour(t, &anotherSigningPrivateKey.PublicKey))
return oidcServer, apiServer, signingPrivateKey, caCertContent, caFilePath
},
configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviourReturningPredefinedJWT(
t,
signingPrivateKey,
oidcServer.URL(),
defaultOIDCClientID,
defaultOIDCClaimedUsername,
defaultStubAccessToken,
defaultStubRefreshToken,
time.Now().Add(time.Second*1200).Unix(),
))
},
configureClient: configureClientFetchingOIDCCredentials,
asserErrFn: func(t *testing.T, errorToCheck error) {
assert.True(t, apierrors.IsUnauthorized(errorToCheck), errorToCheck)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
oidcServer, apiServer, signingPrivateKey, caCert, certPath := tt.configureInfrastructure(t)
tt.configureOIDCServerBehaviour(t, oidcServer, signingPrivateKey)
tokenURL, err := oidcServer.TokenURL()
require.NoError(t, err)
client := tt.configureClient(t, apiServer.ClientConfig, caCert, certPath, oidcServer.URL(), tokenURL)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
_, err = client.CoreV1().Pods(defaultNamespace).List(ctx, metav1.ListOptions{})
tt.asserErrFn(t, err)
})
}
}
func TestUpdatingRefreshTokenInCaseOfExpiredIDToken(t *testing.T) {
var tests = []struct {
name string
configureUpdatingTokenBehaviour func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey)
asserErrFn func(t *testing.T, errorToCheck error)
}{
{
name: "cache returns stale client if refresh token is not updated in config",
configureUpdatingTokenBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviourReturningPredefinedJWT(
t,
signingPrivateKey,
oidcServer.URL(),
defaultOIDCClientID,
defaultOIDCClaimedUsername,
defaultStubAccessToken,
defaultStubRefreshToken,
time.Now().Add(time.Second*1200).Unix(),
))
configureOIDCServerToReturnExpiredRefreshTokenErrorOnTryingToUpdateIDToken(oidcServer)
},
asserErrFn: func(t *testing.T, errorToCheck error) {
urlError, ok := errorToCheck.(*url.Error)
require.True(t, ok)
assert.Equal(
t,
"failed to refresh token: oauth2: cannot fetch token: 400 Bad Request\nResponse: refresh token is expired\n",
urlError.Err.Error(),
)
},
},
}
oidcServer, apiServer, signingPrivateKey, caCert, certPath := configureTestInfrastructure(t)
tokenURL, err := oidcServer.TokenURL()
require.NoError(t, err)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
expiredIDToken, stubRefreshToken := fetchExpiredToken(t, oidcServer, caCert, signingPrivateKey)
clientConfig := configureClientConfigForOIDC(t, apiServer.ClientConfig, defaultOIDCClientID, certPath, expiredIDToken, stubRefreshToken, oidcServer.URL())
expiredClient := kubernetes.NewForConfigOrDie(clientConfig)
configureOIDCServerToReturnExpiredRefreshTokenErrorOnTryingToUpdateIDToken(oidcServer)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
_, err = expiredClient.CoreV1().Pods(defaultNamespace).List(ctx, metav1.ListOptions{})
assert.Error(t, err)
tt.configureUpdatingTokenBehaviour(t, oidcServer, signingPrivateKey)
idToken, stubRefreshToken := fetchOIDCCredentials(t, tokenURL, caCert)
clientConfig = configureClientConfigForOIDC(t, apiServer.ClientConfig, defaultOIDCClientID, certPath, idToken, stubRefreshToken, oidcServer.URL())
expectedOkClient := kubernetes.NewForConfigOrDie(clientConfig)
_, err = expectedOkClient.CoreV1().Pods(defaultNamespace).List(ctx, metav1.ListOptions{})
tt.asserErrFn(t, err)
})
}
}
func configureTestInfrastructure(t *testing.T) (
oidcServer *utilsoidc.TestServer,
apiServer *kubeapiserverapptesting.TestServer,
signingPrivateKey *rsa.PrivateKey,
caCertContent []byte,
caFilePath string,
) {
t.Helper()
caCertContent, _, caFilePath, caKeyFilePath := generateCert(t)
signingPrivateKey, err := rsa.GenerateKey(rand.Reader, rsaKeyBitSize)
require.NoError(t, err)
oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath)
apiServer = startTestAPIServerForOIDC(t, oidcServer.URL(), defaultOIDCClientID, caFilePath)
oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehaviour(t, &signingPrivateKey.PublicKey))
adminClient := kubernetes.NewForConfigOrDie(apiServer.ClientConfig)
configureRBAC(t, adminClient, defaultRole, defaultRoleBinding)
return oidcServer, apiServer, signingPrivateKey, caCertContent, caFilePath
}
func configureClientFetchingOIDCCredentials(t *testing.T, restCfg *rest.Config, caCert []byte, certPath, oidcServerURL, oidcServerTokenURL string) *kubernetes.Clientset {
idToken, stubRefreshToken := fetchOIDCCredentials(t, oidcServerTokenURL, caCert)
clientConfig := configureClientConfigForOIDC(t, restCfg, defaultOIDCClientID, certPath, idToken, stubRefreshToken, oidcServerURL)
return kubernetes.NewForConfigOrDie(clientConfig)
}
func configureClientWithEmptyIDToken(t *testing.T, restCfg *rest.Config, _ []byte, certPath, oidcServerURL, _ string) *kubernetes.Clientset {
emptyIDToken, stubRefreshToken := "", defaultStubRefreshToken
clientConfig := configureClientConfigForOIDC(t, restCfg, defaultOIDCClientID, certPath, emptyIDToken, stubRefreshToken, oidcServerURL)
return kubernetes.NewForConfigOrDie(clientConfig)
}
func configureRBAC(t *testing.T, clientset *kubernetes.Clientset, role *rbacv1.Role, binding *rbacv1.RoleBinding) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
_, err := clientset.RbacV1().Roles(defaultNamespace).Create(ctx, role, metav1.CreateOptions{})
require.NoError(t, err)
_, err = clientset.RbacV1().RoleBindings(defaultNamespace).Create(ctx, binding, metav1.CreateOptions{})
require.NoError(t, err)
}
func configureClientConfigForOIDC(t *testing.T, config *rest.Config, clientID, caFilePath, idToken, refreshToken, oidcServerURL string) *rest.Config {
t.Helper()
cfg := rest.AnonymousClientConfig(config)
cfg.AuthProvider = &api.AuthProviderConfig{
Name: "oidc",
Config: map[string]string{
"client-id": clientID,
"id-token": idToken,
"idp-issuer-url": oidcServerURL,
"idp-certificate-authority": caFilePath,
"refresh-token": refreshToken,
},
}
return cfg
}
func startTestAPIServerForOIDC(t *testing.T, oidcURL, oidcClientID, oidcCAFilePath string) *kubeapiserverapptesting.TestServer {
t.Helper()
server, err := kubeapiserverapptesting.StartTestServer(
t,
kubeapiserverapptesting.NewDefaultTestServerOptions(),
[]string{
fmt.Sprintf("--oidc-issuer-url=%s", oidcURL),
fmt.Sprintf("--oidc-client-id=%s", oidcClientID),
fmt.Sprintf("--oidc-ca-file=%s", oidcCAFilePath),
fmt.Sprintf("--oidc-username-prefix=%s", defaultOIDCUsernamePrefix),
fmt.Sprintf("--authorization-mode=%s", modes.ModeRBAC),
},
framework.SharedEtcd(),
)
require.NoError(t, err)
t.Cleanup(server.TearDownFn)
return &server
}
func fetchOIDCCredentials(t *testing.T, oidcTokenURL string, caCertContent []byte) (idToken, refreshToken string) {
t.Helper()
req, err := http.NewRequest(http.MethodGet, oidcTokenURL, http.NoBody)
require.NoError(t, err)
caPool := x509.NewCertPool()
ok := caPool.AppendCertsFromPEM(caCertContent)
require.True(t, ok)
client := http.Client{Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caPool,
},
}}
token := new(utilsoidc.Token)
resp, err := client.Do(req)
require.NoError(t, err)
err = json.NewDecoder(resp.Body).Decode(token)
require.NoError(t, err)
return token.IDToken, token.RefreshToken
}
func fetchExpiredToken(t *testing.T, oidcServer *utilsoidc.TestServer, caCertContent []byte, signingPrivateKey *rsa.PrivateKey) (expiredToken, stubRefreshToken string) {
t.Helper()
tokenURL, err := oidcServer.TokenURL()
require.NoError(t, err)
configureOIDCServerToReturnExpiredIDToken(t, 1, oidcServer, signingPrivateKey)
expiredToken, stubRefreshToken = fetchOIDCCredentials(t, tokenURL, caCertContent)
return expiredToken, stubRefreshToken
}
func configureOIDCServerToReturnExpiredIDToken(t *testing.T, returningExpiredTokenTimes int, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) {
t.Helper()
oidcServer.TokenHandler().EXPECT().Token().Times(returningExpiredTokenTimes).DoAndReturn(func() (utilsoidc.Token, error) {
token, err := utilsoidc.TokenHandlerBehaviourReturningPredefinedJWT(
t,
signingPrivateKey,
oidcServer.URL(),
defaultOIDCClientID,
defaultOIDCClaimedUsername,
defaultStubAccessToken,
defaultStubRefreshToken,
time.Now().Add(-time.Millisecond).Unix(),
)()
return token, err
})
}
func configureOIDCServerToReturnExpiredRefreshTokenErrorOnTryingToUpdateIDToken(oidcServer *utilsoidc.TestServer) {
oidcServer.TokenHandler().EXPECT().Token().Times(2).Return(utilsoidc.Token{}, utilsoidc.ErrRefreshTokenExpired)
}
func generateCert(t *testing.T) (cert, key []byte, certFilePath, keyFilePath string) {
t.Helper()
tempDir := t.TempDir()
certFilePath = filepath.Join(tempDir, "localhost_127.0.0.1_.crt")
keyFilePath = filepath.Join(tempDir, "localhost_127.0.0.1_.key")
cert, key, err := certutil.GenerateSelfSignedCertKeyWithFixtures("localhost", []net.IP{utilsnet.ParseIPSloppy("127.0.0.1")}, nil, tempDir)
require.NoError(t, err)
return cert, key, certFilePath, keyFilePath
}

View File

@ -0,0 +1,39 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
//go:generate mockgen -source=handlers.go -destination=handlers.mock.go -package=oidc TokenHandler JWKsHandler
package oidc
import (
"gopkg.in/square/go-jose.v2"
)
type Token struct {
IDToken string `json:"id_token"`
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
}
type TokenHandler interface {
Token() (Token, error)
}
type JWKsHandler interface {
KeySet() jose.JSONWebKeySet
}

View File

@ -0,0 +1,103 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by MockGen. DO NOT EDIT.
// Source: handlers.go
// Package oidc is a generated GoMock package.
package oidc
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
go_jose_v2 "gopkg.in/square/go-jose.v2"
)
// MockTokenHandler is a mock of TokenHandler interface.
type MockTokenHandler struct {
ctrl *gomock.Controller
recorder *MockTokenHandlerMockRecorder
}
// MockTokenHandlerMockRecorder is the mock recorder for MockTokenHandler.
type MockTokenHandlerMockRecorder struct {
mock *MockTokenHandler
}
// NewMockTokenHandler creates a new mock instance.
func NewMockTokenHandler(ctrl *gomock.Controller) *MockTokenHandler {
mock := &MockTokenHandler{ctrl: ctrl}
mock.recorder = &MockTokenHandlerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTokenHandler) EXPECT() *MockTokenHandlerMockRecorder {
return m.recorder
}
// Token mocks base method.
func (m *MockTokenHandler) Token() (Token, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Token")
ret0, _ := ret[0].(Token)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Token indicates an expected call of Token.
func (mr *MockTokenHandlerMockRecorder) Token() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Token", reflect.TypeOf((*MockTokenHandler)(nil).Token))
}
// MockJWKsHandler is a mock of JWKsHandler interface.
type MockJWKsHandler struct {
ctrl *gomock.Controller
recorder *MockJWKsHandlerMockRecorder
}
// MockJWKsHandlerMockRecorder is the mock recorder for MockJWKsHandler.
type MockJWKsHandlerMockRecorder struct {
mock *MockJWKsHandler
}
// NewMockJWKsHandler creates a new mock instance.
func NewMockJWKsHandler(ctrl *gomock.Controller) *MockJWKsHandler {
mock := &MockJWKsHandler{ctrl: ctrl}
mock.recorder = &MockJWKsHandlerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockJWKsHandler) EXPECT() *MockJWKsHandlerMockRecorder {
return m.recorder
}
// KeySet mocks base method.
func (m *MockJWKsHandler) KeySet() go_jose_v2.JSONWebKeySet {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "KeySet")
ret0, _ := ret[0].(go_jose_v2.JSONWebKeySet)
return ret0
}
// KeySet indicates an expected call of KeySet.
func (mr *MockJWKsHandlerMockRecorder) KeySet() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeySet", reflect.TypeOf((*MockJWKsHandler)(nil).KeySet))
}

View File

@ -0,0 +1,234 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package oidc
import (
"crypto"
"crypto/rsa"
"crypto/tls"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
"gopkg.in/square/go-jose.v2"
)
const (
openIDWellKnownWebPath = "/.well-known/openid-configuration"
authWebPath = "/auth"
tokenWebPath = "/token"
jwksWebPath = "/jwks"
)
var (
ErrRefreshTokenExpired = errors.New("refresh token is expired")
ErrBadClientID = errors.New("client ID is bad")
)
type TestServer struct {
httpServer *httptest.Server
tokenHandler *MockTokenHandler
jwksHandler *MockJWKsHandler
}
// JwksHandler is getter of JSON Web Key Sets handler
func (ts *TestServer) JwksHandler() *MockJWKsHandler {
return ts.jwksHandler
}
// TokenHandler is getter of JWT token handler
func (ts *TestServer) TokenHandler() *MockTokenHandler {
return ts.tokenHandler
}
// URL returns the public URL of server
func (ts *TestServer) URL() string {
return ts.httpServer.URL
}
// TokenURL returns the public URL of JWT token endpoint
func (ts *TestServer) TokenURL() (string, error) {
url, err := url.JoinPath(ts.httpServer.URL, tokenWebPath)
if err != nil {
return "", fmt.Errorf("error joining paths: %v", err)
}
return url, nil
}
// BuildAndRunTestServer configures OIDC TLS server and its routing
func BuildAndRunTestServer(t *testing.T, caPath, caKeyPath string) *TestServer {
t.Helper()
certContent, err := os.ReadFile(caPath)
require.NoError(t, err)
keyContent, err := os.ReadFile(caKeyPath)
require.NoError(t, err)
cert, err := tls.X509KeyPair(certContent, keyContent)
require.NoError(t, err)
mux := http.NewServeMux()
httpServer := httptest.NewUnstartedServer(mux)
httpServer.TLS = &tls.Config{
Certificates: []tls.Certificate{cert},
}
httpServer.StartTLS()
mockCtrl := gomock.NewController(t)
t.Cleanup(func() {
mockCtrl.Finish()
httpServer.Close()
})
oidcServer := &TestServer{
httpServer: httpServer,
tokenHandler: NewMockTokenHandler(mockCtrl),
jwksHandler: NewMockJWKsHandler(mockCtrl),
}
mux.HandleFunc(openIDWellKnownWebPath, func(writer http.ResponseWriter, request *http.Request) {
authURL, err := url.JoinPath(httpServer.URL + authWebPath)
require.NoError(t, err)
tokenURL, err := url.JoinPath(httpServer.URL + tokenWebPath)
require.NoError(t, err)
jwksURL, err := url.JoinPath(httpServer.URL + jwksWebPath)
require.NoError(t, err)
userInfoURL, err := url.JoinPath(httpServer.URL + authWebPath)
require.NoError(t, err)
err = json.NewEncoder(writer).Encode(struct {
Issuer string `json:"issuer"`
AuthURL string `json:"authorization_endpoint"`
TokenURL string `json:"token_endpoint"`
JWKSURL string `json:"jwks_uri"`
UserInfoURL string `json:"userinfo_endpoint"`
}{
Issuer: httpServer.URL,
AuthURL: authURL,
TokenURL: tokenURL,
JWKSURL: jwksURL,
UserInfoURL: userInfoURL,
})
require.NoError(t, err)
writer.Header().Add("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
})
mux.HandleFunc(tokenWebPath, func(writer http.ResponseWriter, request *http.Request) {
token, err := oidcServer.tokenHandler.Token()
if err != nil {
http.Error(writer, err.Error(), http.StatusBadRequest)
return
}
writer.Header().Add("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
err = json.NewEncoder(writer).Encode(token)
require.NoError(t, err)
})
mux.HandleFunc(authWebPath, func(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusOK)
})
mux.HandleFunc(jwksWebPath, func(writer http.ResponseWriter, request *http.Request) {
keySet := oidcServer.jwksHandler.KeySet()
writer.Header().Add("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
err := json.NewEncoder(writer).Encode(keySet)
require.NoError(t, err)
})
return oidcServer
}
// TokenHandlerBehaviourReturningPredefinedJWT describes the scenario when signed JWT token is being created.
// This behaviour should being applied to the MockTokenHandler.
func TokenHandlerBehaviourReturningPredefinedJWT(
t *testing.T,
rsaPrivateKey *rsa.PrivateKey,
issClaim,
audClaim,
subClaim,
accessToken,
refreshToken string,
expClaim int64,
) func() (Token, error) {
t.Helper()
return func() (Token, error) {
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: rsaPrivateKey}, nil)
require.NoError(t, err)
payload := struct {
Iss string `json:"iss"`
Aud string `json:"aud"`
Sub string `json:"sub"`
Exp int64 `json:"exp"`
}{
Iss: issClaim,
Aud: audClaim,
Sub: subClaim,
Exp: expClaim,
}
payloadJSON, err := json.Marshal(payload)
require.NoError(t, err)
idTokenSignature, err := signer.Sign(payloadJSON)
require.NoError(t, err)
idToken, err := idTokenSignature.CompactSerialize()
require.NoError(t, err)
return Token{
IDToken: idToken,
AccessToken: accessToken,
RefreshToken: refreshToken,
}, nil
}
}
// DefaultJwksHandlerBehaviour describes the scenario when JSON Web Key Set token is being returned.
// This behaviour should being applied to the MockJWKsHandler.
func DefaultJwksHandlerBehaviour(t *testing.T, verificationPublicKey *rsa.PublicKey) func() jose.JSONWebKeySet {
t.Helper()
return func() jose.JSONWebKeySet {
key := jose.JSONWebKey{Key: verificationPublicKey, Use: "sig", Algorithm: string(jose.RS256)}
thumbprint, err := key.Thumbprint(crypto.SHA256)
require.NoError(t, err)
key.KeyID = hex.EncodeToString(thumbprint)
return jose.JSONWebKeySet{
Keys: []jose.JSONWebKey{key},
}
}
}