mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-11-02 20:38:09 +08:00
304 lines
7.5 KiB
Go
304 lines
7.5 KiB
Go
// 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 clientman
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/flate"
|
|
"context"
|
|
"crypto/rsa"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"io"
|
|
"math/rand"
|
|
"time"
|
|
|
|
"github.com/lestrrat-go/jwx/jwa"
|
|
"github.com/lestrrat-go/jwx/jwe"
|
|
"github.com/lestrrat-go/jwx/jwt"
|
|
"github.com/pquerna/otp/totp"
|
|
|
|
"yunion.io/x/jsonutils"
|
|
"yunion.io/x/pkg/errors"
|
|
|
|
"yunion.io/x/onecloud/pkg/apigateway/options"
|
|
"yunion.io/x/onecloud/pkg/httperrors"
|
|
"yunion.io/x/onecloud/pkg/mcclient"
|
|
"yunion.io/x/onecloud/pkg/mcclient/auth"
|
|
)
|
|
|
|
const (
|
|
TotpEnable = '1'
|
|
TotpDisable = '0'
|
|
)
|
|
|
|
var (
|
|
privateKey *rsa.PrivateKey
|
|
)
|
|
|
|
func setPrivateKey(key *rsa.PrivateKey) {
|
|
privateKey = key
|
|
}
|
|
|
|
type SAuthToken struct {
|
|
token string
|
|
verifyTotp bool
|
|
enableTotp bool
|
|
initTotp bool
|
|
isSsoLogin bool
|
|
|
|
retryCount int // 重试计数器
|
|
lockExpireTime uint32 // 锁定时间
|
|
}
|
|
|
|
func (t SAuthToken) encodeBytes() []byte {
|
|
msg := bytes.Buffer{}
|
|
if t.verifyTotp {
|
|
msg.WriteByte(TotpEnable)
|
|
} else {
|
|
msg.WriteByte(TotpDisable)
|
|
}
|
|
if t.enableTotp {
|
|
msg.WriteByte(TotpEnable)
|
|
} else {
|
|
msg.WriteByte(TotpDisable)
|
|
}
|
|
if t.initTotp {
|
|
msg.WriteByte(TotpEnable)
|
|
} else {
|
|
msg.WriteByte(TotpDisable)
|
|
}
|
|
if t.isSsoLogin {
|
|
msg.WriteByte(TotpEnable)
|
|
} else {
|
|
msg.WriteByte(TotpDisable)
|
|
}
|
|
msg.WriteByte(byte(rand.Int()))
|
|
msg.WriteByte(byte(t.retryCount))
|
|
expBytes := make([]byte, 4)
|
|
binary.LittleEndian.PutUint32(expBytes, t.lockExpireTime)
|
|
msg.Write(expBytes)
|
|
msg.WriteString(t.token)
|
|
return msg.Bytes()
|
|
}
|
|
|
|
func (t SAuthToken) Encode() string {
|
|
encBytes := t.encodeBytes()
|
|
if privateKey != nil {
|
|
return EncryptString(encBytes)
|
|
} else {
|
|
return compressString(encBytes)
|
|
}
|
|
}
|
|
|
|
func Decode(t string) (*SAuthToken, error) {
|
|
var tBytes []byte
|
|
var err error
|
|
if privateKey != nil {
|
|
tBytes, err = DecryptString(t)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "decryptString")
|
|
}
|
|
} else {
|
|
tBytes, err = decompressString(t)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "decompressString")
|
|
}
|
|
}
|
|
return decodeBytes(tBytes)
|
|
}
|
|
|
|
func decodeBytes(tt []byte) (*SAuthToken, error) {
|
|
ret := SAuthToken{}
|
|
if len(tt) < 10 {
|
|
return nil, errors.Wrap(errors.ErrInvalidStatus, "too short")
|
|
}
|
|
if tt[0] == TotpEnable {
|
|
ret.verifyTotp = true
|
|
} else {
|
|
ret.verifyTotp = false
|
|
}
|
|
if tt[1] == TotpEnable {
|
|
ret.enableTotp = true
|
|
} else {
|
|
ret.enableTotp = false
|
|
}
|
|
if tt[2] == TotpEnable {
|
|
ret.initTotp = true
|
|
} else {
|
|
ret.initTotp = false
|
|
}
|
|
if tt[3] == TotpEnable {
|
|
ret.isSsoLogin = true
|
|
} else {
|
|
ret.isSsoLogin = false
|
|
}
|
|
// 4: skip rand number
|
|
ret.retryCount = int(tt[5])
|
|
ret.lockExpireTime = binary.LittleEndian.Uint32(tt[6:])
|
|
ret.token = string(tt[10:])
|
|
return &ret, nil
|
|
}
|
|
|
|
func compressString(in []byte) string {
|
|
buf := new(bytes.Buffer)
|
|
compressor, _ := flate.NewWriter(buf, 9)
|
|
compressor.Write(in)
|
|
compressor.Close()
|
|
return base64.URLEncoding.EncodeToString(buf.Bytes())
|
|
}
|
|
|
|
func EncryptString(in []byte) string {
|
|
enc, _ := jwe.Encrypt(in, jwa.RSA1_5, &privateKey.PublicKey, jwa.A128GCM, jwa.Deflate)
|
|
return string(enc)
|
|
}
|
|
|
|
func decompressString(in string) ([]byte, error) {
|
|
inBytes, err := base64.URLEncoding.DecodeString(in)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "base64.URLEncoding.DecodeString")
|
|
}
|
|
buf := new(bytes.Buffer)
|
|
decompressor := flate.NewReader(bytes.NewReader(inBytes))
|
|
_, err = io.Copy(buf, decompressor)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "decompress")
|
|
}
|
|
decompressor.Close()
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func DecryptString(in string) ([]byte, error) {
|
|
return jwe.Decrypt([]byte(in), jwa.RSA1_5, privateKey)
|
|
}
|
|
|
|
func (t SAuthToken) GetToken(ctx context.Context) (mcclient.TokenCredential, error) {
|
|
return auth.Verify(ctx, t.token)
|
|
}
|
|
|
|
func (t SAuthToken) GetAuthCookie(token mcclient.TokenCredential) string {
|
|
sid := t.Encode()
|
|
info := jsonutils.NewDict()
|
|
info.Add(jsonutils.NewTimeString(token.GetExpires()), "exp")
|
|
info.Add(jsonutils.NewString(sid), "session")
|
|
info.Add(jsonutils.NewBool(t.verifyTotp), "totp_verified") // 用户totp验证通过
|
|
info.Add(jsonutils.NewBool(t.initTotp), "totp_init") // 是否初始化TOTP密钥
|
|
info.Add(jsonutils.NewBool(t.enableTotp), "totp_on") // 用户totp 开启状态。 True(已开启)|False(未开启)
|
|
info.Add(jsonutils.NewBool(t.isSsoLogin), "is_sso") // 用户是否通过SSO登录
|
|
info.Add(jsonutils.NewBool(options.Options.EnableTotp), "system_totp_on") // 全局totp 开启状态。 True(已开启)|False(未开启)
|
|
info.Add(jsonutils.NewString(token.GetUserId()), "user_id")
|
|
info.Add(jsonutils.NewString(token.GetUserName()), "user")
|
|
return info.String()
|
|
}
|
|
|
|
func (t SAuthToken) IsTotpVerified() bool {
|
|
if !options.Options.EnableTotp {
|
|
return true
|
|
}
|
|
if !t.enableTotp {
|
|
return true
|
|
}
|
|
return t.verifyTotp
|
|
}
|
|
|
|
func (t SAuthToken) IsTotpEnabled() bool {
|
|
return t.enableTotp
|
|
}
|
|
|
|
func (t SAuthToken) IsTotpInitialized() bool {
|
|
return t.initTotp
|
|
}
|
|
|
|
func (t *SAuthToken) SetTotpInitialized() {
|
|
t.initTotp = true
|
|
}
|
|
|
|
func (t *SAuthToken) SetToken(tid string) {
|
|
t.token = tid
|
|
}
|
|
|
|
func NewAuthToken(tid string, enableTotp bool, isTotpInit bool, isSsoLogin bool) *SAuthToken {
|
|
return &SAuthToken{
|
|
token: tid,
|
|
enableTotp: enableTotp,
|
|
initTotp: isTotpInit,
|
|
isSsoLogin: isSsoLogin,
|
|
verifyTotp: false,
|
|
}
|
|
}
|
|
|
|
func (t *SAuthToken) updateRetryCount() {
|
|
if t.retryCount < MAX_OTP_RETRY {
|
|
t.retryCount += 1
|
|
|
|
// 锁定
|
|
if t.retryCount >= MAX_OTP_RETRY {
|
|
t.lockExpireTime = uint32(time.Now().Add(30 * time.Second).Unix())
|
|
}
|
|
} else {
|
|
// 清零计数器,解除锁定
|
|
if t.lockExpireTime < uint32(time.Now().Unix()) {
|
|
t.lockExpireTime = 0
|
|
t.retryCount = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *SAuthToken) VerifyTotpPasscode(s *mcclient.ClientSession, uid, passcode string) error {
|
|
if t.lockExpireTime > uint32(time.Now().Unix()) {
|
|
return errors.Wrapf(httperrors.ErrResourceBusy, "locked, retry after %d seconds", t.lockExpireTime-uint32(time.Now().Unix()))
|
|
}
|
|
|
|
secret, err := fetchUserTotpCredSecret(s, uid)
|
|
if err != nil {
|
|
return errors.Wrap(err, "fetch totp secrets error")
|
|
}
|
|
|
|
if totp.Validate(passcode, secret) {
|
|
t.verifyTotp = true
|
|
t.lockExpireTime = 0
|
|
t.retryCount = 0
|
|
return nil
|
|
}
|
|
|
|
t.updateRetryCount()
|
|
return errors.Wrap(httperrors.ErrInputParameter, "invalid passcode")
|
|
}
|
|
|
|
func SignJWT(t jwt.Token) (string, error) {
|
|
//jwkKey, err := jwk.New(privateKey)
|
|
//if err != nil {
|
|
// return "", errors.Wrap(err, "jwk.New")
|
|
//}
|
|
signed, err := jwt.Sign(t, jwa.RS256, privateKey)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "jwt.Sign")
|
|
}
|
|
return string(signed), nil
|
|
}
|
|
|
|
func GetJWKs(ctx context.Context) (jsonutils.JSONObject, error) {
|
|
key := jsonutils.NewDict()
|
|
key.Set("use", jsonutils.NewString("sig"))
|
|
key.Set("kty", jsonutils.NewString("RSA"))
|
|
key.Set("alg", jsonutils.NewString("RS256"))
|
|
key.Set("e", jsonutils.NewString("AQAB"))
|
|
key.Set("n", jsonutils.NewString(base64.URLEncoding.EncodeToString(privateKey.PublicKey.N.Bytes())))
|
|
|
|
ret := jsonutils.NewDict()
|
|
ret.Set("keys", jsonutils.NewArray(key))
|
|
return ret, nil
|
|
}
|