Merge pull request #3525 from wansir/oidc-protocol

support OIDC protocol
This commit is contained in:
KubeSphere CI Bot
2021-09-17 20:05:52 +08:00
committed by GitHub
41 changed files with 2195 additions and 815 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"},
},
},

View File

@@ -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
}

View File

@@ -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{

View File

@@ -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"))

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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{

View File

@@ -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"))

View 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)
}

View 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
}

View 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)
}
})
}
}

View File

@@ -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,

View File

@@ -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)
}
}

View File

@@ -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 dont 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.")

View File

@@ -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()
}

View 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)
})
}
}

View File

@@ -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,
}
}

View File

@@ -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")
}
}

View 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
}

View File

@@ -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
}

View File

@@ -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(),

View File

@@ -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: &notification.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",

View File

@@ -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

View File

@@ -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")

View File

@@ -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)
}

View File

@@ -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{}).

View File

@@ -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
View 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())
}

View 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
View 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
}

View File

@@ -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)

View File

@@ -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.