Files
cloudpods/pkg/apigateway/clientman/authtoken.go
huangzekeng 5b506b3917 fix(apigateway): 修复oidc认证中,返回的id_token为空
修复odic认证返回的id_token为空
2022-05-13 14:34:33 +08:00

304 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}