mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-05-09 23:28:20 +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
|
||
}
|