mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-05-22 12:32:36 +08:00
365 lines
9.8 KiB
Go
365 lines
9.8 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 handler
|
||
|
||
import (
|
||
"context"
|
||
"encoding/base32"
|
||
"encoding/base64"
|
||
"fmt"
|
||
"net/http"
|
||
"net/url"
|
||
|
||
"github.com/skip2/go-qrcode"
|
||
|
||
"yunion.io/x/jsonutils"
|
||
"yunion.io/x/log"
|
||
|
||
"yunion.io/x/onecloud/pkg/apigateway/options"
|
||
"yunion.io/x/onecloud/pkg/appsrv"
|
||
"yunion.io/x/onecloud/pkg/httperrors"
|
||
"yunion.io/x/onecloud/pkg/mcclient"
|
||
"yunion.io/x/onecloud/pkg/mcclient/auth"
|
||
"yunion.io/x/onecloud/pkg/mcclient/modules"
|
||
"yunion.io/x/onecloud/pkg/util/httputils"
|
||
)
|
||
|
||
// 转换成base64编码qrcode。
|
||
// https://docs.openstack.org/keystone/rocky/advanced-topics/auth-totp.html
|
||
// otpauth://totp/{name}?secret={secret}&issuer={issuer}
|
||
// https://authenticator.ppl.family/
|
||
func toQrcode(secret string, token mcclient.TokenCredential) (string, error) {
|
||
_secret := base32.StdEncoding.EncodeToString([]byte(secret))
|
||
// https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||
// issuer Cloudpods.Domain
|
||
issuer := options.Options.TotpIssuer
|
||
if len(token.GetDomainName()) > 0 {
|
||
issuer = fmt.Sprintf("%s.%s", options.Options.TotpIssuer, token.GetDomainName())
|
||
issuer = url.PathEscape(issuer)
|
||
}
|
||
|
||
uri := fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s", issuer, token.GetUserName(), _secret, issuer)
|
||
|
||
c, err := qrcode.Encode(uri, qrcode.High, 256)
|
||
if err != nil {
|
||
log.Errorf(err.Error())
|
||
return "", httperrors.NewInternalServerError("generate totp qrcode failed")
|
||
}
|
||
|
||
return base64.StdEncoding.EncodeToString(c), nil
|
||
}
|
||
|
||
// 检查用户是否设置了TOTP credential.true -- 已设置;false--未设置
|
||
func isUserTotpCredInitialed(s *mcclient.ClientSession, uid string) (bool, error) {
|
||
_, err := modules.Credentials.GetTotpSecret(s, uid)
|
||
if err == nil {
|
||
return true, nil
|
||
}
|
||
|
||
switch e := err.(type) {
|
||
case *httputils.JSONClientError:
|
||
if e.Code == 404 {
|
||
return false, nil
|
||
}
|
||
return false, err
|
||
default:
|
||
return false, err
|
||
}
|
||
}
|
||
|
||
// 创建用户TOTP credential.并返回携带认证信息的Qrcode(base64编码,png格式)。
|
||
func doCreateUserTotpCred(s *mcclient.ClientSession, token mcclient.TokenCredential) (string, error) {
|
||
secret, err := modules.Credentials.CreateTotpSecret(s, token.GetUserId())
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
return toQrcode(secret, token)
|
||
}
|
||
|
||
// 初始化用户credential.如果新创建则返回携带认证信息的Qrcode(base64编码,png格式)。否则返回空字符串
|
||
func initializeUserTotpCred(s *mcclient.ClientSession, token mcclient.TokenCredential) (string, error) {
|
||
uid := token.GetUserId()
|
||
if len(uid) == 0 {
|
||
return "", httperrors.NewConflictError("uid is empty")
|
||
}
|
||
|
||
ok, err := isUserTotpCredInitialed(s, uid)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
// 已经创建返回空字符串.否则返回携带认证信息的Qrcode(base64编码,png格式)
|
||
if ok {
|
||
return "", nil
|
||
}
|
||
|
||
return doCreateUserTotpCred(s, token)
|
||
}
|
||
|
||
// 重置totp credential.检查用户密码找回问题答案是否正确。如果正确则进行密码重置操作.返回Qrcode
|
||
func resetUserTotpCred(s *mcclient.ClientSession, token mcclient.TokenCredential) (string, error) {
|
||
uid := token.GetUserId()
|
||
if len(uid) == 0 {
|
||
return "", httperrors.NewConflictError("uid is empty")
|
||
}
|
||
|
||
ok, err := isUserTotpCredInitialed(s, uid)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
// 如果已经设置密钥则删除旧认证信息
|
||
if ok {
|
||
err := modules.Credentials.RemoveTotpSecrets(s, uid)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
}
|
||
|
||
return doCreateUserTotpCred(s, token)
|
||
}
|
||
|
||
// 检查用户是否设置了TOTP 密码恢复问题.true -- 已设置;false--未设置
|
||
func isUserTotpRecoverySecretsInitialed(s *mcclient.ClientSession, uid string) (bool, error) {
|
||
_, err := modules.Credentials.GetRecoverySecrets(s, uid)
|
||
if err == nil {
|
||
return true, nil
|
||
}
|
||
|
||
switch e := err.(type) {
|
||
case *httputils.JSONClientError:
|
||
if e.Code == 404 {
|
||
return false, nil
|
||
}
|
||
return false, err
|
||
default:
|
||
return false, err
|
||
}
|
||
}
|
||
|
||
// 设置重置密码问题.
|
||
func setTotpRecoverySecrets(s *mcclient.ClientSession, uid string, questions []modules.SRecoverySecret) error {
|
||
exists, err := isUserTotpRecoverySecretsInitialed(s, uid)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if exists {
|
||
err := modules.Credentials.RemoveRecoverySecrets(s, uid)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
return modules.Credentials.SaveRecoverySecrets(s, uid, questions)
|
||
}
|
||
|
||
// 验证重置密码问题.验证未通过则返回错误提示
|
||
func validateTotpRecoverySecrets(s *mcclient.ClientSession, uid string, questions jsonutils.JSONObject) error {
|
||
_qs := make([]modules.SRecoverySecret, 0)
|
||
err := questions.Unmarshal(&_qs)
|
||
if err != nil {
|
||
return httperrors.NewInputParameterError("input parameter error")
|
||
}
|
||
|
||
qs := map[string]string{}
|
||
for _, q := range _qs {
|
||
qs[q.Question] = q.Answer
|
||
}
|
||
|
||
ss, err := modules.Credentials.GetRecoverySecrets(s, uid)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if len(ss) == 0 {
|
||
return httperrors.NewConflictError("TOTP recovery questions do not exist")
|
||
}
|
||
|
||
for _, s := range ss {
|
||
if v, existis := qs[s.Question]; !existis {
|
||
return httperrors.NewInputParameterError("questions not found")
|
||
} else {
|
||
if v != s.Answer {
|
||
return httperrors.NewInputParameterError("%s answer is incorrect", s.Question)
|
||
}
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// 获取第一次的QR code
|
||
func initTotpSecrets(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
||
t, authToken, err := fetchAuthInfo(ctx, req)
|
||
if err != nil {
|
||
httperrors.InvalidCredentialError(ctx, w, "fetchAuthInfo fail: %s", err)
|
||
return
|
||
}
|
||
if authToken.IsTotpInitialized() {
|
||
resetTotpSecrets(ctx, w, req)
|
||
return
|
||
}
|
||
|
||
s := auth.GetAdminSession(ctx, FetchRegion(req), "")
|
||
code, err := doCreateUserTotpCred(s, t)
|
||
if err != nil {
|
||
httperrors.GeneralServerError(ctx, w, err)
|
||
return
|
||
}
|
||
|
||
authToken.SetTotpInitialized()
|
||
saveAuthCookie(w, authToken, t)
|
||
|
||
resp := jsonutils.NewDict()
|
||
resp.Add(jsonutils.NewString(code), "qrcode")
|
||
appsrv.SendJSON(w, resp)
|
||
}
|
||
|
||
// 验证OTP
|
||
func validatePasscodeHandler(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
||
t, authToken, err := fetchAuthInfo(ctx, req)
|
||
if err != nil {
|
||
httperrors.InvalidCredentialError(ctx, w, "fetchAuthInfo fail: %s", err)
|
||
return
|
||
}
|
||
|
||
s := auth.GetAdminSession(ctx, FetchRegion(req), "")
|
||
_, _, body := appsrv.FetchEnv(ctx, w, req)
|
||
if body == nil {
|
||
httperrors.InvalidInputError(ctx, w, "request body is empty")
|
||
return
|
||
}
|
||
|
||
passcode, err := body.GetString("passcode")
|
||
if err != nil {
|
||
httperrors.MissingParameterError(ctx, w, "passcode")
|
||
return
|
||
}
|
||
|
||
if len(passcode) != 6 {
|
||
httperrors.InputParameterError(ctx, w, "passcode is a 6-digits string")
|
||
return
|
||
}
|
||
|
||
err = authToken.VerifyTotpPasscode(s, t.GetUserId(), passcode)
|
||
|
||
saveAuthCookie(w, authToken, t)
|
||
|
||
if err != nil {
|
||
log.Warningf("VerifyTotpPasscode %s", err.Error())
|
||
httperrors.InputParameterError(ctx, w, "invalid passcode: %v", err)
|
||
return
|
||
}
|
||
|
||
appsrv.SendJSON(w, jsonutils.NewDict())
|
||
}
|
||
|
||
// 验证OTP credential重置问题.如果答案正确,返回重置后的Qrcode(base64编码,png格式)。
|
||
func resetTotpSecrets(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
||
t, _, err := fetchAuthInfo(ctx, req)
|
||
if err != nil {
|
||
httperrors.InvalidCredentialError(ctx, w, "fetchAuthInfo fail: %s", err)
|
||
return
|
||
}
|
||
|
||
s := auth.GetAdminSession(ctx, FetchRegion(req), "")
|
||
_, _, body := appsrv.FetchEnv(ctx, w, req)
|
||
if body == nil {
|
||
httperrors.InvalidInputError(ctx, w, "request body is empty")
|
||
return
|
||
}
|
||
|
||
uid := t.GetUserId()
|
||
if len(uid) == 0 {
|
||
httperrors.ConflictError(ctx, w, "uid is empty")
|
||
return
|
||
}
|
||
|
||
err = validateTotpRecoverySecrets(s, uid, body)
|
||
if err != nil {
|
||
httperrors.GeneralServerError(ctx, w, err)
|
||
return
|
||
}
|
||
|
||
code, err := resetUserTotpCred(s, t)
|
||
if err != nil {
|
||
httperrors.GeneralServerError(ctx, w, err)
|
||
return
|
||
}
|
||
|
||
resp := jsonutils.NewDict()
|
||
resp.Add(jsonutils.NewString(code), "qrcode")
|
||
appsrv.SendJSON(w, resp)
|
||
}
|
||
|
||
// 获取OTP 重置密码问题列表。
|
||
func listTotpRecoveryQuestions(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
||
t, _, err := fetchAuthInfo(ctx, req)
|
||
if err != nil {
|
||
httperrors.InvalidCredentialError(ctx, w, "fetchAuthInfo fail: %s", err)
|
||
return
|
||
}
|
||
|
||
s := auth.GetAdminSession(ctx, FetchRegion(req), "")
|
||
// 做缓存?
|
||
ss, err := modules.Credentials.GetRecoverySecrets(s, t.GetUserId())
|
||
if len(ss) == 0 {
|
||
log.Errorf("ListTotpRecoveryQuestions %s", err.Error())
|
||
httperrors.NotFoundError(ctx, w, "no revocery questions.")
|
||
return
|
||
}
|
||
|
||
resp := jsonutils.NewDict()
|
||
questions := jsonutils.NewArray()
|
||
for _, s := range ss {
|
||
questions.Add(jsonutils.NewString(s.Question))
|
||
}
|
||
resp.Add(questions, "data")
|
||
appsrv.SendJSON(w, resp)
|
||
}
|
||
|
||
// 提交OTP 重置密码问题。
|
||
func resetTotpRecoveryQuestions(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
||
t, _, err := fetchAuthInfo(ctx, req)
|
||
if err != nil {
|
||
httperrors.InvalidCredentialError(ctx, w, "fetchAuthInfo fail: %s", err)
|
||
return
|
||
}
|
||
|
||
s := auth.GetAdminSession(ctx, FetchRegion(req), "")
|
||
_, _, body := appsrv.FetchEnv(ctx, w, req)
|
||
if body == nil {
|
||
httperrors.InvalidInputError(ctx, w, "request body is empty")
|
||
return
|
||
}
|
||
|
||
questions := make([]modules.SRecoverySecret, 0)
|
||
err = body.Unmarshal(&questions)
|
||
if err != nil {
|
||
httperrors.InvalidInputError(ctx, w, "unmarshal questions: %v", err)
|
||
return
|
||
}
|
||
|
||
err = setTotpRecoverySecrets(s, t.GetUserId(), questions)
|
||
if err != nil {
|
||
httperrors.GeneralServerError(ctx, w, err)
|
||
return
|
||
}
|
||
|
||
appsrv.SendJSON(w, jsonutils.NewDict())
|
||
}
|