oidc.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  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. "encoding/binary"
  19. "fmt"
  20. "io/ioutil"
  21. "net/http"
  22. "net/url"
  23. "strings"
  24. "time"
  25. "github.com/lestrrat-go/jwx/jwa"
  26. "github.com/lestrrat-go/jwx/jwt"
  27. "yunion.io/x/jsonutils"
  28. "yunion.io/x/log"
  29. "yunion.io/x/pkg/errors"
  30. "yunion.io/x/pkg/util/httputils"
  31. "yunion.io/x/pkg/util/netutils"
  32. "yunion.io/x/onecloud/pkg/apigateway/clientman"
  33. "yunion.io/x/onecloud/pkg/apigateway/options"
  34. "yunion.io/x/onecloud/pkg/appsrv"
  35. "yunion.io/x/onecloud/pkg/httperrors"
  36. "yunion.io/x/onecloud/pkg/mcclient"
  37. "yunion.io/x/onecloud/pkg/mcclient/auth"
  38. modules "yunion.io/x/onecloud/pkg/mcclient/modules/identity"
  39. "yunion.io/x/onecloud/pkg/util/netutils2"
  40. "yunion.io/x/onecloud/pkg/util/oidcutils"
  41. )
  42. const (
  43. // OIDC code expires in 5 minutes
  44. OIDC_CODE_EXPIRE_SECONDS = 300
  45. // OIDC token expires in 2 hours
  46. OIDC_TOKEN_EXPIRE_SECONDS = 7200
  47. )
  48. func getLoginCallbackParam() string {
  49. if options.Options.LoginCallbackParam == "" {
  50. return "rf"
  51. }
  52. return options.Options.LoginCallbackParam
  53. }
  54. func addQuery(urlstr string, qs jsonutils.JSONObject) string {
  55. qsPos := strings.LastIndexByte(urlstr, '?')
  56. if qsPos < 0 {
  57. return fmt.Sprintf("%s?%s", urlstr, qs.QueryString())
  58. }
  59. oldQs, _ := jsonutils.ParseQueryString(urlstr[qsPos+1:])
  60. if oldQs != nil {
  61. oldQs.(*jsonutils.JSONDict).Update(qs)
  62. return fmt.Sprintf("%s?%s", urlstr[:qsPos], oldQs.QueryString())
  63. } else {
  64. return fmt.Sprintf("%s?%s", urlstr[:qsPos], qs.QueryString())
  65. }
  66. }
  67. func handleOIDCAuth(ctx context.Context, w http.ResponseWriter, req *http.Request) {
  68. ctx, err := fetchAndSetAuthContext(ctx, w, req)
  69. if err != nil {
  70. // not login redirect to login page
  71. qs := jsonutils.NewDict()
  72. oUrl := req.URL.String()
  73. if !strings.HasPrefix(oUrl, "http") {
  74. oUrl = httputils.JoinPath(options.Options.ApiServer, oUrl)
  75. }
  76. qs.Set(getLoginCallbackParam(), jsonutils.NewString(oUrl))
  77. loginUrl := addQuery(getSsoAuthCallbackUrl(), qs)
  78. appsrv.SendRedirect(w, loginUrl)
  79. return
  80. }
  81. query, _ := jsonutils.ParseQueryString(req.URL.RawQuery)
  82. auth, code, err := doOIDCAuth(ctx, req, query)
  83. if err != nil {
  84. qs := jsonutils.NewDict()
  85. qs.Set("error", jsonutils.NewString(errors.Cause(err).Error()))
  86. qs.Set("error_description", jsonutils.NewString(err.Error()))
  87. errorUrl := addQuery(auth.RedirectUri, qs)
  88. appsrv.SendRedirect(w, errorUrl)
  89. return
  90. }
  91. qs := jsonutils.NewDict()
  92. qs.Set("code", jsonutils.NewString(code))
  93. qs.Set("state", jsonutils.NewString(auth.State))
  94. redirUrl := addQuery(auth.RedirectUri, qs)
  95. appsrv.DisableClientCache(w)
  96. appsrv.SendRedirect(w, redirUrl)
  97. }
  98. func fetchOIDCCredential(ctx context.Context, req *http.Request, clientId string) (modules.SOpenIDConnectCredential, error) {
  99. var oidcSecret modules.SOpenIDConnectCredential
  100. s := auth.GetAdminSession(ctx, FetchRegion(req))
  101. secret, err := modules.Credentials.GetById(s, clientId, nil)
  102. if err != nil {
  103. return oidcSecret, errors.Wrap(err, "Request Credential")
  104. }
  105. oidcSecret, err = modules.DecodeOIDCSecret(secret)
  106. if err != nil {
  107. return oidcSecret, errors.Wrap(err, "DecodeOIDCSecret")
  108. }
  109. return oidcSecret, nil
  110. }
  111. func doOIDCAuth(ctx context.Context, req *http.Request, query jsonutils.JSONObject) (oidcutils.SOIDCAuthRequest, string, error) {
  112. oidcAuth := oidcutils.SOIDCAuthRequest{}
  113. if query == nil {
  114. return oidcAuth, "", errors.Wrap(httperrors.ErrInputParameter, "empty query string")
  115. }
  116. err := query.Unmarshal(&oidcAuth)
  117. if err != nil {
  118. return oidcAuth, "", errors.Wrap(httperrors.ErrInputParameter, "unmarshal request parameter fail")
  119. }
  120. if oidcAuth.ResponseType != oidcutils.OIDC_RESPONSE_TYPE_CODE {
  121. return oidcAuth, "", errors.Wrapf(httperrors.ErrInputParameter, "invalid resposne type %s", oidcAuth.ResponseType)
  122. }
  123. oidcSecret, err := fetchOIDCCredential(ctx, req, oidcAuth.ClientId)
  124. if err != nil {
  125. return oidcAuth, "", errors.Wrap(err, "fetchOIDCCredential")
  126. }
  127. if oidcSecret.RedirectUri != oidcAuth.RedirectUri {
  128. return oidcAuth, "", errors.Wrap(httperrors.ErrInvalidCredential, "redirect uri not match")
  129. }
  130. token := AppContextToken(ctx)
  131. cliIp := netutils2.GetHttpRequestIp(req)
  132. codeInfo := newOIDCClientInfo(token, cliIp, FetchRegion(req))
  133. code := clientman.EncryptString(codeInfo.toBytes())
  134. return oidcAuth, code, nil
  135. }
  136. func handleOIDCToken(ctx context.Context, w http.ResponseWriter, req *http.Request) {
  137. resp, err := validateOIDCToken(ctx, req)
  138. if err != nil {
  139. httperrors.GeneralServerError(ctx, w, err)
  140. return
  141. }
  142. appsrv.SendJSON(w, jsonutils.Marshal(resp))
  143. return
  144. }
  145. type SOIDCClientInfo struct {
  146. Timestamp int64
  147. Ip netutils.IPV4Addr
  148. UserId string
  149. ProjectId string
  150. Region string
  151. }
  152. func (i SOIDCClientInfo) toBytes() []byte {
  153. enc := make([]byte, 12+1+len(i.UserId)+1+len(i.ProjectId)+len(i.Region))
  154. binary.LittleEndian.PutUint64(enc, uint64(i.Timestamp))
  155. binary.LittleEndian.PutUint32(enc[8:], uint32(i.Ip))
  156. enc[12] = byte(len(i.UserId))
  157. enc[13] = byte(len(i.ProjectId))
  158. copy(enc[14:], i.UserId)
  159. copy(enc[14+len(i.UserId):], i.ProjectId)
  160. copy(enc[14+len(i.UserId)+len(i.ProjectId):], i.Region)
  161. return enc
  162. }
  163. func (i SOIDCClientInfo) isExpired() bool {
  164. if time.Now().UnixNano()-i.Timestamp > OIDC_CODE_EXPIRE_SECONDS*1000000000 {
  165. return true
  166. }
  167. return false
  168. }
  169. func (i SOIDCClientInfo) expiresAt(secs int) time.Time {
  170. expires := i.Timestamp + int64(secs)*int64(time.Second)
  171. esecs := expires / int64(time.Second)
  172. nsecs := expires - esecs*int64(time.Second)
  173. return time.Unix(esecs, nsecs)
  174. }
  175. func decodeOIDCClientInfo(enc []byte) (SOIDCClientInfo, error) {
  176. info := SOIDCClientInfo{}
  177. if len(enc) < 8+4+1 {
  178. return info, errors.Wrap(httperrors.ErrInvalidCredential, "code byte length must be 12")
  179. }
  180. info.Timestamp = int64(binary.LittleEndian.Uint64(enc))
  181. info.Ip = netutils.IPV4Addr(binary.LittleEndian.Uint32(enc[8:]))
  182. info.UserId = string(enc[14 : 14+int(enc[12])])
  183. info.ProjectId = string(enc[14+int(enc[12]) : 14+int(enc[12])+int(enc[13])])
  184. info.Region = string(enc[14+int(enc[12])+int(enc[13]):])
  185. return info, nil
  186. }
  187. func newOIDCClientInfo(token mcclient.TokenCredential, ipstr string, region string) SOIDCClientInfo {
  188. info := SOIDCClientInfo{}
  189. info.Timestamp = time.Now().UnixNano()
  190. info.Ip, _ = netutils.NewIPV4Addr(ipstr)
  191. info.UserId = token.GetUserId()
  192. info.ProjectId = token.GetProjectId()
  193. info.Region = region
  194. return info
  195. }
  196. type SOIDCClientToken struct {
  197. Info SOIDCClientInfo
  198. }
  199. func (t SOIDCClientToken) encode() string {
  200. json := jsonutils.NewDict()
  201. json.Add(jsonutils.NewString(string(t.Info.toBytes())), "info")
  202. return clientman.EncryptString([]byte(json.String()))
  203. }
  204. func decodeOIDCClientToken(token string) (SOIDCClientToken, error) {
  205. ret := SOIDCClientToken{}
  206. tBytes, err := clientman.DecryptString(token)
  207. if err != nil {
  208. return ret, errors.Wrap(err, "DecryptString")
  209. }
  210. json, err := jsonutils.Parse(tBytes)
  211. if err != nil {
  212. return ret, errors.Wrap(err, "json.Parse")
  213. }
  214. info, err := json.GetString("info")
  215. if err != nil {
  216. return ret, errors.Wrap(err, "getString(info)")
  217. }
  218. ret.Info, err = decodeOIDCClientInfo([]byte(info))
  219. if err != nil {
  220. return ret, errors.Wrap(err, "decodeOIDCClientInfo")
  221. }
  222. return ret, nil
  223. }
  224. func validateOIDCToken(ctx context.Context, req *http.Request) (oidcutils.SOIDCAccessTokenResponse, error) {
  225. var tokenResp oidcutils.SOIDCAccessTokenResponse
  226. bodyBytes, err := appsrv.Fetch(req)
  227. if err != nil {
  228. return tokenResp, errors.Wrap(err, "Fetch Body")
  229. }
  230. log.Debugf("validateOIDCToken body: %s", string(bodyBytes))
  231. bodyJson, err := jsonutils.ParseQueryString(string(bodyBytes))
  232. if err != nil {
  233. return tokenResp, errors.Wrap(err, "Decode body form data")
  234. }
  235. authReq := oidcutils.SOIDCAccessTokenRequest{}
  236. err = bodyJson.Unmarshal(&authReq)
  237. if err != nil {
  238. return tokenResp, errors.Wrap(err, "Unmarshal Access Token Request")
  239. }
  240. if authReq.GrantType != oidcutils.OIDC_REQUEST_GRANT_TYPE {
  241. return tokenResp, errors.Wrapf(httperrors.ErrInvalidCredential, "invalid grant type %s", authReq.GrantType)
  242. }
  243. codeTimeBytes, err := clientman.DecryptString(authReq.Code)
  244. if err != nil {
  245. return tokenResp, errors.Wrapf(httperrors.ErrInvalidCredential, "invalid code %s", authReq.Code)
  246. }
  247. codeInfo, err := decodeOIDCClientInfo(codeTimeBytes)
  248. if err != nil {
  249. return tokenResp, errors.Wrap(httperrors.ErrInvalidCredential, "fail to decode code")
  250. }
  251. if codeInfo.isExpired() {
  252. return tokenResp, errors.Wrapf(httperrors.ErrInvalidCredential, "code expires")
  253. }
  254. authStr := req.Header.Get("Authorization")
  255. log.Debugf("Authorization: %s", authStr)
  256. authParts := strings.Split(string(authStr), " ")
  257. if len(authParts) != 2 {
  258. return tokenResp, errors.Wrap(httperrors.ErrInvalidCredential, "illegal authorization header")
  259. }
  260. if authParts[0] != "Basic" {
  261. return tokenResp, errors.Wrapf(httperrors.ErrInvalidCredential, "unsupport auth method %s, only Basic supported", authParts)
  262. }
  263. authBytes, err := base64.StdEncoding.DecodeString(authParts[1])
  264. if err != nil {
  265. return tokenResp, errors.Wrap(err, "Decode Authorization Header")
  266. }
  267. log.Debugf("Authorization basic: %s", string(authBytes))
  268. authParts = strings.Split(string(authBytes), ":")
  269. if len(authParts) != 2 {
  270. return tokenResp, errors.Wrap(httperrors.ErrInvalidCredential, "illegal authorization header")
  271. }
  272. clientId, _ := url.QueryUnescape(authParts[0])
  273. clientSecret, _ := url.QueryUnescape(authParts[1])
  274. log.Debugf("clientId %s clientSecret: %s authReq.ClientId %s", clientId, clientSecret, authReq.ClientId)
  275. oidcSecret, err := fetchOIDCCredential(ctx, req, clientId)
  276. if err != nil {
  277. return tokenResp, errors.Wrap(err, "fetchOIDCCredential")
  278. }
  279. if oidcSecret.RedirectUri != authReq.RedirectUri {
  280. return tokenResp, errors.Wrap(httperrors.ErrInvalidCredential, "redirect uri not match")
  281. }
  282. if oidcSecret.Secret != clientSecret {
  283. return tokenResp, errors.Wrap(httperrors.ErrInvalidCredential, "client secret not match")
  284. }
  285. token := SOIDCClientToken{
  286. Info: codeInfo,
  287. }
  288. tokenResp = token2AccessTokenResponse(token, clientId)
  289. return tokenResp, nil
  290. }
  291. func token2AccessTokenResponse(token SOIDCClientToken, clientId string) oidcutils.SOIDCAccessTokenResponse {
  292. resp := oidcutils.SOIDCAccessTokenResponse{}
  293. resp.AccessToken = token.encode()
  294. resp.TokenType = oidcutils.OIDC_BEARER_TOKEN_TYPE
  295. resp.IdToken, _ = token2IdToken(token, clientId)
  296. resp.ExpiresIn = int(token.Info.expiresAt(OIDC_TOKEN_EXPIRE_SECONDS).Unix() - time.Now().Unix())
  297. return resp
  298. }
  299. func token2IdToken(token SOIDCClientToken, clientId string) (string, error) {
  300. jwtToken := jwt.New()
  301. jwtToken.Set(jwt.IssuerKey, options.Options.ApiServer)
  302. jwtToken.Set(jwt.SubjectKey, token.Info.UserId)
  303. jwtToken.Set(jwt.AudienceKey, clientId)
  304. jwtToken.Set(jwt.ExpirationKey, token.Info.expiresAt(OIDC_TOKEN_EXPIRE_SECONDS).Unix())
  305. jwtToken.Set(jwt.IssuedAtKey, time.Now().Unix())
  306. return clientman.SignJWT(jwtToken)
  307. }
  308. func handleOIDCConfiguration(ctx context.Context, w http.ResponseWriter, req *http.Request) {
  309. authUrl := httputils.JoinPath(options.Options.ApiServer, "api/v1/auth/oidc/auth")
  310. tokenUrl := httputils.JoinPath(options.Options.ApiServer, "api/v1/auth/oidc/token")
  311. userinfoUrl := httputils.JoinPath(options.Options.ApiServer, "api/v1/auth/oidc/user")
  312. logoutUrl := httputils.JoinPath(options.Options.ApiServer, "api/v1/auth/oidc/logout")
  313. jwksUrl := httputils.JoinPath(options.Options.ApiServer, "api/v1/auth/oidc/keys")
  314. conf := oidcutils.SOIDCConfiguration{
  315. Issuer: httputils.JoinPath(options.Options.ApiServer, "api/v1/auth/oidc"),
  316. AuthorizationEndpoint: authUrl,
  317. TokenEndpoint: tokenUrl,
  318. UserinfoEndpoint: userinfoUrl,
  319. EndSessionEndpoint: logoutUrl,
  320. JwksUri: jwksUrl,
  321. ResponseTypesSupported: []string{
  322. oidcutils.OIDC_RESPONSE_TYPE_CODE,
  323. },
  324. SubjectTypesSupported: []string{
  325. "public",
  326. },
  327. IdTokenSigningAlgValuesSupported: []string{
  328. string(jwa.RS256),
  329. },
  330. ScopesSupported: []string{
  331. "user",
  332. "profile",
  333. },
  334. TokenEndpointAuthMethodsSupported: []string{
  335. "client_secret_basic",
  336. },
  337. ClaimsSupported: []string{
  338. jwt.IssuerKey,
  339. jwt.SubjectKey,
  340. jwt.AudienceKey,
  341. jwt.ExpirationKey,
  342. jwt.IssuedAtKey,
  343. },
  344. }
  345. appsrv.SendJSON(w, jsonutils.Marshal(conf))
  346. }
  347. func handleOIDCJWKeys(ctx context.Context, w http.ResponseWriter, req *http.Request) {
  348. keyJson, err := clientman.GetJWKs(ctx)
  349. if err != nil {
  350. httperrors.GeneralServerError(ctx, w, err)
  351. return
  352. }
  353. appsrv.SendJSON(w, keyJson)
  354. }
  355. func handleOIDCUserInfo(ctx context.Context, w http.ResponseWriter, req *http.Request) {
  356. tokenHdr := getAuthToken(req)
  357. if len(tokenHdr) == 0 {
  358. httperrors.InvalidCredentialError(ctx, w, "No token in header")
  359. return
  360. }
  361. token, err := decodeOIDCClientToken(tokenHdr)
  362. if err != nil {
  363. log.Errorf("decodeOIDCClientToken %s fail %s", tokenHdr, err)
  364. httperrors.InvalidCredentialError(ctx, w, "Token in header invalid")
  365. return
  366. }
  367. if token.Info.expiresAt(OIDC_TOKEN_EXPIRE_SECONDS).Before(time.Now()) {
  368. httperrors.InvalidCredentialError(ctx, w, "Token expired")
  369. return
  370. }
  371. s := auth.GetAdminSession(ctx, token.Info.Region)
  372. data, err := getUserInfo2(s, token.Info.UserId, token.Info.ProjectId, token.Info.Ip.String())
  373. if err != nil {
  374. httperrors.NotFoundError(ctx, w, "%v", err)
  375. return
  376. }
  377. appsrv.SendJSON(w, data)
  378. }
  379. type SOIDCRPInitLogoutRequest struct {
  380. // RECOMMENDED. ID Token previously issued by the OP to the RP passed to the Logout Endpoint
  381. // as a hint about the End-User's current authenticated session with the Client. This is used
  382. // as an indication of the identity of the End-User that the RP is requesting be logged out by the OP.
  383. IdTokenHint string `json:"id_token_hint"`
  384. // OPTIONAL. Hint to the Authorization Server about the End-User that is logging out. The value
  385. // and meaning of this parameter is left up to the OP's discretion. For instance, the value might
  386. // contain an email address, phone number, username, or session identifier pertaining to the RP's
  387. // session with the OP for the End-User. (This parameter is intended to be analogous to the
  388. // login_hint parameter defined in Section 3.1.2.1 of OpenID Connect Core 1.0 [OpenID.Core] that
  389. // is used in Authentication Requests; whereas, logout_hint is used in RP-Initiated Logout Requests.)
  390. LogoutHint string `json:"logout_hint"`
  391. // OPTIONAL. OAuth 2.0 Client Identifier valid at the Authorization Server. When both client_id and
  392. // id_token_hint are present, the OP MUST verify that the Client Identifier matches the one used when
  393. // issuing the ID Token. The most common use case for this parameter is to specify the Client Identifier
  394. // when post_logout_redirect_uri is used but id_token_hint is not. Another use is for symmetrically
  395. // encrypted ID Tokens used as id_token_hint values that require the Client Identifier to be specified
  396. // by other means, so that the ID Tokens can be decrypted by the OP.
  397. ClientId string `json:"client_id"`
  398. // OPTIONAL. URI to which the RP is requesting that the End-User's User Agent be redirected after a
  399. // logout has been performed. This URI SHOULD use the https scheme and MAY contain port, path, and
  400. // query parameter components; however, it MAY use the http scheme, provided that the Client Type is
  401. // confidential, as defined in Section 2.1 of OAuth 2.0 [RFC6749], and provided the OP allows the use
  402. // of http RP URIs. The URI MAY use an alternate scheme, such as one that is intended to identify a
  403. // callback into a native application. The value MUST have been previously registered with the OP,
  404. // either using the post_logout_redirect_uris Registration parameter or via another mechanism. An
  405. // id_token_hint is also RECOMMENDED when this parameter is included.
  406. PostLogoutRedirectUri string `json:"post_logout_redirect_uri"`
  407. // OPTIONAL. Opaque value used by the RP to maintain state between the logout request and the callback
  408. // to the endpoint specified by the post_logout_redirect_uri parameter. If included in the logout request,
  409. // the OP passes this value back to the RP using the state parameter when redirecting the User Agent back to the RP.
  410. State string `json:"state"`
  411. // OPTIONAL. End-User's preferred languages and scripts for the user interface, represented as a
  412. // space-separated list of BCP47 [RFC5646] language tag values, ordered by preference. For instance,
  413. // the value "fr-CA fr en" represents a preference for French as spoken in Canada, then French (without
  414. // a region designation), followed by English (without a region designation). An error SHOULD NOT result
  415. // if some or all of the requested locales are not supported by the OpenID Provider.
  416. UiLocales string `json:"ui_locales"`
  417. }
  418. func handleOIDCRPInitLogout(ctx context.Context, w http.ResponseWriter, req *http.Request) {
  419. params, err := fetchOIDCRPInitLogoutParam(req)
  420. if err != nil {
  421. httperrors.GeneralServerError(ctx, w, err)
  422. return
  423. }
  424. doLogout(ctx, w, req)
  425. var redirUrl string
  426. if len(params.PostLogoutRedirectUri) > 0 {
  427. redirUrl = params.PostLogoutRedirectUri
  428. if len(params.State) > 0 {
  429. redirUrl = addQuery(redirUrl, jsonutils.Marshal(map[string]string{"state": params.State}))
  430. }
  431. } else {
  432. redirUrl = getSsoAuthCallbackUrl()
  433. }
  434. appsrv.SendRedirect(w, redirUrl)
  435. }
  436. func fetchOIDCRPInitLogoutParam(req *http.Request) (*SOIDCRPInitLogoutRequest, error) {
  437. var qs string
  438. if req.Method == "GET" {
  439. qs = req.URL.RawQuery
  440. } else if req.Method == "POST" {
  441. b, err := req.GetBody()
  442. if err != nil {
  443. return nil, errors.Wrap(err, "GetBody")
  444. }
  445. defer b.Close()
  446. qsBytes, err := ioutil.ReadAll(b)
  447. if err != nil {
  448. return nil, errors.Wrap(err, "ioutil.ReadAll")
  449. }
  450. qs = string(qsBytes)
  451. }
  452. params := SOIDCRPInitLogoutRequest{}
  453. if len(qs) == 0 {
  454. return &params, nil
  455. }
  456. qsJson, err := jsonutils.ParseQueryString(qs)
  457. if err != nil {
  458. return nil, errors.Wrap(err, "jsonutils.ParseQueryString")
  459. }
  460. err = qsJson.Unmarshal(&params)
  461. if err != nil {
  462. return nil, errors.Wrap(err, "qsJson.Unmarshal")
  463. }
  464. return &params, nil
  465. }