Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add api for identity provider login #5534

Merged
merged 7 commits into from
Feb 27, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions pkg/apiserver/authentication/authenticators/basic/basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@ 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
if requestInfo, ok := request.RequestInfoFrom(ctx); ok {
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)
}
}
Expand Down
24 changes: 11 additions & 13 deletions pkg/kapis/oauth/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,16 +361,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.
Expand Down Expand Up @@ -412,7 +402,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)
Expand All @@ -433,8 +423,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:
Expand Down Expand Up @@ -688,3 +678,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)
}
23 changes: 9 additions & 14 deletions pkg/kapis/oauth/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
5 changes: 3 additions & 2 deletions pkg/models/auth/authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@ var (
)

// PasswordAuthenticator is an interface implemented by authenticator which take a
// username and password.
// username and password. provider refers to the identity provider`s name,
// if the provider is empty, authenticate from kubesphere account
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)
}

type OAuthAuthenticator interface {
Expand Down
101 changes: 54 additions & 47 deletions pkg/models/auth/password.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.providerAuthenticate(provider, username, password)
}
return p.accountAuthenticate(username, password)
}

// kubesphere account
// accountAuthenticate authenticate the kubesphere account
func (p *passwordAuthenticator) accountAuthenticate(username, password string) (authuser.Info, string, error) {
user, err := p.userGetter.findUser(username)
if err != nil {
// ignore not found error
Expand Down Expand Up @@ -145,6 +109,49 @@ func (p *passwordAuthenticator) Authenticate(_ context.Context, username, passwo
return nil, "", IncorrectPasswordError
}

func (p *passwordAuthenticator) providerAuthenticate(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)
zhou1203 marked this conversation as resolved.
Show resolved Hide resolved
return nil, "", err
}
authenticated, err := genericProvider.Authenticate(username, password)
if err != nil {
klog.Error(err)
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
Expand Down
60 changes: 59 additions & 1 deletion pkg/models/auth/password_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
},
},
},
},
},
}
Expand Down Expand Up @@ -108,6 +140,7 @@ func Test_passwordAuthenticator_Authenticate(t *testing.T) {
ctx context.Context
username string
password string
provider string
}
tests := []struct {
name string
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions staging/src/kubesphere.io/api/iam/v1alpha2/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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
Expand Down