feature: verify user with sms code

This commit is contained in:
Qiu Jian
2022-08-19 09:31:31 +08:00
parent 354b9c49dc
commit be2c26ad99
13 changed files with 197 additions and 35 deletions

View File

@@ -355,9 +355,14 @@ func (h *AuthHandlers) doCredentialLogin(ctx context.Context, req *http.Request,
idpId, _ := body.GetString("idp_id")
redirectUri := getSsoCallbackUrl(ctx, req, idpId)
token, err = processSsoLoginData(body, cliIp, redirectUri)
if err != nil {
return nil, errors.Wrap(err, "processSsoLoginData")
}
// if err != nil {
// return nil, errors.Wrap(err, "processSsoLoginData")
// }
} else if body.Contains("verify_code") { // verify by mobile
verifyCode, _ := body.GetString("verify_code")
uid, _ := body.GetString("uid")
contactType, _ := body.GetString("contact_type")
token, err = processVerifyLoginData(uid, contactType, verifyCode, cliIp)
} else {
return nil, httperrors.NewInputParameterError("missing credential")
}

View File

@@ -0,0 +1,24 @@
// Copyright 2019 Yunion
//
// 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 handler
import (
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/auth"
)
func processVerifyLoginData(uid, contactType, verifyCode string, cliIp string) (mcclient.TokenCredential, error) {
return auth.Client().AuthenticateVerify(uid, contactType, verifyCode, "", "", "", cliIp)
}

View File

