Files
cloudpods/pkg/util/oidcutils/client/client.go
2022-12-27 01:21:26 +08:00

164 lines
5.2 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 client
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/lestrrat/go-jwx/jwk"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/httputils"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/util/oidcutils"
)
type SOIDCClient struct {
clientId string
secret string
timeout time.Duration
isDebug bool
config oidcutils.SOIDCConfiguration
httpclient *http.Client
keySet *jwk.Set
}
func NewOIDCClient(clientId string, secret string, timeoutSeconds int, isDebug bool) *SOIDCClient {
cli := SOIDCClient{
clientId: clientId,
secret: secret,
timeout: time.Duration(timeoutSeconds) * time.Second,
isDebug: isDebug,
}
cli.httpclient = httputils.GetClient(true, cli.timeout)
return &cli
}
const (
WELL_KNOWN_OIDC_CONFIG_PATH = ".well-known/openid-configuration"
)
func (cli *SOIDCClient) FetchConfiguration(ctx context.Context, endpoint string) error {
path := httputils.JoinPath(endpoint, WELL_KNOWN_OIDC_CONFIG_PATH)
_, resp, err := httputils.JSONRequest(cli.httpclient, ctx, httputils.GET, path, nil, nil, cli.isDebug)
if err != nil {
return errors.Wrap(err, "fetch well-known oidc configuration")
}
err = resp.Unmarshal(&cli.config)
if err != nil {
return errors.Wrap(err, "unmarshal oidc configuration")
}
return nil
}
func (cli *SOIDCClient) SetConfig(authUrl, tokenUrl, userinfoUrl string, scopes []string) {
cli.config = oidcutils.SOIDCConfiguration{
AuthorizationEndpoint: authUrl,
TokenEndpoint: tokenUrl,
UserinfoEndpoint: userinfoUrl,
ScopesSupported: scopes,
}
}
func (cli *SOIDCClient) GetConfig() oidcutils.SOIDCConfiguration {
return cli.config
}
func (cli *SOIDCClient) FetchJWKS(ctx context.Context) error {
if len(cli.config.JwksUri) == 0 {
return errors.Wrap(httperrors.ErrInvalidStatus, "no valid jwks_uri")
}
_, resp, err := httputils.JSONRequest(cli.httpclient, ctx, httputils.GET, cli.config.JwksUri, nil, nil, cli.isDebug)
if err != nil {
return errors.Wrap(err, "fetch jwks_uri")
}
set, err := jwk.ParseString(resp.String())
if err != nil {
return errors.Wrap(err, "parse JWK")
}
cli.keySet = set
return nil
}
func (cli *SOIDCClient) request(ctx context.Context, method httputils.THttpMethod, urlStr string, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
secret := fmt.Sprintf("%s:%s", url.QueryEscape(cli.clientId), url.QueryEscape(cli.secret))
b64Secret := base64.StdEncoding.EncodeToString([]byte(secret))
header := http.Header{}
header.Set("Authorization", fmt.Sprintf("Basic %s", b64Secret))
header.Set("Content-Type", "application/x-www-form-urlencoded")
header.Set("Accept", "application/json")
reqbody := strings.NewReader(data.QueryString())
resp, err := httputils.Request(cli.httpclient, ctx, method, urlStr, header, reqbody, cli.isDebug)
_, body, err := httputils.ParseJSONResponse(data.QueryString(), resp, err, cli.isDebug)
return body, err
}
func (cli *SOIDCClient) FetchToken(ctx context.Context, code string, redirUri string) (*oidcutils.SOIDCAccessTokenResponse, error) {
req := oidcutils.SOIDCAccessTokenRequest{
GrantType: oidcutils.OIDC_REQUEST_GRANT_TYPE,
Code: code,
RedirectUri: redirUri,
ClientId: cli.clientId,
}
respJson, err := cli.request(ctx, "POST", cli.config.TokenEndpoint, jsonutils.Marshal(req))
if err != nil {
return nil, errors.Wrap(err, "request access token")
}
if respJson.Contains("data") && !respJson.Contains("access_token") {
respJson, _ = respJson.Get("data")
}
log.Debugf("AccesToken response: %s", respJson)
accessTokenResp := oidcutils.SOIDCAccessTokenResponse{}
err = respJson.Unmarshal(&accessTokenResp)
if err != nil {
return nil, errors.Wrap(err, "Unmarshal access token response")
}
/*tokenPayload, err := jws.VerifyWithJWKSet([]byte(accessTokenResp.IdToken), cli.keySet, nil)
if err != nil {
return errors.Wrap(err, "jws.VerifyWithJWKSet")
}
log.Debugf("verify %s", tokenPayload) */
return &accessTokenResp, nil
}
func (cli *SOIDCClient) FetchUserInfo(ctx context.Context, accessToken string) (map[string]string, error) {
header := http.Header{}
header.Set("Authorization", "Bearer "+accessToken)
url := cli.config.UserinfoEndpoint
header, body, err := httputils.JSONRequest(cli.httpclient, ctx, httputils.GET, url, header, nil, cli.isDebug)
if err != nil {
return nil, errors.Wrap(err, "request userinfo")
}
if body.Contains("data") {
body, _ = body.Get("data")
}
info := make(map[string]string)
err = body.Unmarshal(&info)
if err != nil {
return nil, errors.Wrap(err, "json unmarshal")
}
return info, nil
}