/* * Copyright 2024 the KubeSphere Authors. * Please refer to the LICENSE file in the root directory of the project. * https://github.com/kubesphere/kubesphere/blob/master/LICENSE */ package oidc import ( "bytes" cryptorand "crypto/rand" "crypto/rsa" "encoding/base64" "encoding/binary" "encoding/json" "fmt" "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" "kubesphere.io/kubesphere/pkg/server/options" "github.com/go-jose/go-jose/v4" "github.com/golang-jwt/jwt/v4" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/onsi/gomega/gexec" "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider" ) var ( oidcServer *httptest.Server ) func TestOIDC(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "OIDC Identity Provider Suite") } var _ = BeforeSuite(func() { privateKey, err := rsa.GenerateKey(cryptorand.Reader, 2048) Expect(err).Should(BeNil()) jwk := jose.JSONWebKey{ Key: privateKey, KeyID: "keyID", Algorithm: "RSA", } oidcServer = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var data interface{} switch r.RequestURI { case "/.well-known/openid-configuration": data = map[string]interface{}{ "issuer": oidcServer.URL, "token_endpoint": fmt.Sprintf("%s/token", oidcServer.URL), "authorization_endpoint": fmt.Sprintf("%s/authorize", oidcServer.URL), "userinfo_endpoint": fmt.Sprintf("%s/userinfo", oidcServer.URL), "end_session_endpoint": fmt.Sprintf("%s/endsession", oidcServer.URL), "jwks_uri": fmt.Sprintf("%s/keys", oidcServer.URL), "response_types_supported": []string{ "code", "token", "id_token", "none", }, "id_token_signing_alg_values_supported": []string{ "RS256", }, "scopes_supported": []string{ "openid", "email", "profile", }, "token_endpoint_auth_methods_supported": []string{ "client_secret_post", "client_secret_basic", }, "claims_supported": []string{ "aud", "email", "email_verified", "exp", "iat", "iss", "name", "sub", }, "code_challenge_methods_supported": []string{ "plain", "S256", }, "grant_types_supported": []string{ "authorization_code", "refresh_token", }, } case "/user": data = map[string]interface{}{ "login": "test", "email": "test@kubesphere.io", } case "/keys": data = map[string]interface{}{ "keys": []map[string]interface{}{{ "alg": jwk.Algorithm, "kty": jwk.Algorithm, "kid": jwk.KeyID, "n": n(&privateKey.PublicKey), "e": e(&privateKey.PublicKey), }}, } case "/token": claims := jwt.MapClaims{ "iss": oidcServer.URL, "sub": "110169484474386276334", "aud": "kubesphere", "email": "test@kubesphere.io", "email_verified": "true", "name": "test", "iat": time.Now().Unix(), "exp": time.Now().Add(10 * time.Hour).Unix(), } idToken, _ := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(privateKey) data = map[string]interface{}{ "access_token": "e72e16c7e42f292c6912e7710c838347ae178b4a", "id_token": idToken, "token_type": "Bearer", "expires_in": 3600, } default: fmt.Println(r.URL) w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("not implemented")) return } w.Header().Add("Content-Type", "application/json") json.NewEncoder(w).Encode(data) })) }) var _ = AfterSuite(func() { By("tearing down the test environment") gexec.KillAndWait(5 * time.Second) oidcServer.Close() }) var _ = Describe("OIDC", func() { Context("OIDC", func() { var ( provider identityprovider.OAuthProvider err error ) It("should configure successfully", func() { config := options.DynamicOptions{ "issuer": oidcServer.URL, "clientID": "kubesphere", "clientSecret": "c53e80ab92d48ab12f4e7f1f6976d1bdc996e0d7", "redirectURL": "https://ks-console.kubesphere-system.svc/oauth/redirect/oidc", "insecureSkipVerify": true, } factory := oidcProviderFactory{} provider, err = factory.Create(config) Expect(err).Should(BeNil()) expected := options.DynamicOptions{ "issuer": oidcServer.URL, "clientID": "kubesphere", "clientSecret": "c53e80ab92d48ab12f4e7f1f6976d1bdc996e0d7", "redirectURL": "https://ks-console.kubesphere-system.svc/oauth/redirect/oidc", "insecureSkipVerify": true, "endpoint": options.DynamicOptions{ "authURL": fmt.Sprintf("%s/authorize", oidcServer.URL), "tokenURL": fmt.Sprintf("%s/token", oidcServer.URL), "userInfoURL": fmt.Sprintf("%s/userinfo", oidcServer.URL), "jwksURL": fmt.Sprintf("%s/keys", oidcServer.URL), "endSessionURL": fmt.Sprintf("%s/endsession?client_id=kubesphere&post_logout_redirect_uri=", oidcServer.URL), }, } Expect(config).Should(Equal(expected)) }) It("should login successfully", func() { url, _ := url.Parse("https://ks-console.kubesphere-system.svc/oauth/redirect/oidc?code=00000") req := &http.Request{URL: url} identity, err := provider.IdentityExchangeCallback(req) Expect(err).Should(BeNil()) Expect(identity.GetUserID()).Should(Equal("110169484474386276334")) Expect(identity.GetUsername()).Should(Equal("test")) Expect(identity.GetEmail()).Should(Equal("test@kubesphere.io")) }) }) }) func n(pub *rsa.PublicKey) string { return encode(pub.N.Bytes()) } func e(pub *rsa.PublicKey) string { data := make([]byte, 8) binary.BigEndian.PutUint64(data, uint64(pub.E)) return encode(bytes.TrimLeft(data, "\x00")) } func encode(payload []byte) string { result := base64.URLEncoding.EncodeToString(payload) return strings.TrimRight(result, "=") }