diff --git a/test/utils/oidc/handlers.go b/test/utils/oidc/handlers.go new file mode 100644 index 00000000000..a25eadccf5d --- /dev/null +++ b/test/utils/oidc/handlers.go @@ -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 +} diff --git a/test/utils/oidc/handlers.mock.go b/test/utils/oidc/handlers.mock.go new file mode 100644 index 00000000000..63dd794efd2 --- /dev/null +++ b/test/utils/oidc/handlers.mock.go @@ -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)) +} diff --git a/test/utils/oidc/testserver.go b/test/utils/oidc/testserver.go new file mode 100644 index 00000000000..7e9a7659204 --- /dev/null +++ b/test/utils/oidc/testserver.go @@ -0,0 +1,233 @@ +/* +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) + + 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}, + } + } +}