idp.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  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/base64"
  18. "net/http"
  19. "net/url"
  20. "regexp"
  21. "strings"
  22. "time"
  23. "yunion.io/x/jsonutils"
  24. "yunion.io/x/log"
  25. "yunion.io/x/pkg/appctx"
  26. "yunion.io/x/pkg/errors"
  27. "yunion.io/x/pkg/util/httputils"
  28. "yunion.io/x/pkg/utils"
  29. "yunion.io/x/onecloud/pkg/apigateway/options"
  30. api "yunion.io/x/onecloud/pkg/apis/identity"
  31. "yunion.io/x/onecloud/pkg/appsrv"
  32. "yunion.io/x/onecloud/pkg/httperrors"
  33. "yunion.io/x/onecloud/pkg/mcclient"
  34. "yunion.io/x/onecloud/pkg/mcclient/auth"
  35. modules "yunion.io/x/onecloud/pkg/mcclient/modules/identity"
  36. "yunion.io/x/onecloud/pkg/util/netutils2"
  37. )
  38. func getSsoBaseCallbackUrl() string {
  39. if options.Options.SsoRedirectUrl == "" {
  40. return httputils.JoinPath(options.Options.ApiServer, "api/v1/auth/ssologin")
  41. }
  42. return options.Options.SsoRedirectUrl
  43. }
  44. func getSsoCallbackUrl(ctx context.Context, req *http.Request, idpId string) string {
  45. baseUrl := getSsoBaseCallbackUrl()
  46. s := auth.GetAdminSession(ctx, FetchRegion(req))
  47. input := api.GetIdpSsoCallbackUriInput{
  48. RedirectUri: baseUrl,
  49. }
  50. resp, err := modules.IdentityProviders.GetSpecific(s, idpId, "sso-callback-uri", jsonutils.Marshal(input))
  51. if err != nil {
  52. return baseUrl
  53. }
  54. ret, err := resp.GetString("redirect_uri")
  55. if err != nil {
  56. return baseUrl
  57. }
  58. return ret
  59. }
  60. func getSsoAuthCallbackUrl() string {
  61. if options.Options.SsoAuthCallbackUrl == "" {
  62. return httputils.JoinPath(options.Options.ApiServer, "auth")
  63. }
  64. return options.Options.SsoAuthCallbackUrl
  65. }
  66. func getSsoLinkCallbackUrl() string {
  67. if options.Options.SsoLinkCallbackUrl == "" {
  68. return httputils.JoinPath(options.Options.ApiServer, "user")
  69. }
  70. return options.Options.SsoLinkCallbackUrl
  71. }
  72. func getSsoUserNotFoundCallbackUrl() string {
  73. if options.Options.SsoUserNotFoundCallbackUrl == "" {
  74. return getSsoAuthCallbackUrl()
  75. }
  76. return options.Options.SsoUserNotFoundCallbackUrl
  77. }
  78. func (h *AuthHandlers) getIdpSsoRedirectUri(ctx context.Context, w http.ResponseWriter, req *http.Request) {
  79. expires := time.Now().Add(time.Minute * 5)
  80. params := appctx.AppContextParams(ctx)
  81. idpId := params["<idp_id>"]
  82. query, _ := jsonutils.ParseQueryString(req.URL.RawQuery)
  83. var linkuser string
  84. if query != nil && query.Contains("linkuser") {
  85. t, _, _ := fetchAuthInfo(ctx, req)
  86. if t == nil {
  87. httperrors.InvalidCredentialError(ctx, w, "invalid credential")
  88. return
  89. }
  90. linkuser = t.GetUserId()
  91. // authCookie := authToken.GetAuthCookie(t)
  92. // saveCookie(w, constants.YUNION_AUTH_COOKIE, authCookie, "", expires, true)
  93. }
  94. referer := req.Header.Get(http.CanonicalHeaderKey("referer"))
  95. if query == nil {
  96. query = jsonutils.NewDict()
  97. }
  98. query.(*jsonutils.JSONDict).Set("idp_nonce", jsonutils.NewString(utils.GenRequestId(4)))
  99. state := base64.URLEncoding.EncodeToString([]byte(query.String()))
  100. redirectUri := getSsoCallbackUrl(ctx, req, idpId)
  101. s := auth.GetAdminSession(ctx, FetchRegion(req))
  102. input := api.GetIdpSsoRedirectUriInput{
  103. RedirectUri: redirectUri,
  104. State: state,
  105. }
  106. resp, err := modules.IdentityProviders.GetSpecific(s, idpId, "sso-redirect-uri", jsonutils.Marshal(input))
  107. if err != nil {
  108. httperrors.GeneralServerError(ctx, w, err)
  109. return
  110. }
  111. redirUrl, _ := resp.GetString("uri")
  112. idpDriver, _ := resp.GetString("driver")
  113. saveCookie(w, "idp_id", idpId, "", expires, true)
  114. saveCookie(w, "idp_state", state, "", expires, true)
  115. saveCookie(w, "idp_driver", idpDriver, "", expires, true)
  116. saveCookie(w, "idp_referer", referer, "", expires, true)
  117. saveCookie(w, "idp_link_user", linkuser, "", expires, true)
  118. appsrv.DisableClientCache(w)
  119. appsrv.SendRedirect(w, redirUrl)
  120. }
  121. func findExtUserId(input string) string {
  122. pattern := regexp.MustCompile(`idp.SyncOrCreateDomainAndUser: ([^:]+): UserNotFound`)
  123. matches := pattern.FindAllStringSubmatch(input, -1)
  124. log.Debugf("%#v", matches)
  125. if len(matches) > 0 && len(matches[0]) > 1 {
  126. return matches[0][1]
  127. }
  128. return ""
  129. }
  130. func (h *AuthHandlers) handleIdpInitSsoLogin(ctx context.Context, w http.ResponseWriter, req *http.Request) {
  131. params := appctx.AppContextParams(ctx)
  132. idpId := params["<idp_id>"]
  133. s := auth.GetAdminSession(ctx, FetchRegion(req))
  134. resp, err := modules.IdentityProviders.Get(s, idpId, nil)
  135. if err != nil {
  136. httperrors.GeneralServerError(ctx, w, err)
  137. return
  138. }
  139. idpDriver, _ := resp.GetString("driver")
  140. h.internalSsoLogin(ctx, w, req, idpId, idpDriver)
  141. }
  142. func (h *AuthHandlers) handleSsoLogin(ctx context.Context, w http.ResponseWriter, req *http.Request) {
  143. h.internalSsoLogin(ctx, w, req, "", "")
  144. }
  145. func (h *AuthHandlers) internalSsoLogin(ctx context.Context, w http.ResponseWriter, req *http.Request, idpId, idpDriver string) {
  146. idpIdC := getCookie(req, "idp_id")
  147. idpDriverC := getCookie(req, "idp_driver")
  148. idpState := getCookie(req, "idp_state")
  149. idpReferer := getCookie(req, "idp_referer")
  150. idpLinkUser := getCookie(req, "idp_link_user")
  151. if len(idpIdC) > 0 {
  152. idpId = idpIdC
  153. }
  154. if len(idpDriverC) > 0 {
  155. idpDriver = idpDriverC
  156. }
  157. for _, k := range []string{"idp_id", "idp_driver", "idp_state", "idp_referer", "idp_link_user"} {
  158. clearCookie(w, k, "")
  159. }
  160. missing := make([]string, 0)
  161. if len(idpId) == 0 {
  162. missing = append(missing, "idp_id")
  163. }
  164. if len(idpDriver) == 0 {
  165. missing = append(missing, "idp_driver")
  166. }
  167. /*if len(idpState) == 0 {
  168. missing = append(missing, "idp_state")
  169. }
  170. if len(idpReferer) == 0 {
  171. missing = append(missing, "idp_referer")
  172. }*/
  173. if len(missing) > 0 {
  174. httperrors.TimeoutError(ctx, w, "session expires, missing %s", strings.Join(missing, ","))
  175. return
  176. }
  177. var idpStateQs jsonutils.JSONObject
  178. if len(idpState) > 0 {
  179. idpStateQsBytes, _ := base64.URLEncoding.DecodeString(idpState)
  180. idpStateQs, _ = jsonutils.Parse(idpStateQsBytes)
  181. log.Debugf("state query sting: %s", idpStateQs)
  182. }
  183. var body jsonutils.JSONObject
  184. var err error
  185. switch req.Method {
  186. case "GET":
  187. body, err = jsonutils.ParseQueryString(req.URL.RawQuery)
  188. if err != nil {
  189. httperrors.InputParameterError(ctx, w, "parse query string error: %s", err)
  190. return
  191. }
  192. case "POST":
  193. formData, err := appsrv.Fetch(req)
  194. if err != nil {
  195. httperrors.InputParameterError(ctx, w, "fetch form data error: %s", err)
  196. }
  197. body, err = jsonutils.ParseQueryString(string(formData))
  198. if err != nil {
  199. httperrors.InputParameterError(ctx, w, "parse form data error: %s", err)
  200. return
  201. }
  202. default:
  203. httperrors.InputParameterError(ctx, w, "invalid request")
  204. return
  205. }
  206. body.(*jsonutils.JSONDict).Set("idp_id", jsonutils.NewString(idpId))
  207. body.(*jsonutils.JSONDict).Set("idp_driver", jsonutils.NewString(idpDriver))
  208. body.(*jsonutils.JSONDict).Set("idp_state", jsonutils.NewString(idpState))
  209. appsrv.DisableClientCache(w)
  210. var referer string
  211. var idpUserId string
  212. if len(idpLinkUser) > 0 {
  213. // link with existing user
  214. err = linkWithExistingUser(ctx, req, idpId, idpLinkUser, body)
  215. referer = getSsoLinkCallbackUrl()
  216. } else {
  217. // ordinary login
  218. err = h.doLogin(ctx, w, req, body)
  219. if err != nil {
  220. if errors.Cause(err) == httperrors.ErrUserNotFound {
  221. idpUserId = findExtUserId(err.Error())
  222. if len(idpUserId) == 0 {
  223. err = httputils.NewJsonClientError(400, string(httperrors.ErrInputParameter), "empty external user id")
  224. } else {
  225. referer = getSsoUserNotFoundCallbackUrl()
  226. }
  227. }
  228. }
  229. if referer == "" {
  230. referer = getSsoAuthCallbackUrl()
  231. }
  232. }
  233. refererUrl, _ := url.Parse(referer)
  234. if refererUrl == nil && len(idpReferer) > 0 {
  235. refererUrl, _ = url.Parse(idpReferer)
  236. }
  237. if refererUrl == nil {
  238. httperrors.InvalidInputError(ctx, w, "empty referer link")
  239. return
  240. }
  241. redirUrl := generateRedirectUrl(refererUrl, idpStateQs, err, idpId, idpUserId)
  242. appsrv.SendRedirect(w, redirUrl)
  243. }
  244. func generateRedirectUrl(originUrl *url.URL, stateQs jsonutils.JSONObject, err error, idpId, idpUserId string) string {
  245. var qs jsonutils.JSONObject
  246. if len(originUrl.RawQuery) > 0 {
  247. qs, _ = jsonutils.ParseQueryString(originUrl.RawQuery)
  248. } else {
  249. qs = jsonutils.NewDict()
  250. }
  251. qs.(*jsonutils.JSONDict).Update(stateQs)
  252. if err != nil {
  253. var errCls, errDetails string
  254. switch je := err.(type) {
  255. case *httputils.JSONClientError:
  256. errCls = je.Class
  257. errDetails = je.Details
  258. default:
  259. errCls = errors.Cause(err).Error()
  260. errDetails = err.Error()
  261. }
  262. msgLen := 100
  263. if len(errDetails) > msgLen {
  264. errDetails = errDetails[:msgLen] + "..."
  265. }
  266. qs.(*jsonutils.JSONDict).Add(jsonutils.NewString(errCls), "error_class")
  267. qs.(*jsonutils.JSONDict).Add(jsonutils.NewString(errDetails), "error_details")
  268. qs.(*jsonutils.JSONDict).Add(jsonutils.NewString("error"), "result")
  269. if len(idpUserId) > 0 {
  270. qs.(*jsonutils.JSONDict).Add(jsonutils.NewString(idpId), "idp_id")
  271. qs.(*jsonutils.JSONDict).Add(jsonutils.NewString(idpUserId), "idp_entity_id")
  272. }
  273. } else {
  274. qs.(*jsonutils.JSONDict).Add(jsonutils.NewString("success"), "result")
  275. }
  276. originUrl.RawQuery = qs.QueryString()
  277. return originUrl.String()
  278. }
  279. func processSsoLoginData(body jsonutils.JSONObject, cliIp string, redirectUri string) (mcclient.TokenCredential, error) {
  280. var token mcclient.TokenCredential
  281. var err error
  282. idpDriver, _ := body.GetString("idp_driver")
  283. idpId, _ := body.GetString("idp_id")
  284. idpState, _ := body.GetString("idp_state")
  285. switch idpDriver {
  286. case api.IdentityDriverCAS:
  287. ticket, _ := body.GetString("ticket")
  288. if len(ticket) == 0 {
  289. return nil, httperrors.NewMissingParameterError("ticket")
  290. }
  291. token, err = auth.Client().AuthenticateCAS(idpId, ticket, redirectUri, "", "", "", cliIp)
  292. case api.IdentityDriverSAML:
  293. samlResp, _ := body.GetString("SAMLResponse")
  294. relayState, _ := body.GetString("RelayState")
  295. if relayState != idpState {
  296. return nil, errors.Wrap(httperrors.ErrInputParameter, "state inconsistent")
  297. }
  298. if len(samlResp) == 0 {
  299. return nil, errors.Wrap(httperrors.ErrMissingParameter, "SAMLResponse")
  300. }
  301. token, err = auth.Client().AuthenticateSAML(idpId, samlResp, "", "", "", cliIp)
  302. case api.IdentityDriverOIDC:
  303. code, _ := body.GetString("code")
  304. state, _ := body.GetString("state")
  305. if state != idpState {
  306. return nil, errors.Wrap(httperrors.ErrInputParameter, "state inconsistent")
  307. }
  308. if len(code) == 0 {
  309. return nil, errors.Wrap(httperrors.ErrMissingParameter, "code")
  310. }
  311. token, err = auth.Client().AuthenticateOIDC(idpId, code, redirectUri, "", "", "", cliIp)
  312. case api.IdentityDriverOAuth2:
  313. state, _ := body.GetString("state")
  314. if state != idpState {
  315. return nil, errors.Wrap(httperrors.ErrInputParameter, "state inconsistent")
  316. }
  317. code, _ := body.GetString("code")
  318. if len(code) == 0 {
  319. code, _ = body.GetString("auth_code")
  320. if len(code) == 0 {
  321. return nil, errors.Wrap(httperrors.ErrMissingParameter, "code")
  322. }
  323. }
  324. token, err = auth.Client().AuthenticateOAuth2(idpId, code, "", "", "", cliIp)
  325. default:
  326. return nil, errors.Wrapf(httperrors.ErrNotSupported, "SSO driver %s not supported", idpDriver)
  327. }
  328. return token, err
  329. }
  330. func linkWithExistingUser(ctx context.Context, req *http.Request, idpId, idpLinkUser string, body jsonutils.JSONObject) error {
  331. t, _, _ := fetchAuthInfo(ctx, req)
  332. if t == nil {
  333. return errors.Wrap(httperrors.ErrInvalidCredential, "invalid credential")
  334. }
  335. if t.GetUserId() != idpLinkUser {
  336. return errors.Wrap(httperrors.ErrConflict, "link user id inconsistent with credential")
  337. }
  338. cliIp := netutils2.GetHttpRequestIp(req)
  339. redirectUri := getSsoCallbackUrl(ctx, req, idpId)
  340. ntoken, err := processSsoLoginData(body, cliIp, redirectUri)
  341. if err != nil {
  342. if errors.Cause(err) != httperrors.ErrUserNotFound {
  343. return errors.Wrap(err, "invalid ssologin result")
  344. }
  345. log.Debugf("error: %s", err)
  346. // not linked, link with user
  347. // fetch userId
  348. jsonErr := err.(*httputils.JSONClientError)
  349. extUserId := findExtUserId(jsonErr.Details)
  350. if len(extUserId) == 0 {
  351. return errors.Wrap(httperrors.ErrInputParameter, "empty external user id")
  352. }
  353. linkInput := api.UserLinkIdpInput{
  354. IdpId: idpId,
  355. IdpEntityId: extUserId,
  356. }
  357. s := auth.GetAdminSession(ctx, FetchRegion(req))
  358. _, err = modules.UsersV3.PerformAction(s, t.GetUserId(), "link-idp", jsonutils.Marshal(linkInput))
  359. if err != nil {
  360. return errors.Wrap(err, "link-idp")
  361. }
  362. } else {
  363. if ntoken.GetUserId() != t.GetUserId() {
  364. return errors.Wrap(httperrors.ErrConflict, "link user id inconsistent with credential")
  365. }
  366. }
  367. return nil
  368. }
  369. func handleUnlinkIdp(ctx context.Context, w http.ResponseWriter, req *http.Request) {
  370. t := AppContextToken(ctx)
  371. body, err := appsrv.FetchJSON(req)
  372. if err != nil {
  373. httperrors.GeneralServerError(ctx, w, err)
  374. return
  375. }
  376. idpId, _ := body.GetString("idp_id")
  377. idpEntityId, _ := body.GetString("idp_entity_id")
  378. if len(idpId) == 0 || len(idpEntityId) == 0 {
  379. httperrors.InputParameterError(ctx, w, "empty idp_id or idp_entity_id")
  380. return
  381. }
  382. s := auth.GetAdminSession(ctx, FetchRegion(req))
  383. input := api.UserUnlinkIdpInput{
  384. IdpId: idpId,
  385. IdpEntityId: idpEntityId,
  386. }
  387. _, err = modules.UsersV3.PerformAction(s, t.GetUserId(), "unlink-idp", jsonutils.Marshal(input))
  388. if err != nil {
  389. httperrors.GeneralServerError(ctx, w, err)
  390. return
  391. }
  392. appsrv.Send(w, "")
  393. }
  394. func fetchIdpBasicConfig(ctx context.Context, w http.ResponseWriter, req *http.Request) {
  395. params := appctx.AppContextParams(ctx)
  396. idpId := params["<idp_id>"]
  397. info, err := getIdpBasicConfig(ctx, req, idpId)
  398. if err != nil {
  399. httperrors.GeneralServerError(ctx, w, err)
  400. return
  401. }
  402. appsrv.SendJSON(w, info)
  403. }
  404. func getIdpBasicConfig(ctx context.Context, req *http.Request, idpId string) (jsonutils.JSONObject, error) {
  405. s := auth.GetAdminSession(ctx, FetchRegion(req))
  406. baseUrl := getSsoBaseCallbackUrl()
  407. input := api.GetIdpSsoCallbackUriInput{
  408. RedirectUri: baseUrl,
  409. }
  410. resp, err := modules.IdentityProviders.GetSpecific(s, idpId, "sso-callback-uri", jsonutils.Marshal(input))
  411. if err != nil {
  412. return nil, errors.Wrap(err, "GetSpecific sso-callback-uri")
  413. }
  414. redir, _ := resp.GetString("redirect_uri")
  415. idpDriver, _ := resp.GetString("driver")
  416. info := jsonutils.NewDict()
  417. switch idpDriver {
  418. case api.IdentityDriverSQL:
  419. case api.IdentityDriverLDAP:
  420. case api.IdentityDriverCAS:
  421. info.Add(jsonutils.NewString(redir), "redirect_uri")
  422. case api.IdentityDriverSAML:
  423. info.Add(jsonutils.NewString(options.Options.ApiServer), "entity_id")
  424. info.Add(jsonutils.NewString(redir), "redirect_uri")
  425. case api.IdentityDriverOIDC:
  426. info.Add(jsonutils.NewString(redir), "redirect_uri")
  427. case api.IdentityDriverOAuth2:
  428. info.Add(jsonutils.NewString(redir), "redirect_uri")
  429. default:
  430. }
  431. return info, nil
  432. }
  433. func fetchIdpSAMLMetadata(ctx context.Context, w http.ResponseWriter, req *http.Request) {
  434. s := auth.GetAdminSession(ctx, FetchRegion(req))
  435. params := appctx.AppContextParams(ctx)
  436. idpId := params["<idp_id>"]
  437. query := jsonutils.NewDict()
  438. query.Set("redirect_uri", jsonutils.NewString(getSsoCallbackUrl(ctx, req, idpId)))
  439. md, err := modules.IdentityProviders.GetSpecific(s, idpId, "saml-metadata", query)
  440. if err != nil {
  441. httperrors.GeneralServerError(ctx, w, err)
  442. return
  443. }
  444. appsrv.SendJSON(w, md)
  445. }