From dc28a0917a5f432cd311f3fbbe273717d5386e0e Mon Sep 17 00:00:00 2001 From: Wenhao Zhou <34303854+zhou1203@users.noreply.github.com> Date: Mon, 27 Feb 2023 18:35:35 +0800 Subject: [PATCH] Add api for identity provider login (#5534) * add api for ldap login * update ldap login to identity provider login for more flexible login type Signed-off-by: wenhaozhou * update PasswordAuthenticate Signed-off-by: wenhaozhou * add test case Signed-off-by: wenhaozhou * update api path Signed-off-by: wenhaozhou * make goimports and add annotations Signed-off-by: wenhaozhou * update func names & add annotations Signed-off-by: wenhaozhou --------- Signed-off-by: wenhaozhou --- .../authenticators/basic/basic.go | 4 +- pkg/kapis/oauth/handler.go | 24 ++-- pkg/kapis/oauth/register.go | 23 ++-- pkg/models/auth/authenticator.go | 12 +- pkg/models/auth/password.go | 105 ++++++++++-------- pkg/models/auth/password_test.go | 60 +++++++++- .../kubesphere.io/api/iam/v1alpha2/types.go | 8 +- 7 files changed, 153 insertions(+), 83 deletions(-) diff --git a/pkg/apiserver/authentication/authenticators/basic/basic.go b/pkg/apiserver/authentication/authenticators/basic/basic.go index 2daec6e289..af5ddcf921 100644 --- a/pkg/apiserver/authentication/authenticators/basic/basic.go +++ b/pkg/apiserver/authentication/authenticators/basic/basic.go @@ -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(ctx, 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 @@ -57,7 +57,7 @@ func (t *basicAuthenticator) AuthenticatePassword(ctx context.Context, username, sourceIP = requestInfo.SourceIP userAgent = requestInfo.UserAgent } - if err := t.loginRecorder.RecordLogin(username, iamv1alpha2.BasicAuth, provider, sourceIP, userAgent, err); err != nil { + if err := t.loginRecorder.RecordLogin(username, iamv1alpha2.Password, provider, sourceIP, userAgent, err); err != nil { klog.Errorf("Failed to record unsuccessful login attempt for user %s, error: %v", username, err) } } diff --git a/pkg/kapis/oauth/handler.go b/pkg/kapis/oauth/handler.go index 0d653520cf..035dcb7cc7 100644 --- a/pkg/kapis/oauth/handler.go +++ b/pkg/kapis/oauth/handler.go @@ -355,16 +355,6 @@ func (h *handler) oauthCallback(req *restful.Request, response *restful.Response response.WriteEntity(result) } -func (h *handler) login(request *restful.Request, response *restful.Response) { - var loginRequest LoginRequest - err := request.ReadEntity(&loginRequest) - if err != nil { - api.HandleBadRequest(response, request, err) - return - } - h.passwordGrant(loginRequest.Username, loginRequest.Password, request, response) -} - // 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. @@ -406,7 +396,7 @@ func (h *handler) token(req *restful.Request, response *restful.Response) { case grantTypePassword: username, _ := req.BodyParameter("username") password, _ := req.BodyParameter("password") - h.passwordGrant(username, password, req, response) + h.passwordGrant("", username, password, req, response) return case grantTypeRefreshToken: h.refreshTokenGrant(req, response) @@ -427,8 +417,8 @@ func (h *handler) token(req *restful.Request, response *restful.Response) { // 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(req.Request.Context(), username, password) +func (h *handler) passwordGrant(provider, username string, password string, req *restful.Request, response *restful.Response) { + authenticated, provider, err := h.passwordAuthenticator.Authenticate(req.Request.Context(), provider, username, password) if err != nil { switch err { case auth.AccountIsNotActiveError: @@ -682,3 +672,11 @@ func (h *handler) userinfo(req *restful.Request, response *restful.Response) { } response.WriteEntity(result) } + +func (h *handler) loginByIdentityProvider(req *restful.Request, response *restful.Response) { + username, _ := req.BodyParameter("username") + password, _ := req.BodyParameter("password") + idp := req.PathParameter("identiyprovider") + + h.passwordGrant(idp, username, password, req, response) +} diff --git a/pkg/kapis/oauth/register.go b/pkg/kapis/oauth/register.go index f03fe3bad5..e52497e7f4 100644 --- a/pkg/kapis/oauth/register.go +++ b/pkg/kapis/oauth/register.go @@ -158,22 +158,17 @@ func AddToContainer(c *restful.Container, im im.IdentityManagementInterface, Returns(http.StatusOK, http.StatusText(http.StatusOK), ""). Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag})) - c.Add(ws) - - // legacy auth API - legacy := &restful.WebService{} - legacy.Path("/kapis/iam.kubesphere.io/v1alpha2/login"). - Consumes(restful.MIME_JSON). - Produces(restful.MIME_JSON) - legacy.Route(legacy.POST(""). - 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{}). - Returns(http.StatusOK, api.StatusOK, oauth.Token{}). + ws.Route(ws.POST("/login/{identityprovider}"). + Consumes(contentTypeFormData). + Doc("Login by identity provider user"). + Param(ws.PathParameter("identityprovider", "The identity provider name")). + Param(ws.FormParameter("username", "The username of the relevant user in ldap")). + Param(ws.FormParameter("password", "The password of the relevant user in ldap")). + To(handler.loginByIdentityProvider). + Returns(http.StatusOK, http.StatusText(http.StatusOK), oauth.Token{}). Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag})) - c.Add(legacy) + c.Add(ws) return nil } diff --git a/pkg/models/auth/authenticator.go b/pkg/models/auth/authenticator.go index fa8762f0ef..14b245873a 100644 --- a/pkg/models/auth/authenticator.go +++ b/pkg/models/auth/authenticator.go @@ -46,11 +46,19 @@ var ( ) // PasswordAuthenticator is an interface implemented by authenticator which take a -// username and password. +// username ,password and provider. provider refers to the identity provider`s name, +// if the provider is empty, authenticate from kubesphere account. Note that implement this +// interface you should also obey the error specification errors.Error defined at package +// "k8s.io/apimachinery/pkg/api", and restful.ServerError defined at package +// "github.com/emicklei/go-restful/v3", or the server cannot handle error correctly. type PasswordAuthenticator interface { - Authenticate(ctx context.Context, username, password string) (authuser.Info, string, error) + Authenticate(ctx context.Context, provider, username, password string) (authuser.Info, string, error) } +// OAuthAuthenticator authenticate users by OAuth 2.0 Authorization Framework. Note that implement this +// interface you should also obey the error specification errors.Error defined at package +// "k8s.io/apimachinery/pkg/api", and restful.ServerError defined at package +// "github.com/emicklei/go-restful/v3", or the server cannot handle error correctly. type OAuthAuthenticator interface { Authenticate(ctx context.Context, provider string, req *http.Request) (authuser.Info, string, error) } diff --git a/pkg/models/auth/password.go b/pkg/models/auth/password.go index ad73ae5590..8ca3389433 100644 --- a/pkg/models/auth/password.go +++ b/pkg/models/auth/password.go @@ -21,22 +21,19 @@ 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" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" authuser "k8s.io/apiserver/pkg/authentication/user" "k8s.io/klog/v2" + iamv1alpha2 "kubesphere.io/api/iam/v1alpha2" + "kubesphere.io/kubesphere/pkg/apiserver/authentication" "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider" "kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth" + kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned" iamv1alpha2listers "kubesphere.io/kubesphere/pkg/client/listers/iam/v1alpha2" - "kubesphere.io/kubesphere/pkg/constants" ) type passwordAuthenticator struct { @@ -56,52 +53,19 @@ func NewPasswordAuthenticator(ksClient kubesphere.Interface, return passwordAuthenticator } -func (p *passwordAuthenticator) Authenticate(_ context.Context, username, password string) (authuser.Info, string, error) { +func (p *passwordAuthenticator) Authenticate(_ context.Context, provider, 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 && !errors.IsNotFound(err) { - klog.Error(err) - 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 - } - } + if provider != "" { + return p.authByProvider(provider, username, password) } + return p.authByKubeSphere(username, password) +} - // kubesphere account +// authByKubeSphere authenticate by the kubesphere user +func (p *passwordAuthenticator) authByKubeSphere(username, password string) (authuser.Info, string, error) { user, err := p.userGetter.findUser(username) if err != nil { // ignore not found error @@ -145,6 +109,53 @@ func (p *passwordAuthenticator) Authenticate(_ context.Context, username, passwo return nil, "", IncorrectPasswordError } +// authByProvider authenticate by the third-party identity provider user +func (p *passwordAuthenticator) authByProvider(provider, username, password string) (authuser.Info, string, error) { + providerOptions, err := p.authOptions.OAuthOptions.IdentityProviderOptions(provider) + if err != nil { + klog.Error(err) + return nil, "", err + } + genericProvider, err := identityprovider.GetGenericProvider(providerOptions.Name) + if err != nil { + klog.Error(err) + return nil, "", err + } + authenticated, err := genericProvider.Authenticate(username, password) + if err != nil { + klog.Error(err) + if errors.IsUnauthorized(err) { + return nil, "", IncorrectPasswordError + } + return nil, "", err + } + linkedAccount, err := p.userGetter.findMappedUser(providerOptions.Name, authenticated.GetUserID()) + + if err != nil && !errors.IsNotFound(err) { + klog.Error(err) + return nil, "", err + } + + if linkedAccount != nil { + return &authuser.DefaultInfo{Name: linkedAccount.Name}, provider, nil + } + + // the user will automatically create and mapping when login successful. + if 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 { + klog.Error(err) + return nil, "", err + } + return &authuser.DefaultInfo{Name: linkedAccount.Name}, provider, nil + } + + return nil, "", err +} + func PasswordVerify(encryptedPassword, password string) error { if err := bcrypt.CompareHashAndPassword([]byte(encryptedPassword), []byte(password)); err != nil { return IncorrectPasswordError diff --git a/pkg/models/auth/password_test.go b/pkg/models/auth/password_test.go index b3bd099480..22b2925a3f 100644 --- a/pkg/models/auth/password_test.go +++ b/pkg/models/auth/password_test.go @@ -79,6 +79,38 @@ func Test_passwordAuthenticator_Authenticate(t *testing.T) { }, }, }, + { + Name: "fakepwd2", + MappingMethod: "auto", + Type: "fakePasswordProvider", + DisableLoginConfirmation: true, + Provider: oauth.DynamicOptions{ + "identities": map[string]interface{}{ + "user5": map[string]string{ + "uid": "100005", + "email": "user5@kubesphere.io", + "username": "user5", + "password": "password", + }, + }, + }, + }, + { + Name: "fakepwd3", + MappingMethod: "lookup", + Type: "fakePasswordProvider", + DisableLoginConfirmation: true, + Provider: oauth.DynamicOptions{ + "identities": map[string]interface{}{ + "user6": map[string]string{ + "uid": "100006", + "email": "user6@kubesphere.io", + "username": "user6", + "password": "password", + }, + }, + }, + }, }, }, } @@ -108,6 +140,7 @@ func Test_passwordAuthenticator_Authenticate(t *testing.T) { ctx context.Context username string password string + provider string } tests := []struct { name string @@ -124,6 +157,7 @@ func Test_passwordAuthenticator_Authenticate(t *testing.T) { ctx: context.Background(), username: "user1", password: "password", + provider: "fakepwd", }, want: &user.DefaultInfo{ Name: "user1", @@ -137,6 +171,7 @@ func Test_passwordAuthenticator_Authenticate(t *testing.T) { ctx: context.Background(), username: "user2", password: "password", + provider: "fakepwd", }, want: &user.DefaultInfo{ Name: "system:pre-registration", @@ -149,6 +184,29 @@ func Test_passwordAuthenticator_Authenticate(t *testing.T) { }, wantErr: false, }, + { + name: "Should create user and return", + passwordAuthenticator: authenticator, + args: args{ + ctx: context.Background(), + username: "user5", + password: "password", + provider: "fakepwd2", + }, + want: &user.DefaultInfo{Name: "user5"}, + wantErr: false, + }, + { + name: "Should return user not found", + passwordAuthenticator: authenticator, + args: args{ + ctx: context.Background(), + username: "user6", + password: "password", + provider: "fakepwd3", + }, + wantErr: true, + }, { name: "Should failed login", passwordAuthenticator: authenticator, @@ -176,7 +234,7 @@ func Test_passwordAuthenticator_Authenticate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := tt.passwordAuthenticator - got, _, err := p.Authenticate(tt.args.ctx, tt.args.username, tt.args.password) + got, _, err := p.Authenticate(tt.args.ctx, tt.args.provider, tt.args.username, tt.args.password) if (err != nil) != tt.wantErr { t.Errorf("passwordAuthenticator.Authenticate() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/staging/src/kubesphere.io/api/iam/v1alpha2/types.go b/staging/src/kubesphere.io/api/iam/v1alpha2/types.go index 520a77b831..1092ce53c1 100644 --- a/staging/src/kubesphere.io/api/iam/v1alpha2/types.go +++ b/staging/src/kubesphere.io/api/iam/v1alpha2/types.go @@ -336,7 +336,7 @@ type LoginRecord struct { } type LoginRecordSpec struct { - // Which authentication method used, BasicAuth/OAuth + // Which authentication method used, Password/OAuth/Token Type LoginType `json:"type"` // Provider of authentication, Ldap/Github etc. Provider string `json:"provider"` @@ -353,9 +353,9 @@ type LoginRecordSpec struct { type LoginType string const ( - BasicAuth LoginType = "Basic" - OAuth LoginType = "OAuth" - Token LoginType = "Token" + Password LoginType = "Password" + OAuth LoginType = "OAuth" + Token LoginType = "Token" ) // +kubebuilder:object:root=true