mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-10-28 03:23:25 +08:00
feature: OpenID Connect provider support (#7490)
Co-authored-by: Qiu Jian <qiujian@yunionyun.com>
This commit is contained in:
@@ -39,9 +39,26 @@ import (
|
||||
)
|
||||
|
||||
func getSsoCallbackUrl() string {
|
||||
if options.Options.SsoRedirectUrl == "" {
|
||||
return httputils.JoinPath(options.Options.ApiServer, "api/v1/auth/ssologin")
|
||||
}
|
||||
return options.Options.SsoRedirectUrl
|
||||
}
|
||||
|
||||
func getSsoAuthCallbackUrl() string {
|
||||
if options.Options.SsoAuthCallbackUrl == "" {
|
||||
return httputils.JoinPath(options.Options.ApiServer, "auth")
|
||||
}
|
||||
return options.Options.SsoAuthCallbackUrl
|
||||
}
|
||||
|
||||
func getSsoLinkCallbackUrl() string {
|
||||
if options.Options.SsoLinkCallbackUrl == "" {
|
||||
return httputils.JoinPath(options.Options.ApiServer, "user")
|
||||
}
|
||||
return options.Options.SsoLinkCallbackUrl
|
||||
}
|
||||
|
||||
func (h *AuthHandlers) getIdpSsoRedirectUri(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
params := appctx.AppContextParams(ctx)
|
||||
idpId := params["<idp_id>"]
|
||||
@@ -141,7 +158,7 @@ func (h *AuthHandlers) handleSsoLogin(ctx context.Context, w http.ResponseWriter
|
||||
if len(idpLinkUser) > 0 {
|
||||
// link with existing user
|
||||
err := linkWithExistingUser(ctx, req, idpId, idpLinkUser, body)
|
||||
referer := options.Options.SsoLinkCallbackUrl
|
||||
referer := getSsoLinkCallbackUrl()
|
||||
refererUrl, _ := url.Parse(referer)
|
||||
if refererUrl == nil {
|
||||
refererUrl, _ = url.Parse(idpReferer)
|
||||
@@ -154,7 +171,7 @@ func (h *AuthHandlers) handleSsoLogin(ctx context.Context, w http.ResponseWriter
|
||||
appsrv.SendRedirect(w, redirUrl)
|
||||
} else {
|
||||
// ordinary login
|
||||
referer := options.Options.SsoAuthCallbackUrl
|
||||
referer := getSsoAuthCallbackUrl()
|
||||
refererUrl, _ := url.Parse(referer)
|
||||
if refererUrl == nil {
|
||||
refererUrl, _ = url.Parse(idpReferer)
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -27,6 +28,7 @@ import (
|
||||
"github.com/lestrrat-go/jwx/jwt"
|
||||
|
||||
"yunion.io/x/jsonutils"
|
||||
"yunion.io/x/log"
|
||||
"yunion.io/x/pkg/errors"
|
||||
"yunion.io/x/pkg/util/netutils"
|
||||
|
||||
@@ -46,6 +48,13 @@ const (
|
||||
OIDC_CODE_EXPIRE_SECONDS = 300
|
||||
)
|
||||
|
||||
func getLoginCallbackParam() string {
|
||||
if options.Options.LoginCallbackParam == "" {
|
||||
return "rf"
|
||||
}
|
||||
return options.Options.LoginCallbackParam
|
||||
}
|
||||
|
||||
func addQuery(urlstr string, qs jsonutils.JSONObject) string {
|
||||
qsPos := strings.LastIndexByte(urlstr, '?')
|
||||
if qsPos < 0 {
|
||||
@@ -65,8 +74,8 @@ func handleOIDCAuth(ctx context.Context, w http.ResponseWriter, req *http.Reques
|
||||
if err != nil {
|
||||
// redirect to login page
|
||||
qs := jsonutils.NewDict()
|
||||
qs.Set("path", jsonutils.NewString(req.URL.String()))
|
||||
loginUrl := addQuery(options.Options.SsoAuthCallbackUrl, qs)
|
||||
qs.Set(getLoginCallbackParam(), jsonutils.NewString(req.URL.String()))
|
||||
loginUrl := addQuery(getSsoAuthCallbackUrl(), qs)
|
||||
appsrv.SendRedirect(w, loginUrl)
|
||||
return
|
||||
}
|
||||
@@ -181,6 +190,7 @@ func validateOIDCToken(ctx context.Context, req *http.Request) (oidcutils.SOIDCA
|
||||
if err != nil {
|
||||
return tokenResp, errors.Wrap(err, "Fetch Body")
|
||||
}
|
||||
log.Debugf("validateOIDCToken body: %s", string(bodyBytes))
|
||||
bodyJson, err := jsonutils.ParseQueryString(string(bodyBytes))
|
||||
if err != nil {
|
||||
return tokenResp, errors.Wrap(err, "Decode body form data")
|
||||
@@ -207,6 +217,7 @@ func validateOIDCToken(ctx context.Context, req *http.Request) (oidcutils.SOIDCA
|
||||
}
|
||||
|
||||
authStr := req.Header.Get("Authorization")
|
||||
log.Debugf("Authorization: %s", authStr)
|
||||
authParts := strings.Split(string(authStr), " ")
|
||||
if len(authParts) != 2 {
|
||||
return tokenResp, errors.Wrap(httperrors.ErrInvalidCredential, "illegal authorization header")
|
||||
@@ -218,31 +229,32 @@ func validateOIDCToken(ctx context.Context, req *http.Request) (oidcutils.SOIDCA
|
||||
if err != nil {
|
||||
return tokenResp, errors.Wrap(err, "Decode Authorization Header")
|
||||
}
|
||||
log.Debugf("Authorization basic: %s", string(authBytes))
|
||||
authParts = strings.Split(string(authBytes), ":")
|
||||
if len(authParts) != 2 {
|
||||
return tokenResp, errors.Wrap(httperrors.ErrInvalidCredential, "illegal authorization header")
|
||||
}
|
||||
if authParts[0] != authReq.ClientId {
|
||||
return tokenResp, errors.Wrap(httperrors.ErrInvalidCredential, "mismatch client id")
|
||||
}
|
||||
clientId, _ := url.QueryUnescape(authParts[0])
|
||||
clientSecret, _ := url.QueryUnescape(authParts[1])
|
||||
log.Debugf("clientId %s clientSecret: %s authReq.ClientId %s", clientId, clientSecret, authReq.ClientId)
|
||||
|
||||
oidcSecret, err := fetchOIDCCredential(ctx, req, authReq.ClientId)
|
||||
oidcSecret, err := fetchOIDCCredential(ctx, req, clientId)
|
||||
if err != nil {
|
||||
return tokenResp, errors.Wrap(err, "fetchOIDCCredential")
|
||||
}
|
||||
if oidcSecret.RedirectUri != authReq.RedirectUri {
|
||||
return tokenResp, errors.Wrap(httperrors.ErrInvalidCredential, "redirect uri not match")
|
||||
}
|
||||
if oidcSecret.Secret != authParts[1] {
|
||||
if oidcSecret.Secret != clientSecret {
|
||||
return tokenResp, errors.Wrap(httperrors.ErrInvalidCredential, "client secret not match")
|
||||
}
|
||||
|
||||
token, err := auth.Client().AuthenticateByAccessKey(authParts[0], authParts[1], codeInfo.Ip.String())
|
||||
token, err := auth.Client().AuthenticateByAccessKey(clientId, clientSecret, codeInfo.Ip.String())
|
||||
if err != nil {
|
||||
return tokenResp, errors.Wrap(err, "invalid client_id/client_secret")
|
||||
}
|
||||
|
||||
tokenResp = token2AccessTokenResponse(token, authParts[0])
|
||||
tokenResp = token2AccessTokenResponse(token, clientId)
|
||||
return tokenResp, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ type GatewayOptions struct {
|
||||
SsoRedirectUrl string `help:"SSO idp redirect URL"`
|
||||
SsoAuthCallbackUrl string `help:"SSO idp auth callback URL"`
|
||||
SsoLinkCallbackUrl string `help:"SSO idp link user callback URL"`
|
||||
LoginCallbackParam string `help:"Redirect callback parameter name after successful login"`
|
||||
|
||||
common_options.CommonOptions `"request_worker_count->default":"32"`
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -102,7 +103,7 @@ func (cli *SOIDCClient) FetchJWKS(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (cli *SOIDCClient) request(ctx context.Context, method httputils.THttpMethod, urlStr string, data jsonutils.JSONObject) (jsonutils.JSONObject, error) {
|
||||
secret := fmt.Sprintf("%s:%s", cli.clientId, cli.secret)
|
||||
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))
|
||||
|
||||
Reference in New Issue
Block a user