mirror of
https://github.com/yunionio/cloudpods.git
synced 2026-09-19 02:37:24 +08:00
feature: OpenID Connect provider support (#7446)
Co-authored-by: Qiu Jian <qiujian@yunionyun.com>
This commit is contained in:
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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???
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -24,6 +24,7 @@ const (
|
||||
ACCESS_SECRET_TYPE = "aksk"
|
||||
TOTP_TYPE = "totp"
|
||||
RECOVERY_SECRETS_TYPE = "recovery_secret"
|
||||
OIDC_CREDENTIAL_TYPE = "oidc"
|
||||
)
|
||||
|
||||
type SAccessKeySecretBlob struct {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user