github.com/kiali/kiali@v1.84.0/business/authentication/openid_auth_controller_test.go (about)

     1  package authentication
     2  
     3  import (
     4  	"crypto/sha256"
     5  	"crypto/x509"
     6  	"encoding/json"
     7  	"encoding/pem"
     8  	"fmt"
     9  	"net"
    10  	"net/http"
    11  	"net/http/httptest"
    12  	"net/url"
    13  	"strings"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/go-jose/go-jose"
    18  	osproject_v1 "github.com/openshift/api/project/v1"
    19  	"github.com/stretchr/testify/assert"
    20  	core_v1 "k8s.io/api/core/v1"
    21  	meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    22  
    23  	"github.com/kiali/kiali/config"
    24  	"github.com/kiali/kiali/kubernetes/cache"
    25  	"github.com/kiali/kiali/kubernetes/kubetest"
    26  	"github.com/kiali/kiali/util"
    27  )
    28  
    29  // Token built with the debugger at jwt.io. Subject is system:serviceaccount:k8s_user
    30  // {
    31  //  "sub": "jdoe@domain.com",
    32  //  "name": "John Doe",
    33  //  "iat": 1516239022,
    34  //  "nonce": "1ba9b834d08ac81feb34e208402eb18e909be084518c328510940184",
    35  //  "exp": 1638316801
    36  // }
    37  
    38  const openIdTestToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqZG9lQGRvbWFpbi5jb20iLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsIm5vbmNlIjoiMWJhOWI4MzRkMDhhYzgxZmViMzRlMjA4NDAyZWIxOGU5MDliZTA4NDUxOGMzMjg1MTA5NDAxODQiLCJleHAiOjE2MzgzMTY4MDF9.agHBziXM7SDLBKCnA6BvjWenU1n6juL8Fz3go4MSzyw"
    39  
    40  /*** Function tests ***/
    41  
    42  // see https://github.com/kiali/kiali/issues/6226
    43  func TestVerifyAudienceClaim(t *testing.T) {
    44  	oidCfg := config.OpenIdConfig{
    45  		ClientId: "kiali-client",
    46  	}
    47  
    48  	oip := openidFlowHelper{
    49  		IdTokenPayload: map[string]interface{}{},
    50  	}
    51  
    52  	oip.IdTokenPayload["aud"] = []interface{}{oidCfg.ClientId}
    53  	err := verifyAudienceClaim(&oip, oidCfg)
    54  	assert.Nil(t, err, "verifyAudienceClaim failed: %v", err)
    55  
    56  	oip.IdTokenPayload["aud"] = []string{oidCfg.ClientId}
    57  	err = verifyAudienceClaim(&oip, oidCfg)
    58  	assert.Nil(t, err, "verifyAudienceClaim failed: %v", err)
    59  
    60  	oip.IdTokenPayload["aud"] = oidCfg.ClientId
    61  	err = verifyAudienceClaim(&oip, oidCfg)
    62  	assert.Nil(t, err, "verifyAudienceClaim failed: %v", err)
    63  
    64  	oip.IdTokenPayload["aud"] = []interface{}{oidCfg.ClientId + "DIFFERENT"}
    65  	err = verifyAudienceClaim(&oip, oidCfg)
    66  	assert.NotNil(t, err, "verifyAudienceClaim should have failed")
    67  
    68  	oip.IdTokenPayload["aud"] = []string{oidCfg.ClientId + "DIFFERENT"}
    69  	err = verifyAudienceClaim(&oip, oidCfg)
    70  	assert.NotNil(t, err, "verifyAudienceClaim should have failed")
    71  
    72  	oip.IdTokenPayload["aud"] = oidCfg.ClientId + "DIFFERENT"
    73  	err = verifyAudienceClaim(&oip, oidCfg)
    74  	assert.NotNil(t, err, "verifyAudienceClaim should have failed")
    75  }
    76  
    77  func TestValidateOpenIdTokenInHouse(t *testing.T) {
    78  	// These tokens were generated via https://dinochiesa.github.io/jwt/ using its own private/public keys generator.
    79  	// There is another public/private key generator website that could be used if needed in the future: https://mkjwk.org/
    80  	//
    81  	// -----BEGIN PUBLIC KEY-----
    82  	// MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2fEsycKFNuQn/IsW+t8+
    83  	// RexDWxvJP37+5BuAviloJvmAYwQguGRN+qwi2Co/yImxZPZVuqY7/hf87bIKopZD
    84  	// CgZrUHtFERPL4qbR6gv+FbanR6ohCXa6f1S+AjXPiQcw0p49t4uT0WwjPD64mNdw
    85  	// IlzWLi+xO4rY1Mo98H2339WmvmgiEukLAa+f3Zz80jRDmWv23Td/ba5ASHVrvhRJ
    86  	// d2jLwOL/lxCHPtjDf6bBHKaPP/ezRjlPg9Qlse3au1KBsVrSThr0uz9G6cGeidEL
    87  	// fvdk9xmAoxhR2CbmGIQoRPiEEOVhKg/yHVqzz+2SvnIKnArx4ISv/yudbP8lU7vC
    88  	// QwIDAQAB
    89  	// -----END PUBLIC KEY-----
    90  	//
    91  	// -----BEGIN PRIVATE KEY-----
    92  	// MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDZ8SzJwoU25Cf8
    93  	// ixb63z5F7ENbG8k/fv7kG4C+KWgm+YBjBCC4ZE36rCLYKj/IibFk9lW6pjv+F/zt
    94  	// sgqilkMKBmtQe0URE8viptHqC/4VtqdHqiEJdrp/VL4CNc+JBzDSnj23i5PRbCM8
    95  	// PriY13AiXNYuL7E7itjUyj3wfbff1aa+aCIS6QsBr5/dnPzSNEOZa/bdN39trkBI
    96  	// dWu+FEl3aMvA4v+XEIc+2MN/psEcpo8/97NGOU+D1CWx7dq7UoGxWtJOGvS7P0bp
    97  	// wZ6J0Qt+92T3GYCjGFHYJuYYhChE+IQQ5WEqD/IdWrPP7ZK+cgqcCvHghK//K51s
    98  	// /yVTu8JDAgMBAAECggEAGGi2h3JN0TQEdnhtfnN6WgJ4GMAn7gCfM5UQ+jtQ+ux+
    99  	// wJg5we0Z/rVAwc0Zj7A8Of6M43ayyWaOYWDLaCJEJ99ILZ9gwOTitOPSJtBpCK2I
   100  	// VrJrONAfWxt2nHDCaapwgWZPqzrqt03RNHIh4pxeZrrXEh0tUGnglxR/k2vBKERk
   101  	// dtP1xwydrMip7VUkUwGvLmYLClsbnkR7O65XzFIto7iG0/yuWA6yw9mNey5gTrr9
   102  	// 7iboE5v0vHOJW/1mVQP1xV/rI8VnLZlZz6XYPktjlzhR5yIzrZD2DZ20J8uo6Zrd
   103  	// iVEz1nE6FmuhfgbUFOt8SBrGUZhx0Zwo0GkLiFdDYQKBgQD+P49qNNw+Dwrjc8dK
   104  	// 85vr89RQ9MX3ioTIZnm+oXHjoLvVrPZmI3SjrZJrqMCZtxP6LpoDnlGjKeJJMA61
   105  	// wD1ceJS5YyDgLVv3jsi/zIpJDQTg2GXaDRY3c3PjHCmRocMvaPKiws8ZY7BoR8nF
   106  	// bpKxxa2C3LEqiyzil2H7NR16vQKBgQDbcZQO9LTiYitoW+qQPhbNHGlrlD6KfXTA
   107  	// SFQBAPFKtR4ncj/x3/i0L1P3+IabkdJQqxOHY68FNGg95sfD7/s9K72RvnRTyXCn
   108  	// Wd46QLULvHLTYWI39obKOZlrbRWBqr/kw1YIxjjgRWTShOuBkS2R6TtPDeoAtTds
   109  	// 995RRgCA/wKBgE4m2X2rC/wjgZRS9XKrmUUZKS1NYEDsGk7DeS7Iz4pJ0RMoXIEe
   110  	// 6u6ZHwXq1HErnn9rrbnpA20lJcKbfBoQIox3IDgwKV3fc4KQKFMUm3lDADnhKsWw
   111  	// +iBHY9ruwDRcxfOfzd2MBj7mrsYPMw12JK9ydRhhoC/UohJwuBSQyiP9AoGAeIg5
   112  	// F8HnPNVJHGgoPZQs9/pcGR/y/iSMpTTVFzwKTMuQxX/miZdIxsecKn7SiM6eo3pk
   113  	// HqBtOMGhZCbHoOLGr8G/vTbMNF1XyEP/YSW7i7e1pk8+IJkDTj42+5+OCYvdHO0B
   114  	// 643dHapgB5XEuYUhb5yY3AI7fqoKyIqZDTETA8cCgYEAt2AnN8HTLqIOxizkulNj
   115  	// adeC53dbv8MmPU0aA1EO5ipLVXcF4y/8zOzIKHijWAB8kEZXPzS/rUURwtyhRG3k
   116  	// f6lSPO2VFE1xNlGSlU3CvQz1VV2qgvbtogIOo7CoXYRdiJJ2j2M/n06MBK2bMoGb
   117  	// UGj7e/VHYwvMxZ6SoMYk3Hc=
   118  	// -----END PRIVATE KEY-----
   119  	//
   120  	// HEADER:
   121  	// {
   122  	//   "alg": "RS256",
   123  	//   "typ": "JWT",
   124  	//   "kid": "kialikey"
   125  	// }
   126  	//
   127  	// PAYLOAD:
   128  	// {
   129  	//  "sub": "jdoe@domain.com",
   130  	//  "name": "John Doe",
   131  	//  "iat": 1516239022,
   132  	//  "nonce": "1ba9b834d08ac81feb34e208402eb18e909be084518c328510940184",
   133  	//  "exp": 1638316801,
   134  	//  "iss": "http://127.0.0.1:33333",
   135  	//  "aud": "kiali-client"
   136  	// }
   137  	openIdTestTokenWithGoodAud := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtpYWxpa2V5In0.eyJzdWIiOiJqZG9lQGRvbWFpbi5jb20iLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsIm5vbmNlIjoiMWJhOWI4MzRkMDhhYzgxZmViMzRlMjA4NDAyZWIxOGU5MDliZTA4NDUxOGMzMjg1MTA5NDAxODQiLCJleHAiOjE2MzgzMTY4MDEsImlzcyI6Imh0dHA6Ly8xMjcuMC4wLjE6MzMzMzMiLCJhdWQiOiJraWFsaS1jbGllbnQifQ.bOu7M_a8DKctApb-RfbglUGyJslJrVjD6ZRU8XlRStQwp4QIQM-tCB5cqYv9OJeC9NSN46RO9jvJ5rw2WnVFNfujDwVWSvjUqQHmYiO3GSobmCfbAtG7ymRWnLLaQMheinpfPjXh5-ohVlSqB23wkk4viC9YCqqXaIKk4bLyZFf14F4u5Zqu2kfzwufjp-AgAt9W93loI6p6kHVyCnnDwPvfmfSmCUxaCPvrFGKnGe6hTCPCc2EBCRndW-si7hz9F693jAyD5OMvt1z_aX4tzPNsqZYuosXw6xwGGM-nepn6XtM6U_MS9-eRSoCyyMyZE6_xSZIO4ir1KeewbSvk1Q"
   138  
   139  	// {
   140  	//  "sub": "jdoe@domain.com",
   141  	//  "name": "John Doe",
   142  	//  "iat": 1516239022,
   143  	//  "nonce": "1ba9b834d08ac81feb34e208402eb18e909be084518c328510940184",
   144  	//  "exp": 1638316801,
   145  	//  "iss": "http://127.0.0.1:33333",
   146  	//  "aud": "bad-aud-client"
   147  	// }
   148  	openIdTestTokenWithBadAud := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtpYWxpa2V5In0.eyJzdWIiOiJqZG9lQGRvbWFpbi5jb20iLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsIm5vbmNlIjoiMWJhOWI4MzRkMDhhYzgxZmViMzRlMjA4NDAyZWIxOGU5MDliZTA4NDUxOGMzMjg1MTA5NDAxODQiLCJleHAiOjE2MzgzMTY4MDEsImlzcyI6Imh0dHA6Ly8xMjcuMC4wLjE6MzMzMzMiLCJhdWQiOiJiYWQtYXVkLWNsaWVudCJ9.LicN-YgaHxfTTg_XtpyTlZBIQyj4BTvYHsXKtDRRskf2uuvKw36Y0WSJN570E0PSYYlAzyjrWp31S_PdZL75cwCIOYnhGbTat7pUVNoAn-aZc7FrMtzYNmQdChB2-ghE_RRQaXP1zNwgeNrQiEQ9jmD5ynd7Qm2esMYYbmCoj1ITM5Uospp5fbRg9eNdfrqXmwoGK3OITC5OVv8tbcb2HY_CUxJfSIC5pT5wBGxGRExjaeXNiIRS1600NmkfK6O-BPsmJhEYTxLIeWbtAn2pn7uZhWMiyJIIX9FHFLeTCIQh2xuwSuWLkyZoMsegr8A_rQqKg-iQkhfXxYxGtRY6mQ"
   149  
   150  	// we start with a good token - our second test will switch this to the token with the bad aud value
   151  	openIdTestTokenToUse := openIdTestTokenWithGoodAud
   152  
   153  	cachedOpenIdMetadata = nil
   154  	var oidcMetadata []byte
   155  	var jwksResponseBytes []byte
   156  	testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   157  		if r.URL.Path == "/.well-known/openid-configuration" {
   158  			w.WriteHeader(200)
   159  			_, _ = w.Write(oidcMetadata)
   160  		} else if r.URL.Path == "/jwks" {
   161  			w.WriteHeader(200)
   162  			_, _ = w.Write(jwksResponseBytes)
   163  		} else if r.URL.Path == "/token" {
   164  			_ = r.ParseForm()
   165  			assert.Equal(t, "f0code", r.Form.Get("code"))
   166  			assert.Equal(t, "authorization_code", r.Form.Get("grant_type"))
   167  			assert.Equal(t, "kiali-client", r.Form.Get("client_id"))
   168  			assert.Equal(t, "https://kiali.io:44/kiali-test", r.Form.Get("redirect_uri"))
   169  
   170  			w.WriteHeader(200)
   171  			_, _ = w.Write([]byte("{ \"id_token\": \"" + openIdTestTokenToUse + "\" }"))
   172  		}
   173  	}))
   174  	defer testServer.Close()
   175  
   176  	// because we have a hardcoded token for this test that is pre-encrypted with the issuer URL, we need to start the server on that same URL
   177  	testServerListenerFixedPort, err := net.Listen("tcp", "127.0.0.1:33333")
   178  	assert.Nil(t, err, "Cannot start test server on fixed port")
   179  	testServer.Listener = testServerListenerFixedPort
   180  	testServer.Start()
   181  
   182  	oidcMeta := openIdMetadata{
   183  		Issuer:                 testServer.URL,
   184  		AuthURL:                testServer.URL + "/auth",
   185  		TokenURL:               testServer.URL + "/token",
   186  		JWKSURL:                testServer.URL + "/jwks",
   187  		UserInfoURL:            "",
   188  		Algorithms:             nil,
   189  		ScopesSupported:        []string{"openid"},
   190  		ResponseTypesSupported: []string{"code"},
   191  	}
   192  	oidcMetadata, err = json.Marshal(oidcMeta)
   193  	assert.Nil(t, err)
   194  
   195  	// jwksResponseBytes is needed for the "/jwks" endpoint. It is used during
   196  	// the validation phase when Kiali wants to make sure the "kid" is valid.
   197  	publicKeyText := `
   198  -----BEGIN PUBLIC KEY-----
   199  MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2fEsycKFNuQn/IsW+t8+
   200  RexDWxvJP37+5BuAviloJvmAYwQguGRN+qwi2Co/yImxZPZVuqY7/hf87bIKopZD
   201  CgZrUHtFERPL4qbR6gv+FbanR6ohCXa6f1S+AjXPiQcw0p49t4uT0WwjPD64mNdw
   202  IlzWLi+xO4rY1Mo98H2339WmvmgiEukLAa+f3Zz80jRDmWv23Td/ba5ASHVrvhRJ
   203  d2jLwOL/lxCHPtjDf6bBHKaPP/ezRjlPg9Qlse3au1KBsVrSThr0uz9G6cGeidEL
   204  fvdk9xmAoxhR2CbmGIQoRPiEEOVhKg/yHVqzz+2SvnIKnArx4ISv/yudbP8lU7vC
   205  QwIDAQAB
   206  -----END PUBLIC KEY-----`
   207  	block, _ := pem.Decode([]byte(publicKeyText))
   208  	publicKeyRSA, _ := x509.ParsePKIXPublicKey(block.Bytes)
   209  	jwksResponseObject := jose.JSONWebKeySet{
   210  		Keys: []jose.JSONWebKey{
   211  			{
   212  				KeyID:     "kialikey",
   213  				Algorithm: "RS256",
   214  				Key:       publicKeyRSA,
   215  			},
   216  		},
   217  	}
   218  	jwksResponseBytes, err = json.Marshal(jwksResponseObject)
   219  	assert.Nil(t, err)
   220  
   221  	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
   222  	util.Clock = util.ClockMock{Time: clockTime}
   223  
   224  	conf := config.NewConfig()
   225  	conf.Server.WebRoot = "/kiali-test"
   226  	conf.LoginToken.SigningKey = "kiali67890123456"
   227  	conf.LoginToken.ExpirationSeconds = 1
   228  	conf.Auth.OpenId.IssuerUri = testServer.URL
   229  	conf.Auth.OpenId.ClientId = "kiali-client"
   230  	conf.Auth.OpenId.DisableRBAC = true // true is needed to trigger the call to validateOpenIdTokenInHouse
   231  	config.Set(conf)
   232  
   233  	// Returning some namespace when a cluster API call is made should have the result of
   234  	// a successful authentication.
   235  	k8s := kubetest.NewFakeK8sClient(
   236  		&osproject_v1.Project{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}},
   237  	)
   238  	k8s.OpenShift = true
   239  	mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s)
   240  	cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *conf)
   241  
   242  	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(conf))))
   243  	uri := fmt.Sprintf("https://kiali.io:44/api/authenticate?code=f0code&state=%x-%s", stateHash, clockTime.UTC().Format("060102150405"))
   244  	request := httptest.NewRequest(http.MethodGet, uri, nil)
   245  	request.AddCookie(&http.Cookie{
   246  		Name:  OpenIdNonceCookieName,
   247  		Value: "nonceString",
   248  	})
   249  
   250  	controller := NewOpenIdAuthController(NewCookieSessionPersistor(conf), cache, mockClientFactory, conf)
   251  
   252  	expectedExpiration := time.Date(2021, 12, 1, 0, 0, 1, 0, time.UTC)
   253  
   254  	rr := httptest.NewRecorder()
   255  	controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   256  		assert.Failf(t, "Callback function shouldn't have been called.", "")
   257  	})).ServeHTTP(rr, request)
   258  
   259  	// Check that cookies are set and have the right expiration.
   260  	response := rr.Result()
   261  	assert.Len(t, response.Cookies(), 2)
   262  
   263  	// nonce cookie cleanup
   264  	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
   265  	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
   266  	assert.True(t, response.Cookies()[0].HttpOnly)
   267  	assert.True(t, response.Cookies()[0].Secure) // the test URL is https://kiali.io:44/kiali-test ; https: means it should be Secure
   268  
   269  	// Session cookie
   270  	assert.Equal(t, AESSessionCookieName, response.Cookies()[1].Name)
   271  	assert.Equal(t, expectedExpiration, response.Cookies()[1].Expires)
   272  	assert.Equal(t, http.StatusFound, response.StatusCode)
   273  	assert.True(t, response.Cookies()[1].HttpOnly)
   274  	assert.True(t, response.Cookies()[1].Secure) // the test URL is https://kiali.io:44/kiali-test ; https: means it should be Secure
   275  
   276  	// Redirection to boot the UI
   277  	assert.Equal(t, "/kiali-test/", response.Header.Get("Location"))
   278  
   279  	///
   280  	/// Now switch to using the token with the bad audience claim and make sure this fails
   281  	///
   282  
   283  	openIdTestTokenToUse = openIdTestTokenWithBadAud
   284  	rr = httptest.NewRecorder()
   285  	controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   286  		assert.Failf(t, "Callback function shouldn't have been called.", "")
   287  	})).ServeHTTP(rr, request)
   288  
   289  	// Check that there is only one cookie (nonce) - the other AES cookie should be missing because the audience claim was bad
   290  	response = rr.Result()
   291  	assert.Len(t, response.Cookies(), 1)
   292  	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
   293  	assert.Equal(t, "/kiali-test/?openid_error=the+OpenID+token+was+rejected%3A+the+OpenId+token+is+not+targeted+for+Kiali%3B+got+aud+%3D+%27bad-aud-client%27", response.Header.Get("Location"))
   294  }
   295  
   296  /*** Implicit flow tests ***/
   297  
   298  func TestOpenIdAuthControllerRejectsImplicitFlow(t *testing.T) {
   299  	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
   300  	util.Clock = util.ClockMock{Time: clockTime}
   301  
   302  	conf := config.NewConfig()
   303  	conf.LoginToken.SigningKey = "kiali67890123456"
   304  	conf.LoginToken.ExpirationSeconds = 1
   305  	config.Set(conf)
   306  
   307  	// Returning some namespace when a cluster API call is made should have the result of
   308  	// a successful authentication.
   309  	k8s := kubetest.NewFakeK8sClient(
   310  		&osproject_v1.Project{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}},
   311  	)
   312  	k8s.OpenShift = true
   313  	mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s)
   314  	cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *conf)
   315  
   316  	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(conf))))
   317  
   318  	requestBody := strings.NewReader(fmt.Sprintf("id_token=%s&state=%x-%s", openIdTestToken, stateHash, clockTime.UTC().Format("060102150405")))
   319  	request := httptest.NewRequest(http.MethodPost, "/api/authenticate", requestBody)
   320  	request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
   321  	request.AddCookie(&http.Cookie{
   322  		Name:  OpenIdNonceCookieName,
   323  		Value: "nonceString",
   324  	})
   325  
   326  	controller := NewOpenIdAuthController(NewCookieSessionPersistor(conf), cache, mockClientFactory, conf)
   327  	rr := httptest.NewRecorder()
   328  	sData, err := controller.Authenticate(request, rr)
   329  
   330  	assert.Nil(t, sData)
   331  	assert.NotNil(t, err)
   332  	assert.Equal(t, err.Error(), "support for OpenID's implicit flow has been removed")
   333  
   334  	response := rr.Result()
   335  	assert.Len(t, response.Cookies(), 0)
   336  }
   337  
   338  /*** Authorization code flow tests ***/
   339  
   340  func TestOpenIdAuthControllerAuthenticatesCorrectlyWithAuthorizationCodeFlow(t *testing.T) {
   341  	cachedOpenIdMetadata = nil
   342  	var oidcMetadata []byte
   343  	testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   344  		if r.URL.Path == "/.well-known/openid-configuration" {
   345  			w.WriteHeader(200)
   346  			_, _ = w.Write(oidcMetadata)
   347  		}
   348  		if r.URL.Path == "/token" {
   349  			_ = r.ParseForm()
   350  			assert.Equal(t, "f0code", r.Form.Get("code"))
   351  			assert.Equal(t, "authorization_code", r.Form.Get("grant_type"))
   352  			assert.Equal(t, "kiali-client", r.Form.Get("client_id"))
   353  			assert.Equal(t, "https://kiali.io:44/kiali-test", r.Form.Get("redirect_uri"))
   354  
   355  			w.WriteHeader(200)
   356  			_, _ = w.Write([]byte("{ \"id_token\": \"" + openIdTestToken + "\" }"))
   357  		}
   358  	}))
   359  	defer testServer.Close()
   360  
   361  	oidcMeta := openIdMetadata{
   362  		Issuer:                 testServer.URL,
   363  		AuthURL:                testServer.URL + "/auth",
   364  		TokenURL:               testServer.URL + "/token",
   365  		JWKSURL:                testServer.URL + "/jwks",
   366  		UserInfoURL:            "",
   367  		Algorithms:             nil,
   368  		ScopesSupported:        []string{"openid"},
   369  		ResponseTypesSupported: []string{"code"},
   370  	}
   371  	oidcMetadata, err := json.Marshal(oidcMeta)
   372  	assert.Nil(t, err)
   373  
   374  	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
   375  	util.Clock = util.ClockMock{Time: clockTime}
   376  
   377  	conf := config.NewConfig()
   378  	conf.Server.WebRoot = "/kiali-test"
   379  	conf.LoginToken.SigningKey = "kiali67890123456"
   380  	conf.LoginToken.ExpirationSeconds = 1
   381  	conf.Auth.OpenId.IssuerUri = testServer.URL
   382  	conf.Auth.OpenId.ClientId = "kiali-client"
   383  	conf.Identity.CertFile = "foo.cert"      // setting conf.Identity will make it look as if the endpoint ...
   384  	conf.Identity.PrivateKeyFile = "foo.key" // ... is HTTPS - this causes the cookies' Secure flag to be true
   385  	config.Set(conf)
   386  
   387  	// Returning some namespace when a cluster API call is made should have the result of
   388  	// a successful authentication.
   389  	k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}})
   390  	mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s)
   391  	cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *conf)
   392  
   393  	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(conf))))
   394  	uri := fmt.Sprintf("https://kiali.io:44/api/authenticate?code=f0code&state=%x-%s", stateHash, clockTime.UTC().Format("060102150405"))
   395  	request := httptest.NewRequest(http.MethodGet, uri, nil)
   396  	request.AddCookie(&http.Cookie{
   397  		Name:  OpenIdNonceCookieName,
   398  		Value: "nonceString",
   399  	})
   400  
   401  	controller := NewOpenIdAuthController(NewCookieSessionPersistor(conf), cache, mockClientFactory, conf)
   402  
   403  	rr := httptest.NewRecorder()
   404  	controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   405  		assert.Failf(t, "Callback function shouldn't have been called.", "")
   406  	})).ServeHTTP(rr, request)
   407  
   408  	expectedExpiration := time.Date(2021, 12, 1, 0, 0, 1, 0, time.UTC)
   409  
   410  	// Check that cookies are set and have the right expiration.
   411  	response := rr.Result()
   412  	assert.Len(t, response.Cookies(), 2)
   413  
   414  	// nonce cookie cleanup
   415  	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
   416  	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
   417  	assert.True(t, response.Cookies()[0].HttpOnly)
   418  	assert.True(t, response.Cookies()[0].Secure)
   419  
   420  	// Session cookie
   421  	assert.Equal(t, AESSessionCookieName, response.Cookies()[1].Name)
   422  	assert.Equal(t, expectedExpiration, response.Cookies()[1].Expires)
   423  	assert.Equal(t, http.StatusFound, response.StatusCode)
   424  	assert.True(t, response.Cookies()[1].HttpOnly)
   425  	assert.True(t, response.Cookies()[1].Secure)
   426  
   427  	// Redirection to boot the UI
   428  	assert.Equal(t, "/kiali-test/", response.Header.Get("Location"))
   429  }
   430  
   431  func TestOpenIdCodeFlowShouldFailWithMissingIdTokenFromOpenIdServer(t *testing.T) {
   432  	cachedOpenIdMetadata = nil
   433  	var oidcMetadata []byte
   434  	testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   435  		if r.URL.Path == "/.well-known/openid-configuration" {
   436  			w.WriteHeader(200)
   437  			_, _ = w.Write(oidcMetadata)
   438  		}
   439  		if r.URL.Path == "/token" {
   440  			w.WriteHeader(200)
   441  			_, _ = w.Write([]byte("{ \"access_token\": \"" + openIdTestToken + "\" }"))
   442  		}
   443  	}))
   444  	defer testServer.Close()
   445  
   446  	oidcMeta := openIdMetadata{
   447  		Issuer:                 testServer.URL,
   448  		AuthURL:                testServer.URL + "/auth",
   449  		TokenURL:               testServer.URL + "/token",
   450  		JWKSURL:                testServer.URL + "/jwks",
   451  		UserInfoURL:            "",
   452  		Algorithms:             nil,
   453  		ScopesSupported:        []string{"openid"},
   454  		ResponseTypesSupported: []string{"code"},
   455  	}
   456  	oidcMetadata, err := json.Marshal(oidcMeta)
   457  	assert.Nil(t, err)
   458  
   459  	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
   460  	util.Clock = util.ClockMock{Time: clockTime}
   461  
   462  	cfg := config.NewConfig()
   463  	cfg.Server.WebRoot = "/kiali-test"
   464  	cfg.LoginToken.SigningKey = "kiali67890123456"
   465  	cfg.LoginToken.ExpirationSeconds = 1
   466  	cfg.Auth.OpenId.IssuerUri = testServer.URL
   467  	cfg.Auth.OpenId.ClientId = "kiali-client"
   468  	config.Set(cfg)
   469  
   470  	// Returning some namespace when a cluster API call is made should have the result of
   471  	// a successful authentication.
   472  	k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}})
   473  	mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s)
   474  	cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg)
   475  
   476  	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(cfg))))
   477  	uri := fmt.Sprintf("https://kiali.io:44/api/authenticate?code=f0code&state=%x-%s", stateHash, clockTime.UTC().Format("060102150405"))
   478  	request := httptest.NewRequest(http.MethodGet, uri, nil)
   479  	request.AddCookie(&http.Cookie{
   480  		Name:  OpenIdNonceCookieName,
   481  		Value: "nonceString",
   482  	})
   483  
   484  	controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg)
   485  
   486  	rr := httptest.NewRecorder()
   487  	controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   488  		assert.Failf(t, "Callback function shouldn't have been called.", "")
   489  	})).ServeHTTP(rr, request)
   490  
   491  	// nonce cookie cleanup
   492  	response := rr.Result()
   493  	assert.Len(t, response.Cookies(), 1)
   494  	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
   495  	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
   496  
   497  	// Redirection to boot the UI
   498  	q := url.Values{}
   499  	q.Add("openid_error", "the IdP did not provide an id_token")
   500  	assert.Equal(t, "/kiali-test/?"+q.Encode(), response.Header.Get("Location"))
   501  	assert.Equal(t, http.StatusFound, response.StatusCode)
   502  }
   503  
   504  func TestOpenIdCodeFlowShouldFailWithBadResponseFromTokenEndpoint(t *testing.T) {
   505  	cachedOpenIdMetadata = nil
   506  	var oidcMetadata []byte
   507  	testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   508  		if r.URL.Path == "/.well-known/openid-configuration" {
   509  			w.WriteHeader(200)
   510  			_, _ = w.Write(oidcMetadata)
   511  		}
   512  		if r.URL.Path == "/token" {
   513  			w.WriteHeader(500)
   514  			_, _ = w.Write([]byte("{ }"))
   515  		}
   516  	}))
   517  	defer testServer.Close()
   518  
   519  	oidcMeta := openIdMetadata{
   520  		Issuer:                 testServer.URL,
   521  		AuthURL:                testServer.URL + "/auth",
   522  		TokenURL:               testServer.URL + "/token",
   523  		JWKSURL:                testServer.URL + "/jwks",
   524  		UserInfoURL:            "",
   525  		Algorithms:             nil,
   526  		ScopesSupported:        []string{"openid"},
   527  		ResponseTypesSupported: []string{"code"},
   528  	}
   529  	oidcMetadata, err := json.Marshal(oidcMeta)
   530  	assert.Nil(t, err)
   531  
   532  	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
   533  	util.Clock = util.ClockMock{Time: clockTime}
   534  
   535  	cfg := config.NewConfig()
   536  	cfg.Server.WebRoot = "/kiali-test"
   537  	cfg.LoginToken.SigningKey = "kiali67890123456"
   538  	cfg.LoginToken.ExpirationSeconds = 1
   539  	cfg.Auth.OpenId.IssuerUri = testServer.URL
   540  	cfg.Auth.OpenId.ClientId = "kiali-client"
   541  	config.Set(cfg)
   542  
   543  	k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}})
   544  	mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s)
   545  	cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg)
   546  
   547  	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(cfg))))
   548  	uri := fmt.Sprintf("https://kiali.io:44/api/authenticate?code=f0code&state=%x-%s", stateHash, clockTime.UTC().Format("060102150405"))
   549  	request := httptest.NewRequest(http.MethodGet, uri, nil)
   550  	request.AddCookie(&http.Cookie{
   551  		Name:  OpenIdNonceCookieName,
   552  		Value: "nonceString",
   553  	})
   554  
   555  	controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg)
   556  
   557  	rr := httptest.NewRecorder()
   558  	controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   559  		assert.Failf(t, "Callback function shouldn't have been called.", "")
   560  	})).ServeHTTP(rr, request)
   561  
   562  	// nonce cookie cleanup
   563  	response := rr.Result()
   564  	assert.Len(t, response.Cookies(), 1)
   565  	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
   566  	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
   567  
   568  	// Redirection to boot the UI
   569  	q := url.Values{}
   570  	q.Add("openid_error", "request failed (HTTP response status = 500 Internal Server Error)")
   571  	assert.Equal(t, "/kiali-test/?"+q.Encode(), response.Header.Get("Location"))
   572  	assert.Equal(t, http.StatusFound, response.StatusCode)
   573  }
   574  
   575  func TestOpenIdCodeFlowShouldFailWithNonJsonResponse(t *testing.T) {
   576  	cachedOpenIdMetadata = nil
   577  	var oidcMetadata []byte
   578  	testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   579  		if r.URL.Path == "/.well-known/openid-configuration" {
   580  			w.WriteHeader(200)
   581  			_, _ = w.Write(oidcMetadata)
   582  		}
   583  		if r.URL.Path == "/token" {
   584  			w.WriteHeader(200)
   585  			_, _ = w.Write([]byte("\"id_token\": \"foo\""))
   586  		}
   587  	}))
   588  	defer testServer.Close()
   589  
   590  	oidcMeta := openIdMetadata{
   591  		Issuer:                 testServer.URL,
   592  		AuthURL:                testServer.URL + "/auth",
   593  		TokenURL:               testServer.URL + "/token",
   594  		JWKSURL:                testServer.URL + "/jwks",
   595  		UserInfoURL:            "",
   596  		Algorithms:             nil,
   597  		ScopesSupported:        []string{"openid"},
   598  		ResponseTypesSupported: []string{"code"},
   599  	}
   600  	oidcMetadata, err := json.Marshal(oidcMeta)
   601  	assert.Nil(t, err)
   602  
   603  	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
   604  	util.Clock = util.ClockMock{Time: clockTime}
   605  
   606  	cfg := config.NewConfig()
   607  	cfg.Server.WebRoot = "/kiali-test"
   608  	cfg.LoginToken.SigningKey = "kiali67890123456"
   609  	cfg.LoginToken.ExpirationSeconds = 1
   610  	cfg.Auth.OpenId.IssuerUri = testServer.URL
   611  	cfg.Auth.OpenId.ClientId = "kiali-client"
   612  	config.Set(cfg)
   613  
   614  	k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}})
   615  	mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s)
   616  	cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg)
   617  
   618  	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(cfg))))
   619  	uri := fmt.Sprintf("https://kiali.io:44/api/authenticate?code=f0code&state=%x-%s", stateHash, clockTime.UTC().Format("060102150405"))
   620  	request := httptest.NewRequest(http.MethodGet, uri, nil)
   621  	request.AddCookie(&http.Cookie{
   622  		Name:  OpenIdNonceCookieName,
   623  		Value: "nonceString",
   624  	})
   625  
   626  	controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg)
   627  
   628  	rr := httptest.NewRecorder()
   629  	controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   630  		assert.Failf(t, "Callback function shouldn't have been called.", "")
   631  	})).ServeHTTP(rr, request)
   632  
   633  	// nonce cookie cleanup
   634  	response := rr.Result()
   635  	assert.Len(t, response.Cookies(), 1)
   636  	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
   637  	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
   638  
   639  	// Redirection to boot the UI
   640  	u, _ := url.Parse(response.Header.Get("Location"))
   641  	q, _ := url.ParseQuery(u.RawQuery)
   642  	assert.Contains(t, q["openid_error"][0], "cannot parse OpenId token response:")
   643  	assert.Equal(t, http.StatusFound, response.StatusCode)
   644  }
   645  
   646  func TestOpenIdCodeFlowShouldFailWithNonJwtIdToken(t *testing.T) {
   647  	cachedOpenIdMetadata = nil
   648  	var oidcMetadata []byte
   649  	testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   650  		if r.URL.Path == "/.well-known/openid-configuration" {
   651  			w.WriteHeader(200)
   652  			_, _ = w.Write(oidcMetadata)
   653  		}
   654  		if r.URL.Path == "/token" {
   655  			w.WriteHeader(200)
   656  			_, _ = w.Write([]byte("{ \"id_token\": \"foo\" }"))
   657  		}
   658  	}))
   659  	defer testServer.Close()
   660  
   661  	oidcMeta := openIdMetadata{
   662  		Issuer:                 testServer.URL,
   663  		AuthURL:                testServer.URL + "/auth",
   664  		TokenURL:               testServer.URL + "/token",
   665  		JWKSURL:                testServer.URL + "/jwks",
   666  		UserInfoURL:            "",
   667  		Algorithms:             nil,
   668  		ScopesSupported:        []string{"openid"},
   669  		ResponseTypesSupported: []string{"code"},
   670  	}
   671  	oidcMetadata, err := json.Marshal(oidcMeta)
   672  	assert.Nil(t, err)
   673  
   674  	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
   675  	util.Clock = util.ClockMock{Time: clockTime}
   676  
   677  	cfg := config.NewConfig()
   678  	cfg.Server.WebRoot = "/kiali-test"
   679  	cfg.LoginToken.SigningKey = "kiali67890123456"
   680  	cfg.LoginToken.ExpirationSeconds = 1
   681  	cfg.Auth.OpenId.IssuerUri = testServer.URL
   682  	cfg.Auth.OpenId.ClientId = "kiali-client"
   683  	config.Set(cfg)
   684  
   685  	k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}})
   686  	mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s)
   687  	cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg)
   688  
   689  	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(cfg))))
   690  	uri := fmt.Sprintf("https://kiali.io:44/api/authenticate?code=f0code&state=%x-%s", stateHash, clockTime.UTC().Format("060102150405"))
   691  	request := httptest.NewRequest(http.MethodGet, uri, nil)
   692  	request.AddCookie(&http.Cookie{
   693  		Name:  OpenIdNonceCookieName,
   694  		Value: "nonceString",
   695  	})
   696  
   697  	controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg)
   698  
   699  	rr := httptest.NewRecorder()
   700  	controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   701  		assert.Failf(t, "Callback function shouldn't have been called.", "")
   702  	})).ServeHTTP(rr, request)
   703  
   704  	// nonce cookie cleanup
   705  	response := rr.Result()
   706  	assert.Len(t, response.Cookies(), 1)
   707  	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
   708  	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
   709  
   710  	// Redirection to boot the UI
   711  	u, _ := url.Parse(response.Header.Get("Location"))
   712  	q, _ := url.ParseQuery(u.RawQuery)
   713  	assert.Contains(t, q["openid_error"][0], "cannot parse received id_token from the OpenId provider")
   714  	assert.Equal(t, http.StatusFound, response.StatusCode)
   715  }
   716  
   717  func TestOpenIdCodeFlowShouldRejectMissingAuthorizationCode(t *testing.T) {
   718  	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
   719  	util.Clock = util.ClockMock{Time: clockTime}
   720  
   721  	cfg := config.NewConfig()
   722  	cfg.Server.WebRoot = "/kiali-test"
   723  	cfg.LoginToken.SigningKey = "kiali67890123456"
   724  	cfg.LoginToken.ExpirationSeconds = 1
   725  	config.Set(cfg)
   726  
   727  	k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}})
   728  	mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s)
   729  	cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg)
   730  
   731  	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(cfg))))
   732  	uri := fmt.Sprintf("/api/authenticate?state=%x-%s", stateHash, clockTime.UTC().Format("060102150405"))
   733  	request := httptest.NewRequest(http.MethodGet, uri, nil)
   734  	request.AddCookie(&http.Cookie{
   735  		Name:  OpenIdNonceCookieName,
   736  		Value: "nonceString",
   737  	})
   738  
   739  	controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg)
   740  
   741  	callbackCalled := false
   742  	rr := httptest.NewRecorder()
   743  	controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   744  		callbackCalled = true
   745  	})).ServeHTTP(rr, request)
   746  
   747  	response := rr.Result()
   748  	// No cleanup is done if there are not enough params so that the authorization code flow is triggered
   749  	assert.Len(t, response.Cookies(), 0)
   750  
   751  	// A missing State parameter has the effect that the auth controller ignores the request and
   752  	// passes it to the next handler.
   753  	assert.True(t, callbackCalled)
   754  }
   755  
   756  func TestOpenIdCodeFlowShouldFailWithIdTokenWithoutExpiration(t *testing.T) {
   757  	cachedOpenIdMetadata = nil
   758  	var oidcMetadata []byte
   759  	testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   760  		if r.URL.Path == "/.well-known/openid-configuration" {
   761  			w.WriteHeader(200)
   762  			_, _ = w.Write(oidcMetadata)
   763  		}
   764  		if r.URL.Path == "/token" {
   765  			w.WriteHeader(200)
   766  			_, _ = w.Write([]byte("{ \"id_token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqZG9lQGRvbWFpbi5jb20iLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsIm5vbmNlIjoiMWJhOWI4MzRkMDhhYzgxZmViMzRlMjA4NDAyZWIxOGU5MDliZTA4NDUxOGMzMjg1MTA5NDAxODQifQ.ih34Mh3Sao9bnXCjaobfAEO1BnHnuuLBWxihAzwUqw8\" }"))
   767  		}
   768  	}))
   769  	defer testServer.Close()
   770  
   771  	oidcMeta := openIdMetadata{
   772  		Issuer:                 testServer.URL,
   773  		AuthURL:                testServer.URL + "/auth",
   774  		TokenURL:               testServer.URL + "/token",
   775  		JWKSURL:                testServer.URL + "/jwks",
   776  		UserInfoURL:            "",
   777  		Algorithms:             nil,
   778  		ScopesSupported:        []string{"openid"},
   779  		ResponseTypesSupported: []string{"code"},
   780  	}
   781  	oidcMetadata, err := json.Marshal(oidcMeta)
   782  	assert.Nil(t, err)
   783  
   784  	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
   785  	util.Clock = util.ClockMock{Time: clockTime}
   786  
   787  	cfg := config.NewConfig()
   788  	cfg.Server.WebRoot = "/kiali-test"
   789  	cfg.LoginToken.SigningKey = "kiali67890123456"
   790  	cfg.LoginToken.ExpirationSeconds = 1
   791  	cfg.Auth.OpenId.IssuerUri = testServer.URL
   792  	cfg.Auth.OpenId.ClientId = "kiali-client"
   793  	config.Set(cfg)
   794  
   795  	k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}})
   796  	mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s)
   797  	cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg)
   798  
   799  	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(cfg))))
   800  	uri := fmt.Sprintf("https://kiali.io:44/api/authenticate?code=f0code&state=%x-%s", stateHash, clockTime.UTC().Format("060102150405"))
   801  	request := httptest.NewRequest(http.MethodGet, uri, nil)
   802  	request.AddCookie(&http.Cookie{
   803  		Name:  OpenIdNonceCookieName,
   804  		Value: "nonceString",
   805  	})
   806  
   807  	controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg)
   808  
   809  	rr := httptest.NewRecorder()
   810  	controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   811  		assert.Failf(t, "Callback function shouldn't have been called.", "")
   812  	})).ServeHTTP(rr, request)
   813  
   814  	// nonce cookie cleanup
   815  	response := rr.Result()
   816  	assert.Len(t, response.Cookies(), 1)
   817  	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
   818  	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
   819  
   820  	// Redirection to boot the UI
   821  	q := url.Values{}
   822  	q.Add("openid_error", "the received id_token from the OpenId provider has missing the required 'exp' claim")
   823  	assert.Equal(t, "/kiali-test/?"+q.Encode(), response.Header.Get("Location"))
   824  	assert.Equal(t, http.StatusFound, response.StatusCode)
   825  }
   826  
   827  func TestOpenIdCodeFlowShouldFailWithIdTokenWithNonNumericExpClaim(t *testing.T) {
   828  	cachedOpenIdMetadata = nil
   829  	var oidcMetadata []byte
   830  	testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   831  		if r.URL.Path == "/.well-known/openid-configuration" {
   832  			w.WriteHeader(200)
   833  			_, _ = w.Write(oidcMetadata)
   834  		}
   835  		if r.URL.Path == "/token" {
   836  			w.WriteHeader(200)
   837  			_, _ = w.Write([]byte("{ \"id_token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqZG9lQGRvbWFpbi5jb20iLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsIm5vbmNlIjoiMWJhOWI4MzRkMDhhYzgxZmViMzRlMjA4NDAyZWIxOGU5MDliZTA4NDUxOGMzMjg1MTA5NDAxODQiLCJleHAiOiJmb28ifQ.wdM3yQPwAXLaqZbVku_fcXpisC3tzES8_UUwjbxSPrc\" }"))
   838  		}
   839  	}))
   840  	defer testServer.Close()
   841  
   842  	oidcMeta := openIdMetadata{
   843  		Issuer:                 testServer.URL,
   844  		AuthURL:                testServer.URL + "/auth",
   845  		TokenURL:               testServer.URL + "/token",
   846  		JWKSURL:                testServer.URL + "/jwks",
   847  		UserInfoURL:            "",
   848  		Algorithms:             nil,
   849  		ScopesSupported:        []string{"openid"},
   850  		ResponseTypesSupported: []string{"code"},
   851  	}
   852  	oidcMetadata, err := json.Marshal(oidcMeta)
   853  	assert.Nil(t, err)
   854  
   855  	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
   856  	util.Clock = util.ClockMock{Time: clockTime}
   857  
   858  	cfg := config.NewConfig()
   859  	cfg.Server.WebRoot = "/kiali-test"
   860  	cfg.LoginToken.SigningKey = "kiali67890123456"
   861  	cfg.LoginToken.ExpirationSeconds = 1
   862  	cfg.Auth.OpenId.IssuerUri = testServer.URL
   863  	cfg.Auth.OpenId.ClientId = "kiali-client"
   864  	config.Set(cfg)
   865  
   866  	k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}})
   867  	mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s)
   868  	cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg)
   869  
   870  	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(cfg))))
   871  	uri := fmt.Sprintf("https://kiali.io:44/api/authenticate?code=f0code&state=%x-%s", stateHash, clockTime.UTC().Format("060102150405"))
   872  	request := httptest.NewRequest(http.MethodGet, uri, nil)
   873  	request.AddCookie(&http.Cookie{
   874  		Name:  OpenIdNonceCookieName,
   875  		Value: "nonceString",
   876  	})
   877  
   878  	controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg)
   879  
   880  	rr := httptest.NewRecorder()
   881  	controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   882  		assert.Failf(t, "Callback function shouldn't have been called.", "")
   883  	})).ServeHTTP(rr, request)
   884  
   885  	// nonce cookie cleanup
   886  	response := rr.Result()
   887  	assert.Len(t, response.Cookies(), 1)
   888  	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
   889  	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
   890  
   891  	// Redirection to boot the UI
   892  	u, _ := url.Parse(response.Header.Get("Location"))
   893  	q, _ := url.ParseQuery(u.RawQuery)
   894  	assert.Contains(t, q["openid_error"][0], "token exp claim is present, but invalid")
   895  	assert.Equal(t, http.StatusFound, response.StatusCode)
   896  }
   897  
   898  func TestOpenIdCodeFlowShouldRejectInvalidState(t *testing.T) {
   899  	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
   900  	util.Clock = util.ClockMock{Time: clockTime}
   901  
   902  	cfg := config.NewConfig()
   903  	cfg.Server.WebRoot = "/kiali-test"
   904  	cfg.LoginToken.SigningKey = "kiali67890123456"
   905  	cfg.LoginToken.ExpirationSeconds = 1
   906  	config.Set(cfg)
   907  
   908  	// Calculate a hash of the wrong string.
   909  	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "badNonceString", clockTime.UTC().Format("060102150405"), getSigningKey(cfg))))
   910  	uri := fmt.Sprintf("/api/authenticate?code=f0code&state=%x-%s", stateHash, clockTime.UTC().Format("060102150405"))
   911  	request := httptest.NewRequest(http.MethodGet, uri, nil)
   912  	request.AddCookie(&http.Cookie{
   913  		Name:  OpenIdNonceCookieName,
   914  		Value: "nonceString",
   915  	})
   916  
   917  	k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}})
   918  	mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s)
   919  	cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg)
   920  
   921  	controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg)
   922  
   923  	rr := httptest.NewRecorder()
   924  	controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   925  		assert.Failf(t, "Callback function shouldn't have been called.", "")
   926  	})).ServeHTTP(rr, request)
   927  
   928  	// nonce cookie cleanup
   929  	response := rr.Result()
   930  	assert.Len(t, response.Cookies(), 1)
   931  	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
   932  	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
   933  
   934  	// Redirection to boot the UI
   935  	q := url.Values{}
   936  	q.Add("openid_error", "Request rejected: CSRF mitigation")
   937  	assert.Equal(t, "/kiali-test/?"+q.Encode(), response.Header.Get("Location"))
   938  	assert.Equal(t, http.StatusFound, response.StatusCode)
   939  }
   940  
   941  func TestOpenIdCodeFlowShouldRejectBadStateFormat(t *testing.T) {
   942  	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
   943  	util.Clock = util.ClockMock{Time: clockTime}
   944  
   945  	cfg := config.NewConfig()
   946  	cfg.Server.WebRoot = "/kiali-test"
   947  	cfg.LoginToken.SigningKey = "kiali67890123456"
   948  	cfg.LoginToken.ExpirationSeconds = 1
   949  	config.Set(cfg)
   950  
   951  	// Calculate a hash of the wrong string.
   952  	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(cfg))))
   953  	uri := fmt.Sprintf("/api/authenticate?code=f0code&state=%xp%s", stateHash, clockTime.UTC().Format("060102150405"))
   954  	request := httptest.NewRequest(http.MethodGet, uri, nil)
   955  	request.AddCookie(&http.Cookie{
   956  		Name:  OpenIdNonceCookieName,
   957  		Value: "nonceString",
   958  	})
   959  
   960  	k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}})
   961  	mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s)
   962  	cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg)
   963  
   964  	controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg)
   965  
   966  	rr := httptest.NewRecorder()
   967  	controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   968  		assert.Failf(t, "Callback function shouldn't have been called.", "")
   969  	})).ServeHTTP(rr, request)
   970  
   971  	// nonce cookie cleanup
   972  	response := rr.Result()
   973  	assert.Len(t, response.Cookies(), 1)
   974  	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
   975  	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
   976  
   977  	// Redirection to boot the UI
   978  	q := url.Values{}
   979  	q.Add("openid_error", "Request rejected: State parameter is invalid")
   980  	assert.Equal(t, "/kiali-test/?"+q.Encode(), response.Header.Get("Location"))
   981  	assert.Equal(t, http.StatusFound, response.StatusCode)
   982  }
   983  
   984  func TestOpenIdCodeFlowShouldRejectMissingState(t *testing.T) {
   985  	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
   986  	util.Clock = util.ClockMock{Time: clockTime}
   987  
   988  	cfg := config.NewConfig()
   989  	cfg.Server.WebRoot = "/kiali-test"
   990  	cfg.LoginToken.SigningKey = "kiali67890123456"
   991  	cfg.LoginToken.ExpirationSeconds = 1
   992  	config.Set(cfg)
   993  
   994  	uri := "/api/authenticate?code=f0code"
   995  	request := httptest.NewRequest(http.MethodGet, uri, nil)
   996  	request.AddCookie(&http.Cookie{
   997  		Name:  OpenIdNonceCookieName,
   998  		Value: "nonceString",
   999  	})
  1000  
  1001  	k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}})
  1002  	mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s)
  1003  	cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg)
  1004  
  1005  	controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg)
  1006  
  1007  	callbackCalled := false
  1008  	rr := httptest.NewRecorder()
  1009  	controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1010  		callbackCalled = true
  1011  	})).ServeHTTP(rr, request)
  1012  
  1013  	response := rr.Result()
  1014  	// No cleanup is done if there are not enough params so that the authorization code flow is triggered
  1015  	assert.Len(t, response.Cookies(), 0)
  1016  
  1017  	// A missing State parameter has the effect that the auth controller ignores the request and
  1018  	// passes it to the next handler.
  1019  	assert.True(t, callbackCalled)
  1020  }
  1021  
  1022  func TestOpenIdCodeFlowShouldRejectMissingNonceCookie(t *testing.T) {
  1023  	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
  1024  	util.Clock = util.ClockMock{Time: clockTime}
  1025  
  1026  	cfg := config.NewConfig()
  1027  	cfg.Server.WebRoot = "/kiali-test"
  1028  	cfg.LoginToken.SigningKey = "kiali67890123456"
  1029  	cfg.LoginToken.ExpirationSeconds = 1
  1030  	config.Set(cfg)
  1031  
  1032  	k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}})
  1033  	mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s)
  1034  	cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg)
  1035  
  1036  	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(cfg))))
  1037  	uri := fmt.Sprintf("/api/authenticate?code=f0code&state=%x-%s", stateHash, clockTime.UTC().Format("060102150405"))
  1038  	request := httptest.NewRequest(http.MethodGet, uri, nil)
  1039  
  1040  	controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg)
  1041  
  1042  	callbackCalled := false
  1043  	rr := httptest.NewRecorder()
  1044  	controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1045  		callbackCalled = true
  1046  	})).ServeHTTP(rr, request)
  1047  
  1048  	// No cleanup is done if there are not enough params so that the authorization code flow is triggered
  1049  	response := rr.Result()
  1050  	assert.Len(t, response.Cookies(), 0)
  1051  
  1052  	// A missing nonce cookie has the effect that the auth controller ignores the request and
  1053  	// passes it to the next handler.
  1054  	assert.True(t, callbackCalled)
  1055  }
  1056  
  1057  func TestOpenIdCodeFlowShouldRejectMissingNonceInToken(t *testing.T) {
  1058  	cachedOpenIdMetadata = nil
  1059  	var oidcMetadata []byte
  1060  	testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1061  		if r.URL.Path == "/.well-known/openid-configuration" {
  1062  			w.WriteHeader(200)
  1063  			_, _ = w.Write(oidcMetadata)
  1064  		}
  1065  		if r.URL.Path == "/token" {
  1066  			w.WriteHeader(200)
  1067  			_, _ = w.Write([]byte("{ \"id_token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqZG9lQGRvbWFpbi5jb20iLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTMxMTI4MTk3MH0.xAoq7T-wti__Je1PDuTgNonoVSu059FzpOHsNm26YTg\" }"))
  1068  		}
  1069  	}))
  1070  	defer testServer.Close()
  1071  
  1072  	oidcMeta := openIdMetadata{
  1073  		Issuer:                 testServer.URL,
  1074  		AuthURL:                testServer.URL + "/auth",
  1075  		TokenURL:               testServer.URL + "/token",
  1076  		JWKSURL:                testServer.URL + "/jwks",
  1077  		UserInfoURL:            "",
  1078  		Algorithms:             nil,
  1079  		ScopesSupported:        []string{"openid"},
  1080  		ResponseTypesSupported: []string{"code"},
  1081  	}
  1082  	oidcMetadata, err := json.Marshal(oidcMeta)
  1083  	assert.Nil(t, err)
  1084  
  1085  	clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC)
  1086  	util.Clock = util.ClockMock{Time: clockTime}
  1087  
  1088  	cfg := config.NewConfig()
  1089  	cfg.Server.WebRoot = "/kiali-test"
  1090  	cfg.LoginToken.SigningKey = "kiali67890123456"
  1091  	cfg.LoginToken.ExpirationSeconds = 1
  1092  	cfg.Auth.OpenId.IssuerUri = testServer.URL
  1093  	cfg.Auth.OpenId.ClientId = "kiali-client"
  1094  	config.Set(cfg)
  1095  
  1096  	k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}})
  1097  	mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s)
  1098  	cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg)
  1099  
  1100  	stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(cfg))))
  1101  	uri := fmt.Sprintf("https://kiali.io:44/api/authenticate?code=f0code&state=%x-%s", stateHash, clockTime.UTC().Format("060102150405"))
  1102  	request := httptest.NewRequest(http.MethodGet, uri, nil)
  1103  	request.AddCookie(&http.Cookie{
  1104  		Name:  OpenIdNonceCookieName,
  1105  		Value: "nonceString",
  1106  	})
  1107  
  1108  	controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg)
  1109  
  1110  	rr := httptest.NewRecorder()
  1111  	controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1112  		assert.Failf(t, "Callback function shouldn't have been called.", "")
  1113  	})).ServeHTTP(rr, request)
  1114  
  1115  	// nonce cookie cleanup
  1116  	response := rr.Result()
  1117  	assert.Len(t, response.Cookies(), 1)
  1118  	assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name)
  1119  	assert.True(t, clockTime.After(response.Cookies()[0].Expires))
  1120  
  1121  	// Redirection to boot the UI
  1122  	q := url.Values{}
  1123  	q.Add("openid_error", "OpenId token rejected: nonce code mismatch")
  1124  	assert.Equal(t, "/kiali-test/?"+q.Encode(), response.Header.Get("Location"))
  1125  	assert.Equal(t, http.StatusFound, response.StatusCode)
  1126  }