mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-15 23:03:40 +00:00
Add OIDC test server
This commit is contained in:
parent
98406e2a3e
commit
59592ba463
39
test/utils/oidc/handlers.go
Normal file
39
test/utils/oidc/handlers.go
Normal 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
|
||||
}
|
103
test/utils/oidc/handlers.mock.go
Normal file
103
test/utils/oidc/handlers.mock.go
Normal 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))
|
||||
}
|
233
test/utils/oidc/testserver.go
Normal file
233
test/utils/oidc/testserver.go
Normal file
@ -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},
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user