diff --git a/pkg/config/config.go b/pkg/config/config.go index 307c06569b..c4bed389e9 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -98,6 +98,10 @@ func SecretKey() string { return viper.GetString(setting.ENVSecretKey) } +func SsoTokenSecret() string { + return viper.GetString(setting.ENVSsoTokenSecret) +} + func AslanServiceAddress() string { s := AslanServiceInfo() return GetServiceAddress(s.Name, s.Port) diff --git a/pkg/microservice/user/config/consts.go b/pkg/microservice/user/config/consts.go index 9cbe5b8123..15e22b6678 100644 --- a/pkg/microservice/user/config/consts.go +++ b/pkg/microservice/user/config/consts.go @@ -21,10 +21,11 @@ import ( ) const ( - AppState = setting.ProductName + "user" - SystemIdentityType = "system" - OauthIdentityType = "oauth" - FeiShuEmailHost = "smtp.feishu.cn" + AppState = setting.ProductName + "user" + SystemIdentityType = "system" + SsoTokenIdentityType = "sso_token" + OauthIdentityType = "oauth" + FeiShuEmailHost = "smtp.feishu.cn" UserGroupCacheKeyFormat = "user_group_%s" ) diff --git a/pkg/microservice/user/core/handler/login/local.go b/pkg/microservice/user/core/handler/login/local.go index ef41348c91..1cc38d3962 100644 --- a/pkg/microservice/user/core/handler/login/local.go +++ b/pkg/microservice/user/core/handler/login/local.go @@ -17,8 +17,12 @@ limitations under the License. package login import ( + "net/http" + "net/url" + "github.com/gin-gonic/gin" + configbase "github.com/koderover/zadig/v2/pkg/config" "github.com/koderover/zadig/v2/pkg/microservice/user/core/service/login" internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler" ) @@ -40,6 +44,23 @@ func LocalLogin(c *gin.Context) { ctx.Resp, ctx.RespErr = resp, err } +func SsoTokenCallback(c *gin.Context) { + ctx := internalhandler.NewContext(c) + defer func() { internalhandler.JSONResponse(c, ctx) }() + + ssoToken := c.Query("token") + token, err := login.SsoTokenCallback(ssoToken, ctx.Logger) + if err != nil { + ctx.RespErr = err + return + } + + v := url.Values{} + v.Add("token", token) + redirectUrl := configbase.SystemAddress() + "/signin?" + v.Encode() + c.Redirect(http.StatusSeeOther, redirectUrl) +} + type getCaptchaResp struct { ID string `json:"id"` Content string `json:"content"` diff --git a/pkg/microservice/user/core/handler/router.go b/pkg/microservice/user/core/handler/router.go index 5897947891..01d33b40ec 100644 --- a/pkg/microservice/user/core/handler/router.go +++ b/pkg/microservice/user/core/handler/router.go @@ -78,6 +78,7 @@ func (*Router) Inject(router *gin.RouterGroup) { general.GET("/callback", login.Callback) general.GET("/login", login.Login) general.POST("/login", login.LocalLogin) + general.GET("/sso/token-callback", login.SsoTokenCallback) general.GET("/login-enabled", login.ThirdPartyLoginEnabled) general.GET("/captcha", login.GetCaptcha) general.GET("/logout", login.LocalLogout) diff --git a/pkg/microservice/user/core/service/login/local.go b/pkg/microservice/user/core/service/login/local.go index 365c743d62..0afe383030 100644 --- a/pkg/microservice/user/core/service/login/local.go +++ b/pkg/microservice/user/core/service/login/local.go @@ -17,10 +17,13 @@ limitations under the License. package login import ( + "errors" "fmt" "time" + "github.com/go-sql-driver/mysql" "github.com/golang-jwt/jwt" + "github.com/google/uuid" "github.com/mojocn/base64Captcha" "github.com/patrickmn/go-cache" "go.uber.org/zap" @@ -29,12 +32,14 @@ import ( configbase "github.com/koderover/zadig/v2/pkg/config" "github.com/koderover/zadig/v2/pkg/microservice/user/config" "github.com/koderover/zadig/v2/pkg/microservice/user/core/repository" + "github.com/koderover/zadig/v2/pkg/microservice/user/core/repository/models" "github.com/koderover/zadig/v2/pkg/microservice/user/core/repository/orm" "github.com/koderover/zadig/v2/pkg/microservice/user/core/service/common" "github.com/koderover/zadig/v2/pkg/setting" "github.com/koderover/zadig/v2/pkg/shared/client/aslan" "github.com/koderover/zadig/v2/pkg/shared/client/plutusvendor" zadigCache "github.com/koderover/zadig/v2/pkg/tool/cache" + "github.com/koderover/zadig/v2/pkg/tool/log" ) type LoginArgs struct { @@ -271,3 +276,133 @@ func GetCaptcha(logger *zap.SugaredLogger) (string, string, error) { } return id, b64s, nil } + +type SsoTokenClaims struct { + UserID string `json:"userId"` + Account string `json:"account"` + jwt.StandardClaims +} + +func SsoTokenCallback(tokenString string, logger *zap.SugaredLogger) (string, error) { + parsedToken, err := jwt.ParseWithClaims(tokenString, &SsoTokenClaims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(configbase.SsoTokenSecret()), nil + }) + if err != nil { + return "", err + } + + claims, ok := parsedToken.Claims.(*SsoTokenClaims) + if !ok || !parsedToken.Valid { + return "", fmt.Errorf("invalid token") + } + + log.Infof("[SsoTokenCallback]: userId: %s, account: %s", claims.UserID, claims.Account) + + var userLogin *models.UserLogin + identityType := config.SsoTokenIdentityType + user, err := orm.GetUser(claims.UserID, identityType, repository.DB) + if err != nil { + err = fmt.Errorf("SsoTokenLogin get user account:%s error, error msg:%s", claims.UserID, err.Error()) + log.Errorf(err.Error()) + return "", err + } + if user == nil { + uid, _ := uuid.NewUUID() + user := &models.User{ + Name: claims.Account, + Email: fmt.Sprintf("%s-%s@poc.example", claims.UserID, claims.Account), + IdentityType: identityType, + Account: claims.UserID, + UID: uid.String(), + } + + tx := repository.DB.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + err = orm.CreateUser(user, tx) + if err != nil { + tx.Rollback() + logger.Errorf("[SsoTokenCallback] CreateUser :%v error, error msg:%s", user, err.Error()) + var mysqlErr *mysql.MySQLError + if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 { + return "", fmt.Errorf("存在相同用户名") + } + return "", fmt.Errorf("创建用户失败, error: %s", err.Error()) + } + userLogin := &models.UserLogin{ + UID: user.UID, + LastLoginTime: time.Now().Unix(), + LoginId: user.Account, + LoginType: int(config.AccountLoginType), + } + err = orm.CreateUserLogin(userLogin, tx) + if err != nil { + tx.Rollback() + err = fmt.Errorf("[SsoTokenCallback] CreateUserLogin:%v error, error msg:%s", user, err.Error()) + log.Errorf(err.Error()) + return "", err + } + if tx.Commit().Error != nil { + return "", fmt.Errorf("创建用户登录信息失败, error: %s", tx.Commit().Error) + } + } else { + userLogin, err = orm.GetUserLogin(user.UID, claims.UserID, config.AccountLoginType, repository.DB) + if err != nil { + err = fmt.Errorf("SsoTokenLogin get user:%s user login not exist, error msg:%s", claims.UserID, err.Error()) + log.Errorf(err.Error()) + return "", err + } + } + + if userLogin != nil { + err = CheckSignature(userLogin.LastLoginTime, logger) + if err != nil { + return "", err + } + } + + userLogin.LastLoginTime = time.Now().Unix() + err = orm.UpdateUserLogin(userLogin.UID, userLogin, repository.DB) + if err != nil { + err = fmt.Errorf("[SsoTokenCallback] user:%s update user login info error, error msg:%s", claims.UserID, err.Error()) + log.Errorf(err.Error()) + return "", err + } + + systemSettings, err := aslan.New(configbase.AslanServiceAddress()).GetSystemSecurityAndPrivacySettings() + if err != nil { + err = fmt.Errorf("failed to get system security settings, error: %s", err) + log.Errorf(err.Error()) + return "", err + } + + token, err := CreateToken(&Claims{ + Name: user.Name, + UID: user.UID, + Email: user.Email, + PreferredUsername: user.Account, + StandardClaims: jwt.StandardClaims{ + Audience: setting.ProductName, + ExpiresAt: time.Now().Add(time.Duration(systemSettings.TokenExpirationTime) * time.Hour).Unix(), + }, + FederatedClaims: FederatedClaims{ + ConnectorId: user.IdentityType, + UserId: user.Account, + }, + }) + if err != nil { + err = fmt.Errorf("[SsoTokenCallback] user:%s create token error, error msg:%s", claims.UserID, err.Error()) + log.Errorf(err.Error()) + return "", err + } + + err = zadigCache.NewRedisCache(config.RedisUserTokenDB()).Write(user.UID, token, time.Duration(systemSettings.TokenExpirationTime)*time.Hour) + if err != nil { + logger.Errorf("failed to write token into cache, error: %s\n warn: this will cause login failure", err) + } + + return token, nil +} diff --git a/pkg/microservice/user/core/service/permission/authn.go b/pkg/microservice/user/core/service/permission/authn.go index f531597dc9..710419a41d 100644 --- a/pkg/microservice/user/core/service/permission/authn.go +++ b/pkg/microservice/user/core/service/permission/authn.go @@ -136,6 +136,10 @@ func IsPublicURL(reqPath, method string) bool { return true } + if realPath == "/api/v1/sso/token-callback" && method == http.MethodGet { + return true + } + if realPath == "/api/v1/captcha" && method == http.MethodGet { return true } diff --git a/pkg/setting/consts.go b/pkg/setting/consts.go index ca8b13b29b..d6802afb37 100644 --- a/pkg/setting/consts.go +++ b/pkg/setting/consts.go @@ -109,6 +109,7 @@ const ( ENVClientSecret = "CLIENT_SECRET" ENVRedirectURI = "REDIRECT_URI" ENVSecretKey = "SECRET_KEY" + ENVSsoTokenSecret = "SSO_TOKEN_SECRET" ENVMysqlUserDB = "MYSQL_USER_DB" ENVScopes = "SCOPES" ENVTokenExpiresAt = "TOKEN_EXPIRES_AT"