feature: OpenID Connect provider support (#7490)

Co-authored-by: Qiu Jian <qiujian@yunionyun.com>
This commit is contained in:
Jian Qiu
2020-08-06 12:10:15 +08:00
committed by GitHub
co-authored by Qiu Jian
parent 327b60baf6
commit 604b6b7fff
4 changed files with 43 additions and 12 deletions
+19 -2
View File
@@ -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)
+21 -9
View File
@@ -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
}
+1
View File
@@ -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"`
}
+2 -1
View File
@@ -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))