Merge pull request #3525 from wansir/oidc-protocol
support OIDC protocol
This commit is contained in:
@@ -25,9 +25,10 @@ import (
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/controller/storage/snapshotclass"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
|
||||
iamv1alpha2 "kubesphere.io/api/iam/v1alpha2"
|
||||
|
||||
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
|
||||
"kubesphere.io/kubesphere/pkg/controller/certificatesigningrequest"
|
||||
"kubesphere.io/kubesphere/pkg/controller/cluster"
|
||||
"kubesphere.io/kubesphere/pkg/controller/clusterrolebinding"
|
||||
@@ -62,7 +63,7 @@ func addControllers(
|
||||
s3Client s3.Interface,
|
||||
ldapClient ldapclient.Interface,
|
||||
options *k8s.KubernetesOptions,
|
||||
authenticationOptions *authoptions.AuthenticationOptions,
|
||||
authenticationOptions *authentication.Options,
|
||||
multiClusterOptions *multicluster.Options,
|
||||
networkOptions *network.Options,
|
||||
serviceMeshEnabled bool,
|
||||
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
@@ -28,7 +30,6 @@ import (
|
||||
cliflag "k8s.io/component-base/cli/flag"
|
||||
"k8s.io/klog"
|
||||
|
||||
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
|
||||
"kubesphere.io/kubesphere/pkg/simple/client/devops/jenkins"
|
||||
"kubesphere.io/kubesphere/pkg/simple/client/gateway"
|
||||
"kubesphere.io/kubesphere/pkg/simple/client/k8s"
|
||||
@@ -44,7 +45,7 @@ type KubeSphereControllerManagerOptions struct {
|
||||
KubernetesOptions *k8s.KubernetesOptions
|
||||
DevopsOptions *jenkins.Options
|
||||
S3Options *s3.Options
|
||||
AuthenticationOptions *authoptions.AuthenticationOptions
|
||||
AuthenticationOptions *authentication.Options
|
||||
LdapOptions *ldapclient.Options
|
||||
OpenPitrixOptions *openpitrix.Options
|
||||
NetworkOptions *network.Options
|
||||
@@ -75,7 +76,7 @@ func NewKubeSphereControllerManagerOptions() *KubeSphereControllerManagerOptions
|
||||
NetworkOptions: network.NewNetworkOptions(),
|
||||
MultiClusterOptions: multicluster.NewOptions(),
|
||||
ServiceMeshOptions: servicemesh.NewServiceMeshOptions(),
|
||||
AuthenticationOptions: authoptions.NewAuthenticateOptions(),
|
||||
AuthenticationOptions: authentication.NewOptions(),
|
||||
GatewayOptions: gateway.NewGatewayOptions(),
|
||||
LeaderElection: &leaderelection.LeaderElectionConfig{
|
||||
LeaseDuration: 30 * time.Second,
|
||||
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/token"
|
||||
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
cliflag "k8s.io/component-base/cli/flag"
|
||||
"k8s.io/klog"
|
||||
@@ -238,6 +240,11 @@ func (s *ServerRunOptions) NewAPIServer(stopCh <-chan struct{}) (*apiserver.APIS
|
||||
klog.Fatalf("unable to create controller runtime client: %v", err)
|
||||
}
|
||||
|
||||
apiServer.Issuer, err = token.NewIssuer(s.AuthenticationOptions)
|
||||
if err != nil {
|
||||
klog.Fatalf("unable to create issuer: %v", err)
|
||||
}
|
||||
|
||||
apiServer.Server = server
|
||||
|
||||
return apiServer, nil
|
||||
|
||||
@@ -24,6 +24,10 @@ import (
|
||||
rt "runtime"
|
||||
"time"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/token"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authorization"
|
||||
|
||||
"kubesphere.io/api/notification/v2beta1"
|
||||
|
||||
openpitrixv2alpha1 "kubesphere.io/kubesphere/pkg/kapis/openpitrix/v2alpha1"
|
||||
@@ -48,13 +52,12 @@ import (
|
||||
|
||||
audit "kubesphere.io/kubesphere/pkg/apiserver/auditing"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/authenticators/basic"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/authenticators/jwttoken"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/authenticators/jwt"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/request/anonymous"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/request/basictoken"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/request/bearertoken"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizerfactory"
|
||||
authorizationoptions "kubesphere.io/kubesphere/pkg/apiserver/authorization/options"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authorization/path"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authorization/rbac"
|
||||
unionauthorizer "kubesphere.io/kubesphere/pkg/apiserver/authorization/union"
|
||||
@@ -107,23 +110,10 @@ import (
|
||||
utilnet "kubesphere.io/kubesphere/pkg/utils/net"
|
||||
)
|
||||
|
||||
const (
|
||||
// ApiRootPath defines the root path of all KubeSphere apis.
|
||||
ApiRootPath = "/kapis"
|
||||
|
||||
// MimeMergePatchJson is the mime header used in merge request
|
||||
MimeMergePatchJson = "application/merge-patch+json"
|
||||
|
||||
//
|
||||
MimeJsonPatchJson = "application/json-patch+json"
|
||||
)
|
||||
|
||||
type APIServer struct {
|
||||
|
||||
// number of kubesphere apiserver
|
||||
ServerCount int
|
||||
|
||||
//
|
||||
Server *http.Server
|
||||
|
||||
Config *apiserverconfig.Config
|
||||
@@ -146,13 +136,10 @@ type APIServer struct {
|
||||
|
||||
MetricsClient monitoring.Interface
|
||||
|
||||
//
|
||||
LoggingClient logging.Client
|
||||
|
||||
//
|
||||
DevopsClient devops.Interface
|
||||
|
||||
//
|
||||
S3Client s3.Interface
|
||||
|
||||
SonarClient sonarqube.SonarInterface
|
||||
@@ -165,6 +152,10 @@ type APIServer struct {
|
||||
|
||||
// controller-runtime cache
|
||||
RuntimeCache runtimecache.Cache
|
||||
|
||||
// entity that issues tokens
|
||||
Issuer token.Issuer
|
||||
|
||||
// controller-runtime client
|
||||
RuntimeClient runtimeclient.Client
|
||||
}
|
||||
@@ -178,7 +169,6 @@ func (s *APIServer) PrepareRun(stopCh <-chan struct{}) error {
|
||||
})
|
||||
|
||||
s.installKubeSphereAPIs()
|
||||
|
||||
s.installMetricsAPI()
|
||||
s.container.Filter(monitorRequest)
|
||||
|
||||
@@ -246,19 +236,12 @@ func (s *APIServer) installKubeSphereAPIs() {
|
||||
group.New(s.InformerFactory, s.KubernetesClient.KubeSphere(), s.KubernetesClient.Kubernetes()),
|
||||
rbacAuthorizer))
|
||||
|
||||
userLister := s.InformerFactory.KubeSphereSharedInformerFactory().Iam().V1alpha2().Users().Lister()
|
||||
urlruntime.Must(oauth.AddToContainer(s.container, imOperator,
|
||||
auth.NewTokenOperator(
|
||||
s.CacheClient,
|
||||
s.Config.AuthenticationOptions),
|
||||
auth.NewPasswordAuthenticator(
|
||||
s.KubernetesClient.KubeSphere(),
|
||||
s.InformerFactory.KubeSphereSharedInformerFactory().Iam().V1alpha2().Users().Lister(),
|
||||
s.Config.AuthenticationOptions),
|
||||
auth.NewOAuthAuthenticator(s.KubernetesClient.KubeSphere(),
|
||||
s.InformerFactory.KubeSphereSharedInformerFactory(),
|
||||
s.Config.AuthenticationOptions),
|
||||
auth.NewLoginRecorder(s.KubernetesClient.KubeSphere(),
|
||||
s.InformerFactory.KubeSphereSharedInformerFactory().Iam().V1alpha2().Users().Lister()),
|
||||
auth.NewTokenOperator(s.CacheClient, s.Issuer, s.Config.AuthenticationOptions),
|
||||
auth.NewPasswordAuthenticator(s.KubernetesClient.KubeSphere(), userLister, s.Config.AuthenticationOptions),
|
||||
auth.NewOAuthAuthenticator(s.KubernetesClient.KubeSphere(), userLister, s.Config.AuthenticationOptions),
|
||||
auth.NewLoginRecorder(s.KubernetesClient.KubeSphere(), userLister),
|
||||
s.Config.AuthenticationOptions))
|
||||
urlruntime.Must(servicemeshv1alpha2.AddToContainer(s.Config.ServiceMeshOptions, s.container, s.KubernetesClient.Kubernetes(), s.CacheClient))
|
||||
urlruntime.Must(networkv1alpha2.AddToContainer(s.container, s.Config.NetworkOptions.WeaveScopeHost))
|
||||
@@ -330,13 +313,13 @@ func (s *APIServer) buildHandlerChain(stopCh <-chan struct{}) {
|
||||
var authorizers authorizer.Authorizer
|
||||
|
||||
switch s.Config.AuthorizationOptions.Mode {
|
||||
case authorizationoptions.AlwaysAllow:
|
||||
case authorization.AlwaysAllow:
|
||||
authorizers = authorizerfactory.NewAlwaysAllowAuthorizer()
|
||||
case authorizationoptions.AlwaysDeny:
|
||||
case authorization.AlwaysDeny:
|
||||
authorizers = authorizerfactory.NewAlwaysDenyAuthorizer()
|
||||
default:
|
||||
fallthrough
|
||||
case authorizationoptions.RBAC:
|
||||
case authorization.RBAC:
|
||||
excludedPaths := []string{"/oauth/*", "/kapis/config.kubesphere.io/*", "/kapis/version", "/kapis/metrics"}
|
||||
pathAuthorizer, _ := path.NewAuthorizer(excludedPaths)
|
||||
amOperator := am.NewReadOnlyOperator(s.InformerFactory)
|
||||
@@ -349,16 +332,19 @@ func (s *APIServer) buildHandlerChain(stopCh <-chan struct{}) {
|
||||
handler = filters.WithMultipleClusterDispatcher(handler, clusterDispatcher)
|
||||
}
|
||||
|
||||
loginRecorder := auth.NewLoginRecorder(s.KubernetesClient.KubeSphere(),
|
||||
s.InformerFactory.KubeSphereSharedInformerFactory().Iam().V1alpha2().Users().Lister())
|
||||
userLister := s.InformerFactory.KubeSphereSharedInformerFactory().Iam().V1alpha2().Users().Lister()
|
||||
loginRecorder := auth.NewLoginRecorder(s.KubernetesClient.KubeSphere(), userLister)
|
||||
|
||||
// authenticators are unordered
|
||||
authn := unionauth.New(anonymous.NewAuthenticator(),
|
||||
basictoken.New(basic.NewBasicAuthenticator(auth.NewPasswordAuthenticator(s.KubernetesClient.KubeSphere(),
|
||||
s.InformerFactory.KubeSphereSharedInformerFactory().Iam().V1alpha2().Users().Lister(),
|
||||
s.Config.AuthenticationOptions), loginRecorder)),
|
||||
bearertoken.New(jwttoken.NewTokenAuthenticator(auth.NewTokenOperator(s.CacheClient,
|
||||
basictoken.New(basic.NewBasicAuthenticator(auth.NewPasswordAuthenticator(
|
||||
s.KubernetesClient.KubeSphere(),
|
||||
userLister,
|
||||
s.Config.AuthenticationOptions),
|
||||
s.InformerFactory.KubeSphereSharedInformerFactory().Iam().V1alpha2().Users().Lister())))
|
||||
loginRecorder)),
|
||||
bearertoken.New(jwt.NewTokenAuthenticator(
|
||||
auth.NewTokenOperator(s.CacheClient, s.Issuer, s.Config.AuthenticationOptions),
|
||||
userLister)))
|
||||
handler = filters.WithAuthentication(handler, authn)
|
||||
handler = filters.WithRequestInfo(handler, requestInfoResolver)
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ func NewBasicAuthenticator(authenticator auth.PasswordAuthenticator, loginRecord
|
||||
}
|
||||
|
||||
func (t *basicAuthenticator) AuthenticatePassword(ctx context.Context, username, password string) (*authenticator.Response, bool, error) {
|
||||
authenticated, provider, err := t.authenticator.Authenticate(username, password)
|
||||
authenticated, provider, err := t.authenticator.Authenticate(ctx, username, password)
|
||||
if err != nil {
|
||||
if t.loginRecorder != nil && err == auth.IncorrectPasswordError {
|
||||
var sourceIP, userAgent string
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package jwttoken
|
||||
package jwt
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -48,31 +48,27 @@ func NewTokenAuthenticator(tokenOperator auth.TokenManagementInterface, userList
|
||||
}
|
||||
|
||||
func (t *tokenAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {
|
||||
providedUser, err := t.tokenOperator.Verify(token)
|
||||
verified, err := t.tokenOperator.Verify(token)
|
||||
if err != nil {
|
||||
klog.Warning(err)
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if providedUser.GetName() == iamv1alpha2.PreRegistrationUser {
|
||||
if verified.User.GetName() == iamv1alpha2.PreRegistrationUser {
|
||||
return &authenticator.Response{
|
||||
User: &user.DefaultInfo{
|
||||
Name: providedUser.GetName(),
|
||||
Extra: providedUser.GetExtra(),
|
||||
Groups: providedUser.GetGroups(),
|
||||
},
|
||||
User: verified.User,
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
dbUser, err := t.userLister.Get(providedUser.GetName())
|
||||
u, err := t.userLister.Get(verified.User.GetName())
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return &authenticator.Response{
|
||||
User: &user.DefaultInfo{
|
||||
Name: dbUser.GetName(),
|
||||
Groups: append(dbUser.Spec.Groups, user.AllAuthenticated),
|
||||
Name: u.GetName(),
|
||||
Groups: append(u.Spec.Groups, user.AllAuthenticated),
|
||||
},
|
||||
}, true, nil
|
||||
}
|
||||
@@ -17,10 +17,10 @@ limitations under the License.
|
||||
package aliyunidaas
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
||||
@@ -120,13 +120,16 @@ func (a idaasIdentity) GetEmail() string {
|
||||
return a.Email
|
||||
}
|
||||
|
||||
func (a *aliyunIDaaS) IdentityExchange(code string) (identityprovider.Identity, error) {
|
||||
token, err := a.Config.Exchange(context.TODO(), code)
|
||||
func (a *aliyunIDaaS) IdentityExchangeCallback(req *http.Request) (identityprovider.Identity, error) {
|
||||
// OAuth2 callback, see also https://tools.ietf.org/html/rfc6749#section-4.1.2
|
||||
code := req.URL.Query().Get("code")
|
||||
ctx := req.Context()
|
||||
token, err := a.Config.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := oauth2.NewClient(context.TODO(), oauth2.StaticTokenSource(token)).Get(a.Endpoint.UserInfoURL)
|
||||
resp, err := oauth2.NewClient(ctx, oauth2.StaticTokenSource(token)).Get(a.Endpoint.UserInfoURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ endpoint:
|
||||
userInfoUrl: "https://xxxxx.login.aliyunidaas.com/api/bff/v1.2/oauth2/userinfo"
|
||||
authURL: "https://xxxx.login.aliyunidaas.com/oauth/authorize"
|
||||
tokenURL: "https://xxxx.login.aliyunidaas.com/oauth/token"
|
||||
redirectURL: "http://ks-console/oauth/redirect"
|
||||
redirectURL: "https://ks-console.kubesphere-system.svc/oauth/redirect/idaas"
|
||||
scopes:
|
||||
- read
|
||||
`)},
|
||||
@@ -65,7 +65,7 @@ scopes:
|
||||
TokenURL: "https://xxxx.login.aliyunidaas.com/oauth/token",
|
||||
UserInfoURL: "https://xxxxx.login.aliyunidaas.com/api/bff/v1.2/oauth2/userinfo",
|
||||
},
|
||||
RedirectURL: "http://ks-console/oauth/redirect",
|
||||
RedirectURL: "https://ks-console.kubesphere-system.svc/oauth/redirect/idaas",
|
||||
Scopes: []string{"read"},
|
||||
Config: &oauth2.Config{
|
||||
ClientID: "xxxx",
|
||||
@@ -75,7 +75,7 @@ scopes:
|
||||
TokenURL: "https://xxxx.login.aliyunidaas.com/oauth/token",
|
||||
AuthStyle: oauth2.AuthStyleAutoDetect,
|
||||
},
|
||||
RedirectURL: "http://ks-console/oauth/redirect",
|
||||
RedirectURL: "https://ks-console.kubesphere-system.svc/oauth/redirect/idaas",
|
||||
Scopes: []string{"read"},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -89,10 +89,12 @@ func (f casProviderFactory) Create(options oauth.DynamicOptions) (identityprovid
|
||||
return &cas, nil
|
||||
}
|
||||
|
||||
func (c cas) IdentityExchange(ticket string) (identityprovider.Identity, error) {
|
||||
func (c cas) IdentityExchangeCallback(req *http.Request) (identityprovider.Identity, error) {
|
||||
// CAS callback, see also https://apereo.github.io/cas/6.3.x/protocol/CAS-Protocol-V2-Specification.html#25-servicevalidate-cas-20
|
||||
ticket := req.URL.Query().Get("ticket")
|
||||
resp, err := c.client.ValidateServiceTicket(gocas.ServiceTicket(ticket))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cas validate service ticket failed: %v", err)
|
||||
return nil, fmt.Errorf("cas: failed to validate service ticket : %v", err)
|
||||
}
|
||||
return &casIdentity{User: resp.User}, nil
|
||||
}
|
||||
|
||||
@@ -167,8 +167,10 @@ func (g githubIdentity) GetEmail() string {
|
||||
return g.Email
|
||||
}
|
||||
|
||||
func (g *github) IdentityExchange(code string) (identityprovider.Identity, error) {
|
||||
ctx := context.TODO()
|
||||
func (g *github) IdentityExchangeCallback(req *http.Request) (identityprovider.Identity, error) {
|
||||
// OAuth2 callback, see also https://tools.ietf.org/html/rfc6749#section-4.1.2
|
||||
code := req.URL.Query().Get("code")
|
||||
ctx := req.Context()
|
||||
if g.InsecureSkipVerify {
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -86,7 +87,7 @@ var _ = Describe("GitHub", func() {
|
||||
configYAML := `
|
||||
clientID: de6ff8bed0304e487b6e
|
||||
clientSecret: 2b70536f79ec8d2939863509d05e2a71c268b9af
|
||||
redirectURL: "http://ks-console/oauth/redirect"
|
||||
redirectURL: "https://ks-console.kubesphere-system.svc/oauth/redirect/github"
|
||||
scopes:
|
||||
- user
|
||||
`
|
||||
@@ -102,7 +103,7 @@ scopes:
|
||||
TokenURL: tokenURL,
|
||||
UserInfoURL: userInfoURL,
|
||||
},
|
||||
RedirectURL: "http://ks-console/oauth/redirect",
|
||||
RedirectURL: "https://ks-console.kubesphere-system.svc/oauth/redirect/github",
|
||||
Scopes: []string{"user"},
|
||||
Config: &oauth2.Config{
|
||||
ClientID: "de6ff8bed0304e487b6e",
|
||||
@@ -111,7 +112,7 @@ scopes:
|
||||
AuthURL: authURL,
|
||||
TokenURL: tokenURL,
|
||||
},
|
||||
RedirectURL: "http://ks-console/oauth/redirect",
|
||||
RedirectURL: "https://ks-console.kubesphere-system.svc/oauth/redirect/github",
|
||||
Scopes: []string{"user"},
|
||||
},
|
||||
}
|
||||
@@ -121,7 +122,7 @@ scopes:
|
||||
config := oauth.DynamicOptions{
|
||||
"clientID": "de6ff8bed0304e487b6e",
|
||||
"clientSecret": "2b70536f79ec8d2939863509d05e2a71c268b9af",
|
||||
"redirectURL": "http://ks-console/oauth/redirect",
|
||||
"redirectURL": "https://ks-console.kubesphere-system.svc/oauth/redirect/github",
|
||||
"insecureSkipVerify": true,
|
||||
"endpoint": oauth.DynamicOptions{
|
||||
"authURL": fmt.Sprintf("%s/login/oauth/authorize", githubServer.URL),
|
||||
@@ -135,7 +136,7 @@ scopes:
|
||||
expected := oauth.DynamicOptions{
|
||||
"clientID": "de6ff8bed0304e487b6e",
|
||||
"clientSecret": "2b70536f79ec8d2939863509d05e2a71c268b9af",
|
||||
"redirectURL": "http://ks-console/oauth/redirect",
|
||||
"redirectURL": "https://ks-console.kubesphere-system.svc/oauth/redirect/github",
|
||||
"insecureSkipVerify": true,
|
||||
"endpoint": oauth.DynamicOptions{
|
||||
"authURL": fmt.Sprintf("%s/login/oauth/authorize", githubServer.URL),
|
||||
@@ -146,7 +147,9 @@ scopes:
|
||||
Expect(config).Should(Equal(expected))
|
||||
})
|
||||
It("should login successfully", func() {
|
||||
identity, err := provider.IdentityExchange("3389")
|
||||
url, _ := url.Parse("https://ks-console.kubesphere-system.svc/oauth/redirect/test?code=00000")
|
||||
req := &http.Request{URL: url}
|
||||
identity, err := provider.IdentityExchangeCallback(req)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(identity.GetUserID()).Should(Equal("test"))
|
||||
Expect(identity.GetUsername()).Should(Equal("test"))
|
||||
|
||||
@@ -35,13 +35,13 @@ var (
|
||||
|
||||
// Identity represents the account mapped to kubesphere
|
||||
type Identity interface {
|
||||
// required
|
||||
// GetUserID required
|
||||
// Identifier for the End-User at the Issuer.
|
||||
GetUserID() string
|
||||
// optional
|
||||
// GetUsername optional
|
||||
// The username which the End-User wishes to be referred to kubesphere.
|
||||
GetUsername() string
|
||||
// optional
|
||||
// GetEmail optional
|
||||
GetEmail() string
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
package identityprovider
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
@@ -50,7 +51,7 @@ func (e emptyIdentity) GetEmail() string {
|
||||
return "test@test.com"
|
||||
}
|
||||
|
||||
func (e emptyOAuthProvider) IdentityExchange(code string) (Identity, error) {
|
||||
func (e emptyOAuthProvider) IdentityExchangeCallback(req *http.Request) (Identity, error) {
|
||||
return emptyIdentity{}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -13,20 +13,23 @@ 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 identityprovider
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
)
|
||||
|
||||
type OAuthProvider interface {
|
||||
// IdentityExchange exchange identity from remote server
|
||||
IdentityExchange(code string) (Identity, error)
|
||||
// IdentityExchangeCallback handle oauth callback, exchange identity from remote server
|
||||
IdentityExchangeCallback(req *http.Request) (Identity, error)
|
||||
}
|
||||
|
||||
type OAuthProviderFactory interface {
|
||||
// Type unique type of the provider
|
||||
Type() string
|
||||
// Apply the dynamic options from kubesphere-config
|
||||
// Create Apply the dynamic options
|
||||
Create(options oauth.DynamicOptions) (OAuthProvider, error)
|
||||
}
|
||||
|
||||
@@ -196,8 +196,10 @@ func (f *oidcProviderFactory) Create(options oauth.DynamicOptions) (identityprov
|
||||
return &oidcProvider, nil
|
||||
}
|
||||
|
||||
func (o *oidcProvider) IdentityExchange(code string) (identityprovider.Identity, error) {
|
||||
ctx := context.TODO()
|
||||
func (o *oidcProvider) IdentityExchangeCallback(req *http.Request) (identityprovider.Identity, error) {
|
||||
//OAuth2 callback, see also https://tools.ietf.org/html/rfc6749#section-4.1.2
|
||||
code := req.URL.Query().Get("code")
|
||||
ctx := req.Context()
|
||||
if o.InsecureSkipVerify {
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -170,7 +171,7 @@ var _ = Describe("OIDC", func() {
|
||||
"issuer": oidcServer.URL,
|
||||
"clientID": "kubesphere",
|
||||
"clientSecret": "c53e80ab92d48ab12f4e7f1f6976d1bdc996e0d7",
|
||||
"redirectURL": "http://ks-console/oauth/redirect",
|
||||
"redirectURL": "https://ks-console.kubesphere-system.svc/oauth/redirect/oidc",
|
||||
"insecureSkipVerify": true,
|
||||
}
|
||||
factory := oidcProviderFactory{}
|
||||
@@ -180,7 +181,7 @@ var _ = Describe("OIDC", func() {
|
||||
"issuer": oidcServer.URL,
|
||||
"clientID": "kubesphere",
|
||||
"clientSecret": "c53e80ab92d48ab12f4e7f1f6976d1bdc996e0d7",
|
||||
"redirectURL": "http://ks-console/oauth/redirect",
|
||||
"redirectURL": "https://ks-console.kubesphere-system.svc/oauth/redirect/oidc",
|
||||
"insecureSkipVerify": true,
|
||||
"endpoint": oauth.DynamicOptions{
|
||||
"authURL": fmt.Sprintf("%s/authorize", oidcServer.URL),
|
||||
@@ -193,7 +194,9 @@ var _ = Describe("OIDC", func() {
|
||||
Expect(config).Should(Equal(expected))
|
||||
})
|
||||
It("should login successfully", func() {
|
||||
identity, err := provider.IdentityExchange("3389")
|
||||
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"))
|
||||
|
||||
129
pkg/apiserver/authentication/oauth/error.go
Normal file
129
pkg/apiserver/authentication/oauth/error.go
Normal file
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
Copyright 2021 The KubeSphere 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 oauth
|
||||
|
||||
import "fmt"
|
||||
|
||||
// The following error type is defined in https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
||||
var (
|
||||
// ErrorInvalidClient
|
||||
// Client authentication failed (e.g., unknown client, no
|
||||
// client authentication included, or unsupported
|
||||
// authentication method). The authorization server MAY
|
||||
// return an HTTP 401 (Unauthorized) status code to indicate
|
||||
// which HTTP authentication schemes are supported. If the
|
||||
// client attempted to authenticate via the "Authorization"
|
||||
// request header field, the authorization server MUST
|
||||
// respond with an HTTP 401 (Unauthorized) status code and
|
||||
// include the "WWW-Authenticate" response header field
|
||||
// matching the authentication scheme used by the client.
|
||||
ErrorInvalidClient = Error{Type: "invalid_client"}
|
||||
|
||||
// ErrorInvalidRequest The request is missing a required parameter,
|
||||
// includes an unsupported parameter value (other than grant type),
|
||||
// repeats a parameter, includes multiple credentials,
|
||||
// utilizes more than one mechanism for authenticating the client,
|
||||
// or is otherwise malformed.
|
||||
ErrorInvalidRequest = Error{Type: "invalid_request"}
|
||||
|
||||
// ErrorInvalidGrant
|
||||
// The provided authorization grant (e.g., authorization code,
|
||||
// resource owner credentials) or refresh token is invalid, expired, revoked,
|
||||
// does not match the redirection URI used in the authorization request,
|
||||
// or was issued to another client.
|
||||
ErrorInvalidGrant = Error{Type: "invalid_grant"}
|
||||
|
||||
// ErrorUnsupportedGrantType
|
||||
// The authorization grant type is not supported by the authorization server.
|
||||
ErrorUnsupportedGrantType = Error{Type: "unsupported_grant_type"}
|
||||
|
||||
ErrorUnsupportedResponseType = Error{Type: "unsupported_response_type"}
|
||||
|
||||
// ErrorUnauthorizedClient
|
||||
// The authenticated client is not authorized to use this authorization grant type.
|
||||
ErrorUnauthorizedClient = Error{Type: "unauthorized_client"}
|
||||
|
||||
// ErrorInvalidScope The requested scope is invalid, unknown, malformed,
|
||||
// or exceeds the scope granted by the resource owner.
|
||||
ErrorInvalidScope = Error{Type: "invalid_scope"}
|
||||
|
||||
// ErrorLoginRequired The Authorization Server requires End-User authentication.
|
||||
// This error MAY be returned when the prompt parameter value in the Authentication Request is none,
|
||||
// but the Authentication Request cannot be completed without displaying a user interface
|
||||
// for End-User authentication.
|
||||
ErrorLoginRequired = Error{Type: "login_required"}
|
||||
|
||||
// ErrorServerError
|
||||
// The authorization server encountered an unexpected
|
||||
// condition that prevented it from fulfilling the request.
|
||||
// (This error code is needed because a 500 Internal Server
|
||||
// Error HTTP status code cannot be returned to the client
|
||||
// via an HTTP redirect.)
|
||||
ErrorServerError = Error{Type: "server_error"}
|
||||
)
|
||||
|
||||
func NewInvalidRequest(error error) Error {
|
||||
err := ErrorInvalidRequest
|
||||
err.Description = error.Error()
|
||||
return err
|
||||
}
|
||||
|
||||
func NewInvalidScope(error error) Error {
|
||||
err := ErrorInvalidScope
|
||||
err.Description = error.Error()
|
||||
return err
|
||||
}
|
||||
|
||||
func NewInvalidClient(error error) Error {
|
||||
err := ErrorInvalidClient
|
||||
err.Description = error.Error()
|
||||
return err
|
||||
}
|
||||
|
||||
func NewInvalidGrant(error error) Error {
|
||||
err := ErrorInvalidGrant
|
||||
err.Description = error.Error()
|
||||
return err
|
||||
}
|
||||
|
||||
func NewServerError(error error) Error {
|
||||
err := ErrorServerError
|
||||
err.Description = error.Error()
|
||||
return err
|
||||
}
|
||||
|
||||
// Error wrapped OAuth error Response, for more details: https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
||||
// The authorization server responds with an HTTP 400 (Bad Request)
|
||||
// status code (unless specified otherwise) and includes the following
|
||||
// parameters with the response:
|
||||
type Error struct {
|
||||
// Type REQUIRED
|
||||
// A single ASCII [USASCII] error code from the following:
|
||||
// Values for the "error" parameter MUST NOT include characters
|
||||
// outside the set %x20-21 / %x23-5B / %x5D-7E.
|
||||
Type string `json:"error"`
|
||||
// Description OPTIONAL. Human-readable ASCII [USASCII] text providing
|
||||
// additional information, used to assist the client developer in
|
||||
// understanding the error that occurred.
|
||||
// Values for the "error_description" parameter MUST NOT include
|
||||
// characters outside the set %x20-21 / %x23-5B / %x5D-7E.
|
||||
Description string `json:"error_description,omitempty"`
|
||||
}
|
||||
|
||||
func (e Error) Error() string {
|
||||
return fmt.Sprintf("error=\"%s\", error_description=\"%s\"", e.Type, e.Description)
|
||||
}
|
||||
64
pkg/apiserver/authentication/oauth/oidc.go
Normal file
64
pkg/apiserver/authentication/oauth/oidc.go
Normal file
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
|
||||
Copyright 2021 The KubeSphere 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 oauth
|
||||
|
||||
import (
|
||||
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
|
||||
)
|
||||
|
||||
const (
|
||||
// ScopeOpenID Verify that a scope parameter is present and contains the openid scope value.
|
||||
// If no openid scope value is present, the request may still be a valid OAuth 2.0 request,
|
||||
// but is not an OpenID Connect request.
|
||||
ScopeOpenID = "openid"
|
||||
// ScopeEmail This scope value requests access to the email and email_verified Claims.
|
||||
ScopeEmail = "email"
|
||||
// ScopeProfile This scope value requests access to the End-User's default profile Claims,
|
||||
// which are: name, family_name, given_name, middle_name, nickname, preferred_username,
|
||||
// profile, picture, website, gender, birthdate, zoneinfo, locale, and updated_at.
|
||||
ScopeProfile = "profile"
|
||||
// ScopePhone This scope value requests access to the phone_number and phone_number_verified Claims.
|
||||
ScopePhone = "phone"
|
||||
// ScopeAddress This scope value requests access to the address Claim.
|
||||
ScopeAddress = "address"
|
||||
ResponseCode = "code"
|
||||
ResponseIDToken = "id_token"
|
||||
ResponseToken = "token"
|
||||
)
|
||||
|
||||
var ValidScopes = []string{ScopeOpenID, ScopeEmail, ScopeProfile}
|
||||
var ValidResponseTypes = []string{ResponseCode, ResponseIDToken, ResponseToken}
|
||||
|
||||
func IsValidScopes(scopes []string) bool {
|
||||
for _, scope := range scopes {
|
||||
if !sliceutil.HasString(ValidScopes, scope) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func IsValidResponseTypes(responseTypes []string) bool {
|
||||
for _, responseType := range responseTypes {
|
||||
if !sliceutil.HasString(ValidResponseTypes, responseType) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
78
pkg/apiserver/authentication/oauth/oidc_test.go
Normal file
78
pkg/apiserver/authentication/oauth/oidc_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
|
||||
Copyright 2021 The KubeSphere 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 oauth
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIsValidResponseTypes(t *testing.T) {
|
||||
type args struct {
|
||||
responseTypes []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "valid response type",
|
||||
args: args{responseTypes: []string{"code", "id_token"}},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "invalid response type",
|
||||
args: args{responseTypes: []string{"value"}},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsValidResponseTypes(tt.args.responseTypes); got != tt.want {
|
||||
t.Errorf("IsValidResponseTypes() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidScopes(t *testing.T) {
|
||||
type args struct {
|
||||
scopes []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "valid scope",
|
||||
args: args{scopes: []string{"openid", "email"}},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "invalid scope",
|
||||
args: args{scopes: []string{"user"}},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsValidScopes(tt.args.scopes); got != tt.want {
|
||||
t.Errorf("IsValidScopes() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,8 @@ const (
|
||||
// MappingMethodMixed A user entity can be mapped with multiple identifyProvider.
|
||||
// not supported yet.
|
||||
MappingMethodMixed MappingMethod = "mixed"
|
||||
|
||||
DefaultIssuer string = "kubesphere"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -56,6 +58,16 @@ var (
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
// An Issuer Identifier is a case-sensitive URL using the https scheme that contains scheme,
|
||||
// host, and optionally, port number and path components and no query or fragment components.
|
||||
Issuer string `json:"issuer,omitempty" yaml:"issuer,omitempty"`
|
||||
|
||||
// RSA private key file used to sign the id token
|
||||
SignKey string `json:"signKey,omitempty" yaml:"signKey"`
|
||||
|
||||
// Raw RSA private key. Base64 encoded PEM file
|
||||
SignKeyData string `json:"-,omitempty" yaml:"signKeyData"`
|
||||
|
||||
// Register identity providers.
|
||||
IdentityProviders []IdentityProviderOptions `json:"identityProviders,omitempty" yaml:"identityProviders,omitempty"`
|
||||
|
||||
@@ -79,7 +91,7 @@ type Options struct {
|
||||
AccessTokenInactivityTimeout time.Duration `json:"accessTokenInactivityTimeout" yaml:"accessTokenInactivityTimeout"`
|
||||
}
|
||||
|
||||
// the type of key must be string
|
||||
// DynamicOptions accept dynamic configuration, the type of key MUST be string
|
||||
type DynamicOptions map[string]interface{}
|
||||
|
||||
func (o DynamicOptions) MarshalJSON() ([]byte, error) {
|
||||
@@ -169,6 +181,9 @@ type Token struct {
|
||||
// if it expires.
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
|
||||
// ID Token value associated with the authenticated session.
|
||||
IDToken string `json:"id_token,omitempty"`
|
||||
|
||||
// ExpiresIn is the optional expiration second of the access token.
|
||||
ExpiresIn int `json:"expires_in,omitempty"`
|
||||
}
|
||||
@@ -176,7 +191,7 @@ type Token struct {
|
||||
type Client struct {
|
||||
// The name of the OAuth client is used as the client_id parameter when making requests to <master>/oauth/authorize
|
||||
// and <master>/oauth/token.
|
||||
Name string
|
||||
Name string `json:"name" yaml:"name,omitempty"`
|
||||
|
||||
// Secret is the unique secret associated with a client
|
||||
Secret string `json:"-" yaml:"secret,omitempty"`
|
||||
@@ -209,20 +224,8 @@ type Client struct {
|
||||
}
|
||||
|
||||
var (
|
||||
// Allow any redirect URI if the redirectURI is defined in request
|
||||
AllowAllRedirectURI = "*"
|
||||
DefaultTokenMaxAge = time.Second * 86400
|
||||
DefaultAccessTokenInactivityTimeout = time.Duration(0)
|
||||
DefaultClients = []Client{{
|
||||
Name: "default",
|
||||
Secret: "kubesphere",
|
||||
RespondWithChallenges: true,
|
||||
RedirectURIs: []string{AllowAllRedirectURI},
|
||||
GrantMethod: GrantHandlerAuto,
|
||||
ScopeRestrictions: []string{"full"},
|
||||
AccessTokenMaxAge: &DefaultTokenMaxAge,
|
||||
AccessTokenInactivityTimeout: &DefaultAccessTokenInactivityTimeout,
|
||||
}}
|
||||
// AllowAllRedirectURI Allow any redirect URI if the redirectURI is defined in request
|
||||
AllowAllRedirectURI = "*"
|
||||
)
|
||||
|
||||
func (o *Options) OAuthClient(name string) (Client, error) {
|
||||
@@ -231,11 +234,6 @@ func (o *Options) OAuthClient(name string) (Client, error) {
|
||||
return found, nil
|
||||
}
|
||||
}
|
||||
for _, defaultClient := range DefaultClients {
|
||||
if defaultClient.Name == name {
|
||||
return defaultClient, nil
|
||||
}
|
||||
}
|
||||
return Client{}, ErrorClientNotFound
|
||||
}
|
||||
|
||||
@@ -259,10 +257,10 @@ func (c Client) anyRedirectAbleURI() []string {
|
||||
return uris
|
||||
}
|
||||
|
||||
func (c Client) ResolveRedirectURL(expectURL string) (string, error) {
|
||||
func (c Client) ResolveRedirectURL(expectURL string) (*url.URL, error) {
|
||||
// RedirectURIs is empty
|
||||
if len(c.RedirectURIs) == 0 {
|
||||
return "", ErrorRedirectURLNotAllowed
|
||||
return nil, ErrorRedirectURLNotAllowed
|
||||
}
|
||||
allowAllRedirectURI := sliceutil.HasString(c.RedirectURIs, AllowAllRedirectURI)
|
||||
redirectAbleURIs := c.anyRedirectAbleURI()
|
||||
@@ -270,20 +268,21 @@ func (c Client) ResolveRedirectURL(expectURL string) (string, error) {
|
||||
if expectURL == "" {
|
||||
// Need to specify at least one RedirectURI
|
||||
if len(redirectAbleURIs) > 0 {
|
||||
return redirectAbleURIs[0], nil
|
||||
return url.Parse(redirectAbleURIs[0])
|
||||
} else {
|
||||
return "", ErrorRedirectURLNotAllowed
|
||||
return nil, ErrorRedirectURLNotAllowed
|
||||
}
|
||||
}
|
||||
if allowAllRedirectURI || sliceutil.HasString(redirectAbleURIs, expectURL) {
|
||||
return expectURL, nil
|
||||
return url.Parse(expectURL)
|
||||
}
|
||||
|
||||
return "", ErrorRedirectURLNotAllowed
|
||||
return nil, ErrorRedirectURLNotAllowed
|
||||
}
|
||||
|
||||
func NewOptions() *Options {
|
||||
return &Options{
|
||||
Issuer: DefaultIssuer,
|
||||
IdentityProviders: make([]IdentityProviderOptions, 0),
|
||||
Clients: make([]Client, 0),
|
||||
AccessTokenMaxAge: time.Hour * 2,
|
||||
@@ -19,86 +19,49 @@ package oauth
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestDefaultAuthOptions(t *testing.T) {
|
||||
oneDay := time.Second * 86400
|
||||
zero := time.Duration(0)
|
||||
expect := Client{
|
||||
Name: "default",
|
||||
RespondWithChallenges: true,
|
||||
Secret: "kubesphere",
|
||||
RedirectURIs: []string{AllowAllRedirectURI},
|
||||
GrantMethod: GrantHandlerAuto,
|
||||
ScopeRestrictions: []string{"full"},
|
||||
AccessTokenMaxAge: &oneDay,
|
||||
AccessTokenInactivityTimeout: &zero,
|
||||
}
|
||||
|
||||
options := NewOptions()
|
||||
client, err := options.OAuthClient("default")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if diff := cmp.Diff(expect, client); len(diff) != 0 {
|
||||
t.Errorf("%T differ (-got, +expected), %s", expect, diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientResolveRedirectURL(t *testing.T) {
|
||||
options := NewOptions()
|
||||
defaultClient, err := options.OAuthClient("default")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
Name string
|
||||
client Client
|
||||
expectError error
|
||||
expectURL string
|
||||
Name string
|
||||
client Client
|
||||
wantErr bool
|
||||
expectURL string
|
||||
}{
|
||||
{
|
||||
Name: "default client test",
|
||||
client: defaultClient,
|
||||
expectError: nil,
|
||||
expectURL: "https://localhost:8080/auth/cb",
|
||||
},
|
||||
{
|
||||
Name: "custom client test",
|
||||
client: Client{
|
||||
Name: "default",
|
||||
RespondWithChallenges: true,
|
||||
RedirectURIs: []string{"https://foo.bar.com/oauth/cb"},
|
||||
GrantMethod: GrantHandlerAuto,
|
||||
ScopeRestrictions: []string{"full"},
|
||||
},
|
||||
expectError: ErrorRedirectURLNotAllowed,
|
||||
expectURL: "https://foo.bar.com/oauth/err",
|
||||
},
|
||||
{
|
||||
Name: "custom client test",
|
||||
client: Client{
|
||||
Name: "default",
|
||||
Name: "custom",
|
||||
RespondWithChallenges: true,
|
||||
RedirectURIs: []string{AllowAllRedirectURI, "https://foo.bar.com/oauth/cb"},
|
||||
GrantMethod: GrantHandlerAuto,
|
||||
ScopeRestrictions: []string{"full"},
|
||||
},
|
||||
expectError: nil,
|
||||
expectURL: "https://foo.bar.com/oauth/err2",
|
||||
wantErr: false,
|
||||
expectURL: "https://foo.bar.com/oauth/cb",
|
||||
},
|
||||
{
|
||||
Name: "custom client test",
|
||||
client: Client{
|
||||
Name: "custom",
|
||||
RespondWithChallenges: true,
|
||||
RedirectURIs: []string{"https://foo.bar.com/oauth/cb"},
|
||||
GrantMethod: GrantHandlerAuto,
|
||||
},
|
||||
wantErr: true,
|
||||
expectURL: "https://foo.bar.com/oauth/cb2",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
redirectURL, err := test.client.ResolveRedirectURL(test.expectURL)
|
||||
if err != test.expectError {
|
||||
t.Errorf("expected error: %s, got: %s", test.expectError, err)
|
||||
if (err != nil) != test.wantErr {
|
||||
t.Errorf("ResolveRedirectURL() error = %+v, wantErr %+v", err, test.wantErr)
|
||||
return
|
||||
}
|
||||
if test.expectError == nil && test.expectURL != redirectURL {
|
||||
if redirectURL != nil && test.expectURL != redirectURL.String() {
|
||||
t.Errorf("expected redirect url: %s, got: %s", test.expectURL, redirectURL)
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,22 @@
|
||||
/*
|
||||
Copyright 2020 The KubeSphere 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
|
||||
Copyright 2021 The KubeSphere Authors.
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
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.
|
||||
|
||||
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 options
|
||||
package authentication
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -31,7 +33,7 @@ import (
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
)
|
||||
|
||||
type AuthenticationOptions struct {
|
||||
type Options struct {
|
||||
// AuthenticateRateLimiter defines under which circumstances we will block user.
|
||||
// A user will be blocked if his/her failed login attempt reaches AuthenticateRateLimiterMaxTries in
|
||||
// AuthenticateRateLimiterDuration for about AuthenticateRateLimiterDuration. For example,
|
||||
@@ -40,7 +42,10 @@ type AuthenticationOptions struct {
|
||||
// A user will be blocked for 10m if he/she logins with incorrect credentials for at least 5 times in 10m.
|
||||
AuthenticateRateLimiterMaxTries int `json:"authenticateRateLimiterMaxTries" yaml:"authenticateRateLimiterMaxTries"`
|
||||
AuthenticateRateLimiterDuration time.Duration `json:"authenticateRateLimiterDuration" yaml:"authenticateRateLimiterDuration"`
|
||||
// Token verification maximum time difference
|
||||
// Token verification maximum time difference, default to 10s.
|
||||
// You should consider allowing a clock skew when checking the time-based values.
|
||||
// This should be values of a few seconds, and we don’t recommend using more than 30 seconds for this purpose,
|
||||
// as this would rather indicate problems with the server, rather than a common clock skew.
|
||||
MaximumClockSkew time.Duration `json:"maximumClockSkew" yaml:"maximumClockSkew"`
|
||||
// retention login history, records beyond this amount will be deleted
|
||||
LoginHistoryRetentionPeriod time.Duration `json:"loginHistoryRetentionPeriod" yaml:"loginHistoryRetentionPeriod"`
|
||||
@@ -57,8 +62,8 @@ type AuthenticationOptions struct {
|
||||
KubectlImage string `json:"kubectlImage" yaml:"kubectlImage"`
|
||||
}
|
||||
|
||||
func NewAuthenticateOptions() *AuthenticationOptions {
|
||||
return &AuthenticationOptions{
|
||||
func NewOptions() *Options {
|
||||
return &Options{
|
||||
AuthenticateRateLimiterMaxTries: 5,
|
||||
AuthenticateRateLimiterDuration: time.Minute * 30,
|
||||
MaximumClockSkew: 10 * time.Second,
|
||||
@@ -71,7 +76,7 @@ func NewAuthenticateOptions() *AuthenticationOptions {
|
||||
}
|
||||
}
|
||||
|
||||
func (options *AuthenticationOptions) Validate() []error {
|
||||
func (options *Options) Validate() []error {
|
||||
var errs []error
|
||||
if len(options.JwtSecret) == 0 {
|
||||
errs = append(errs, errors.New("JWT secret MUST not be empty"))
|
||||
@@ -85,7 +90,7 @@ func (options *AuthenticationOptions) Validate() []error {
|
||||
return errs
|
||||
}
|
||||
|
||||
func (options *AuthenticationOptions) AddFlags(fs *pflag.FlagSet, s *AuthenticationOptions) {
|
||||
func (options *Options) AddFlags(fs *pflag.FlagSet, s *Options) {
|
||||
fs.IntVar(&options.AuthenticateRateLimiterMaxTries, "authenticate-rate-limiter-max-retries", s.AuthenticateRateLimiterMaxTries, "")
|
||||
fs.DurationVar(&options.AuthenticateRateLimiterDuration, "authenticate-rate-limiter-duration", s.AuthenticateRateLimiterDuration, "")
|
||||
fs.BoolVar(&options.MultipleLogin, "multiple-login", s.MultipleLogin, "Allow multiple login with the same account, disable means only one user can login at the same time.")
|
||||
@@ -17,24 +17,316 @@ limitations under the License.
|
||||
package token
|
||||
|
||||
import (
|
||||
cryptorand "crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
|
||||
"github.com/form3tech-oss/jwt-go"
|
||||
"k8s.io/klog"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
)
|
||||
|
||||
const (
|
||||
AccessToken TokenType = "access_token"
|
||||
RefreshToken TokenType = "refresh_token"
|
||||
StaticToken TokenType = "static_token"
|
||||
AccessToken Type = "access_token"
|
||||
RefreshToken Type = "refresh_token"
|
||||
StaticToken Type = "static_token"
|
||||
AuthorizationCode Type = "code"
|
||||
IDToken Type = "id_token"
|
||||
headerKeyID string = "kid"
|
||||
headerAlgorithm string = "alg"
|
||||
)
|
||||
|
||||
type TokenType string
|
||||
type Type string
|
||||
|
||||
type IssueRequest struct {
|
||||
User user.Info
|
||||
ExpiresIn time.Duration
|
||||
Claims
|
||||
}
|
||||
|
||||
type VerifiedResponse struct {
|
||||
User user.Info
|
||||
Claims
|
||||
}
|
||||
|
||||
// Keys hold encryption and signing keys.
|
||||
type Keys struct {
|
||||
SigningKey *jose.JSONWebKey
|
||||
SigningKeyPub *jose.JSONWebKey
|
||||
}
|
||||
|
||||
// Issuer issues token to user, tokens are required to perform mutating requests to resources
|
||||
type Issuer interface {
|
||||
// IssueTo issues a token a User, return error if issuing process failed
|
||||
IssueTo(user user.Info, tokenType TokenType, expiresIn time.Duration) (string, error)
|
||||
IssueTo(request *IssueRequest) (string, error)
|
||||
|
||||
// Verify verifies a token, and return a user info if it's a valid token, otherwise return error
|
||||
Verify(string) (user.Info, TokenType, error)
|
||||
Verify(string) (*VerifiedResponse, error)
|
||||
|
||||
// Keys hold encryption and signing keys.
|
||||
Keys() *Keys
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
jwt.StandardClaims
|
||||
// Private Claim Names
|
||||
// TokenType defined the type of the token
|
||||
TokenType Type `json:"token_type,omitempty"`
|
||||
// Username user identity, deprecated field
|
||||
Username string `json:"username,omitempty"`
|
||||
// Extra contains the additional information
|
||||
Extra map[string][]string `json:"extra,omitempty"`
|
||||
|
||||
// Used for issuing authorization code
|
||||
// Scopes can be used to request that specific sets of information be made available as Claim Values.
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
|
||||
// The following is well-known ID Token fields
|
||||
|
||||
// End-User's full name in displayable form including all name parts,
|
||||
// possibly including titles and suffixes, ordered according to the End-User's locale and preferences.
|
||||
Name string `json:"name,omitempty"`
|
||||
// String value used to associate a Client session with an ID Token, and to mitigate replay attacks.
|
||||
// The value is passed through unmodified from the Authentication Request to the ID Token.
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
// End-User's preferred e-mail address.
|
||||
Email string `json:"email,omitempty"`
|
||||
// End-User's locale, represented as a BCP47 [RFC5646] language tag.
|
||||
Locale string `json:"locale,omitempty"`
|
||||
// Shorthand name by which the End-User wishes to be referred to at the RP,
|
||||
PreferredUsername string `json:"preferred_username,omitempty"`
|
||||
}
|
||||
|
||||
type issuer struct {
|
||||
// Issuer Identity
|
||||
name string
|
||||
// signing access_token and refresh_token
|
||||
secret []byte
|
||||
// signing id_token
|
||||
signKey *Keys
|
||||
// Token verification maximum time difference
|
||||
maximumClockSkew time.Duration
|
||||
}
|
||||
|
||||
func (s *issuer) IssueTo(request *IssueRequest) (string, error) {
|
||||
issueAt := time.Now().Unix()
|
||||
claims := Claims{
|
||||
Username: request.User.GetName(),
|
||||
Extra: request.User.GetExtra(),
|
||||
TokenType: request.TokenType,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
IssuedAt: issueAt,
|
||||
Subject: request.User.GetName(),
|
||||
Issuer: s.name,
|
||||
},
|
||||
}
|
||||
|
||||
if len(request.Audience) > 0 {
|
||||
claims.Audience = request.Audience
|
||||
}
|
||||
if request.Name != "" {
|
||||
claims.Name = request.Name
|
||||
}
|
||||
if request.Nonce != "" {
|
||||
claims.Nonce = request.Nonce
|
||||
}
|
||||
if request.Email != "" {
|
||||
claims.Email = request.Email
|
||||
}
|
||||
if request.PreferredUsername != "" {
|
||||
claims.PreferredUsername = request.PreferredUsername
|
||||
}
|
||||
if request.Locale != "" {
|
||||
claims.Locale = request.Locale
|
||||
}
|
||||
if len(request.Scopes) > 0 {
|
||||
claims.Scopes = request.Scopes
|
||||
}
|
||||
if request.ExpiresIn > 0 {
|
||||
claims.ExpiresAt = claims.IssuedAt + int64(request.ExpiresIn.Seconds())
|
||||
}
|
||||
|
||||
var token string
|
||||
var err error
|
||||
if request.TokenType == IDToken {
|
||||
t := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
t.Header[headerKeyID] = s.signKey.SigningKey.KeyID
|
||||
token, err = t.SignedString(s.signKey.SigningKey.Key)
|
||||
} else {
|
||||
token, err = jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(s.secret)
|
||||
}
|
||||
if err != nil {
|
||||
klog.Warningf("jwt: failed to issue token: %v", err)
|
||||
return "", err
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (s *issuer) Verify(token string) (*VerifiedResponse, error) {
|
||||
parser := jwt.Parser{
|
||||
ValidMethods: []string{jwt.SigningMethodHS256.Alg(), jwt.SigningMethodRS256.Alg()},
|
||||
UseJSONNumber: false,
|
||||
SkipClaimsValidation: true,
|
||||
}
|
||||
|
||||
var claims Claims
|
||||
_, err := parser.ParseWithClaims(token, &claims, s.keyFunc)
|
||||
if err != nil {
|
||||
klog.Warningf("jwt: failed to parse token: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
if claims.VerifyExpiresAt(now, false) == false {
|
||||
delta := time.Unix(now, 0).Sub(time.Unix(claims.ExpiresAt, 0))
|
||||
err = fmt.Errorf("jwt: token is expired by %v", delta)
|
||||
klog.V(4).Info(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// allowing a clock skew when checking the time-based values.
|
||||
skewedTime := now + int64(s.maximumClockSkew.Seconds())
|
||||
if claims.VerifyIssuedAt(skewedTime, false) == false {
|
||||
err = fmt.Errorf("jwt: token used before issued, iat:%v, now:%v", claims.IssuedAt, now)
|
||||
klog.Warning(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
verified := &VerifiedResponse{
|
||||
User: &user.DefaultInfo{
|
||||
Name: claims.Username,
|
||||
Extra: claims.Extra,
|
||||
},
|
||||
Claims: claims,
|
||||
}
|
||||
|
||||
return verified, nil
|
||||
}
|
||||
|
||||
func (s *issuer) Keys() *Keys {
|
||||
return s.signKey
|
||||
}
|
||||
|
||||
func (s *issuer) keyFunc(token *jwt.Token) (i interface{}, err error) {
|
||||
alg, _ := token.Header[headerAlgorithm].(string)
|
||||
switch alg {
|
||||
case jwt.SigningMethodHS256.Alg():
|
||||
return s.secret, nil
|
||||
case jwt.SigningMethodRS256.Alg():
|
||||
return s.signKey.SigningKey.Key, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpect signature algorithm %v", token.Header[headerAlgorithm])
|
||||
}
|
||||
}
|
||||
|
||||
func loadPrivateKey(data []byte) (*rsa.PrivateKey, error) {
|
||||
block, _ := pem.Decode(data)
|
||||
if block == nil {
|
||||
return nil, errors.New("private key not in pem format")
|
||||
}
|
||||
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to key file: %v", err)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func generatePrivateKeyData() ([]byte, error) {
|
||||
privateKey, err := rsa.GenerateKey(cryptorand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate private key: %v", err)
|
||||
}
|
||||
data := x509.MarshalPKCS1PrivateKey(privateKey)
|
||||
pemData := pem.EncodeToMemory(
|
||||
&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: data,
|
||||
},
|
||||
)
|
||||
return pemData, nil
|
||||
}
|
||||
|
||||
func loadSignKey(options *authentication.Options) (*rsa.PrivateKey, string, error) {
|
||||
var signKey *rsa.PrivateKey
|
||||
var signKeyData []byte
|
||||
var err error
|
||||
|
||||
if options.OAuthOptions.SignKey != "" {
|
||||
signKeyData, err = ioutil.ReadFile(options.OAuthOptions.SignKey)
|
||||
if err != nil {
|
||||
klog.Errorf("issuer: failed to read private key file %s: %v", options.OAuthOptions.SignKey, err)
|
||||
return nil, "", err
|
||||
}
|
||||
} else if options.OAuthOptions.SignKeyData != "" {
|
||||
signKeyData, err = base64.StdEncoding.DecodeString(options.OAuthOptions.SignKeyData)
|
||||
if err != nil {
|
||||
klog.Errorf("issuer: failed to decode sign key data: %s", err)
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
// automatically generate private key
|
||||
if len(signKeyData) == 0 {
|
||||
signKeyData, err = generatePrivateKeyData()
|
||||
if err != nil {
|
||||
klog.Errorf("issuer: failed to generate private key: %v", err)
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
if len(signKeyData) > 0 {
|
||||
signKey, err = loadPrivateKey(signKeyData)
|
||||
if err != nil {
|
||||
klog.Errorf("issuer: failed to load private key from data: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
keyID := fmt.Sprint(fnv32a(signKeyData))
|
||||
return signKey, keyID, nil
|
||||
}
|
||||
|
||||
func NewIssuer(options *authentication.Options) (Issuer, error) {
|
||||
// TODO(hongming) automatically rotates keys
|
||||
signKey, keyID, err := loadSignKey(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &issuer{
|
||||
name: options.OAuthOptions.Issuer,
|
||||
secret: []byte(options.JwtSecret),
|
||||
maximumClockSkew: options.MaximumClockSkew,
|
||||
signKey: &Keys{
|
||||
SigningKey: &jose.JSONWebKey{
|
||||
Key: signKey,
|
||||
KeyID: keyID,
|
||||
Algorithm: jwt.SigningMethodRS256.Alg(),
|
||||
Use: "sig",
|
||||
},
|
||||
SigningKeyPub: &jose.JSONWebKey{
|
||||
Key: signKey.Public(),
|
||||
KeyID: keyID,
|
||||
Algorithm: jwt.SigningMethodRS256.Alg(),
|
||||
Use: "sig",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// fnv32a hashes using fnv32a algorithm
|
||||
func fnv32a(data []byte) uint32 {
|
||||
algorithm := fnv.New32a()
|
||||
algorithm.Write(data)
|
||||
return algorithm.Sum32()
|
||||
}
|
||||
|
||||
349
pkg/apiserver/authentication/token/issuer_test.go
Normal file
349
pkg/apiserver/authentication/token/issuer_test.go
Normal file
@@ -0,0 +1,349 @@
|
||||
/*
|
||||
|
||||
Copyright 2021 The KubeSphere 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 token
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
|
||||
"github.com/form3tech-oss/jwt-go"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
)
|
||||
|
||||
const privateKeyData = `
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEoQIBAAKCAQEAnDK2bNmX+tBWY/JHll1T1LF3/6RTbJ2qUsvwZVuVP/XbmWeY
|
||||
vDZyTR+YL6JaqRC/NibphgCV0p6cKZNuoGCEHpS0Ix9ZZwkA8BhwrFwAU0O1Qmrv
|
||||
v7It3p0Lc9WKN7PBWDQnUIdeSWnAeSbmWETP8Y2e+vG/iusLojPJenEaiOiwzU8p
|
||||
3CGSh7IPBXF3aUeB3dgJuaiumDuzlp0Oe/xKvWo0faB2hFXi36KaLMpugNcbejKl
|
||||
R6w3jH5wJjto5XqTEpW4a77K4rt7CFXVGfcbLo+n/5j3oC0lw4KOy7OX0Qf1jY+x
|
||||
sa1Q+3UoDC02sQRf77uj3eITol8Spoo7wfJqmwIDAQABAoIBAGEArYIT7+p3j+8p
|
||||
+4NKGlGwlRFR/+0oTSp2NKj9o0bBbMtsJtJcDcgPoveSIDN2jwkWSVhK7MCMd/bp
|
||||
9H3s8p/7QZO+WEtAsDBrPS4NRLZxChRhTNsD0LC7Xu1k5B2LqLsaSIAeUVPONRYI
|
||||
Lm0K7wjYJq85iva+2c610p4Tt6LlxuOu41Zw7RAaW8nBoMdQzi19X+hUloogVo7S
|
||||
hid8gm2KUPY6xF+RpHGQ5OUND0d+2wBkHxbYNRIfYrxCKt8+dLykLzAmm+ScCfyG
|
||||
jKcNoRwW5s/3ttR7r7hn3whttydkper5YvxM3+EvL83H7JL11KHcHy/yPYv+2IxQ
|
||||
psvEtIECgYEAykCm/w58pdifLuWG1mwHCkdQ6wCd9gAHIDfaqbFhTcdwGGYXb1xQ
|
||||
3CHjkkX6rpB3rJReCxlGq01OemVNlpIGLhdnK87aX5pRVn2fHGaMoC2V5RWv3pyE
|
||||
3gJ41h9FtPX2juKFG9PNiR7FrtKPzQczfh2L1OMpLOXfPgxvo/fXBQsCgYEAxbTz
|
||||
mibb4F/TBVXMuSL7Pk9hBPlFgFIEUKbiqt+sKQCqSZPGjV5giDfQDGsJ5EqOkRg0
|
||||
qlCrKk+DW+d+Pmc4yo58cd2xPnQETholV19+dM3AGiy4BMjeUnJD+Dme7M/fhrlW
|
||||
IK/1ZErKSZ3nN20qeneIFltm6+4pgQ1HB9KwirECgYAy65wf0xHm32cUc41DJueO
|
||||
2u2wfPNIIDGrFuTinFoXLwM14V49F0z0X0Pga+X1VUIMHT6gJLj6H/iGMEMciZ8s
|
||||
s4+yI94u+7dGw1Hv4JG/Mjru9krVDSsWiiDKKA1wxgxRZQ6GNwkkYK78mN7Di/CW
|
||||
6/Fso9SWDTnrcU4aRifIiQKBgQCQ+kJwVfKCtIIPtX0sfeRzKs5gUVKP6JTVd6tb
|
||||
1i1u29gDoGPHIt/yw8rCcHOOfsXQzElCY2lA25HeAQFoTVUt5BKJhSIGRBksFKwx
|
||||
SAt5J6+pAgXnLE0rdDM3gTlzOnQVXS81RRLTeqygEzSMRncR2zll+5ybgcfZpJzj
|
||||
tbJT4QJ/Y02wfkm1dL/BFg520/otVeuC+Bt+YyWMVs867xLLzFci7tj6ZzlzMorQ
|
||||
PsSsOHhPx0g+Wl8K2+Edg3FQRZ1m0rQFAZn66jd96u85aA9NH/bw3A3VYUdVJyHh
|
||||
4ZgZLx9JMCkmRfa7Dp2mzoqGUC1cjNvm722baeMqXpHSXDP2Jg==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
`
|
||||
|
||||
func TestNewIssuer(t *testing.T) {
|
||||
signKeyData := base64.StdEncoding.EncodeToString([]byte(privateKeyData))
|
||||
options := &authentication.Options{
|
||||
MaximumClockSkew: 10 * time.Second,
|
||||
JwtSecret: "test-secret",
|
||||
OAuthOptions: &oauth.Options{
|
||||
Issuer: "kubesphere",
|
||||
SignKeyData: signKeyData,
|
||||
},
|
||||
}
|
||||
got, err := NewIssuer(options)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
signKey, keyID, err := loadSignKey(options)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := &issuer{
|
||||
name: options.OAuthOptions.Issuer,
|
||||
secret: []byte(options.JwtSecret),
|
||||
maximumClockSkew: options.MaximumClockSkew,
|
||||
signKey: &Keys{
|
||||
SigningKey: &jose.JSONWebKey{
|
||||
Key: signKey,
|
||||
KeyID: keyID,
|
||||
Algorithm: jwt.SigningMethodRS256.Alg(),
|
||||
Use: "sig",
|
||||
},
|
||||
SigningKeyPub: &jose.JSONWebKey{
|
||||
Key: signKey.Public(),
|
||||
KeyID: keyID,
|
||||
Algorithm: jwt.SigningMethodRS256.Alg(),
|
||||
Use: "sig",
|
||||
},
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("NewIssuer() got = %v, want %v", got, want)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewIssuerGenerateSignKey(t *testing.T) {
|
||||
options := &authentication.Options{
|
||||
MaximumClockSkew: 10 * time.Second,
|
||||
JwtSecret: "test-secret",
|
||||
OAuthOptions: &oauth.Options{
|
||||
Issuer: "kubesphere",
|
||||
},
|
||||
}
|
||||
|
||||
got, err := NewIssuer(options)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
iss := got.(*issuer)
|
||||
assert.NotNil(t, iss.signKey)
|
||||
assert.NotNil(t, iss.signKey.SigningKey)
|
||||
assert.NotNil(t, iss.signKey.SigningKeyPub)
|
||||
assert.NotNil(t, iss.signKey.SigningKey.KeyID)
|
||||
assert.NotNil(t, iss.signKey.SigningKeyPub.KeyID)
|
||||
}
|
||||
|
||||
func Test_issuer_IssueTo(t *testing.T) {
|
||||
type fields struct {
|
||||
name string
|
||||
secret []byte
|
||||
maximumClockSkew time.Duration
|
||||
}
|
||||
type args struct {
|
||||
request *IssueRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *VerifiedResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "token is successfully issued",
|
||||
fields: fields{
|
||||
name: "kubesphere",
|
||||
secret: []byte("kubesphere"),
|
||||
maximumClockSkew: 0,
|
||||
},
|
||||
args: args{request: &IssueRequest{
|
||||
User: &user.DefaultInfo{
|
||||
Name: "user1",
|
||||
},
|
||||
Claims: Claims{
|
||||
TokenType: AccessToken,
|
||||
},
|
||||
ExpiresIn: 2 * time.Hour},
|
||||
},
|
||||
want: &VerifiedResponse{
|
||||
User: &user.DefaultInfo{
|
||||
Name: "user1",
|
||||
},
|
||||
Claims: Claims{
|
||||
Username: "user1",
|
||||
TokenType: AccessToken,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "token is successfully issued",
|
||||
fields: fields{
|
||||
name: "kubesphere",
|
||||
secret: []byte("kubesphere"),
|
||||
maximumClockSkew: 0,
|
||||
},
|
||||
args: args{request: &IssueRequest{
|
||||
User: &user.DefaultInfo{
|
||||
Name: "user2",
|
||||
},
|
||||
Claims: Claims{
|
||||
Username: "user2",
|
||||
TokenType: RefreshToken,
|
||||
},
|
||||
ExpiresIn: 0},
|
||||
},
|
||||
want: &VerifiedResponse{
|
||||
User: &user.DefaultInfo{
|
||||
Name: "user2",
|
||||
},
|
||||
Claims: Claims{
|
||||
Username: "user2",
|
||||
TokenType: RefreshToken,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &issuer{
|
||||
name: tt.fields.name,
|
||||
secret: tt.fields.secret,
|
||||
maximumClockSkew: tt.fields.maximumClockSkew,
|
||||
}
|
||||
token, err := s.IssueTo(tt.args.request)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("IssueTo() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
got, err := s.Verify(token)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Verify() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got == nil {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, got.TokenType, tt.want.TokenType)
|
||||
assert.Equal(t, got.Issuer, tt.fields.name)
|
||||
assert.Equal(t, got.Username, tt.want.Username)
|
||||
assert.Equal(t, got.Subject, tt.want.User.GetName())
|
||||
assert.NotZero(t, got.IssuedAt)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_issuer_Verify(t *testing.T) {
|
||||
type fields struct {
|
||||
name string
|
||||
secret []byte
|
||||
maximumClockSkew time.Duration
|
||||
}
|
||||
type args struct {
|
||||
token string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *VerifiedResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "token validation failed",
|
||||
fields: fields{
|
||||
name: "kubesphere",
|
||||
secret: []byte("kubesphere"),
|
||||
maximumClockSkew: 0,
|
||||
},
|
||||
args: args{token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwidG9rZW5fdHlwZSI6ImFjY2Vzc190b2tlbiIsImV4cCI6MTYzMDY0MDMyMywiaWF0IjoxNjMwNjM2NzIzLCJpc3MiOiJrdWJlc3BoZXJlIiwibmJmIjoxNjMwNjM2NzIzfQ.4ENxyPTIe-BoQfuY5F4Mon5tB3KeV06B4i2JITRlPA8"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "token is successfully verified",
|
||||
fields: fields{
|
||||
name: "kubesphere",
|
||||
secret: []byte("kubesphere"),
|
||||
maximumClockSkew: 0,
|
||||
},
|
||||
args: args{token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MzA2MzczOTgsImlzcyI6Imt1YmVzcGhlcmUiLCJzdWIiOiJ1c2VyMiIsInRva2VuX3R5cGUiOiJyZWZyZXNoX3Rva2VuIiwidXNlcm5hbWUiOiJ1c2VyMiJ9.vqPczw4SyytVOQmgaK9ip2dvg2fSQStUUE_Y7Ts45WY"},
|
||||
want: &VerifiedResponse{
|
||||
User: &user.DefaultInfo{
|
||||
Name: "user2",
|
||||
},
|
||||
Claims: Claims{
|
||||
Username: "user2",
|
||||
TokenType: RefreshToken,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &issuer{
|
||||
name: tt.fields.name,
|
||||
secret: tt.fields.secret,
|
||||
maximumClockSkew: tt.fields.maximumClockSkew,
|
||||
}
|
||||
got, err := s.Verify(tt.args.token)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Verify() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got == nil {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, got.TokenType, tt.want.TokenType)
|
||||
assert.Equal(t, got.Issuer, tt.fields.name)
|
||||
assert.Equal(t, got.Username, tt.want.Username)
|
||||
assert.Equal(t, got.Subject, tt.want.User.GetName())
|
||||
assert.NotZero(t, got.IssuedAt)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_issuer_keyFunc(t *testing.T) {
|
||||
type fields struct {
|
||||
name string
|
||||
secret []byte
|
||||
maximumClockSkew time.Duration
|
||||
}
|
||||
type args struct {
|
||||
token *jwt.Token
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
}{
|
||||
{
|
||||
name: "sign key obtained successfully",
|
||||
fields: fields{
|
||||
secret: []byte("kubesphere"),
|
||||
},
|
||||
args: args{token: &jwt.Token{
|
||||
Method: jwt.SigningMethodHS256,
|
||||
Header: map[string]interface{}{"alg": "HS256"},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "sign key obtained successfully",
|
||||
fields: fields{},
|
||||
args: args{token: &jwt.Token{
|
||||
Method: jwt.SigningMethodRS256,
|
||||
Header: map[string]interface{}{"alg": "RS256"},
|
||||
}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s, err := NewIssuer(authentication.NewOptions())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
iss := s.(*issuer)
|
||||
got, err := iss.keyFunc(tt.args.token)
|
||||
assert.NotNil(t, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The KubeSphere 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 token
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/form3tech-oss/jwt-go"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/klog"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultIssuerName = "kubesphere"
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
Username string `json:"username"`
|
||||
Groups []string `json:"groups,omitempty"`
|
||||
Extra map[string][]string `json:"extra,omitempty"`
|
||||
TokenType TokenType `json:"token_type"`
|
||||
// Currently, we are not using any field in jwt.StandardClaims
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
type jwtTokenIssuer struct {
|
||||
name string
|
||||
secret []byte
|
||||
// Maximum time difference
|
||||
maximumClockSkew time.Duration
|
||||
}
|
||||
|
||||
func (s *jwtTokenIssuer) Verify(tokenString string) (user.Info, TokenType, error) {
|
||||
clm := &Claims{}
|
||||
// verify token signature and expiration time
|
||||
_, err := jwt.ParseWithClaims(tokenString, clm, s.keyFunc)
|
||||
if err != nil {
|
||||
klog.V(4).Info(err)
|
||||
return nil, "", err
|
||||
}
|
||||
return &user.DefaultInfo{Name: clm.Username, Groups: clm.Groups, Extra: clm.Extra}, clm.TokenType, nil
|
||||
}
|
||||
|
||||
func (s *jwtTokenIssuer) IssueTo(user user.Info, tokenType TokenType, expiresIn time.Duration) (string, error) {
|
||||
issueAt := time.Now().Unix() - int64(s.maximumClockSkew.Seconds())
|
||||
notBefore := issueAt
|
||||
clm := &Claims{
|
||||
Username: user.GetName(),
|
||||
Groups: user.GetGroups(),
|
||||
Extra: user.GetExtra(),
|
||||
TokenType: tokenType,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
IssuedAt: issueAt,
|
||||
Issuer: s.name,
|
||||
NotBefore: notBefore,
|
||||
},
|
||||
}
|
||||
|
||||
if expiresIn > 0 {
|
||||
clm.ExpiresAt = clm.IssuedAt + int64(expiresIn.Seconds())
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, clm)
|
||||
|
||||
tokenString, err := token.SignedString(s.secret)
|
||||
if err != nil {
|
||||
klog.V(4).Info(err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
func (s *jwtTokenIssuer) keyFunc(token *jwt.Token) (i interface{}, err error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); ok {
|
||||
return s.secret, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("expect token signed with HMAC but got %v", token.Header["alg"])
|
||||
}
|
||||
}
|
||||
|
||||
func NewTokenIssuer(secret string, maximumClockSkew time.Duration) Issuer {
|
||||
return &jwtTokenIssuer{
|
||||
name: DefaultIssuerName,
|
||||
secret: []byte(secret),
|
||||
maximumClockSkew: maximumClockSkew,
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The KubeSphere 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 token
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
)
|
||||
|
||||
func TestTokenVerifyWithoutCacheValidate(t *testing.T) {
|
||||
|
||||
issuer := NewTokenIssuer("kubesphere", 0)
|
||||
|
||||
admin := &user.DefaultInfo{
|
||||
Name: "admin",
|
||||
}
|
||||
|
||||
tokenString, err := issuer.IssueTo(admin, AccessToken, 0)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, _, err := issuer.Verify(tokenString)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(got, admin); diff != "" {
|
||||
t.Error("token validate failed")
|
||||
}
|
||||
}
|
||||
56
pkg/apiserver/authorization/options.go
Normal file
56
pkg/apiserver/authorization/options.go
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
|
||||
Copyright 2021 The KubeSphere 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 authorization
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"k8s.io/klog"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Mode string `json:"mode" yaml:"mode"`
|
||||
}
|
||||
|
||||
func NewOptions() *Options {
|
||||
return &Options{Mode: RBAC}
|
||||
}
|
||||
|
||||
var (
|
||||
AlwaysDeny = "AlwaysDeny"
|
||||
AlwaysAllow = "AlwaysAllow"
|
||||
RBAC = "RBAC"
|
||||
)
|
||||
|
||||
func (o *Options) AddFlags(fs *pflag.FlagSet, s *Options) {
|
||||
fs.StringVar(&o.Mode, "authorization", s.Mode, "Authorization setting, allowed values: AlwaysDeny, AlwaysAllow, RBAC.")
|
||||
}
|
||||
|
||||
func (o *Options) Validate() []error {
|
||||
errs := make([]error, 0)
|
||||
if !sliceutil.HasString([]string{AlwaysAllow, AlwaysDeny, RBAC}, o.Mode) {
|
||||
err := fmt.Errorf("authorization mode %s not support", o.Mode)
|
||||
klog.Error(err)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
Copyright 2020 The KubeSphere 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 options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"k8s.io/klog"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
|
||||
)
|
||||
|
||||
type AuthorizationOptions struct {
|
||||
Mode string `json:"mode" yaml:"mode"`
|
||||
}
|
||||
|
||||
func NewAuthorizationOptions() *AuthorizationOptions {
|
||||
return &AuthorizationOptions{Mode: RBAC}
|
||||
}
|
||||
|
||||
var (
|
||||
AlwaysDeny = "AlwaysDeny"
|
||||
AlwaysAllow = "AlwaysAllow"
|
||||
RBAC = "RBAC"
|
||||
)
|
||||
|
||||
func (o *AuthorizationOptions) AddFlags(fs *pflag.FlagSet, s *AuthorizationOptions) {
|
||||
fs.StringVar(&o.Mode, "authorization", s.Mode, "Authorization setting, allowed values: AlwaysDeny, AlwaysAllow, RBAC.")
|
||||
}
|
||||
|
||||
func (o *AuthorizationOptions) Validate() []error {
|
||||
errs := make([]error, 0)
|
||||
if !sliceutil.HasString([]string{AlwaysAllow, AlwaysDeny, RBAC}, o.Mode) {
|
||||
err := fmt.Errorf("authorization mode %s not support", o.Mode)
|
||||
klog.Error(err)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
@@ -21,12 +21,13 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authorization"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
networkv1alpha1 "kubesphere.io/api/network/v1alpha1"
|
||||
|
||||
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
|
||||
authorizationoptions "kubesphere.io/kubesphere/pkg/apiserver/authorization/options"
|
||||
"kubesphere.io/kubesphere/pkg/simple/client/alerting"
|
||||
"kubesphere.io/kubesphere/pkg/simple/client/auditing"
|
||||
"kubesphere.io/kubesphere/pkg/simple/client/cache"
|
||||
@@ -86,28 +87,28 @@ const (
|
||||
|
||||
// Config defines everything needed for apiserver to deal with external services
|
||||
type Config struct {
|
||||
DevopsOptions *jenkins.Options `json:"devops,omitempty" yaml:"devops,omitempty" mapstructure:"devops"`
|
||||
SonarQubeOptions *sonarqube.Options `json:"sonarqube,omitempty" yaml:"sonarQube,omitempty" mapstructure:"sonarqube"`
|
||||
KubernetesOptions *k8s.KubernetesOptions `json:"kubernetes,omitempty" yaml:"kubernetes,omitempty" mapstructure:"kubernetes"`
|
||||
ServiceMeshOptions *servicemesh.Options `json:"servicemesh,omitempty" yaml:"servicemesh,omitempty" mapstructure:"servicemesh"`
|
||||
NetworkOptions *network.Options `json:"network,omitempty" yaml:"network,omitempty" mapstructure:"network"`
|
||||
LdapOptions *ldap.Options `json:"-,omitempty" yaml:"ldap,omitempty" mapstructure:"ldap"`
|
||||
RedisOptions *cache.Options `json:"redis,omitempty" yaml:"redis,omitempty" mapstructure:"redis"`
|
||||
S3Options *s3.Options `json:"s3,omitempty" yaml:"s3,omitempty" mapstructure:"s3"`
|
||||
OpenPitrixOptions *openpitrix.Options `json:"openpitrix,omitempty" yaml:"openpitrix,omitempty" mapstructure:"openpitrix"`
|
||||
MonitoringOptions *prometheus.Options `json:"monitoring,omitempty" yaml:"monitoring,omitempty" mapstructure:"monitoring"`
|
||||
LoggingOptions *logging.Options `json:"logging,omitempty" yaml:"logging,omitempty" mapstructure:"logging"`
|
||||
AuthenticationOptions *authoptions.AuthenticationOptions `json:"authentication,omitempty" yaml:"authentication,omitempty" mapstructure:"authentication"`
|
||||
AuthorizationOptions *authorizationoptions.AuthorizationOptions `json:"authorization,omitempty" yaml:"authorization,omitempty" mapstructure:"authorization"`
|
||||
MultiClusterOptions *multicluster.Options `json:"multicluster,omitempty" yaml:"multicluster,omitempty" mapstructure:"multicluster"`
|
||||
EventsOptions *events.Options `json:"events,omitempty" yaml:"events,omitempty" mapstructure:"events"`
|
||||
AuditingOptions *auditing.Options `json:"auditing,omitempty" yaml:"auditing,omitempty" mapstructure:"auditing"`
|
||||
AlertingOptions *alerting.Options `json:"alerting,omitempty" yaml:"alerting,omitempty" mapstructure:"alerting"`
|
||||
NotificationOptions *notification.Options `json:"notification,omitempty" yaml:"notification,omitempty" mapstructure:"notification"`
|
||||
KubeEdgeOptions *kubeedge.Options `json:"kubeedge,omitempty" yaml:"kubeedge,omitempty" mapstructure:"kubeedge"`
|
||||
MeteringOptions *metering.Options `json:"metering,omitempty" yaml:"metering,omitempty" mapstructure:"metering"`
|
||||
GatewayOptions *gateway.Options `json:"gateway,omitempty" yaml:"gateway,omitempty" mapstructure:"gateway"`
|
||||
GPUOptions *gpu.Options `json:"gpu,omitempty" yaml:"gpu,omitempty" mapstructure:"gpu"`
|
||||
DevopsOptions *jenkins.Options `json:"devops,omitempty" yaml:"devops,omitempty" mapstructure:"devops"`
|
||||
SonarQubeOptions *sonarqube.Options `json:"sonarqube,omitempty" yaml:"sonarQube,omitempty" mapstructure:"sonarqube"`
|
||||
KubernetesOptions *k8s.KubernetesOptions `json:"kubernetes,omitempty" yaml:"kubernetes,omitempty" mapstructure:"kubernetes"`
|
||||
ServiceMeshOptions *servicemesh.Options `json:"servicemesh,omitempty" yaml:"servicemesh,omitempty" mapstructure:"servicemesh"`
|
||||
NetworkOptions *network.Options `json:"network,omitempty" yaml:"network,omitempty" mapstructure:"network"`
|
||||
LdapOptions *ldap.Options `json:"-,omitempty" yaml:"ldap,omitempty" mapstructure:"ldap"`
|
||||
RedisOptions *cache.Options `json:"redis,omitempty" yaml:"redis,omitempty" mapstructure:"redis"`
|
||||
S3Options *s3.Options `json:"s3,omitempty" yaml:"s3,omitempty" mapstructure:"s3"`
|
||||
OpenPitrixOptions *openpitrix.Options `json:"openpitrix,omitempty" yaml:"openpitrix,omitempty" mapstructure:"openpitrix"`
|
||||
MonitoringOptions *prometheus.Options `json:"monitoring,omitempty" yaml:"monitoring,omitempty" mapstructure:"monitoring"`
|
||||
LoggingOptions *logging.Options `json:"logging,omitempty" yaml:"logging,omitempty" mapstructure:"logging"`
|
||||
AuthenticationOptions *authentication.Options `json:"authentication,omitempty" yaml:"authentication,omitempty" mapstructure:"authentication"`
|
||||
AuthorizationOptions *authorization.Options `json:"authorization,omitempty" yaml:"authorization,omitempty" mapstructure:"authorization"`
|
||||
MultiClusterOptions *multicluster.Options `json:"multicluster,omitempty" yaml:"multicluster,omitempty" mapstructure:"multicluster"`
|
||||
EventsOptions *events.Options `json:"events,omitempty" yaml:"events,omitempty" mapstructure:"events"`
|
||||
AuditingOptions *auditing.Options `json:"auditing,omitempty" yaml:"auditing,omitempty" mapstructure:"auditing"`
|
||||
AlertingOptions *alerting.Options `json:"alerting,omitempty" yaml:"alerting,omitempty" mapstructure:"alerting"`
|
||||
NotificationOptions *notification.Options `json:"notification,omitempty" yaml:"notification,omitempty" mapstructure:"notification"`
|
||||
KubeEdgeOptions *kubeedge.Options `json:"kubeedge,omitempty" yaml:"kubeedge,omitempty" mapstructure:"kubeedge"`
|
||||
MeteringOptions *metering.Options `json:"metering,omitempty" yaml:"metering,omitempty" mapstructure:"metering"`
|
||||
GatewayOptions *gateway.Options `json:"gateway,omitempty" yaml:"gateway,omitempty" mapstructure:"gateway"`
|
||||
GPUOptions *gpu.Options `json:"gpu,omitempty" yaml:"gpu,omitempty" mapstructure:"gpu"`
|
||||
}
|
||||
|
||||
// newConfig creates a default non-empty Config
|
||||
@@ -126,8 +127,8 @@ func New() *Config {
|
||||
AlertingOptions: alerting.NewAlertingOptions(),
|
||||
NotificationOptions: notification.NewNotificationOptions(),
|
||||
LoggingOptions: logging.NewLoggingOptions(),
|
||||
AuthenticationOptions: authoptions.NewAuthenticateOptions(),
|
||||
AuthorizationOptions: authorizationoptions.NewAuthorizationOptions(),
|
||||
AuthenticationOptions: authentication.NewOptions(),
|
||||
AuthorizationOptions: authorization.NewOptions(),
|
||||
MultiClusterOptions: multicluster.NewOptions(),
|
||||
EventsOptions: events.NewEventsOptions(),
|
||||
AuditingOptions: auditing.NewAuditingOptions(),
|
||||
|
||||
@@ -23,14 +23,15 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authorization"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
networkv1alpha1 "kubesphere.io/api/network/v1alpha1"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
|
||||
authorizationoptions "kubesphere.io/kubesphere/pkg/apiserver/authorization/options"
|
||||
"kubesphere.io/kubesphere/pkg/simple/client/alerting"
|
||||
"kubesphere.io/kubesphere/pkg/simple/client/auditing"
|
||||
"kubesphere.io/kubesphere/pkg/simple/client/cache"
|
||||
@@ -142,13 +143,14 @@ func newTestConfig() (*Config, error) {
|
||||
NotificationOptions: ¬ification.Options{
|
||||
Endpoint: "http://notification.kubesphere-alerting-system.svc:9200",
|
||||
},
|
||||
AuthorizationOptions: authorizationoptions.NewAuthorizationOptions(),
|
||||
AuthenticationOptions: &authoptions.AuthenticationOptions{
|
||||
AuthorizationOptions: authorization.NewOptions(),
|
||||
AuthenticationOptions: &authentication.Options{
|
||||
AuthenticateRateLimiterMaxTries: 5,
|
||||
AuthenticateRateLimiterDuration: 30 * time.Minute,
|
||||
JwtSecret: "xxxxxx",
|
||||
MultipleLogin: false,
|
||||
OAuthOptions: &oauth.Options{
|
||||
Issuer: oauth.DefaultIssuer,
|
||||
IdentityProviders: []oauth.IdentityProviderOptions{},
|
||||
Clients: []oauth.Client{{
|
||||
Name: "kubesphere-console-client",
|
||||
|
||||
@@ -31,7 +31,7 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
|
||||
@@ -78,7 +78,7 @@ type Reconciler struct {
|
||||
MultiClusterEnabled bool
|
||||
DevopsClient devops.Interface
|
||||
LdapClient ldapclient.Interface
|
||||
AuthenticationOptions *authoptions.AuthenticationOptions
|
||||
AuthenticationOptions *authentication.Options
|
||||
Logger logr.Logger
|
||||
Scheme *runtime.Scheme
|
||||
Recorder record.EventRecorder
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
@@ -60,7 +60,7 @@ func newUser(name string) *iamv1alpha2.User {
|
||||
}
|
||||
|
||||
func TestDoNothing(t *testing.T) {
|
||||
authenticateOptions := options.NewAuthenticateOptions()
|
||||
authenticateOptions := authentication.NewOptions()
|
||||
authenticateOptions.AuthenticateRateLimiterMaxTries = 1
|
||||
authenticateOptions.AuthenticateRateLimiterDuration = 2 * time.Second
|
||||
user := newUser("test")
|
||||
|
||||
@@ -20,6 +20,19 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/utils/sliceutil"
|
||||
|
||||
"github.com/form3tech-oss/jwt-go"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/token"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/server/errors"
|
||||
|
||||
@@ -32,7 +45,6 @@ import (
|
||||
iamv1alpha2 "kubesphere.io/api/iam/v1alpha2"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/api"
|
||||
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/query"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/request"
|
||||
"kubesphere.io/kubesphere/pkg/models/auth"
|
||||
@@ -41,8 +53,9 @@ import (
|
||||
|
||||
const (
|
||||
KindTokenReview = "TokenReview"
|
||||
passwordGrantType = "password"
|
||||
refreshTokenGrantType = "refresh_token"
|
||||
grantTypePassword = "password"
|
||||
grantTypeRefreshToken = "refresh_token"
|
||||
grantTypeCode = "code"
|
||||
)
|
||||
|
||||
type Spec struct {
|
||||
@@ -73,32 +86,66 @@ func (request *TokenReview) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
||||
type discovery struct {
|
||||
// URL using the https scheme with no query or fragment component that the OP
|
||||
// asserts as its Issuer Identifier.
|
||||
Issuer string `json:"issuer"`
|
||||
// URL of the OP's OAuth 2.0 Authorization Endpoint.
|
||||
Auth string `json:"authorization_endpoint"`
|
||||
// URL of the OP's OAuth 2.0 Token Endpoint.
|
||||
Token string `json:"token_endpoint"`
|
||||
// URL of the OP's UserInfo Endpoint
|
||||
UserInfo string `json:"userinfo_endpoint"`
|
||||
// URL of the OP's JSON Web Key Set [JWK] document.
|
||||
Keys string `json:"jwks_uri"`
|
||||
// JSON array containing a list of the OAuth 2.0 Grant Type values that this OP supports.
|
||||
GrantTypes []string `json:"grant_types_supported"`
|
||||
// JSON array containing a list of the OAuth 2.0 response_type values that this OP supports.
|
||||
ResponseTypes []string `json:"response_types_supported"`
|
||||
// JSON array containing a list of the Subject Identifier types that this OP supports.
|
||||
Subjects []string `json:"subject_types_supported"`
|
||||
// JSON array containing a list of the JWS signing algorithms (alg values) supported by
|
||||
// the OP for the ID Token to encode the Claims in a JWT [JWT].
|
||||
IDTokenAlgs []string `json:"id_token_signing_alg_values_supported"`
|
||||
// JSON array containing a list of Proof Key for Code
|
||||
// Exchange (PKCE) [RFC7636] code challenge methods supported by this authorization server.
|
||||
CodeChallengeAlgs []string `json:"code_challenge_methods_supported"`
|
||||
// JSON array containing a list of the OAuth 2.0 [RFC6749] scope values that this server supports.
|
||||
Scopes []string `json:"scopes_supported"`
|
||||
// JSON array containing a list of Client Authentication methods supported by this Token Endpoint.
|
||||
AuthMethods []string `json:"token_endpoint_auth_methods_supported"`
|
||||
// JSON array containing a list of the Claim Names of the Claims that the OpenID Provider
|
||||
// MAY be able to supply values for.
|
||||
Claims []string `json:"claims_supported"`
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
im im.IdentityManagementInterface
|
||||
options *authoptions.AuthenticationOptions
|
||||
options *authentication.Options
|
||||
tokenOperator auth.TokenManagementInterface
|
||||
passwordAuthenticator auth.PasswordAuthenticator
|
||||
oauth2Authenticator auth.OAuthAuthenticator
|
||||
oauthAuthenticator auth.OAuthAuthenticator
|
||||
loginRecorder auth.LoginRecorder
|
||||
}
|
||||
|
||||
func newHandler(im im.IdentityManagementInterface,
|
||||
tokenOperator auth.TokenManagementInterface,
|
||||
passwordAuthenticator auth.PasswordAuthenticator,
|
||||
oauth2Authenticator auth.OAuthAuthenticator,
|
||||
oauthAuthenticator auth.OAuthAuthenticator,
|
||||
loginRecorder auth.LoginRecorder,
|
||||
options *authoptions.AuthenticationOptions) *handler {
|
||||
options *authentication.Options) *handler {
|
||||
return &handler{im: im,
|
||||
tokenOperator: tokenOperator,
|
||||
passwordAuthenticator: passwordAuthenticator,
|
||||
oauth2Authenticator: oauth2Authenticator,
|
||||
oauthAuthenticator: oauthAuthenticator,
|
||||
loginRecorder: loginRecorder,
|
||||
options: options}
|
||||
}
|
||||
|
||||
// Implement webhook authentication interface
|
||||
// tokenReview Implement webhook authentication interface
|
||||
// https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication
|
||||
func (h *handler) TokenReview(req *restful.Request, resp *restful.Response) {
|
||||
func (h *handler) tokenReview(req *restful.Request, resp *restful.Response) {
|
||||
var tokenReview TokenReview
|
||||
|
||||
err := req.ReadEntity(&tokenReview)
|
||||
@@ -112,12 +159,13 @@ func (h *handler) TokenReview(req *restful.Request, resp *restful.Response) {
|
||||
return
|
||||
}
|
||||
|
||||
authenticated, err := h.tokenOperator.Verify(tokenReview.Spec.Token)
|
||||
verified, err := h.tokenOperator.Verify(tokenReview.Spec.Token)
|
||||
if err != nil {
|
||||
api.HandleInternalError(resp, req, err)
|
||||
api.HandleBadRequest(resp, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
authenticated := verified.User
|
||||
success := TokenReview{APIVersion: tokenReview.APIVersion,
|
||||
Kind: KindTokenReview,
|
||||
Status: &Status{
|
||||
@@ -129,77 +177,179 @@ func (h *handler) TokenReview(req *restful.Request, resp *restful.Response) {
|
||||
resp.WriteEntity(success)
|
||||
}
|
||||
|
||||
func (h *handler) Authorize(req *restful.Request, resp *restful.Response) {
|
||||
authenticated, ok := request.UserFrom(req.Request.Context())
|
||||
clientId := req.QueryParameter("client_id")
|
||||
responseType := req.QueryParameter("response_type")
|
||||
redirectURI := req.QueryParameter("redirect_uri")
|
||||
|
||||
conf, err := h.options.OAuthOptions.OAuthClient(clientId)
|
||||
if err != nil {
|
||||
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
|
||||
api.HandleError(resp, req, err)
|
||||
return
|
||||
func (h *handler) discovery(req *restful.Request, response *restful.Response) {
|
||||
result := discovery{
|
||||
Issuer: h.options.OAuthOptions.Issuer,
|
||||
Auth: h.options.OAuthOptions.Issuer + "/authorize",
|
||||
Token: h.options.OAuthOptions.Issuer + "/token",
|
||||
Keys: h.options.OAuthOptions.Issuer + "/keys",
|
||||
UserInfo: h.options.OAuthOptions.Issuer + "/userinfo",
|
||||
Subjects: []string{"public"},
|
||||
GrantTypes: []string{"authorization_code", "refresh_token"},
|
||||
IDTokenAlgs: []string{string(jose.RS256)},
|
||||
CodeChallengeAlgs: []string{"S256", "plain"},
|
||||
Scopes: []string{"openid", "email", "profile", "offline_access"},
|
||||
// TODO(hongming) support client_secret_jwt
|
||||
AuthMethods: []string{"client_secret_basic", "client_secret_post"},
|
||||
Claims: []string{
|
||||
"iss", "sub", "aud", "iat", "exp", "email", "locale", "preferred_username",
|
||||
},
|
||||
ResponseTypes: []string{
|
||||
"code",
|
||||
"token",
|
||||
},
|
||||
}
|
||||
|
||||
if responseType != "token" {
|
||||
err := apierrors.NewBadRequest(fmt.Sprintf("Response type %s is not supported", responseType))
|
||||
api.HandleError(resp, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !ok {
|
||||
err := apierrors.NewUnauthorized("Unauthorized")
|
||||
api.HandleError(resp, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := h.tokenOperator.IssueTo(authenticated)
|
||||
if err != nil {
|
||||
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
|
||||
api.HandleError(resp, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
redirectURL, err := conf.ResolveRedirectURL(redirectURI)
|
||||
if err != nil {
|
||||
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
|
||||
api.HandleError(resp, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
redirectURL = fmt.Sprintf("%s#access_token=%s&token_type=Bearer", redirectURL, token.AccessToken)
|
||||
|
||||
if token.ExpiresIn > 0 {
|
||||
redirectURL = fmt.Sprintf("%s&expires_in=%v", redirectURL, token.ExpiresIn)
|
||||
}
|
||||
resp.Header().Set("Content-Type", "text/plain")
|
||||
http.Redirect(resp, req.Request, redirectURL, http.StatusFound)
|
||||
response.WriteEntity(result)
|
||||
}
|
||||
|
||||
func (h *handler) oauthCallback(req *restful.Request, resp *restful.Response) {
|
||||
func (h *handler) keys(req *restful.Request, response *restful.Response) {
|
||||
jwks := jose.JSONWebKeySet{
|
||||
Keys: []jose.JSONWebKey{*h.tokenOperator.Keys().SigningKeyPub},
|
||||
}
|
||||
response.WriteEntity(jwks)
|
||||
}
|
||||
|
||||
// The Authorization Endpoint performs Authentication of the End-User.
|
||||
func (h *handler) authorize(req *restful.Request, response *restful.Response) {
|
||||
var scope, responseType, clientID, redirectURI, state, nonce string
|
||||
scope = req.QueryParameter("scope")
|
||||
clientID = req.QueryParameter("client_id")
|
||||
redirectURI = req.QueryParameter("redirect_uri")
|
||||
//prompt = req.QueryParameter("prompt")
|
||||
responseType = req.QueryParameter("response_type")
|
||||
state = req.QueryParameter("state")
|
||||
nonce = req.QueryParameter("nonce")
|
||||
|
||||
// Authorization Servers MUST support the use of the HTTP GET and POST methods
|
||||
// defined in RFC 2616 [RFC2616] at the Authorization Endpoint.
|
||||
if req.Request.Method == http.MethodPost {
|
||||
scope, _ = req.BodyParameter("scope")
|
||||
clientID, _ = req.BodyParameter("client_id")
|
||||
redirectURI, _ = req.BodyParameter("redirect_uri")
|
||||
responseType, _ = req.BodyParameter("response_type")
|
||||
state, _ = req.BodyParameter("state")
|
||||
nonce, _ = req.BodyParameter("nonce")
|
||||
}
|
||||
|
||||
oauthClient, err := h.options.OAuthOptions.OAuthClient(clientID)
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidClient(err))
|
||||
return
|
||||
}
|
||||
|
||||
redirectURL, err := oauthClient.ResolveRedirectURL(redirectURI)
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
authenticated, _ := request.UserFrom(req.Request.Context())
|
||||
if authenticated == nil || authenticated.GetName() == user.Anonymous {
|
||||
response.Header().Add("WWW-Authenticate", "Basic")
|
||||
response.WriteHeaderAndEntity(http.StatusUnauthorized, oauth.ErrorLoginRequired)
|
||||
return
|
||||
}
|
||||
|
||||
// If no openid scope value is present, the request may still be a valid OAuth 2.0 request,
|
||||
// but is not an OpenID Connect request.
|
||||
var scopes []string
|
||||
if scope != "" {
|
||||
scopes = strings.Split(scope, " ")
|
||||
}
|
||||
var responseTypes []string
|
||||
if responseType != "" {
|
||||
responseTypes = strings.Split(responseType, " ")
|
||||
}
|
||||
|
||||
// If the resource owner denies the access request or if the request
|
||||
// fails for reasons other than a missing or invalid redirection URI,
|
||||
// the authorization server informs the client by adding the following
|
||||
// parameters to the query component of the redirection URI using the
|
||||
// "application/x-www-form-urlencoded" format
|
||||
informsError := func(err oauth.Error) {
|
||||
values := make(url.Values, 0)
|
||||
values.Add("error", err.Type)
|
||||
if err.Description != "" {
|
||||
values.Add("error_description", err.Description)
|
||||
}
|
||||
if state != "" {
|
||||
values.Add("state", state)
|
||||
}
|
||||
redirectURL.RawQuery = values.Encode()
|
||||
http.Redirect(response.ResponseWriter, req.Request, redirectURL.String(), http.StatusFound)
|
||||
}
|
||||
|
||||
// Other scope values MAY be present.
|
||||
// Scope values used that are not understood by an implementation SHOULD be ignored.
|
||||
if !oauth.IsValidScopes(scopes) {
|
||||
klog.Warningf("Some requested scopes were invalid: %v", scopes)
|
||||
}
|
||||
|
||||
if !oauth.IsValidResponseTypes(responseTypes) {
|
||||
err := fmt.Errorf("Some requested response types were invalid")
|
||||
informsError(oauth.NewInvalidRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(hongming) support Hybrid Flow
|
||||
// Authorization Code Flow
|
||||
if responseType == oauth.ResponseCode {
|
||||
code, err := h.tokenOperator.IssueTo(&token.IssueRequest{
|
||||
User: authenticated,
|
||||
Claims: token.Claims{
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Audience: []string{clientID},
|
||||
},
|
||||
TokenType: token.AuthorizationCode,
|
||||
Nonce: nonce,
|
||||
Scopes: scopes,
|
||||
},
|
||||
// A maximum authorization code lifetime of 10 minutes is
|
||||
ExpiresIn: 10 * time.Minute,
|
||||
})
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
return
|
||||
}
|
||||
values := redirectURL.Query()
|
||||
values.Add("code", code)
|
||||
redirectURL.RawQuery = values.Encode()
|
||||
http.Redirect(response, req.Request, redirectURL.String(), http.StatusFound)
|
||||
}
|
||||
|
||||
// Implicit Flow
|
||||
if responseType != oauth.ResponseToken {
|
||||
informsError(oauth.ErrorUnsupportedResponseType)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.issueTokenTo(authenticated)
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
return
|
||||
}
|
||||
|
||||
values := make(url.Values, 0)
|
||||
values.Add("access_token", result.AccessToken)
|
||||
values.Add("refresh_token", result.RefreshToken)
|
||||
values.Add("token_type", result.TokenType)
|
||||
values.Add("expires_in", fmt.Sprint(result.ExpiresIn))
|
||||
redirectURL.Fragment = values.Encode()
|
||||
http.Redirect(response, req.Request, redirectURL.String(), http.StatusFound)
|
||||
}
|
||||
|
||||
func (h *handler) oauthCallback(req *restful.Request, response *restful.Response) {
|
||||
provider := req.PathParameter("callback")
|
||||
// OAuth2 callback, see also https://tools.ietf.org/html/rfc6749#section-4.1.2
|
||||
code := req.QueryParameter("code")
|
||||
// CAS callback, see also https://apereo.github.io/cas/6.3.x/protocol/CAS-Protocol-V2-Specification.html#25-servicevalidate-cas-20
|
||||
if code == "" {
|
||||
code = req.QueryParameter("ticket")
|
||||
}
|
||||
if code == "" {
|
||||
err := apierrors.NewUnauthorized("Unauthorized: missing code")
|
||||
api.HandleError(resp, req, err)
|
||||
authenticated, provider, err := h.oauthAuthenticator.Authenticate(req.Request.Context(), provider, req.Request)
|
||||
if err != nil {
|
||||
api.HandleUnauthorized(response, req, apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err)))
|
||||
return
|
||||
}
|
||||
|
||||
authenticated, provider, err := h.oauth2Authenticator.Authenticate(provider, code)
|
||||
result, err := h.issueTokenTo(authenticated)
|
||||
if err != nil {
|
||||
api.HandleUnauthorized(resp, req, apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err)))
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.tokenOperator.IssueTo(authenticated)
|
||||
if err != nil {
|
||||
api.HandleInternalError(resp, req, apierrors.NewInternalError(err))
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -208,10 +358,10 @@ func (h *handler) oauthCallback(req *restful.Request, resp *restful.Response) {
|
||||
klog.Errorf("Failed to record successful login for user %s, error: %v", authenticated.GetName(), err)
|
||||
}
|
||||
|
||||
resp.WriteEntity(result)
|
||||
response.WriteEntity(result)
|
||||
}
|
||||
|
||||
func (h *handler) Login(request *restful.Request, response *restful.Response) {
|
||||
func (h *handler) login(request *restful.Request, response *restful.Response) {
|
||||
var loginRequest LoginRequest
|
||||
err := request.ReadEntity(&loginRequest)
|
||||
if err != nil {
|
||||
@@ -221,29 +371,70 @@ func (h *handler) Login(request *restful.Request, response *restful.Response) {
|
||||
h.passwordGrant(loginRequest.Username, loginRequest.Password, request, response)
|
||||
}
|
||||
|
||||
func (h *handler) Token(req *restful.Request, response *restful.Response) {
|
||||
grantType, err := req.BodyParameter("grant_type")
|
||||
// To obtain an Access Token, an ID Token, and optionally a Refresh Token,
|
||||
// the RP (Client) sends a Token Request to the Token Endpoint to obtain a Token Response,
|
||||
// as described in Section 3.2 of OAuth 2.0 [RFC6749], when using the Authorization Code Flow.
|
||||
// Communication with the Token Endpoint MUST utilize TLS.
|
||||
func (h *handler) token(req *restful.Request, response *restful.Response) {
|
||||
// TODO(hongming) support basic auth
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-2.3
|
||||
clientID, err := req.BodyParameter("client_id")
|
||||
if err != nil {
|
||||
api.HandleBadRequest(response, req, err)
|
||||
response.WriteHeaderAndEntity(http.StatusUnauthorized, oauth.NewInvalidClient(err))
|
||||
return
|
||||
}
|
||||
clientSecret, err := req.BodyParameter("client_secret")
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusUnauthorized, oauth.NewInvalidClient(err))
|
||||
return
|
||||
}
|
||||
|
||||
client, err := h.options.OAuthOptions.OAuthClient(clientID)
|
||||
if err != nil {
|
||||
oauthError := oauth.NewInvalidClient(err)
|
||||
response.WriteHeaderAndEntity(http.StatusUnauthorized, oauthError)
|
||||
return
|
||||
}
|
||||
|
||||
if client.Secret != clientSecret {
|
||||
oauthError := oauth.NewInvalidClient(fmt.Errorf("invalid client credential"))
|
||||
response.WriteHeaderAndEntity(http.StatusUnauthorized, oauthError)
|
||||
return
|
||||
}
|
||||
|
||||
grantType, err := req.BodyParameter("grant_type")
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
switch grantType {
|
||||
case passwordGrantType:
|
||||
case grantTypePassword:
|
||||
username, _ := req.BodyParameter("username")
|
||||
password, _ := req.BodyParameter("password")
|
||||
h.passwordGrant(username, password, req, response)
|
||||
break
|
||||
case refreshTokenGrantType:
|
||||
return
|
||||
case grantTypeRefreshToken:
|
||||
h.refreshTokenGrant(req, response)
|
||||
break
|
||||
return
|
||||
case grantTypeCode:
|
||||
h.codeGrant(req, response)
|
||||
return
|
||||
default:
|
||||
err := apierrors.NewBadRequest(fmt.Sprintf("Grant type %s is not supported", grantType))
|
||||
api.HandleBadRequest(response, req, err)
|
||||
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.ErrorUnsupportedGrantType)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// passwordGrant handle Resource Owner Password Credentials Grant
|
||||
// for more details: https://datatracker.ietf.org/doc/html/rfc6749#section-4.3
|
||||
// The resource owner password credentials grant type is suitable in
|
||||
// cases where the resource owner has a trust relationship with the client,
|
||||
// such as the device operating system or a highly privileged application.
|
||||
// The authorization server should take special care when enabling this
|
||||
// grant type and only allow it when other flows are not viable.
|
||||
func (h *handler) passwordGrant(username string, password string, req *restful.Request, response *restful.Response) {
|
||||
authenticated, provider, err := h.passwordAuthenticator.Authenticate(username, password)
|
||||
authenticated, provider, err := h.passwordAuthenticator.Authenticate(req.Request.Context(), username, password)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case auth.IncorrectPasswordError:
|
||||
@@ -251,45 +442,80 @@ func (h *handler) passwordGrant(username string, password string, req *restful.R
|
||||
if err := h.loginRecorder.RecordLogin(username, iamv1alpha2.Token, provider, requestInfo.SourceIP, requestInfo.UserAgent, err); err != nil {
|
||||
klog.Errorf("Failed to record unsuccessful login attempt for user %s, error: %v", username, err)
|
||||
}
|
||||
api.HandleUnauthorized(response, req, apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err)))
|
||||
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant(err))
|
||||
return
|
||||
case auth.RateLimitExceededError:
|
||||
api.HandleTooManyRequests(response, req, apierrors.NewTooManyRequestsError(fmt.Sprintf("Unauthorized: %s", err)))
|
||||
response.WriteHeaderAndEntity(http.StatusTooManyRequests, oauth.NewInvalidGrant(err))
|
||||
return
|
||||
default:
|
||||
api.HandleInternalError(response, req, apierrors.NewInternalError(err))
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result, err := h.tokenOperator.IssueTo(authenticated)
|
||||
result, err := h.issueTokenTo(authenticated)
|
||||
if err != nil {
|
||||
api.HandleInternalError(response, req, apierrors.NewInternalError(err))
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
return
|
||||
}
|
||||
|
||||
requestInfo, _ := request.RequestInfoFrom(req.Request.Context())
|
||||
if err = h.loginRecorder.RecordLogin(authenticated.GetName(), iamv1alpha2.Token, provider, requestInfo.SourceIP, requestInfo.UserAgent, nil); err != nil {
|
||||
klog.Errorf("Failed to record successful login for user %s, error: %v", username, err)
|
||||
klog.Errorf("Failed to record successful login for user %s, error: %v", authenticated.GetName(), err)
|
||||
}
|
||||
|
||||
response.WriteEntity(result)
|
||||
}
|
||||
|
||||
func (h *handler) issueTokenTo(user user.Info) (*oauth.Token, error) {
|
||||
accessToken, err := h.tokenOperator.IssueTo(&token.IssueRequest{
|
||||
User: user,
|
||||
Claims: token.Claims{TokenType: token.AccessToken},
|
||||
ExpiresIn: h.options.OAuthOptions.AccessTokenMaxAge,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refreshToken, err := h.tokenOperator.IssueTo(&token.IssueRequest{
|
||||
User: user,
|
||||
Claims: token.Claims{TokenType: token.RefreshToken},
|
||||
ExpiresIn: h.options.OAuthOptions.AccessTokenMaxAge + h.options.OAuthOptions.AccessTokenInactivityTimeout,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := oauth.Token{
|
||||
AccessToken: accessToken,
|
||||
// The OAuth 2.0 token_type response parameter value MUST be Bearer,
|
||||
// as specified in OAuth 2.0 Bearer Token Usage [RFC6750]
|
||||
TokenType: "Bearer",
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: int(h.options.OAuthOptions.AccessTokenMaxAge.Seconds()),
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (h *handler) refreshTokenGrant(req *restful.Request, response *restful.Response) {
|
||||
refreshToken, err := req.BodyParameter("refresh_token")
|
||||
if err != nil {
|
||||
api.HandleBadRequest(response, req, apierrors.NewBadRequest(err.Error()))
|
||||
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
authenticated, err := h.tokenOperator.Verify(refreshToken)
|
||||
verified, err := h.tokenOperator.Verify(refreshToken)
|
||||
if err != nil {
|
||||
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
|
||||
api.HandleUnauthorized(response, req, apierrors.NewUnauthorized(err.Error()))
|
||||
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant(err))
|
||||
return
|
||||
}
|
||||
|
||||
if verified.TokenType != token.RefreshToken {
|
||||
err = fmt.Errorf("ivalid token type %v want %v", verified.TokenType, token.RefreshToken)
|
||||
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant(err))
|
||||
return
|
||||
}
|
||||
|
||||
authenticated := verified.User
|
||||
// update token after registration
|
||||
if authenticated.GetName() == iamv1alpha2.PreRegistrationUser &&
|
||||
authenticated.GetExtra() != nil &&
|
||||
@@ -304,28 +530,106 @@ func (h *handler) refreshTokenGrant(req *restful.Request, response *restful.Resp
|
||||
iamv1alpha2.OriginUIDLabel: uid}).String()
|
||||
result, err := h.im.ListUsers(queryParam)
|
||||
if err != nil {
|
||||
api.HandleInternalError(response, req, apierrors.NewInternalError(err))
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
return
|
||||
}
|
||||
if len(result.Items) != 1 {
|
||||
err := apierrors.NewUnauthorized("authenticated user does not exist")
|
||||
api.HandleUnauthorized(response, req, apierrors.NewUnauthorized(err.Error()))
|
||||
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant(fmt.Errorf("authenticated user does not exist")))
|
||||
return
|
||||
}
|
||||
|
||||
authenticated = &user.DefaultInfo{Name: result.Items[0].(*iamv1alpha2.User).Name}
|
||||
}
|
||||
|
||||
result, err := h.tokenOperator.IssueTo(authenticated)
|
||||
result, err := h.issueTokenTo(authenticated)
|
||||
if err != nil {
|
||||
err := apierrors.NewUnauthorized(fmt.Sprintf("Unauthorized: %s", err))
|
||||
api.HandleUnauthorized(response, req, apierrors.NewUnauthorized(err.Error()))
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteEntity(result)
|
||||
}
|
||||
|
||||
func (h *handler) Logout(req *restful.Request, resp *restful.Response) {
|
||||
func (h *handler) codeGrant(req *restful.Request, response *restful.Response) {
|
||||
code, err := req.BodyParameter("code")
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
authorizeContext, err := h.tokenOperator.Verify(code)
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant(err))
|
||||
return
|
||||
}
|
||||
|
||||
if authorizeContext.TokenType != token.AuthorizationCode {
|
||||
err = fmt.Errorf("ivalid token type %v want %v", authorizeContext.TokenType, token.AuthorizationCode)
|
||||
response.WriteHeaderAndEntity(http.StatusBadRequest, oauth.NewInvalidGrant(err))
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// The client MUST NOT use the authorization code more than once.
|
||||
err = h.tokenOperator.Revoke(code)
|
||||
if err != nil {
|
||||
klog.Warningf("grant: failed to revoke authorization code: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
result, err := h.issueTokenTo(authorizeContext.User)
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
return
|
||||
}
|
||||
|
||||
// If no openid scope value is present, the request may still be a valid OAuth 2.0 request,
|
||||
// but is not an OpenID Connect request.
|
||||
if !sliceutil.HasString(authorizeContext.Scopes, oauth.ScopeOpenID) {
|
||||
response.WriteEntity(result)
|
||||
return
|
||||
}
|
||||
|
||||
authenticated, err := h.im.DescribeUser(authorizeContext.User.GetName())
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
return
|
||||
}
|
||||
|
||||
idTokenRequest := &token.IssueRequest{
|
||||
User: authorizeContext.User,
|
||||
Claims: token.Claims{
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Audience: authorizeContext.Audience,
|
||||
},
|
||||
Nonce: authorizeContext.Nonce,
|
||||
TokenType: token.IDToken,
|
||||
Name: authorizeContext.User.GetName(),
|
||||
},
|
||||
ExpiresIn: h.options.OAuthOptions.AccessTokenMaxAge + h.options.OAuthOptions.AccessTokenInactivityTimeout,
|
||||
}
|
||||
|
||||
if sliceutil.HasString(authorizeContext.Scopes, oauth.ScopeProfile) {
|
||||
idTokenRequest.PreferredUsername = authenticated.Name
|
||||
idTokenRequest.Locale = authenticated.Spec.Lang
|
||||
}
|
||||
|
||||
if sliceutil.HasString(authorizeContext.Scopes, oauth.ScopeEmail) {
|
||||
idTokenRequest.Email = authenticated.Spec.Email
|
||||
}
|
||||
|
||||
idToken, err := h.tokenOperator.IssueTo(idTokenRequest)
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
return
|
||||
}
|
||||
|
||||
result.IDToken = idToken
|
||||
|
||||
response.WriteEntity(result)
|
||||
}
|
||||
|
||||
func (h *handler) logout(req *restful.Request, resp *restful.Response) {
|
||||
authenticated, ok := request.UserFrom(req.Request.Context())
|
||||
if ok {
|
||||
if err := h.tokenOperator.RevokeAllUserTokens(authenticated.GetName()); err != nil {
|
||||
@@ -354,3 +658,28 @@ func (h *handler) Logout(req *restful.Request, resp *restful.Response) {
|
||||
resp.Header().Set("Content-Type", "text/plain")
|
||||
http.Redirect(resp, req.Request, redirectURL.String(), http.StatusFound)
|
||||
}
|
||||
|
||||
// userinfo Endpoint is an OAuth 2.0 Protected Resource that returns Claims about the authenticated End-User.
|
||||
func (h *handler) userinfo(req *restful.Request, response *restful.Response) {
|
||||
authenticated, _ := request.UserFrom(req.Request.Context())
|
||||
if authenticated == nil || authenticated.GetName() == user.Anonymous {
|
||||
response.WriteHeaderAndEntity(http.StatusUnauthorized, oauth.ErrorLoginRequired)
|
||||
return
|
||||
}
|
||||
detail, err := h.im.DescribeUser(authenticated.GetName())
|
||||
if err != nil {
|
||||
response.WriteHeaderAndEntity(http.StatusInternalServerError, oauth.NewServerError(err))
|
||||
return
|
||||
}
|
||||
|
||||
result := token.Claims{
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
Subject: detail.Name,
|
||||
},
|
||||
Name: detail.Name,
|
||||
Email: detail.Spec.Email,
|
||||
Locale: detail.Spec.Lang,
|
||||
PreferredUsername: detail.Name,
|
||||
}
|
||||
response.WriteEntity(result)
|
||||
}
|
||||
|
||||
@@ -19,18 +19,21 @@ package oauth
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
|
||||
"github.com/emicklei/go-restful"
|
||||
restfulspec "github.com/emicklei/go-restful-openapi"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/api"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
|
||||
"kubesphere.io/kubesphere/pkg/constants"
|
||||
"kubesphere.io/kubesphere/pkg/models/auth"
|
||||
"kubesphere.io/kubesphere/pkg/models/iam/im"
|
||||
)
|
||||
|
||||
// ks-apiserver includes a built-in OAuth server. Users obtain OAuth access tokens to authenticate themselves to the API.
|
||||
const contentTypeFormData = "application/x-www-form-urlencoded"
|
||||
|
||||
// AddToContainer ks-apiserver includes a built-in OAuth server. Users obtain OAuth access tokens to authenticate themselves to the API.
|
||||
// The OAuth server supports standard authorization code grant and the implicit grant OAuth authorization flows.
|
||||
// All requests for OAuth tokens involve a request to <ks-apiserver>/oauth/authorize.
|
||||
// Most authentication integrations place an authenticating proxy in front of this endpoint, or configure ks-apiserver
|
||||
@@ -41,7 +44,7 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface,
|
||||
passwordAuthenticator auth.PasswordAuthenticator,
|
||||
oauth2Authenticator auth.OAuthAuthenticator,
|
||||
loginRecorder auth.LoginRecorder,
|
||||
options *authoptions.AuthenticationOptions) error {
|
||||
options *authentication.Options) error {
|
||||
|
||||
ws := &restful.WebService{}
|
||||
ws.Path("/oauth").
|
||||
@@ -50,42 +53,70 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface,
|
||||
|
||||
handler := newHandler(im, tokenOperator, passwordAuthenticator, oauth2Authenticator, loginRecorder, options)
|
||||
|
||||
ws.Route(ws.GET("/.well-known/openid-configuration").To(handler.discovery).
|
||||
Doc("The OpenID Provider's configuration information can be retrieved."))
|
||||
ws.Route(ws.GET("/keys").To(handler.keys).
|
||||
Doc("OP's JSON Web Key Set [JWK] document."))
|
||||
ws.Route(ws.GET("/userinfo").To(handler.userinfo).
|
||||
Doc("UserInfo Endpoint is an OAuth 2.0 Protected Resource that returns Claims about the authenticated End-User."))
|
||||
|
||||
// Implement webhook authentication interface
|
||||
// https://kubernetes.io/docs/reference/access-authn-authz/authentication/#webhook-token-authentication
|
||||
ws.Route(ws.POST("/authenticate").
|
||||
Doc("TokenReview attempts to authenticate a token to a known user. Note: TokenReview requests may be "+
|
||||
"cached by the webhook token authenticator plugin in the kube-apiserver.").
|
||||
Reads(TokenReview{}).
|
||||
To(handler.TokenReview).
|
||||
To(handler.tokenReview).
|
||||
Returns(http.StatusOK, api.StatusOK, TokenReview{}).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag}))
|
||||
|
||||
// Only support implicit grant flow
|
||||
// https://tools.ietf.org/html/rfc6749#section-4.2
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-3.1
|
||||
ws.Route(ws.GET("/authorize").
|
||||
Doc("All requests for OAuth tokens involve a request to <ks-apiserver>/oauth/authorize.").
|
||||
Doc("The authorization endpoint is used to interact with the resource owner and obtain an authorization grant.").
|
||||
Param(ws.QueryParameter("response_type", "The value MUST be one of \"code\" for requesting an "+
|
||||
"authorization code as described by [RFC6749] Section 4.1.1, \"token\" for requesting an access token (implicit grant)"+
|
||||
" as described by [RFC6749] Section 4.2.2.").Required(true)).
|
||||
Param(ws.QueryParameter("client_id", "The client identifier issued to the client during the "+
|
||||
"registration process described by [RFC6749] Section 2.2.").Required(true)).
|
||||
Param(ws.QueryParameter("redirect_uri", "After completing its interaction with the resource owner, "+
|
||||
"the authorization server directs the resource owner's user-agent back to the client.The redirection endpoint "+
|
||||
"URI MUST be an absolute URI as defined by [RFC3986] Section 4.3.").Required(false)).
|
||||
To(handler.Authorize).
|
||||
Returns(http.StatusFound, http.StatusText(http.StatusFound), "").
|
||||
Param(ws.QueryParameter("client_id", "OAuth 2.0 Client Identifier valid at the Authorization Server.").Required(true)).
|
||||
Param(ws.QueryParameter("redirect_uri", "Redirection URI to which the response will be sent. "+
|
||||
"This URI MUST exactly match one of the Redirection URI values for the Client pre-registered at the OpenID Provider.").Required(true)).
|
||||
Param(ws.QueryParameter("scope", "OpenID Connect requests MUST contain the openid scope value. "+
|
||||
"If the openid scope value is not present, the behavior is entirely unspecified.").Required(false)).
|
||||
Param(ws.QueryParameter("state", "Opaque value used to maintain state between the request and the callback.").Required(false)).
|
||||
To(handler.authorize).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag}))
|
||||
// Resource Owner Password Credentials Grant
|
||||
// https://tools.ietf.org/html/rfc6749#section-4.3
|
||||
|
||||
// Authorization Servers MUST support the use of the HTTP GET and POST methods
|
||||
// defined in RFC 2616 [RFC2616] at the Authorization Endpoint.
|
||||
ws.Route(ws.POST("/authorize").
|
||||
Consumes(contentTypeFormData).
|
||||
Doc("The authorization endpoint is used to interact with the resource owner and obtain an authorization grant.").
|
||||
Param(ws.BodyParameter("response_type", "The value MUST be one of \"code\" for requesting an "+
|
||||
"authorization code as described by [RFC6749] Section 4.1.1, \"token\" for requesting an access token (implicit grant)"+
|
||||
" as described by [RFC6749] Section 4.2.2.").Required(true)).
|
||||
Param(ws.BodyParameter("client_id", "OAuth 2.0 Client Identifier valid at the Authorization Server.").Required(true)).
|
||||
Param(ws.BodyParameter("redirect_uri", "Redirection URI to which the response will be sent. "+
|
||||
"This URI MUST exactly match one of the Redirection URI values for the Client pre-registered at the OpenID Provider.").Required(true)).
|
||||
Param(ws.BodyParameter("scope", "OpenID Connect requests MUST contain the openid scope value. "+
|
||||
"If the openid scope value is not present, the behavior is entirely unspecified.").Required(false)).
|
||||
Param(ws.BodyParameter("state", "Opaque value used to maintain state between the request and the callback.").Required(false)).
|
||||
To(handler.authorize).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag}))
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749#section-3.2
|
||||
ws.Route(ws.POST("/token").
|
||||
Consumes("application/x-www-form-urlencoded").
|
||||
Consumes(contentTypeFormData).
|
||||
Doc("The resource owner password credentials grant type is suitable in\n"+
|
||||
"cases where the resource owner has a trust relationship with the\n"+
|
||||
"client, such as the device operating system or a highly privileged application.").
|
||||
Param(ws.FormParameter("grant_type", "Value MUST be set to \"password\".").Required(true)).
|
||||
Param(ws.FormParameter("username", "The resource owner username.").Required(true)).
|
||||
Param(ws.FormParameter("password", "The resource owner password.").Required(true)).
|
||||
To(handler.Token).
|
||||
Param(ws.FormParameter("grant_type", "OAuth defines four grant types: "+
|
||||
"authorization code, implicit, resource owner password credentials, and client credentials.").
|
||||
Required(true)).
|
||||
Param(ws.FormParameter("client_id", "Valid client credential.").Required(true)).
|
||||
Param(ws.FormParameter("client_secret", "Valid client credential.").Required(true)).
|
||||
Param(ws.FormParameter("username", "The resource owner username.").Required(false)).
|
||||
Param(ws.FormParameter("password", "The resource owner password.").Required(false)).
|
||||
Param(ws.FormParameter("code", "Valid authorization code.").Required(false)).
|
||||
To(handler.token).
|
||||
Returns(http.StatusOK, http.StatusText(http.StatusOK), &oauth.Token{}).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag}))
|
||||
|
||||
@@ -123,7 +154,7 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface,
|
||||
Param(ws.QueryParameter("state", "Opaque value used by the RP to maintain state between "+
|
||||
"the logout request and the callback to the endpoint specified by the post_logout_redirect_uri parameter.").
|
||||
Required(false)).
|
||||
To(handler.Logout).
|
||||
To(handler.logout).
|
||||
Returns(http.StatusOK, http.StatusText(http.StatusOK), "").
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag}))
|
||||
|
||||
@@ -135,7 +166,7 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface,
|
||||
Consumes(restful.MIME_JSON).
|
||||
Produces(restful.MIME_JSON)
|
||||
legacy.Route(legacy.POST("").
|
||||
To(handler.Login).
|
||||
To(handler.login).
|
||||
Deprecate().
|
||||
Doc("KubeSphere APIs support token-based authentication via the Authtoken request header. The POST Login API is used to retrieve the authentication token. After the authentication token is obtained, it must be inserted into the Authtoken header for all requests.").
|
||||
Reads(LoginRequest{}).
|
||||
|
||||
@@ -21,27 +21,21 @@ package auth
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"strings"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
informers "kubesphere.io/kubesphere/pkg/client/informers/externalversions"
|
||||
"kubesphere.io/kubesphere/pkg/constants"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
authuser "k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/klog"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
|
||||
iamv1alpha2 "kubesphere.io/api/iam/v1alpha2"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
|
||||
kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
|
||||
iamv1alpha2listers "kubesphere.io/kubesphere/pkg/client/listers/iam/v1alpha2"
|
||||
)
|
||||
|
||||
@@ -51,139 +45,20 @@ var (
|
||||
AccountIsNotActiveError = fmt.Errorf("account is not active")
|
||||
)
|
||||
|
||||
// PasswordAuthenticator is an interface implemented by authenticator which take a
|
||||
// username and password.
|
||||
type PasswordAuthenticator interface {
|
||||
Authenticate(username, password string) (authuser.Info, string, error)
|
||||
Authenticate(ctx context.Context, username, password string) (authuser.Info, string, error)
|
||||
}
|
||||
|
||||
type OAuthAuthenticator interface {
|
||||
Authenticate(provider, code string) (authuser.Info, string, error)
|
||||
}
|
||||
|
||||
type passwordAuthenticator struct {
|
||||
ksClient kubesphere.Interface
|
||||
userGetter *userGetter
|
||||
authOptions *authoptions.AuthenticationOptions
|
||||
}
|
||||
|
||||
type oauth2Authenticator struct {
|
||||
ksClient kubesphere.Interface
|
||||
userGetter *userGetter
|
||||
authOptions *authoptions.AuthenticationOptions
|
||||
Authenticate(ctx context.Context, provider string, req *http.Request) (authuser.Info, string, error)
|
||||
}
|
||||
|
||||
type userGetter struct {
|
||||
userLister iamv1alpha2listers.UserLister
|
||||
}
|
||||
|
||||
func NewPasswordAuthenticator(ksClient kubesphere.Interface,
|
||||
userLister iamv1alpha2listers.UserLister,
|
||||
options *authoptions.AuthenticationOptions) PasswordAuthenticator {
|
||||
passwordAuthenticator := &passwordAuthenticator{
|
||||
ksClient: ksClient,
|
||||
userGetter: &userGetter{userLister: userLister},
|
||||
authOptions: options,
|
||||
}
|
||||
return passwordAuthenticator
|
||||
}
|
||||
|
||||
func NewOAuthAuthenticator(ksClient kubesphere.Interface,
|
||||
ksInformer informers.SharedInformerFactory,
|
||||
options *authoptions.AuthenticationOptions) OAuthAuthenticator {
|
||||
oauth2Authenticator := &oauth2Authenticator{
|
||||
ksClient: ksClient,
|
||||
userGetter: &userGetter{userLister: ksInformer.Iam().V1alpha2().Users().Lister()},
|
||||
authOptions: options,
|
||||
}
|
||||
return oauth2Authenticator
|
||||
}
|
||||
|
||||
func (p *passwordAuthenticator) Authenticate(username, password string) (authuser.Info, string, error) {
|
||||
// empty username or password are not allowed
|
||||
if username == "" || password == "" {
|
||||
return nil, "", IncorrectPasswordError
|
||||
}
|
||||
// generic identity provider has higher priority
|
||||
for _, providerOptions := range p.authOptions.OAuthOptions.IdentityProviders {
|
||||
// the admin account in kubesphere has the highest priority
|
||||
if username == constants.AdminUserName {
|
||||
break
|
||||
}
|
||||
if genericProvider, _ := identityprovider.GetGenericProvider(providerOptions.Name); genericProvider != nil {
|
||||
authenticated, err := genericProvider.Authenticate(username, password)
|
||||
if err != nil {
|
||||
if errors.IsUnauthorized(err) {
|
||||
continue
|
||||
}
|
||||
return nil, providerOptions.Name, err
|
||||
}
|
||||
linkedAccount, err := p.userGetter.findLinkedAccount(providerOptions.Name, authenticated.GetUserID())
|
||||
if err != nil {
|
||||
return nil, providerOptions.Name, err
|
||||
}
|
||||
// using this method requires you to manually provision users.
|
||||
if providerOptions.MappingMethod == oauth.MappingMethodLookup && linkedAccount == nil {
|
||||
continue
|
||||
}
|
||||
// the user will automatically create and mapping when login successful.
|
||||
if linkedAccount == nil && providerOptions.MappingMethod == oauth.MappingMethodAuto {
|
||||
if !providerOptions.DisableLoginConfirmation {
|
||||
return preRegistrationUser(providerOptions.Name, authenticated), providerOptions.Name, nil
|
||||
}
|
||||
|
||||
linkedAccount, err = p.ksClient.IamV1alpha2().Users().Create(context.Background(), mappedUser(providerOptions.Name, authenticated), metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return nil, providerOptions.Name, err
|
||||
}
|
||||
}
|
||||
if linkedAccount != nil {
|
||||
return &authuser.DefaultInfo{Name: linkedAccount.GetName()}, providerOptions.Name, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// kubesphere account
|
||||
user, err := p.userGetter.findUser(username)
|
||||
if err != nil {
|
||||
// ignore not found error
|
||||
if !errors.IsNotFound(err) {
|
||||
klog.Error(err)
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
// check user status
|
||||
if user != nil && (user.Status.State == nil || *user.Status.State != iamv1alpha2.UserActive) {
|
||||
if user.Status.State != nil && *user.Status.State == iamv1alpha2.UserAuthLimitExceeded {
|
||||
klog.Errorf("%s, username: %s", RateLimitExceededError, username)
|
||||
return nil, "", RateLimitExceededError
|
||||
} else {
|
||||
// state not active
|
||||
klog.Errorf("%s, username: %s", AccountIsNotActiveError, username)
|
||||
return nil, "", AccountIsNotActiveError
|
||||
}
|
||||
}
|
||||
|
||||
// if the password is not empty, means that the password has been reset, even if the user was mapping from IDP
|
||||
if user != nil && user.Spec.EncryptedPassword != "" {
|
||||
if err = PasswordVerify(user.Spec.EncryptedPassword, password); err != nil {
|
||||
klog.Error(err)
|
||||
return nil, "", err
|
||||
}
|
||||
u := &authuser.DefaultInfo{
|
||||
Name: user.Name,
|
||||
}
|
||||
// check if the password is initialized
|
||||
if uninitialized := user.Annotations[iamv1alpha2.UninitializedAnnotation]; uninitialized != "" {
|
||||
u.Extra = map[string][]string{
|
||||
iamv1alpha2.ExtraUninitialized: {uninitialized},
|
||||
}
|
||||
}
|
||||
return u, "", nil
|
||||
}
|
||||
|
||||
return nil, "", IncorrectPasswordError
|
||||
}
|
||||
|
||||
func preRegistrationUser(idp string, identity identityprovider.Identity) authuser.Info {
|
||||
return &authuser.DefaultInfo{
|
||||
Name: iamv1alpha2.PreRegistrationUser,
|
||||
@@ -193,7 +68,6 @@ func preRegistrationUser(idp string, identity identityprovider.Identity) authuse
|
||||
iamv1alpha2.ExtraUsername: {identity.GetUsername()},
|
||||
iamv1alpha2.ExtraEmail: {identity.GetEmail()},
|
||||
},
|
||||
Groups: []string{iamv1alpha2.PreRegistrationUserGroup},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,76 +86,29 @@ func mappedUser(idp string, identity identityprovider.Identity) *iamv1alpha2.Use
|
||||
}
|
||||
}
|
||||
|
||||
func (o *oauth2Authenticator) Authenticate(provider, code string) (authuser.Info, string, error) {
|
||||
providerOptions, err := o.authOptions.OAuthOptions.IdentityProviderOptions(provider)
|
||||
// identity provider not registered
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, "", err
|
||||
}
|
||||
oauthIdentityProvider, err := identityprovider.GetOAuthProvider(providerOptions.Name)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, "", err
|
||||
}
|
||||
authenticated, err := oauthIdentityProvider.IdentityExchange(code)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
user, err := o.userGetter.findLinkedAccount(providerOptions.Name, authenticated.GetUserID())
|
||||
if user == nil && providerOptions.MappingMethod == oauth.MappingMethodLookup {
|
||||
klog.Error(err)
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// the user will automatically create and mapping when login successful.
|
||||
if user == nil && providerOptions.MappingMethod == oauth.MappingMethodAuto {
|
||||
if !providerOptions.DisableLoginConfirmation {
|
||||
return preRegistrationUser(providerOptions.Name, authenticated), providerOptions.Name, nil
|
||||
}
|
||||
user, err = o.ksClient.IamV1alpha2().Users().Create(context.Background(), mappedUser(providerOptions.Name, authenticated), metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return nil, providerOptions.Name, err
|
||||
}
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
return &authuser.DefaultInfo{Name: user.GetName()}, providerOptions.Name, nil
|
||||
}
|
||||
|
||||
return nil, "", errors.NewNotFound(iamv1alpha2.Resource("user"), authenticated.GetUsername())
|
||||
}
|
||||
|
||||
func PasswordVerify(encryptedPassword, password string) error {
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(encryptedPassword), []byte(password)); err != nil {
|
||||
return IncorrectPasswordError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findUser
|
||||
// findUser returns the user associated with the username or email
|
||||
func (u *userGetter) findUser(username string) (*iamv1alpha2.User, error) {
|
||||
if _, err := mail.ParseAddress(username); err != nil {
|
||||
return u.userLister.Get(username)
|
||||
} else {
|
||||
users, err := u.userLister.List(labels.Everything())
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
for _, find := range users {
|
||||
if find.Spec.Email == username {
|
||||
return find, nil
|
||||
}
|
||||
}
|
||||
|
||||
users, err := u.userLister.List(labels.Everything())
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
if user.Spec.Email == username {
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.NewNotFound(iamv1alpha2.Resource("user"), username)
|
||||
}
|
||||
|
||||
func (u *userGetter) findLinkedAccount(idp, uid string) (*iamv1alpha2.User, error) {
|
||||
// findMappedUser returns the user which mapped to the identity
|
||||
func (u *userGetter) findMappedUser(idp, uid string) (*iamv1alpha2.User, error) {
|
||||
selector := labels.SelectorFromSet(labels.Set{
|
||||
iamv1alpha2.IdentifyProviderLabel: idp,
|
||||
iamv1alpha2.OriginUIDLabel: uid,
|
||||
|
||||
98
pkg/models/auth/oauth.go
Normal file
98
pkg/models/auth/oauth.go
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
|
||||
Copyright 2021 The KubeSphere 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 auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
authuser "k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/klog"
|
||||
iamv1alpha2 "kubesphere.io/api/iam/v1alpha2"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
iamv1alpha2listers "kubesphere.io/kubesphere/pkg/client/listers/iam/v1alpha2"
|
||||
)
|
||||
|
||||
type oauthAuthenticator struct {
|
||||
ksClient kubesphere.Interface
|
||||
userGetter *userGetter
|
||||
options *authentication.Options
|
||||
}
|
||||
|
||||
func NewOAuthAuthenticator(ksClient kubesphere.Interface,
|
||||
userLister iamv1alpha2listers.UserLister,
|
||||
options *authentication.Options) OAuthAuthenticator {
|
||||
authenticator := &oauthAuthenticator{
|
||||
ksClient: ksClient,
|
||||
userGetter: &userGetter{userLister: userLister},
|
||||
options: options,
|
||||
}
|
||||
return authenticator
|
||||
}
|
||||
|
||||
func (o *oauthAuthenticator) Authenticate(_ context.Context, provider string, req *http.Request) (authuser.Info, string, error) {
|
||||
providerOptions, err := o.options.OAuthOptions.IdentityProviderOptions(provider)
|
||||
// identity provider not registered
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, "", err
|
||||
}
|
||||
oauthIdentityProvider, err := identityprovider.GetOAuthProvider(providerOptions.Name)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, "", err
|
||||
}
|
||||
authenticated, err := oauthIdentityProvider.IdentityExchangeCallback(req)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
user, err := o.userGetter.findMappedUser(providerOptions.Name, authenticated.GetUserID())
|
||||
if user == nil && providerOptions.MappingMethod == oauth.MappingMethodLookup {
|
||||
klog.Error(err)
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// the user will automatically create and mapping when login successful.
|
||||
if user == nil && providerOptions.MappingMethod == oauth.MappingMethodAuto {
|
||||
if !providerOptions.DisableLoginConfirmation {
|
||||
return preRegistrationUser(providerOptions.Name, authenticated), providerOptions.Name, nil
|
||||
}
|
||||
user, err = o.ksClient.IamV1alpha2().Users().Create(context.Background(), mappedUser(providerOptions.Name, authenticated), metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return nil, providerOptions.Name, err
|
||||
}
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
return &authuser.DefaultInfo{Name: user.GetName()}, providerOptions.Name, nil
|
||||
}
|
||||
|
||||
return nil, "", errors.NewNotFound(iamv1alpha2.Resource("user"), authenticated.GetUsername())
|
||||
}
|
||||
205
pkg/models/auth/oauth_test.go
Normal file
205
pkg/models/auth/oauth_test.go
Normal file
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
|
||||
Copyright 2021 The KubeSphere 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 auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
iamv1alpha2 "kubesphere.io/api/iam/v1alpha2"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
fakeks "kubesphere.io/kubesphere/pkg/client/clientset/versioned/fake"
|
||||
ksinformers "kubesphere.io/kubesphere/pkg/client/informers/externalversions"
|
||||
)
|
||||
|
||||
func Test_oauthAuthenticator_Authenticate(t *testing.T) {
|
||||
|
||||
oauthOptions := &authentication.Options{
|
||||
OAuthOptions: &oauth.Options{
|
||||
IdentityProviders: []oauth.IdentityProviderOptions{
|
||||
{
|
||||
Name: "fake",
|
||||
MappingMethod: "auto",
|
||||
Type: "FakeIdentityProvider",
|
||||
Provider: oauth.DynamicOptions{
|
||||
"identities": map[string]interface{}{
|
||||
"code1": map[string]string{
|
||||
"uid": "100001",
|
||||
"email": "user1@kubesphere.io",
|
||||
"username": "user1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
identityprovider.RegisterOAuthProvider(&fakeProviderFactory{})
|
||||
if err := identityprovider.SetupWithOptions(oauthOptions.OAuthOptions.IdentityProviders); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ksClient := fakeks.NewSimpleClientset()
|
||||
ksInformerFactory := ksinformers.NewSharedInformerFactory(ksClient, 0)
|
||||
err := ksInformerFactory.Iam().V1alpha2().Users().Informer().GetIndexer().Add(newUser("user1", "100001", "fake"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
provider string
|
||||
req *http.Request
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
oauthAuthenticator OAuthAuthenticator
|
||||
args args
|
||||
userInfo user.Info
|
||||
provider string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Should successfully",
|
||||
oauthAuthenticator: NewOAuthAuthenticator(
|
||||
nil,
|
||||
ksInformerFactory.Iam().V1alpha2().Users().Lister(),
|
||||
oauthOptions,
|
||||
),
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
provider: "fake",
|
||||
req: must(http.NewRequest(http.MethodGet, "https://ks-console.kubesphere.io/oauth/callback/test?code=code1&state=100001", nil)),
|
||||
},
|
||||
userInfo: &user.DefaultInfo{
|
||||
Name: "user1",
|
||||
},
|
||||
provider: "fake",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Should successfully",
|
||||
oauthAuthenticator: NewOAuthAuthenticator(
|
||||
nil,
|
||||
ksInformerFactory.Iam().V1alpha2().Users().Lister(),
|
||||
oauthOptions,
|
||||
),
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
provider: "fake1",
|
||||
req: must(http.NewRequest(http.MethodGet, "https://ks-console.kubesphere.io/oauth/callback/test?code=code1&state=100001", nil)),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
userInfo, provider, err := tt.oauthAuthenticator.Authenticate(tt.args.ctx, tt.args.provider, tt.args.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Authenticate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(userInfo, tt.userInfo) {
|
||||
t.Errorf("Authenticate() got = %v, want %v", userInfo, tt.userInfo)
|
||||
}
|
||||
if provider != tt.provider {
|
||||
t.Errorf("Authenticate() got = %v, want %v", provider, tt.provider)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func must(r *http.Request, err error) *http.Request {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func newUser(username string, uid string, idp string) *iamv1alpha2.User {
|
||||
return &iamv1alpha2.User{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamv1alpha2.ResourceKindUser,
|
||||
APIVersion: iamv1alpha2.SchemeGroupVersion.String(),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: username,
|
||||
Labels: map[string]string{
|
||||
iamv1alpha2.IdentifyProviderLabel: idp,
|
||||
iamv1alpha2.OriginUIDLabel: uid,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type fakeProviderFactory struct {
|
||||
}
|
||||
|
||||
type fakeProvider struct {
|
||||
Identities map[string]fakeIdentity `json:"identities"`
|
||||
}
|
||||
|
||||
type fakeIdentity struct {
|
||||
UID string `json:"uid"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func (f fakeIdentity) GetUserID() string {
|
||||
return f.UID
|
||||
}
|
||||
|
||||
func (f fakeIdentity) GetUsername() string {
|
||||
return f.Username
|
||||
}
|
||||
|
||||
func (f fakeIdentity) GetEmail() string {
|
||||
return f.Email
|
||||
}
|
||||
|
||||
func (fakeProviderFactory) Type() string {
|
||||
return "FakeIdentityProvider"
|
||||
}
|
||||
|
||||
func (fakeProviderFactory) Create(options oauth.DynamicOptions) (identityprovider.OAuthProvider, error) {
|
||||
var fakeProvider fakeProvider
|
||||
if err := mapstructure.Decode(options, &fakeProvider); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &fakeProvider, nil
|
||||
}
|
||||
|
||||
func (f fakeProvider) IdentityExchangeCallback(req *http.Request) (identityprovider.Identity, error) {
|
||||
code := req.URL.Query().Get("code")
|
||||
if identity, ok := f.Identities[code]; ok {
|
||||
return identity, nil
|
||||
}
|
||||
return nil, fmt.Errorf("authorization failed")
|
||||
}
|
||||
151
pkg/models/auth/password.go
Normal file
151
pkg/models/auth/password.go
Normal file
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
|
||||
Copyright 2021 The KubeSphere 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 auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
authuser "k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/klog"
|
||||
iamv1alpha2 "kubesphere.io/api/iam/v1alpha2"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
iamv1alpha2listers "kubesphere.io/kubesphere/pkg/client/listers/iam/v1alpha2"
|
||||
"kubesphere.io/kubesphere/pkg/constants"
|
||||
)
|
||||
|
||||
type passwordAuthenticator struct {
|
||||
ksClient kubesphere.Interface
|
||||
userGetter *userGetter
|
||||
authOptions *authentication.Options
|
||||
}
|
||||
|
||||
func NewPasswordAuthenticator(ksClient kubesphere.Interface,
|
||||
userLister iamv1alpha2listers.UserLister,
|
||||
options *authentication.Options) PasswordAuthenticator {
|
||||
passwordAuthenticator := &passwordAuthenticator{
|
||||
ksClient: ksClient,
|
||||
userGetter: &userGetter{userLister: userLister},
|
||||
authOptions: options,
|
||||
}
|
||||
return passwordAuthenticator
|
||||
}
|
||||
|
||||
func (p *passwordAuthenticator) Authenticate(_ context.Context, username, password string) (authuser.Info, string, error) {
|
||||
// empty username or password are not allowed
|
||||
if username == "" || password == "" {
|
||||
return nil, "", IncorrectPasswordError
|
||||
}
|
||||
// generic identity provider has higher priority
|
||||
for _, providerOptions := range p.authOptions.OAuthOptions.IdentityProviders {
|
||||
// the admin account in kubesphere has the highest priority
|
||||
if username == constants.AdminUserName {
|
||||
break
|
||||
}
|
||||
if genericProvider, _ := identityprovider.GetGenericProvider(providerOptions.Name); genericProvider != nil {
|
||||
authenticated, err := genericProvider.Authenticate(username, password)
|
||||
if err != nil {
|
||||
if errors.IsUnauthorized(err) {
|
||||
continue
|
||||
}
|
||||
return nil, providerOptions.Name, err
|
||||
}
|
||||
linkedAccount, err := p.userGetter.findMappedUser(providerOptions.Name, authenticated.GetUserID())
|
||||
if err != nil {
|
||||
return nil, providerOptions.Name, err
|
||||
}
|
||||
// using this method requires you to manually provision users.
|
||||
if providerOptions.MappingMethod == oauth.MappingMethodLookup && linkedAccount == nil {
|
||||
continue
|
||||
}
|
||||
// the user will automatically create and mapping when login successful.
|
||||
if linkedAccount == nil && providerOptions.MappingMethod == oauth.MappingMethodAuto {
|
||||
if !providerOptions.DisableLoginConfirmation {
|
||||
return preRegistrationUser(providerOptions.Name, authenticated), providerOptions.Name, nil
|
||||
}
|
||||
|
||||
linkedAccount, err = p.ksClient.IamV1alpha2().Users().Create(context.Background(), mappedUser(providerOptions.Name, authenticated), metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return nil, providerOptions.Name, err
|
||||
}
|
||||
}
|
||||
if linkedAccount != nil {
|
||||
return &authuser.DefaultInfo{Name: linkedAccount.GetName()}, providerOptions.Name, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// kubesphere account
|
||||
user, err := p.userGetter.findUser(username)
|
||||
if err != nil {
|
||||
// ignore not found error
|
||||
if !errors.IsNotFound(err) {
|
||||
klog.Error(err)
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
// check user status
|
||||
if user != nil && (user.Status.State == nil || *user.Status.State != iamv1alpha2.UserActive) {
|
||||
if user.Status.State != nil && *user.Status.State == iamv1alpha2.UserAuthLimitExceeded {
|
||||
klog.Errorf("%s, username: %s", RateLimitExceededError, username)
|
||||
return nil, "", RateLimitExceededError
|
||||
} else {
|
||||
// state not active
|
||||
klog.Errorf("%s, username: %s", AccountIsNotActiveError, username)
|
||||
return nil, "", AccountIsNotActiveError
|
||||
}
|
||||
}
|
||||
|
||||
// if the password is not empty, means that the password has been reset, even if the user was mapping from IDP
|
||||
if user != nil && user.Spec.EncryptedPassword != "" {
|
||||
if err = PasswordVerify(user.Spec.EncryptedPassword, password); err != nil {
|
||||
klog.Error(err)
|
||||
return nil, "", err
|
||||
}
|
||||
u := &authuser.DefaultInfo{
|
||||
Name: user.Name,
|
||||
}
|
||||
// check if the password is initialized
|
||||
if uninitialized := user.Annotations[iamv1alpha2.UninitializedAnnotation]; uninitialized != "" {
|
||||
u.Extra = map[string][]string{
|
||||
iamv1alpha2.ExtraUninitialized: {uninitialized},
|
||||
}
|
||||
}
|
||||
return u, "", nil
|
||||
}
|
||||
|
||||
return nil, "", IncorrectPasswordError
|
||||
}
|
||||
|
||||
func PasswordVerify(encryptedPassword, password string) error {
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(encryptedPassword), []byte(password)); err != nil {
|
||||
return IncorrectPasswordError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -23,99 +23,89 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
|
||||
"k8s.io/klog"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/token"
|
||||
"kubesphere.io/kubesphere/pkg/simple/client/cache"
|
||||
)
|
||||
|
||||
// TokenManagementInterface Cache issued token, support revocation of tokens after issuance
|
||||
type TokenManagementInterface interface {
|
||||
// Verify verifies a token, and return a User if it's a valid token, otherwise return error
|
||||
Verify(token string) (user.Info, error)
|
||||
// IssueTo issues a token a User, return error if issuing process failed
|
||||
IssueTo(user user.Info) (*oauth.Token, error)
|
||||
// Verify the given token and returns token.VerifiedResponse
|
||||
Verify(token string) (*token.VerifiedResponse, error)
|
||||
// IssueTo issue a token for the specified user
|
||||
IssueTo(request *token.IssueRequest) (string, error)
|
||||
// Revoke revoke the specified token
|
||||
Revoke(token string) error
|
||||
// RevokeAllUserTokens revoke all user tokens
|
||||
RevokeAllUserTokens(username string) error
|
||||
// Keys hold encryption and signing keys.
|
||||
Keys() *token.Keys
|
||||
}
|
||||
|
||||
type tokenOperator struct {
|
||||
issuer token.Issuer
|
||||
options *authoptions.AuthenticationOptions
|
||||
options *authentication.Options
|
||||
cache cache.Interface
|
||||
}
|
||||
|
||||
func NewTokenOperator(cache cache.Interface, options *authoptions.AuthenticationOptions) TokenManagementInterface {
|
||||
func (t tokenOperator) Revoke(token string) error {
|
||||
pattern := fmt.Sprintf("kubesphere:user:*:token:%s", token)
|
||||
if keys, err := t.cache.Keys(pattern); err != nil {
|
||||
klog.Error(err)
|
||||
return err
|
||||
} else if len(keys) > 0 {
|
||||
if err := t.cache.Del(keys...); err != nil {
|
||||
klog.Error(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewTokenOperator(cache cache.Interface, issuer token.Issuer, options *authentication.Options) TokenManagementInterface {
|
||||
operator := &tokenOperator{
|
||||
issuer: token.NewTokenIssuer(options.JwtSecret, options.MaximumClockSkew),
|
||||
issuer: issuer,
|
||||
options: options,
|
||||
cache: cache,
|
||||
}
|
||||
return operator
|
||||
}
|
||||
|
||||
func (t tokenOperator) Verify(tokenStr string) (user.Info, error) {
|
||||
authenticated, tokenType, err := t.issuer.Verify(tokenStr)
|
||||
func (t *tokenOperator) Verify(tokenStr string) (*token.VerifiedResponse, error) {
|
||||
response, err := t.issuer.Verify(tokenStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t.options.OAuthOptions.AccessTokenMaxAge == 0 ||
|
||||
tokenType == token.StaticToken {
|
||||
return authenticated, nil
|
||||
response.TokenType == token.StaticToken {
|
||||
return response, nil
|
||||
}
|
||||
if err := t.tokenCacheValidate(authenticated.GetName(), tokenStr); err != nil {
|
||||
if err := t.tokenCacheValidate(response.User.GetName(), tokenStr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return authenticated, nil
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (t tokenOperator) IssueTo(user user.Info) (*oauth.Token, error) {
|
||||
accessTokenExpiresIn := t.options.OAuthOptions.AccessTokenMaxAge
|
||||
refreshTokenExpiresIn := accessTokenExpiresIn + t.options.OAuthOptions.AccessTokenInactivityTimeout
|
||||
|
||||
accessToken, err := t.issuer.IssueTo(user, token.AccessToken, accessTokenExpiresIn)
|
||||
func (t *tokenOperator) IssueTo(request *token.IssueRequest) (string, error) {
|
||||
tokenStr, err := t.issuer.IssueTo(request)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
refreshToken, err := t.issuer.IssueTo(user, token.RefreshToken, refreshTokenExpiresIn)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &oauth.Token{
|
||||
AccessToken: accessToken,
|
||||
TokenType: "Bearer",
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: int(accessTokenExpiresIn.Seconds()),
|
||||
}
|
||||
|
||||
if !t.options.MultipleLogin {
|
||||
if err = t.RevokeAllUserTokens(user.GetName()); err != nil {
|
||||
if request.ExpiresIn > 0 {
|
||||
if err = t.cacheToken(request.User.GetName(), tokenStr, request.ExpiresIn); err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if accessTokenExpiresIn > 0 {
|
||||
if err = t.cacheToken(user.GetName(), accessToken, accessTokenExpiresIn); err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
if err = t.cacheToken(user.GetName(), refreshToken, refreshTokenExpiresIn); err != nil {
|
||||
klog.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
return tokenStr, nil
|
||||
}
|
||||
|
||||
func (t tokenOperator) RevokeAllUserTokens(username string) error {
|
||||
// RevokeAllUserTokens revoke all user tokens in the cache
|
||||
func (t *tokenOperator) RevokeAllUserTokens(username string) error {
|
||||
pattern := fmt.Sprintf("kubesphere:user:%s:token:*", username)
|
||||
if keys, err := t.cache.Keys(pattern); err != nil {
|
||||
klog.Error(err)
|
||||
@@ -129,7 +119,12 @@ func (t tokenOperator) RevokeAllUserTokens(username string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t tokenOperator) tokenCacheValidate(username, token string) error {
|
||||
func (t *tokenOperator) Keys() *token.Keys {
|
||||
return t.issuer.Keys()
|
||||
}
|
||||
|
||||
// tokenCacheValidate verify that the token is in the cache
|
||||
func (t *tokenOperator) tokenCacheValidate(username, token string) error {
|
||||
key := fmt.Sprintf("kubesphere:user:%s:token:%s", username, token)
|
||||
if exist, err := t.cache.Exists(key); err != nil {
|
||||
return err
|
||||
@@ -141,7 +136,8 @@ func (t tokenOperator) tokenCacheValidate(username, token string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t tokenOperator) cacheToken(username, token string, duration time.Duration) error {
|
||||
// cacheToken cache the token for a period of time
|
||||
func (t *tokenOperator) cacheToken(username, token string, duration time.Duration) error {
|
||||
key := fmt.Sprintf("kubesphere:user:%s:token:%s", username, token)
|
||||
if err := t.cache.Set(key, token, duration); err != nil {
|
||||
klog.Error(err)
|
||||
|
||||
@@ -19,13 +19,14 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/klog"
|
||||
|
||||
iamv1alpha2 "kubesphere.io/api/iam/v1alpha2"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/api"
|
||||
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/query"
|
||||
kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned"
|
||||
"kubesphere.io/kubesphere/pkg/models/auth"
|
||||
@@ -43,7 +44,7 @@ type IdentityManagementInterface interface {
|
||||
PasswordVerify(username string, password string) error
|
||||
}
|
||||
|
||||
func NewOperator(ksClient kubesphere.Interface, userGetter resources.Interface, loginRecordGetter resources.Interface, options *authoptions.AuthenticationOptions) IdentityManagementInterface {
|
||||
func NewOperator(ksClient kubesphere.Interface, userGetter resources.Interface, loginRecordGetter resources.Interface, options *authentication.Options) IdentityManagementInterface {
|
||||
im := &imOperator{
|
||||
ksClient: ksClient,
|
||||
userGetter: userGetter,
|
||||
@@ -57,7 +58,7 @@ type imOperator struct {
|
||||
ksClient kubesphere.Interface
|
||||
userGetter resources.Interface
|
||||
loginRecordGetter resources.Interface
|
||||
options *authoptions.AuthenticationOptions
|
||||
options *authentication.Options
|
||||
}
|
||||
|
||||
// UpdateUser returns user information after update.
|
||||
|
||||
Reference in New Issue
Block a user