| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- // 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
- }
|