@@ -32,8 +32,6 @@ type SRegionsReponse struct {
Regions []string `json:"regions,allowempty"`
Domains []string `json:"domains,allowempty"`
Captcha bool `json:"captcha,allowempty"`
Idps []SIdp `json:"idps,allowempty"`
ReturnFullDomains bool `json:"return_full_domains"`

View File

@@ -37,6 +37,7 @@ const (
AUTH_METHOD_SAML = "saml"
AUTH_METHOD_OIDC = "oidc"
AUTH_METHOD_OAuth2 = "oauth2"
AUTH_METHOD_VERIFY = "verify"
// AUTH_METHOD_ID_PASSWORD = 1
// AUTH_METHOD_ID_TOKEN = 2

View File

@@ -43,6 +43,9 @@ type ReceiverCreateInput struct {
// description: enabled contact types for user
// example: {"email", "mobile", "feishu", "dingtalk", "workwx"}
EnabledContactTypes []string `json:"enabled_contact_types"`
// force verified if admin create the records
ForceVerified bool `json:"force_verified"`
}
type SInternationalMobile struct {
@@ -129,6 +132,8 @@ type ReceiverUpdateInput struct {
// description: enabled contacts for user
// example: {"email", "mobile", "feishu", "dingtalk", "workwx"}
EnabledContactTypes []string `json:"enabled_contact_types"`
ForceVerified bool `json:"force_verified"`
}
type ReceiverTriggerVerifyInput struct {

View File

@@ -26,12 +26,14 @@ import (
"yunion.io/x/sqlchemy"
api "yunion.io/x/onecloud/pkg/apis/identity"
notify_api "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/keystone/driver"
"yunion.io/x/onecloud/pkg/keystone/models"
"yunion.io/x/onecloud/pkg/keystone/options"
"yunion.io/x/onecloud/pkg/keystone/saml"
"yunion.io/x/onecloud/pkg/mcclient"
notify_modules "yunion.io/x/onecloud/pkg/mcclient/modules/notify"
"yunion.io/x/onecloud/pkg/util/logclient"
"yunion.io/x/onecloud/pkg/util/s3auth"
)
@@ -391,6 +393,26 @@ func authUserByAccessKeyV3(ctx context.Context, input mcclient.SAuthenticationIn
return usrExt, credential.ProjectId, aksk, nil
}
func authUserByVerify(ctx context.Context, input mcclient.SAuthenticationInputV3) (*api.SUserExtended, error) {
extUser, err := models.UserManager.FetchUserExtended(input.Auth.Identity.Verify.Uid, "", "", "")
if err != nil {
return nil, errors.Wrap(err, "FetchUserExtended")
}
s, err := GetDefaulAdminSession(ctx, "")
if err != nil {
return nil, errors.Wrap(err, "GetDefaultAdminSession")
}
verifyInput := notify_api.ReceiverVerifyInput{
ContactType: input.Auth.Identity.Verify.ContactType,
Token: input.Auth.Identity.Verify.VerifyCode,
}
_, err = notify_modules.NotifyReceiver.PerformAction(s, extUser.Id, "verify", jsonutils.Marshal(verifyInput))
if err != nil {
return nil, errors.Wrap(err, "Verify")
}
return extUser, nil
}
// +onecloud:swagger-gen-route-method=POST
// +onecloud:swagger-gen-route-path=/v3/auth/tokens
// +onecloud:swagger-gen-route-tag=authentication
@@ -445,6 +467,11 @@ func AuthenticateV3(ctx context.Context, input mcclient.SAuthenticationInputV3)
if err != nil {
return nil, errors.Wrap(err, "authUserByOAuth2")
}
case api.AUTH_METHOD_VERIFY:
user, err = authUserByVerify(ctx, input)
if err != nil {
return nil, errors.Wrap(err, "authUserByVerify")
}
default:
// auth by other methods, e.g. password , etc...
user, err = authUserByIdentityV3(ctx, input)

View File

@@ -0,0 +1,26 @@
// Copyright 2019 Yunion
//
// 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 tokens
import (
"context"
"yunion.io/x/onecloud/pkg/keystone/models"
"yunion.io/x/onecloud/pkg/mcclient"
)
func GetDefaulAdminSession(ctx context.Context, region string) (*mcclient.ClientSession, error) {
return models.GetDefaultClientSession(ctx, GetDefaultAdminCredToken(), region), nil
}

View File

@@ -17,11 +17,10 @@ package util
import (
"context"
"yunion.io/x/onecloud/pkg/keystone/models"
"yunion.io/x/onecloud/pkg/keystone/tokens"
"yunion.io/x/onecloud/pkg/mcclient"
)
func GetDefaulAdminSession(ctx context.Context, region string) (*mcclient.ClientSession, error) {
return models.GetDefaultClientSession(ctx, tokens.GetDefaultAdminCredToken(), region), nil
return tokens.GetDefaulAdminSession(ctx, region)
}

View File

@@ -83,6 +83,7 @@ type SAuthenticationIdentity struct {
// | saml | 作为SAML 2.0 SP通过IDP认证 |
// | oidc | 作为OpenID Connect/OAuth2 Client认证 |
// | oauth2 | OAuth2认证 |
// | verify | 手机短信或邮箱认证 |
//
Methods []string `json:"methods,omitempty"`
// 当认证方式为password时通过该字段提供密码认证信息
@@ -127,7 +128,12 @@ type SAuthenticationIdentity struct {
} `json:"oidc_auth,omitempty"`
OAuth2 struct {
Code string `json:"code,omitempty"`
}
} `json:"oauth2,omitempty"`
Verify struct {
Uid string `json:"uid,omitempty"`
VerifyCode string `json:"verify_code,omitempty"`
ContactType string `json:"contact_type,omitempty"`
} `json:"mobile,omitempty"`
}
type SAuthenticationInputV3 struct {

51
pkg/mcclient/verify.go Normal file
View File

@@ -0,0 +1,51 @@
// Copyright 2019 Yunion
//
// 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 mcclient
import (
api "yunion.io/x/onecloud/pkg/apis/identity"
"yunion.io/x/onecloud/pkg/httperrors"
)
func (this *Client) AuthenticateVerify(uid, contactType, verifyCode string, projectId, projectName, projectDomain string, cliIp string) (TokenCredential, error) {
aCtx := SAuthContext{
// OpenID Connect auth must comes from Web
Source: AuthSourceWeb,
Ip: cliIp,
}
return this.authenticateVerifyWithContext(uid, contactType, verifyCode, projectId, projectName, projectDomain, aCtx)
}
func (this *Client) authenticateVerifyWithContext(uid, contactType, verifyCode string, projectId, projectName, projectDomain string, aCtx SAuthContext) (TokenCredential, error) {
if this.AuthVersion() != "v3" {
return nil, httperrors.ErrNotSupported
}
input := SAuthenticationInputV3{}
input.Auth.Identity.Methods = []string{api.AUTH_METHOD_VERIFY}
input.Auth.Identity.Verify.Uid = uid
input.Auth.Identity.Verify.ContactType = contactType
input.Auth.Identity.Verify.VerifyCode = verifyCode
if len(projectId) > 0 {
input.Auth.Scope.Project.Id = projectId
}
if len(projectName) > 0 {
input.Auth.Scope.Project.Name = projectName
if len(projectDomain) > 0 {
input.Auth.Scope.Project.Domain.Name = projectDomain
}
}
input.Auth.Context = aCtx
return this._authV3Input(input)
}

View File

@@ -845,13 +845,15 @@ func (r *SReceiver) CustomizeCreate(ctx context.Context, userCred mcclient.Token
}
// 需求:管理后台新建的联系人,手机号和邮箱无需进行校验
// 方案:检查请求者对于创建联系人 是否具有system scope
allowScope, _ := policy.PolicyManager.AllowScope(userCred, api.SERVICE_TYPE, ReceiverManager.KeywordPlural(), policy.PolicyActionCreate)
if allowScope == rbacutils.ScopeSystem {
if r.EnabledEmail.Bool() {
r.VerifiedEmail = tristate.True
}
if r.EnabledMobile.Bool() {
r.VerifiedMobile = tristate.True
if input.ForceVerified {
allowScope, _ := policy.PolicyManager.AllowScope(userCred, api.SERVICE_TYPE, ReceiverManager.KeywordPlural(), policy.PolicyActionCreate)
if allowScope == rbacutils.ScopeSystem {
if r.EnabledEmail.Bool() {
r.VerifiedEmail = tristate.True
}
if r.EnabledMobile.Bool() {
r.VerifiedMobile = tristate.True
}
}
}
return nil
@@ -914,21 +916,23 @@ func (r *SReceiver) PreUpdate(ctx context.Context, userCred mcclient.TokenCreden
log.Errorf("PushCache: %v", err)
}
// 管理后台修改联系人,如果修改或者启用手机号和邮箱,无需进行校验
allowScope, _ := policy.PolicyManager.AllowScope(userCred, api.SERVICE_TYPE, ReceiverManager.KeywordPlural(), policy.PolicyActionCreate)
if allowScope == rbacutils.ScopeSystem {
// 修改并启用
if len(input.Email) != 0 && input.Email != r.Email && r.EnabledEmail.Bool() {
r.VerifiedEmail = tristate.True
}
if len(mobile) != 0 && mobile != r.Mobile && r.EnabledMobile.Bool() {
r.VerifiedMobile = tristate.True
}
// 从禁用变启用
if !originEmailEnable.Bool() && r.EnabledEmail.Bool() {
r.VerifiedEmail = tristate.True
}
if !originMobileEnable.Bool() && r.EnabledMobile.Bool() {
r.VerifiedMobile = tristate.True
if input.ForceVerified {
allowScope, _ := policy.PolicyManager.AllowScope(userCred, api.SERVICE_TYPE, ReceiverManager.KeywordPlural(), policy.PolicyActionCreate)
if allowScope == rbacutils.ScopeSystem {
// 修改并启用
if len(input.Email) != 0 && input.Email != r.Email && r.EnabledEmail.Bool() {
r.VerifiedEmail = tristate.True
}
if len(mobile) != 0 && mobile != r.Mobile && r.EnabledMobile.Bool() {
r.VerifiedMobile = tristate.True
}
// 从禁用变启用
if !originEmailEnable.Bool() && r.EnabledEmail.Bool() {
r.VerifiedEmail = tristate.True
}
if !originMobileEnable.Bool() && r.EnabledMobile.Bool() {
r.VerifiedMobile = tristate.True
}
}
}
r.Mobile = mobile
@@ -947,7 +951,6 @@ func (r *SReceiver) PostUpdate(ctx context.Context, userCred mcclient.TokenCrede
if err != nil {
log.Errorf("unable to StartSubcontactPullTask: %v", err)
}
}
func (r *SReceiver) StartSubcontactPullTask(ctx context.Context, userCred mcclient.TokenCredential, params *jsonutils.JSONDict, parentTaskId string) error {
@@ -1056,11 +1059,11 @@ func (r *SReceiver) PerformTriggerVerify(ctx context.Context, userCred mcclient.
return nil, r.StartSubcontactPullTask(ctx, userCred, params, "")
}
_, err := VerificationManager.Create(ctx, r.Id, input.ContactType)
if err == ErrVerifyFrequently {
/*if err == ErrVerifyFrequently {
return nil, httperrors.NewForbiddenError("Send verify message too frequently, please try again later")
}
}*/
if err != nil {
return nil, err
return nil, errors.Wrap(err, "VerifyManager.Create")
}
params := jsonutils.Marshal(input).(*jsonutils.JSONDict)

View File

@@ -24,6 +24,7 @@ import (
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/notify/options"
)
@@ -54,7 +55,7 @@ type SVerification struct {
Token string `width:"200" nullable:"false"`
}
var ErrVerifyFrequently = errors.Error("Send validation messages too frequently")
var ErrVerifyFrequently = errors.Wrap(httperrors.ErrTooManyRequests, "Send validation messages too frequently")
func (vm *SVerificationManager) generateVerifyToken() string {
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))

View File

@@ -27,6 +27,8 @@ import (
apis "yunion.io/x/onecloud/pkg/apis/notify"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/mcclient/modules/identity"
"yunion.io/x/onecloud/pkg/notify"
"yunion.io/x/onecloud/pkg/notify/models"
"yunion.io/x/onecloud/pkg/util/logclient"
@@ -103,6 +105,20 @@ func (self *SubcontactPullTask) OnInit(ctx context.Context, obj db.IStandaloneMo
self.taskFailed(ctx, receiver, reason)
return
}
// sync email and mobile to keystone
s := auth.GetSession(ctx, self.UserCred, "")
mobile := receiver.Mobile
if strings.HasPrefix(mobile, "+86 ") {
mobile = strings.TrimSpace(mobile[4:])
}
params := map[string]string{
"email": receiver.Email,
"mobile": receiver.Mobile,
}
_, err = identity.UsersV3.Update(s, receiver.Id, jsonutils.Marshal(params))
if err != nil {
log.Errorf("update user email and mobile fail %s", err)
}
// success
receiver.SetStatus(self.UserCred, apis.RECEIVER_STATUS_READY, "")
logclient.AddActionLogWithContext(ctx, receiver, logclient.ACT_PULL_SUBCONTACT, "", self.UserCred, true)