client.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  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 client
  15. import (
  16. "context"
  17. "encoding/base64"
  18. "fmt"
  19. "net/http"
  20. "net/url"
  21. "strings"
  22. "time"
  23. "github.com/lestrrat/go-jwx/jwk"
  24. "yunion.io/x/jsonutils"
  25. "yunion.io/x/log"
  26. "yunion.io/x/pkg/errors"
  27. "yunion.io/x/pkg/util/httputils"
  28. "yunion.io/x/onecloud/pkg/httperrors"
  29. "yunion.io/x/onecloud/pkg/util/oidcutils"
  30. )
  31. type SOIDCClient struct {
  32. clientId string
  33. secret string
  34. timeout time.Duration
  35. isDebug bool
  36. config oidcutils.SOIDCConfiguration
  37. httpclient *http.Client
  38. keySet *jwk.Set
  39. }
  40. func NewOIDCClient(clientId string, secret string, timeoutSeconds int, isDebug bool) *SOIDCClient {
  41. cli := SOIDCClient{
  42. clientId: clientId,
  43. secret: secret,
  44. timeout: time.Duration(timeoutSeconds) * time.Second,
  45. isDebug: isDebug,
  46. }
  47. cli.httpclient = httputils.GetClient(true, cli.timeout)
  48. return &cli
  49. }
  50. const (
  51. WELL_KNOWN_OIDC_CONFIG_PATH = ".well-known/openid-configuration"
  52. )
  53. func (cli *SOIDCClient) FetchConfiguration(ctx context.Context, endpoint string) error {
  54. path := httputils.JoinPath(endpoint, WELL_KNOWN_OIDC_CONFIG_PATH)
  55. _, resp, err := httputils.JSONRequest(cli.httpclient, ctx, httputils.GET, path, nil, nil, cli.isDebug)
  56. if err != nil {
  57. return errors.Wrap(err, "fetch well-known oidc configuration")
  58. }
  59. err = resp.Unmarshal(&cli.config)
  60. if err != nil {
  61. return errors.Wrap(err, "unmarshal oidc configuration")
  62. }
  63. return nil
  64. }
  65. func (cli *SOIDCClient) SetConfig(authUrl, tokenUrl, userinfoUrl string, scopes []string) {
  66. cli.config = oidcutils.SOIDCConfiguration{
  67. AuthorizationEndpoint: authUrl,
  68. TokenEndpoint: tokenUrl,
  69. UserinfoEndpoint: userinfoUrl,
  70. ScopesSupported: scopes,
  71. }
  72. }
  73. func (cli *SOIDCClient) GetConfig() oidcutils.SOIDCConfiguration {
  74. return cli.config
  75. }
  76. func (cli *SOIDCClient) FetchJWKS(ctx context.Context) error {
  77. if len(cli.config.JwksUri) == 0 {
  78. return errors.Wrap(httperrors.ErrInvalidStatus, "no valid jwks_uri")
  79. }
  80. _, resp, err := httputils.JSONRequest(cli.httpclient, ctx, httputils.GET, cli.config.JwksUri, nil, nil, cli.isDebug)
  81. if err != nil {
  82. return errors.Wrap(err, "fetch jwks_uri")
  83. }
  84. set, err := jwk.ParseString(resp.String())
  85. if err != nil {
  86. return errors.Wrap(err, "parse JWK")
  87. }
  88. cli.keySet = set
  89. return nil
  90. }
  91. func (cli *SOIDCClient) request(ctx context.Context, method httputils.THttpMethod, urlStr string, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
  92. secret := fmt.Sprintf("%s:%s", url.QueryEscape(cli.clientId), url.QueryEscape(cli.secret))
  93. b64Secret := base64.StdEncoding.EncodeToString([]byte(secret))
  94. header := http.Header{}
  95. header.Set("Authorization", fmt.Sprintf("Basic %s", b64Secret))
  96. header.Set("Content-Type", "application/x-www-form-urlencoded")
  97. header.Set("Accept", "application/json")
  98. reqbody := strings.NewReader(data.QueryString())
  99. resp, err := httputils.Request(cli.httpclient, ctx, method, urlStr, header, reqbody, cli.isDebug)
  100. _, body, err := httputils.ParseJSONResponse(data.QueryString(), resp, err, cli.isDebug)
  101. return body, err
  102. }
  103. func (cli *SOIDCClient) FetchToken(ctx context.Context, code string, redirUri string) (*oidcutils.SOIDCAccessTokenResponse, error) {
  104. req := oidcutils.SOIDCAccessTokenRequest{
  105. GrantType: oidcutils.OIDC_REQUEST_GRANT_TYPE,
  106. Code: code,
  107. RedirectUri: redirUri,
  108. ClientId: cli.clientId,
  109. }
  110. respJson, err := cli.request(ctx, "POST", cli.config.TokenEndpoint, jsonutils.Marshal(req))
  111. if err != nil {
  112. return nil, errors.Wrap(err, "request access token")
  113. }
  114. if respJson.Contains("data") && !respJson.Contains("access_token") {
  115. respJson, _ = respJson.Get("data")
  116. }
  117. log.Debugf("AccesToken response: %s", respJson)
  118. accessTokenResp := oidcutils.SOIDCAccessTokenResponse{}
  119. err = respJson.Unmarshal(&accessTokenResp)
  120. if err != nil {
  121. return nil, errors.Wrap(err, "Unmarshal access token response")
  122. }
  123. /*tokenPayload, err := jws.VerifyWithJWKSet([]byte(accessTokenResp.IdToken), cli.keySet, nil)
  124. if err != nil {
  125. return errors.Wrap(err, "jws.VerifyWithJWKSet")
  126. }
  127. log.Debugf("verify %s", tokenPayload) */
  128. return &accessTokenResp, nil
  129. }
  130. func (cli *SOIDCClient) FetchUserInfo(ctx context.Context, accessToken string) (map[string]string, error) {
  131. header := http.Header{}
  132. header.Set("Authorization", "Bearer "+accessToken)
  133. url := cli.config.UserinfoEndpoint
  134. header, body, err := httputils.JSONRequest(cli.httpclient, ctx, httputils.GET, url, header, nil, cli.isDebug)
  135. if err != nil {
  136. return nil, errors.Wrap(err, "request userinfo")
  137. }
  138. if body.Contains("data") {
  139. body, _ = body.Get("data")
  140. }
  141. info := make(map[string]string)
  142. err = body.Unmarshal(&info)
  143. if err != nil {
  144. return nil, errors.Wrap(err, "json unmarshal")
  145. }
  146. return info, nil
  147. }