feature: OpenID Connect provider support (#7446)

Co-authored-by: Qiu Jian <qiujian@yunionyun.com>
This commit is contained in:
Jian Qiu
2020-08-04 13:09:05 +08:00
committed by GitHub
co-authored by Qiu Jian
parent d4a93f0dcd
commit 0e775e68ec
12 changed files with 604 additions and 35 deletions
+84
View File
@@ -237,6 +237,90 @@ func init() {
return nil
})
type OIDCCredentialOptions struct {
User string `help:"User"`
UserDomain string `help:"domain of user"`
Project string `help:"Project"`
ProjectDomain string `help:"domain of user"`
}
type OIDCCredentialCreateOptions struct {
RedirectUri string `help:"redirect URL"`
OIDCCredentialOptions
}
R(&OIDCCredentialCreateOptions{}, "credential-create-oidc", "Create OpenID Connection Credential", func(s *mcclient.ClientSession, args *OIDCCredentialCreateOptions) error {
var uid string
var pid string
var err error
if len(args.User) > 0 {
uid, err = modules.UsersV3.FetchId(s, args.User, args.UserDomain)
if err != nil {
return err
}
}
if len(args.Project) > 0 {
pid, err = modules.Projects.FetchId(s, args.Project, args.ProjectDomain)
if err != nil {
return err
}
}
secret, err := modules.Credentials.CreateOIDCSecret(s, uid, pid, args.RedirectUri)
if err != nil {
return err
}
printObject(jsonutils.Marshal(&secret))
return nil
})
R(&OIDCCredentialOptions{}, "credential-get-oidc", "Get OpenID Connect credential for user and project", func(s *mcclient.ClientSession, args *OIDCCredentialOptions) error {
var uid string
var err error
if len(args.User) > 0 {
uid, err = modules.UsersV3.FetchId(s, args.User, args.UserDomain)
if err != nil {
return err
}
}
var pid string
if len(args.Project) > 0 {
pid, err = modules.Projects.FetchId(s, args.Project, args.ProjectDomain)
if err != nil {
return err
}
}
secrets, err := modules.Credentials.GetOIDCSecret(s, uid, pid)
if err != nil {
return err
}
result := modulebase.ListResult{}
result.Data = make([]jsonutils.JSONObject, len(secrets))
for i := range secrets {
result.Data[i] = jsonutils.Marshal(secrets[i])
}
printList(&result, nil)
return nil
})
R(&OIDCCredentialOptions{}, "credential-remove-oidc", "Remove OpenID Connect credential for user and project", func(s *mcclient.ClientSession, args *OIDCCredentialOptions) error {
uid, err := modules.UsersV3.FetchId(s, args.User, args.UserDomain)
if err != nil {
return err
}
var pid string
if len(args.Project) > 0 {
pid, err = modules.Projects.FetchId(s, args.Project, args.ProjectDomain)
if err != nil {
return err
}
}
err = modules.Credentials.RemoveOIDCSecrets(s, uid, pid)
if err != nil {
return err
}
fmt.Println("success")
return nil
})
type CredentialDeleteOptions struct {
ID string `help:"ID of credentail"`
}
+33 -6
View File
@@ -27,6 +27,8 @@ import (
"github.com/lestrrat-go/jwx/jwa"
"github.com/lestrrat-go/jwx/jwe"
"github.com/lestrrat-go/jwx/jwk"
"github.com/lestrrat-go/jwx/jwt"
"github.com/pquerna/otp/totp"
"yunion.io/x/jsonutils"
@@ -86,10 +88,10 @@ func (t SAuthToken) encodeBytes() []byte {
return msg.Bytes()
}
func (t SAuthToken) encode() string {
func (t SAuthToken) Encode() string {
encBytes := t.encodeBytes()
if privateKey != nil {
return encryptString(encBytes)
return EncryptString(encBytes)
} else {
return compressString(encBytes)
}
@@ -99,7 +101,7 @@ func Decode(t string) (*SAuthToken, error) {
var tBytes []byte
var err error
if privateKey != nil {
tBytes, err = decryptString(t)
tBytes, err = DecryptString(t)
if err != nil {
return nil, errors.Wrap(err, "decryptString")
}
@@ -147,7 +149,7 @@ func compressString(in []byte) string {
return base64.URLEncoding.EncodeToString(buf.Bytes())
}
func encryptString(in []byte) string {
func EncryptString(in []byte) string {
enc, _ := jwe.Encrypt(in, jwa.RSA1_5, &privateKey.PublicKey, jwa.A128GCM, jwa.Deflate)
return string(enc)
}
@@ -167,7 +169,7 @@ func decompressString(in string) ([]byte, error) {
return buf.Bytes(), nil
}
func decryptString(in string) ([]byte, error) {
func DecryptString(in string) ([]byte, error) {
return jwe.Decrypt([]byte(in), jwa.RSA1_5, privateKey)
}
@@ -176,7 +178,7 @@ func (t SAuthToken) GetToken(ctx context.Context) (mcclient.TokenCredential, err
}
func (t SAuthToken) GetAuthCookie(token mcclient.TokenCredential) string {
sid := t.encode()
sid := t.Encode()
info := jsonutils.NewDict()
info.Add(jsonutils.NewTimeString(token.GetExpires()), "exp")
info.Add(jsonutils.NewString(sid), "session")
@@ -258,3 +260,28 @@ func (t *SAuthToken) VerifyTotpPasscode(s *mcclient.ClientSession, uid, passcode
t.updateRetryCount()
return errors.Wrap(httperrors.ErrInvalidCredential, "invalid passcode")
}
func SignJWT(t jwt.Token) (string, error) {
jwkKey, err := jwk.New(privateKey)
if err != nil {
return "", errors.Wrap(err, "jwk.New")
}
signed, err := jwt.Sign(t, jwa.RS256, jwkKey)
if err != nil {
return "", errors.Wrap(err, "jwt.Sign")
}
return string(signed), nil
}
func GetJWKs(ctx context.Context) (jsonutils.JSONObject, error) {
key := jsonutils.NewDict()
key.Set("use", jsonutils.NewString("sig"))
key.Set("kty", jsonutils.NewString("RSA"))
key.Set("alg", jsonutils.NewString("RS256"))
key.Set("e", jsonutils.NewString("AQAB"))
key.Set("n", jsonutils.NewString(base64.URLEncoding.EncodeToString(privateKey.PublicKey.N.Bytes())))
ret := jsonutils.NewDict()
ret.Set("keys", jsonutils.NewArray(key))
return ret, nil
}
+2 -2
View File
@@ -31,7 +31,7 @@ func TestEncoeDecode(t *testing.T) {
setPrivateKey(key)
et := token.encodeBytes()
plainEt := compressString(et)
encEt := encryptString(et)
encEt := EncryptString(et)
t.Logf("origin token: %s", token.token)
t.Logf("plain token: %s (%d)", plainEt, len(plainEt))
t.Logf("encrypt token: %s (%d)", encEt, len(encEt))
@@ -40,7 +40,7 @@ func TestEncoeDecode(t *testing.T) {
if err != nil {
t.Fatalf("decompressString fail %s", err)
}
decBytes2, err := decryptString(encEt)
decBytes2, err := DecryptString(encEt)
if err != nil {
t.Fatalf("decryptString fail %s", err)
}
+5 -1
View File
@@ -72,6 +72,9 @@ func (h *AuthHandlers) AddMethods() {
NewHP(h.getIdpSsoRedirectUri, "sso", "redirect", "<idp_id>"),
NewHP(h.listTotpRecoveryQuestions, "recovery"),
NewHP(h.handleSsoLogin, "ssologin"),
NewHP(handleOIDCAuth, "oidc", "auth"),
NewHP(handleOIDCConfiguration, "oidc", ".well-known", "openid-configuration"),
NewHP(handleOIDCJWKeys, "oidc", "keys"),
)
h.AddByMethod(POST, nil,
NewHP(h.initTotpSecrets, "initcredential"),
@@ -81,6 +84,7 @@ func (h *AuthHandlers) AddMethods() {
NewHP(h.postLoginHandler, "login"),
NewHP(h.postLogoutHandler, "logout"),
NewHP(h.handleSsoLogin, "ssologin"),
NewHP(handleOIDCToken, "oidc", "token"),
)
// auth middleware handler
@@ -91,6 +95,7 @@ func (h *AuthHandlers) AddMethods() {
NewHP(h.getResources, "scoped_resources"),
NewHP(fetchIdpBasicConfig, "idp", "<idp_id>", "info"),
NewHP(fetchIdpSAMLMetadata, "idp", "<idp_id>", "saml-metadata"),
NewHP(handleOIDCUserInfo, "oidc", "user"),
)
h.AddByMethod(POST, FetchAuthToken,
NewHP(h.resetUserPassword, "password"),
@@ -188,7 +193,6 @@ func (h *AuthHandlers) getRegions(ctx context.Context, w http.ResponseWriter, re
}
func (h *AuthHandlers) getUser(ctx context.Context, w http.ResponseWriter, req *http.Request) {
data, err := getUserInfo(ctx, req)
if err != nil {
httperrors.NotFoundError(w, err.Error())
+14 -12
View File
@@ -50,14 +50,14 @@ func Base64UrlDecode(str string) ([]byte, error) {
return base64.StdEncoding.DecodeString(str)
}
/*func getAuthToken(r *http.Request) string {
func getAuthToken(r *http.Request) string {
auth := r.Header.Get(constants.AUTH_HEADER)
if len(auth) > 0 && auth[:len(constants.AUTH_PREFIX)] == constants.AUTH_PREFIX {
return auth[len(constants.AUTH_PREFIX):]
} else {
return ""
}
}*/
}
func getAuthCookie(r *http.Request) string {
return getCookie(r, constants.YUNION_AUTH_COOKIE)
@@ -73,16 +73,18 @@ func fetchAuthInfo(ctx context.Context, r *http.Request) (mcclient.TokenCredenti
// no more use Auth header
// auth1 := getAuthToken(r)
auth := "" // getAuthToken(r)
authCookieStr := getAuthCookie(r)
if len(authCookieStr) > 0 {
authCookie, err := jsonutils.ParseString(authCookieStr)
if err != nil {
return nil, nil, errors.Wrap(httperrors.ErrInputParameter, "Auth cookie decode")
}
auth, err = authCookie.GetString("session")
if err != nil {
return nil, nil, errors.Wrap(httperrors.ErrInputParameter, "authCookie missing session field")
auth := getAuthToken(r)
if len(auth) == 0 {
authCookieStr := getAuthCookie(r)
if len(authCookieStr) > 0 {
authCookie, err := jsonutils.ParseString(authCookieStr)
if err != nil {
return nil, nil, errors.Wrap(httperrors.ErrInputParameter, "Auth cookie decode")
}
auth, err = authCookie.GetString("session")
if err != nil {
return nil, nil, errors.Wrap(httperrors.ErrInputParameter, "authCookie missing session field")
}
}
}
// if len(auth) > 0 && auth != auth1 { // hack!!! browser cache problem???
+327
View File
@@ -0,0 +1,327 @@
// 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 handler
import (
"context"
"encoding/base64"
"encoding/binary"
"fmt"
"net/http"
"strings"
"time"
"github.com/lestrrat-go/jwx/jwa"
"github.com/lestrrat-go/jwx/jwt"
"yunion.io/x/jsonutils"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/netutils"
"yunion.io/x/onecloud/pkg/apigateway/clientman"
"yunion.io/x/onecloud/pkg/apigateway/options"
"yunion.io/x/onecloud/pkg/appsrv"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/mcclient/modules"
"yunion.io/x/onecloud/pkg/util/httputils"
"yunion.io/x/onecloud/pkg/util/netutils2"
"yunion.io/x/onecloud/pkg/util/oidcutils"
)
const (
OIDC_CODE_EXPIRE_SECONDS = 300
)
func addQuery(urlstr string, qs jsonutils.JSONObject) string {
qsPos := strings.LastIndexByte(urlstr, '?')
if qsPos < 0 {
return fmt.Sprintf("%s?%s", urlstr, qs.QueryString())
}
oldQs, _ := jsonutils.ParseQueryString(urlstr[qsPos+1:])
if oldQs != nil {
oldQs.(*jsonutils.JSONDict).Update(qs)
return fmt.Sprintf("%s?%s", urlstr[:qsPos], oldQs.QueryString())
} else {
return fmt.Sprintf("%s?%s", urlstr[:qsPos], qs.QueryString())
}
}
func handleOIDCAuth(ctx context.Context, w http.ResponseWriter, req *http.Request) {
ctx, err := fetchAndSetAuthContext(ctx, w, req)
if err != nil {
// redirect to login page
qs := jsonutils.NewDict()
qs.Set("path", jsonutils.NewString(req.URL.String()))
loginUrl := addQuery(options.Options.SsoAuthCallbackUrl, qs)
appsrv.SendRedirect(w, loginUrl)
return
}
query, _ := jsonutils.ParseQueryString(req.URL.RawQuery)
auth, code, err := doOIDCAuth(ctx, req, query)
if err != nil {
qs := jsonutils.NewDict()
qs.Set("error", jsonutils.NewString(errors.Cause(err).Error()))
qs.Set("error_description", jsonutils.NewString(err.Error()))
errorUrl := addQuery(auth.RedirectUri, qs)
appsrv.SendRedirect(w, errorUrl)
return
}
qs := jsonutils.NewDict()
qs.Set("code", jsonutils.NewString(code))
qs.Set("state", jsonutils.NewString(auth.State))
redirUrl := addQuery(auth.RedirectUri, qs)
appsrv.SendRedirect(w, redirUrl)
}
func fetchOIDCCredential(ctx context.Context, req *http.Request, clientId string) (modules.SOpenIDConnectCredential, error) {
var oidcSecret modules.SOpenIDConnectCredential
s := auth.GetAdminSession(ctx, FetchRegion(req), "")
secret, err := modules.Credentials.GetById(s, clientId, nil)
if err != nil {
return oidcSecret, errors.Wrap(err, "Request Credential")
}
oidcSecret, err = modules.DecodeOIDCSecret(secret)
if err != nil {
return oidcSecret, errors.Wrap(err, "DecodeOIDCSecret")
}
return oidcSecret, nil
}
func doOIDCAuth(ctx context.Context, req *http.Request, query jsonutils.JSONObject) (oidcutils.SOIDCAuthRequest, string, error) {
oidcAuth := oidcutils.SOIDCAuthRequest{}
if query == nil {
return oidcAuth, "", errors.Wrap(httperrors.ErrInputParameter, "empty query string")
}
err := query.Unmarshal(&oidcAuth)
if err != nil {
return oidcAuth, "", errors.Wrap(httperrors.ErrInputParameter, "unmarshal request parameter fail")
}
if oidcAuth.ResponseType != oidcutils.OIDC_RESPONSE_TYPE_CODE {
return oidcAuth, "", errors.Wrapf(httperrors.ErrInputParameter, "invalid resposne type %s", oidcAuth.ResponseType)
}
oidcSecret, err := fetchOIDCCredential(ctx, req, oidcAuth.ClientId)
if err != nil {
return oidcAuth, "", errors.Wrap(err, "fetchOIDCCredential")
}
if oidcSecret.RedirectUri != oidcAuth.RedirectUri {
return oidcAuth, "", errors.Wrap(httperrors.ErrInvalidCredential, "redirect uri not match")
}
cliIp := netutils2.GetHttpRequestIp(req)
codeInfo := newOIDCClientInfo(cliIp)
code := clientman.EncryptString(codeInfo.toBytes())
return oidcAuth, code, nil
}
func handleOIDCToken(ctx context.Context, w http.ResponseWriter, req *http.Request) {
resp, err := validateOIDCToken(ctx, req)
if err != nil {
httperrors.GeneralServerError(w, err)
return
}
appsrv.SendJSON(w, jsonutils.Marshal(resp))
return
}
type SOIDCClientInfo struct {
Timestamp int64
Ip netutils.IPV4Addr
}
func (i SOIDCClientInfo) toBytes() []byte {
enc := make([]byte, 12)
binary.LittleEndian.PutUint64(enc, uint64(i.Timestamp))
binary.LittleEndian.PutUint32(enc[8:], uint32(i.Ip))
return enc
}
func (i SOIDCClientInfo) isExpired() bool {
if time.Now().UnixNano()-i.Timestamp > OIDC_CODE_EXPIRE_SECONDS*1000000000 {
return true
}
return false
}
func decodeOIDCClientInfo(enc []byte) (SOIDCClientInfo, error) {
info := SOIDCClientInfo{}
if len(enc) != 8+4 {
return info, errors.Wrap(httperrors.ErrInvalidCredential, "code byte length must be 12")
}
info.Timestamp = int64(binary.LittleEndian.Uint64(enc))
info.Ip = netutils.IPV4Addr(binary.LittleEndian.Uint32(enc[8:]))
return info, nil
}
func newOIDCClientInfo(ipstr string) SOIDCClientInfo {
info := SOIDCClientInfo{}
info.Timestamp = time.Now().UnixNano()
info.Ip, _ = netutils.NewIPV4Addr(ipstr)
return info
}
func validateOIDCToken(ctx context.Context, req *http.Request) (oidcutils.SOIDCAccessTokenResponse, error) {
var tokenResp oidcutils.SOIDCAccessTokenResponse
bodyBytes, err := appsrv.Fetch(req)
if err != nil {
return tokenResp, errors.Wrap(err, "Fetch Body")
}
bodyJson, err := jsonutils.ParseQueryString(string(bodyBytes))
if err != nil {
return tokenResp, errors.Wrap(err, "Decode body form data")
}
authReq := oidcutils.SOIDCAccessTokenRequest{}
err = bodyJson.Unmarshal(&authReq)
if err != nil {
return tokenResp, errors.Wrap(err, "Unmarshal Access Token Request")
}
if authReq.GrantType != oidcutils.OIDC_REQUEST_GRANT_TYPE {
return tokenResp, errors.Wrapf(httperrors.ErrInvalidCredential, "invalid grant type %s", authReq.GrantType)
}
codeTimeBytes, err := clientman.DecryptString(authReq.Code)
if err != nil {
return tokenResp, errors.Wrapf(httperrors.ErrInvalidCredential, "invalid code %s", authReq.Code)
}
codeInfo, err := decodeOIDCClientInfo(codeTimeBytes)
if err != nil {
return tokenResp, errors.Wrap(httperrors.ErrInvalidCredential, "fail to decode code")
}
if codeInfo.isExpired() {
return tokenResp, errors.Wrapf(httperrors.ErrInvalidCredential, "code expires")
}
authStr := req.Header.Get("Authorization")
authParts := strings.Split(string(authStr), " ")
if len(authParts) != 2 {
return tokenResp, errors.Wrap(httperrors.ErrInvalidCredential, "illegal authorization header")
}
if authParts[0] != "Basic" {
return tokenResp, errors.Wrapf(httperrors.ErrInvalidCredential, "unsupport auth method %s, only Basic supported", authParts)
}
authBytes, err := base64.StdEncoding.DecodeString(authParts[1])
if err != nil {
return tokenResp, errors.Wrap(err, "Decode Authorization Header")
}
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")
}
oidcSecret, err := fetchOIDCCredential(ctx, req, authReq.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] {
return tokenResp, errors.Wrap(httperrors.ErrInvalidCredential, "client secret not match")
}
token, err := auth.Client().AuthenticateByAccessKey(authParts[0], authParts[1], codeInfo.Ip.String())
if err != nil {
return tokenResp, errors.Wrap(err, "invalid client_id/client_secret")
}
tokenResp = token2AccessTokenResponse(token, authParts[0])
return tokenResp, nil
}
func token2AccessTokenResponse(token mcclient.TokenCredential, clientId string) oidcutils.SOIDCAccessTokenResponse {
resp := oidcutils.SOIDCAccessTokenResponse{}
resp.AccessToken = token2AccessToken(token)
resp.TokenType = oidcutils.OIDC_BEARER_TOKEN_TYPE
resp.IdToken, _ = token2IdToken(token, clientId)
resp.ExpiresIn = int(token.GetExpires().Unix() - time.Now().Unix())
return resp
}
func token2AccessToken(token mcclient.TokenCredential) string {
authToken := clientman.NewAuthToken(token.GetTokenString(), false, false)
return authToken.Encode()
}
func token2IdToken(token mcclient.TokenCredential, clientId string) (string, error) {
jwtToken := jwt.New()
jwtToken.Set(jwt.IssuerKey, options.Options.ApiServer)
jwtToken.Set(jwt.SubjectKey, token.GetUserId())
jwtToken.Set(jwt.AudienceKey, clientId)
jwtToken.Set(jwt.ExpirationKey, token.GetExpires().Unix())
jwtToken.Set(jwt.IssuedAtKey, time.Now().Unix())
return clientman.SignJWT(jwtToken)
}
func handleOIDCConfiguration(ctx context.Context, w http.ResponseWriter, req *http.Request) {
authUrl := httputils.JoinPath(options.Options.ApiServer, "api/v1/auth/oidc/auth")
tokenUrl := httputils.JoinPath(options.Options.ApiServer, "api/v1/auth/oidc/token")
userinfoUrl := httputils.JoinPath(options.Options.ApiServer, "api/v1/auth/oidc/user")
jwksUrl := httputils.JoinPath(options.Options.ApiServer, "api/v1/auth/oidc/keys")
conf := oidcutils.SOIDCConfiguration{
Issuer: options.Options.ApiServer,
AuthorizationEndpoint: authUrl,
TokenEndpoint: tokenUrl,
UserinfoEndpoint: userinfoUrl,
JwksUri: jwksUrl,
ResponseTypesSupported: []string{
oidcutils.OIDC_RESPONSE_TYPE_CODE,
},
SubjectTypesSupported: []string{
"public",
},
IdTokenSigningAlgValuesSupported: []string{
string(jwa.RS256),
},
ScopesSupported: []string{
"user",
"profile",
},
TokenEndpointAuthMethodsSupported: []string{
"client_secret_basic",
},
ClaimsSupported: []string{
jwt.IssuerKey,
jwt.SubjectKey,
jwt.AudienceKey,
jwt.ExpirationKey,
jwt.IssuedAtKey,
},
}
appsrv.SendJSON(w, jsonutils.Marshal(conf))
}
func handleOIDCJWKeys(ctx context.Context, w http.ResponseWriter, req *http.Request) {
keyJson, err := clientman.GetJWKs(ctx)
if err != nil {
httperrors.GeneralServerError(w, err)
return
}
appsrv.SendJSON(w, keyJson)
}
func handleOIDCUserInfo(ctx context.Context, w http.ResponseWriter, req *http.Request) {
data, err := getUserInfo(ctx, req)
if err != nil {
httperrors.NotFoundError(w, err.Error())
return
}
appsrv.SendJSON(w, data)
}
+1
View File
@@ -24,6 +24,7 @@ const (
ACCESS_SECRET_TYPE = "aksk"
TOTP_TYPE = "totp"
RECOVERY_SECRETS_TYPE = "recovery_secret"
OIDC_CREDENTIAL_TYPE = "oidc"
)
type SAccessKeySecretBlob struct {
+7 -6
View File
@@ -28,6 +28,7 @@ import (
"yunion.io/x/onecloud/pkg/keystone/driver"
"yunion.io/x/onecloud/pkg/keystone/models"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/util/oidcutils"
"yunion.io/x/onecloud/pkg/util/oidcutils/client"
)
@@ -109,12 +110,12 @@ func (oidc *SOIDCDriver) GetSsoRedirectUri(ctx context.Context, callbackUrl, sta
return "", errors.Wrap(err, "getOIDCClient")
}
conf := cli.GetConfig()
qs := map[string]string{
"response_type": "code",
"client_id": oidc.oidcConfig.ClientId,
"redirect_uri": callbackUrl,
"state": state,
"scope": strings.Join(conf.ScopesSupported, " "),
qs := oidcutils.SOIDCAuthRequest{
ResponseType: oidcutils.OIDC_RESPONSE_TYPE_CODE,
ClientId: oidc.oidcConfig.ClientId,
RedirectUri: callbackUrl,
State: state,
Scope: strings.Join(conf.ScopesSupported, " "),
}
urlstr := fmt.Sprintf("%s?%s", conf.AuthorizationEndpoint, jsonutils.Marshal(qs).QueryString())
return urlstr, nil
+1 -1
View File
@@ -223,7 +223,7 @@ func (self *SCredential) getBlob() []byte {
}
func (self *SCredential) GetAccessKeySecret() (*api.SAccessKeySecretBlob, error) {
if self.Type == api.ACCESS_SECRET_TYPE {
if self.Type == api.ACCESS_SECRET_TYPE || self.Type == api.OIDC_CREDENTIAL_TYPE {
blobJson, err := jsonutils.Parse(self.getBlob())
if err != nil {
return nil, errors.Wrap(err, "jsonutils.Parse")
+113 -4
View File
@@ -17,6 +17,8 @@ package modules
import (
"encoding/base64"
"fmt"
"net/url"
"strings"
"time"
"github.com/pkg/errors"
@@ -40,16 +42,17 @@ const (
ACCESS_SECRET_TYPE = api.ACCESS_SECRET_TYPE
TOTP_TYPE = api.TOTP_TYPE
RECOVERY_SECRETS_TYPE = api.RECOVERY_SECRETS_TYPE
OIDC_CREDENTIAL_TYPE = api.OIDC_CREDENTIAL_TYPE
)
type STotpSecret struct {
Totp string
Timestamp int64
Totp string `json:"totp"`
Timestamp int64 `json:"timestamp"`
}
type SRecoverySecret struct {
Question string
Answer string
Question string `json:"question"`
Answer string `json:"answer"`
}
type SAccessKeySecret struct {
@@ -64,6 +67,13 @@ type SRecoverySecretSet struct {
Timestamp int64
}
type SOpenIDConnectCredential struct {
ClientId string `json:"client_id"`
// Secret string `json:"secret"`
RedirectUri string `json:"redirect_uri"`
api.SAccessKeySecretBlob
}
func (manager *SCredentialManager) fetchCredentials(s *mcclient.ClientSession, secType string, uid string, pid string) ([]jsonutils.JSONObject, error) {
query := jsonutils.NewDict()
query.Add(jsonutils.NewString(secType), "type")
@@ -91,6 +101,10 @@ func (manager *SCredentialManager) FetchRecoverySecrets(s *mcclient.ClientSessio
return manager.fetchCredentials(s, RECOVERY_SECRETS_TYPE, uid, "")
}
func (manager *SCredentialManager) FetchOIDCSecrets(s *mcclient.ClientSession, uid string, pid string) ([]jsonutils.JSONObject, error) {
return manager.fetchCredentials(s, OIDC_CREDENTIAL_TYPE, uid, pid)
}
func (manager *SCredentialManager) GetTotpSecret(s *mcclient.ClientSession, uid string) (string, error) {
secrets, err := manager.FetchTotpSecrets(s, uid)
if err != nil {
@@ -186,6 +200,43 @@ func (manager *SCredentialManager) GetAccessKeySecrets(s *mcclient.ClientSession
return aksk, nil
}
func DecodeOIDCSecret(secret jsonutils.JSONObject) (SOpenIDConnectCredential, error) {
curr := SOpenIDConnectCredential{}
blobStr, err := secret.GetString("blob")
if err != nil {
return curr, errors.Wrap(err, "secret.GetString")
}
blobJson, err := jsonutils.ParseString(blobStr)
if err != nil {
return curr, errors.Wrap(err, "jsonutils.ParseString")
}
err = blobJson.Unmarshal(&curr)
if err != nil {
return curr, errors.Wrap(err, "blobJson.Unmarshal")
}
curr.ClientId, err = secret.GetString("id")
if err != nil {
return curr, errors.Wrap(err, "secret.GetString('id')")
}
return curr, nil
}
func (manager *SCredentialManager) GetOIDCSecret(s *mcclient.ClientSession, uid string, pid string) ([]SOpenIDConnectCredential, error) {
secrets, err := manager.FetchOIDCSecrets(s, uid, pid)
if err != nil {
return nil, err
}
oidcCreds := make([]SOpenIDConnectCredential, 0)
for i := range secrets {
curr, err := DecodeOIDCSecret(secrets[i])
if err != nil {
return nil, errors.Wrap(err, "DecodeOIDCSecret")
}
oidcCreds = append(oidcCreds, curr)
}
return oidcCreds, nil
}
func (manager *SCredentialManager) DoCreateAccessKeySecret(s *mcclient.ClientSession, params jsonutils.JSONObject) (jsonutils.JSONObject, error) {
key, err := manager.CreateAccessKeySecret(s, "", "", time.Time{})
if err != nil {
@@ -224,6 +275,60 @@ func (manager *SCredentialManager) CreateAccessKeySecret(s *mcclient.ClientSessi
return aksk, nil
}
func (manager *SCredentialManager) DoCreateOIDCSecret(s *mcclient.ClientSession, params jsonutils.JSONObject) (jsonutils.JSONObject, error) {
redirectUri, _ := params.GetString("redirect_uri")
key, err := manager.CreateOIDCSecret(s, "", "", redirectUri)
if err != nil {
return nil, errors.Wrap(err, "CreateOIDCSecret")
}
result := jsonutils.Marshal(key)
// result.(*jsonutils.JSONDict).Add(jsonutils.NewString(key.ClientId), "client_id")
return result, nil
}
func isValidRedirectURL(redirectUri string) error {
if len(redirectUri) == 0 {
return errors.Wrap(httperrors.ErrInputParameter, "empty redirect uri")
}
if !strings.HasPrefix(redirectUri, "http://") && !strings.HasPrefix(redirectUri, "https://") {
return errors.Wrap(httperrors.ErrInputParameter, "invalid schema")
}
_, err := url.Parse(redirectUri)
if err != nil {
return errors.Wrapf(httperrors.ErrInputParameter, "invalid redirect_uri %s", redirectUri)
}
return nil
}
func (manager *SCredentialManager) CreateOIDCSecret(s *mcclient.ClientSession, uid string, pid string, redirectUri string) (SOpenIDConnectCredential, error) {
oidcCred := SOpenIDConnectCredential{}
err := isValidRedirectURL(redirectUri)
if err != nil {
return oidcCred, errors.Wrap(err, "isValidRedirectURL")
}
oidcCred.Secret = base64.URLEncoding.EncodeToString([]byte(seclib.RandomPassword(32)))
oidcCred.RedirectUri = redirectUri
blobJson := jsonutils.Marshal(&oidcCred)
params := jsonutils.NewDict()
name := fmt.Sprintf("oidc-%s-%s-%d", uid, pid, time.Now().Unix())
if len(pid) > 0 {
params.Add(jsonutils.NewString(pid), "project_id")
}
params.Add(jsonutils.NewString(OIDC_CREDENTIAL_TYPE), "type")
if len(uid) > 0 {
params.Add(jsonutils.NewString(uid), "user_id")
}
params.Add(jsonutils.NewString(blobJson.String()), "blob")
params.Add(jsonutils.NewString(name), "name")
result, err := manager.Create(s, params)
if err != nil {
return oidcCred, err
}
oidcCred.ClientId, _ = result.GetString("id")
return oidcCred, nil
}
func (manager *SCredentialManager) CreateTotpSecret(s *mcclient.ClientSession, uid string) (string, error) {
_, err := manager.GetTotpSecret(s, uid)
if err == nil {
@@ -297,6 +402,10 @@ func (manager *SCredentialManager) RemoveRecoverySecrets(s *mcclient.ClientSessi
return manager.removeCredentials(s, RECOVERY_SECRETS_TYPE, uid, "")
}
func (manager *SCredentialManager) RemoveOIDCSecrets(s *mcclient.ClientSession, uid string, pid string) error {
return manager.removeCredentials(s, OIDC_CREDENTIAL_TYPE, uid, pid)
}
var (
Credentials SCredentialManager
)
+2 -2
View File
@@ -115,8 +115,8 @@ func (cli *SOIDCClient) request(ctx context.Context, method httputils.THttpMetho
}
func (cli *SOIDCClient) FetchToken(ctx context.Context, code string, redirUri string) (*oidcutils.SOIDCAccessTokenResponse, error) {
req := oidcutils.SOIDCAccessTokebReqest{
GrantType: "authorization_code",
req := oidcutils.SOIDCAccessTokenRequest{
GrantType: oidcutils.OIDC_REQUEST_GRANT_TYPE,
Code: code,
RedirectUri: redirUri,
ClientId: cli.clientId,
+15 -1
View File
@@ -70,7 +70,7 @@ type SOIDCConfiguration struct {
UiLocalesSupported []string `json:"ui_locales_supported"`
}
type SOIDCAccessTokebReqest struct {
type SOIDCAccessTokenRequest struct {
// grant_type
// REQUIRED. Value MUST be set to "authorization_code".
GrantType string `json:"grant_type"`
@@ -99,3 +99,17 @@ type SOIDCAccessTokenResponse struct {
ExpiresIn int `json:"expires_in"`
IdToken string `json:"id_token"`
}
const (
OIDC_RESPONSE_TYPE_CODE = "code"
OIDC_REQUEST_GRANT_TYPE = "authorization_code"
OIDC_BEARER_TOKEN_TYPE = "Bearer"
)
type SOIDCAuthRequest struct {
ResponseType string `json:"response_type"`
ClientId string `json:"client_id"`
RedirectUri string `json:"redirect_uri"`
State string `json:"state"`
Scope string `json:"scope"`
}