auth_totp.go 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package handler
  15. import (
  16. "context"
  17. "encoding/base32"
  18. "encoding/base64"
  19. "fmt"
  20. "net/http"
  21. "net/url"
  22. "github.com/skip2/go-qrcode"
  23. "yunion.io/x/jsonutils"
  24. "yunion.io/x/log"
  25. "yunion.io/x/pkg/util/httputils"
  26. "yunion.io/x/onecloud/pkg/apigateway/options"
  27. "yunion.io/x/onecloud/pkg/appsrv"
  28. "yunion.io/x/onecloud/pkg/httperrors"
  29. "yunion.io/x/onecloud/pkg/mcclient"
  30. "yunion.io/x/onecloud/pkg/mcclient/auth"
  31. modules "yunion.io/x/onecloud/pkg/mcclient/modules/identity"
  32. )
  33. // 转换成base64编码qrcode。
  34. // https://docs.openstack.org/keystone/rocky/advanced-topics/auth-totp.html
  35. // otpauth://totp/{name}?secret={secret}&issuer={issuer}
  36. // https://authenticator.ppl.family/
  37. func toQrcode(secret string, token mcclient.TokenCredential) (string, error) {
  38. _secret := base32.StdEncoding.EncodeToString([]byte(secret))
  39. // https://github.com/google/google-authenticator/wiki/Key-Uri-Format
  40. // issuer Cloudpods.Domain
  41. issuer := options.Options.TotpIssuer
  42. if len(token.GetDomainName()) > 0 {
  43. issuer = fmt.Sprintf("%s.%s", options.Options.TotpIssuer, token.GetDomainName())
  44. issuer = url.PathEscape(issuer)
  45. }
  46. uri := fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s", issuer, token.GetUserName(), _secret, issuer)
  47. c, err := qrcode.Encode(uri, qrcode.High, 256)
  48. if err != nil {
  49. log.Errorf("%s", err.Error())
  50. return "", httperrors.NewInternalServerError("generate totp qrcode failed")
  51. }
  52. return base64.StdEncoding.EncodeToString(c), nil
  53. }
  54. // 检查用户是否设置了TOTP credential.true -- 已设置;false--未设置
  55. func isUserTotpCredInitialed(s *mcclient.ClientSession, uid string) (bool, error) {
  56. _, err := modules.Credentials.GetTotpSecret(s, uid)
  57. if err == nil {
  58. return true, nil
  59. }
  60. switch e := err.(type) {
  61. case *httputils.JSONClientError:
  62. if e.Code == 404 {
  63. return false, nil
  64. }
  65. return false, err
  66. default:
  67. return false, err
  68. }
  69. }
  70. // 创建用户TOTP credential.并返回携带认证信息的Qrcode(base64编码,png格式)。
  71. func doCreateUserTotpCred(s *mcclient.ClientSession, token mcclient.TokenCredential) (string, error) {
  72. secret, err := modules.Credentials.CreateTotpSecret(s, token.GetUserId())
  73. if err != nil {
  74. return "", err
  75. }
  76. return toQrcode(secret, token)
  77. }
  78. // 初始化用户credential.如果新创建则返回携带认证信息的Qrcode(base64编码,png格式)。否则返回空字符串
  79. func initializeUserTotpCred(s *mcclient.ClientSession, token mcclient.TokenCredential) (string, error) {
  80. uid := token.GetUserId()
  81. if len(uid) == 0 {
  82. return "", httperrors.NewConflictError("uid is empty")
  83. }
  84. ok, err := isUserTotpCredInitialed(s, uid)
  85. if err != nil {
  86. return "", err
  87. }
  88. // 已经创建返回空字符串.否则返回携带认证信息的Qrcode(base64编码,png格式)
  89. if ok {
  90. return "", nil
  91. }
  92. return doCreateUserTotpCred(s, token)
  93. }
  94. // 重置totp credential.检查用户密码找回问题答案是否正确。如果正确则进行密码重置操作.返回Qrcode
  95. func resetUserTotpCred(s *mcclient.ClientSession, token mcclient.TokenCredential) (string, error) {
  96. uid := token.GetUserId()
  97. if len(uid) == 0 {
  98. return "", httperrors.NewConflictError("uid is empty")
  99. }
  100. ok, err := isUserTotpCredInitialed(s, uid)
  101. if err != nil {
  102. return "", err
  103. }
  104. // 如果已经设置密钥则删除旧认证信息
  105. if ok {
  106. err := modules.Credentials.RemoveTotpSecrets(s, uid)
  107. if err != nil {
  108. return "", err
  109. }
  110. }
  111. return doCreateUserTotpCred(s, token)
  112. }
  113. // 检查用户是否设置了TOTP 密码恢复问题.true -- 已设置;false--未设置
  114. func isUserTotpRecoverySecretsInitialed(s *mcclient.ClientSession, uid string) (bool, error) {
  115. _, err := modules.Credentials.GetRecoverySecrets(s, uid)
  116. if err == nil {
  117. return true, nil
  118. }
  119. switch e := err.(type) {
  120. case *httputils.JSONClientError:
  121. if e.Code == 404 {
  122. return false, nil
  123. }
  124. return false, err
  125. default:
  126. return false, err
  127. }
  128. }
  129. // 设置重置密码问题.
  130. func setTotpRecoverySecrets(s *mcclient.ClientSession, uid string, questions []modules.SRecoverySecret) error {
  131. exists, err := isUserTotpRecoverySecretsInitialed(s, uid)
  132. if err != nil {
  133. return err
  134. }
  135. if exists {
  136. err := modules.Credentials.RemoveRecoverySecrets(s, uid)
  137. if err != nil {
  138. return err
  139. }
  140. }
  141. return modules.Credentials.SaveRecoverySecrets(s, uid, questions)
  142. }
  143. // 验证重置密码问题.验证未通过则返回错误提示
  144. func validateTotpRecoverySecrets(s *mcclient.ClientSession, uid string, questions jsonutils.JSONObject) error {
  145. _qs := make([]modules.SRecoverySecret, 0)
  146. err := questions.Unmarshal(&_qs)
  147. if err != nil {
  148. return httperrors.NewInputParameterError("input parameter error")
  149. }
  150. qs := map[string]string{}
  151. for _, q := range _qs {
  152. qs[q.Question] = q.Answer
  153. }
  154. ss, err := modules.Credentials.GetRecoverySecrets(s, uid)
  155. if err != nil {
  156. return err
  157. }
  158. if len(ss) == 0 {
  159. return httperrors.NewConflictError("TOTP recovery questions do not exist")
  160. }
  161. for _, s := range ss {
  162. if v, existis := qs[s.Question]; !existis {
  163. return httperrors.NewInputParameterError("questions not found")
  164. } else {
  165. if v != s.Answer {
  166. return httperrors.NewInputParameterError("%s answer is incorrect", s.Question)
  167. }
  168. }
  169. }
  170. return nil
  171. }
  172. // 获取第一次的QR code
  173. func initTotpSecrets(ctx context.Context, w http.ResponseWriter, req *http.Request) {
  174. t, authToken, err := fetchAuthInfo(ctx, req)
  175. if err != nil {
  176. httperrors.InvalidCredentialError(ctx, w, "fetchAuthInfo fail: %s", err)
  177. return
  178. }
  179. if authToken.IsTotpInitialized() {
  180. resetTotpSecrets(ctx, w, req)
  181. return
  182. }
  183. s := auth.GetAdminSession(ctx, FetchRegion(req))
  184. code, err := doCreateUserTotpCred(s, t)
  185. if err != nil {
  186. httperrors.GeneralServerError(ctx, w, err)
  187. return
  188. }
  189. authToken.SetTotpInitialized()
  190. saveAuthCookie(w, authToken, t)
  191. resp := jsonutils.NewDict()
  192. resp.Add(jsonutils.NewString(code), "qrcode")
  193. appsrv.SendJSON(w, resp)
  194. }
  195. // 验证OTP
  196. func validatePasscodeHandler(ctx context.Context, w http.ResponseWriter, req *http.Request) {
  197. t, authToken, err := fetchAuthInfo(ctx, req)
  198. if err != nil {
  199. httperrors.InvalidCredentialError(ctx, w, "fetchAuthInfo fail: %s", err)
  200. return
  201. }
  202. s := auth.GetAdminSession(ctx, FetchRegion(req))
  203. _, _, body := appsrv.FetchEnv(ctx, w, req)
  204. if body == nil {
  205. httperrors.InvalidInputError(ctx, w, "request body is empty")
  206. return
  207. }
  208. passcode, err := body.GetString("passcode")
  209. if err != nil {
  210. httperrors.MissingParameterError(ctx, w, "passcode")
  211. return
  212. }
  213. if len(passcode) != 6 {
  214. httperrors.InputParameterError(ctx, w, "passcode is a 6-digits string")
  215. return
  216. }
  217. err = authToken.VerifyTotpPasscode(s, t.GetUserId(), passcode)
  218. saveAuthCookie(w, authToken, t)
  219. if err != nil {
  220. log.Warningf("VerifyTotpPasscode %s", err.Error())
  221. httperrors.InputParameterError(ctx, w, "invalid passcode: %v", err)
  222. return
  223. }
  224. appsrv.SendJSON(w, jsonutils.NewDict())
  225. }
  226. // 验证OTP credential重置问题.如果答案正确,返回重置后的Qrcode(base64编码,png格式)。
  227. func resetTotpSecrets(ctx context.Context, w http.ResponseWriter, req *http.Request) {
  228. t, _, err := fetchAuthInfo(ctx, req)
  229. if err != nil {
  230. httperrors.InvalidCredentialError(ctx, w, "fetchAuthInfo fail: %s", err)
  231. return
  232. }
  233. s := auth.GetAdminSession(ctx, FetchRegion(req))
  234. _, _, body := appsrv.FetchEnv(ctx, w, req)
  235. if body == nil {
  236. httperrors.InvalidInputError(ctx, w, "request body is empty")
  237. return
  238. }
  239. uid := t.GetUserId()
  240. if len(uid) == 0 {
  241. httperrors.ConflictError(ctx, w, "uid is empty")
  242. return
  243. }
  244. err = validateTotpRecoverySecrets(s, uid, body)
  245. if err != nil {
  246. httperrors.GeneralServerError(ctx, w, err)
  247. return
  248. }
  249. code, err := resetUserTotpCred(s, t)
  250. if err != nil {
  251. httperrors.GeneralServerError(ctx, w, err)
  252. return
  253. }
  254. resp := jsonutils.NewDict()
  255. resp.Add(jsonutils.NewString(code), "qrcode")
  256. appsrv.SendJSON(w, resp)
  257. }
  258. // 获取OTP 重置密码问题列表。
  259. func listTotpRecoveryQuestions(ctx context.Context, w http.ResponseWriter, req *http.Request) {
  260. t, _, err := fetchAuthInfo(ctx, req)
  261. if err != nil {
  262. httperrors.InvalidCredentialError(ctx, w, "fetchAuthInfo fail: %s", err)
  263. return
  264. }
  265. s := auth.GetAdminSession(ctx, FetchRegion(req))
  266. // 做缓存?
  267. ss, err := modules.Credentials.GetRecoverySecrets(s, t.GetUserId())
  268. if len(ss) == 0 {
  269. log.Errorf("ListTotpRecoveryQuestions %s", err.Error())
  270. httperrors.NotFoundError(ctx, w, "no revocery questions.")
  271. return
  272. }
  273. resp := jsonutils.NewDict()
  274. questions := jsonutils.NewArray()
  275. for _, s := range ss {
  276. questions.Add(jsonutils.NewString(s.Question))
  277. }
  278. resp.Add(questions, "data")
  279. appsrv.SendJSON(w, resp)
  280. }
  281. // 提交OTP 重置密码问题。
  282. func resetTotpRecoveryQuestions(ctx context.Context, w http.ResponseWriter, req *http.Request) {
  283. t, _, err := fetchAuthInfo(ctx, req)
  284. if err != nil {
  285. httperrors.InvalidCredentialError(ctx, w, "fetchAuthInfo fail: %s", err)
  286. return
  287. }
  288. s := auth.GetAdminSession(ctx, FetchRegion(req))
  289. _, _, body := appsrv.FetchEnv(ctx, w, req)
  290. if body == nil {
  291. httperrors.InvalidInputError(ctx, w, "request body is empty")
  292. return
  293. }
  294. questions := make([]modules.SRecoverySecret, 0)
  295. err = body.Unmarshal(&questions)
  296. if err != nil {
  297. httperrors.InvalidInputError(ctx, w, "unmarshal questions: %v", err)
  298. return
  299. }
  300. err = setTotpRecoverySecrets(s, t.GetUserId(), questions)
  301. if err != nil {
  302. httperrors.GeneralServerError(ctx, w, err)
  303. return
  304. }
  305. appsrv.SendJSON(w, jsonutils.NewDict())
  306. }