Merge pull request #8163 from swordqiu/hotfix/qj-oidc-auth-response-user-info

fix: oidc response user's info
This commit is contained in:
yunion-ci-robot
2020-09-30 12:11:47 +08:00
committed by GitHub
6 changed files with 270 additions and 73 deletions
+1 -4
View File
@@ -15,20 +15,17 @@
package clientman
import (
"crypto/rand"
"crypto/rsa"
"reflect"
"testing"
)
func TestEncoeDecode(t *testing.T) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
SetupTest()
token := SAuthToken{
token: `gAAAAABe-gUMAawOPrP-mA4jY6-b1UPalPJw9WlZJVqHZMtc3IBKUOvHTbKm60YyZQtnVBa3O3QDfS2ss5_Xwi_n0L-jfuUstguLHfDyztAvT_IAKupw8YNK0FvJg25LKC4IR3bmDzCNzTwMO-rEeb4ha2e1vkGOwko9GT1Bn-xN7UM2qeEsm5PiLBg0ZTMuv4Jm5RWIXk2K`,
verifyTotp: true,
enableTotp: false,
}
setPrivateKey(key)
et := token.encodeBytes()
plainEt := compressString(et)
encEt := EncryptString(et)
+7
View File
@@ -15,6 +15,8 @@
package clientman
import (
"crypto/rand"
"crypto/rsa"
"io/ioutil"
"github.com/pkg/errors"
@@ -38,3 +40,8 @@ func InitClient() error {
return nil
}
func SetupTest() {
key, _ := rsa.GenerateKey(rand.Reader, 2048)
setPrivateKey(key)
}
+74 -48
View File
@@ -71,6 +71,7 @@ func (h *AuthHandlers) AddMethods() {
NewHP(handleOIDCAuth, "oidc", "auth"),
NewHP(handleOIDCConfiguration, "oidc", ".well-known", "openid-configuration"),
NewHP(handleOIDCJWKeys, "oidc", "keys"),
NewHP(handleOIDCUserInfo, "oidc", "user"),
)
h.AddByMethod(POST, nil,
NewHP(h.initTotpSecrets, "initcredential"),
@@ -91,8 +92,6 @@ func (h *AuthHandlers) AddMethods() {
NewHP(h.getResources, "scoped_resources"),
NewHP(fetchIdpBasicConfig, "idp", "<idp_id>", "info"),
NewHP(fetchIdpSAMLMetadata, "idp", "<idp_id>", "saml-metadata"),
// oidc
NewHP(handleOIDCUserInfo, "oidc", "user"),
)
h.AddByMethod(POST, FetchAuthToken,
NewHP(h.resetUserPassword, "password"),
@@ -251,22 +250,17 @@ func doTenantLogin(ctx context.Context, req *http.Request, body jsonutils.JSONOb
func fetchUserInfoFromToken(ctx context.Context, req *http.Request, token mcclient.TokenCredential) (jsonutils.JSONObject, error) {
s := auth.GetAdminSession(ctx, FetchRegion(req), "")
info, err := modules.UsersV3.Get(s, token.GetUserId(), nil)
return fetchUserInfoById(s, token.GetUserId())
}
func fetchUserInfoById(s *mcclient.ClientSession, userId string) (jsonutils.JSONObject, error) {
info, err := modules.UsersV3.Get(s, userId, nil)
if err != nil {
return nil, errors.Wrap(err, "UsersV3.Get")
}
return info, nil
}
func FetchProjectMetadata(ctx context.Context, req *http.Request, pid string) (jsonutils.JSONObject, error) {
s := auth.GetAdminSession(ctx, FetchRegion(req), "")
meta, err := modules.Projects.GetSpecific(s, pid, "metadata", nil)
if err != nil {
return nil, errors.Wrap(err, "GetProjectMetadata")
}
return meta, nil
}
func isUserEnableTotp(userInfo jsonutils.JSONObject) bool {
return jsonutils.QueryBoolean(userInfo, "enable_mfa", false)
}
@@ -759,9 +753,14 @@ func getUserInfo(ctx context.Context, req *http.Request) (*jsonutils.JSONDict, e
log.Errorf("modules.UsersV3.Get fail %s", err)
return nil, fmt.Errorf("not found user %s", token.GetUserId())
}*/
usr, err := fetchUserInfoFromToken(ctx, req, token)
// usr, err := fetchUserInfoFromToken(ctx, req, token)
return getUserInfo2(s, token.GetUserId(), token.GetProjectId(), token.GetLoginIp())
}
func getUserInfo2(s *mcclient.ClientSession, uid string, pid string, loginIp string) (*jsonutils.JSONDict, error) {
usr, err := fetchUserInfoById(s, uid)
if err != nil {
return nil, errors.Wrapf(err, "fetchUserInfoFromToken %s", token.GetUserId())
return nil, errors.Wrapf(err, "fetchUserInfoFromToken %s", uid)
}
data := jsonutils.NewDict()
for _, k := range []string{
@@ -779,21 +778,37 @@ func getUserInfo(ctx context.Context, req *http.Request) (*jsonutils.JSONDict, e
data.Add(v, k)
}
}
data.Add(jsonutils.NewString(token.GetDomainId()), "domain", "id")
data.Add(jsonutils.NewString(token.GetDomainName()), "domain", "name")
data.Add(jsonutils.NewStringArray(auth.AdminCredential().GetRegions()), "regions")
data.Add(jsonutils.NewStringArray(token.GetRoles()), "roles")
data.Add(jsonutils.NewString(token.GetProjectName()), "projectName")
data.Add(jsonutils.NewString(token.GetProjectId()), "projectId")
data.Add(jsonutils.NewString(token.GetProjectDomain()), "projectDomain")
data.Add(jsonutils.NewString(token.GetProjectDomainId()), "projectDomainId")
usrId, _ := usr.GetString("id")
usrName, _ := usr.GetString("name")
usrDomainId, _ := usr.GetString("domain_id")
usrDomainName, _ := usr.GetString("project_domain")
data.Add(jsonutils.NewString(usrDomainId), "domain", "id")
data.Add(jsonutils.NewString(usrDomainName), "domain", "name")
data.Add(jsonutils.NewStringArray(auth.AdminCredential().GetRegions()), "regions")
var projName string
var projDomainId string
if len(pid) > 0 {
projInfo, err := modules.Projects.GetById(s, pid, nil)
if err != nil {
return nil, errors.Wrapf(err, "fetchProjectById %s", pid)
}
projName, _ = projInfo.GetString("name")
projId, _ := projInfo.GetString("id")
projDomainId, _ = projInfo.GetString("domain_id")
projDomainName, _ := projInfo.GetString("project_domain")
data.Add(jsonutils.NewString(projName), "projectName")
data.Add(jsonutils.NewString(projId), "projectId")
data.Add(jsonutils.NewString(projDomainName), "projectDomain")
data.Add(jsonutils.NewString(projDomainId), "projectDomainId")
pmeta, err := projInfo.Get("metadata")
if pmeta != nil {
data.Add(pmeta, "project_meta")
}
pmeta, err := FetchProjectMetadata(ctx, req, token.GetProjectId())
if err != nil {
return nil, errors.Wrap(err, "FetchProjectMetadata")
}
if pmeta != nil {
data.Add(pmeta, "project_meta")
}
log.Infof("getUserInfo modules.RoleAssignments.List")
@@ -802,11 +817,12 @@ func getUserInfo(ctx context.Context, req *http.Request) (*jsonutils.JSONDict, e
query.Add(jsonutils.JSONNull, "include_names")
query.Add(jsonutils.JSONNull, "include_system")
query.Add(jsonutils.NewInt(0), "limit")
query.Add(jsonutils.NewString(token.GetUserId()), "user", "id")
query.Add(jsonutils.NewString(uid), "user", "id")
roleAssigns, err := modules.RoleAssignments.List(s, query)
if err != nil {
return nil, errors.Wrapf(err, "get RoleAssignments list")
}
currentRoles := make([]string, 0)
projects := make(map[string]*projectRoles)
for _, roleAssign := range roleAssigns.Data {
roleId, _ := roleAssign.GetString("role", "id")
@@ -815,6 +831,9 @@ func getUserInfo(ctx context.Context, req *http.Request) (*jsonutils.JSONDict, e
projectName, _ := roleAssign.GetString("scope", "project", "name")
domainId, _ := roleAssign.GetString("scope", "project", "domain", "id")
domain, _ := roleAssign.GetString("scope", "project", "domain", "name")
if projectId == pid {
currentRoles = append(currentRoles, roleName)
}
_, ok := projects[projectId]
if ok {
projects[projectId].add(roleId, roleName)
@@ -822,31 +841,38 @@ func getUserInfo(ctx context.Context, req *http.Request) (*jsonutils.JSONDict, e
projects[projectId] = newProjectRoles(projectId, projectName, roleId, roleName, domainId, domain)
}
}
data.Add(jsonutils.NewStringArray(currentRoles), "roles")
projJson := jsonutils.NewArray()
for _, proj := range projects {
projJson.Add(proj.json(
token.GetUserName(),
token.GetUserId(),
token.GetDomainName(),
token.GetDomainId(),
token.GetLoginIp(),
usrName,
usrId,
usrDomainName,
usrDomainId,
loginIp,
))
}
data.Add(projJson, "projects")
for _, scope := range []rbacutils.TRbacScope{
rbacutils.ScopeSystem,
rbacutils.ScopeDomain,
rbacutils.ScopeProject,
} {
p := policy.PolicyManager.MatchedPolicyNames(scope, token)
data.Add(jsonutils.NewStringArray(p), fmt.Sprintf("%s_policies", scope))
if scope == rbacutils.ScopeSystem {
data.Add(jsonutils.NewStringArray(p), "admin_policies")
} else if scope == rbacutils.ScopeProject {
data.Add(jsonutils.NewStringArray(p), "policies")
if len(pid) > 0 {
ident := rbacutils.NewRbacIdentity2(projDomainId, projName, currentRoles, loginIp)
for _, scope := range []rbacutils.TRbacScope{
rbacutils.ScopeSystem,
rbacutils.ScopeDomain,
rbacutils.ScopeProject,
} {
p := policy.PolicyManager.MatchedPolicyNames(scope, ident)
data.Add(jsonutils.NewStringArray(p), fmt.Sprintf("%s_policies", scope))
if scope == rbacutils.ScopeSystem {
data.Add(jsonutils.NewStringArray(p), "admin_policies")
} else if scope == rbacutils.ScopeProject {
data.Add(jsonutils.NewStringArray(p), "policies")
}
}
}
allPolicies := policy.PolicyManager.AllPolicies()
data.Add(jsonutils.Marshal(allPolicies), "all_policies")
@@ -854,7 +880,7 @@ func getUserInfo(ctx context.Context, req *http.Request) (*jsonutils.JSONDict, e
menus := jsonutils.NewArray()
k8s := jsonutils.NewArray()
curReg := FetchRegion(req)
curReg := s.GetRegion()
srvCat := auth.Client().GetServiceCatalog()
var allsrv []string
var alleps []mcclient.ExternalService
@@ -908,13 +934,13 @@ func getUserInfo(ctx context.Context, req *http.Request) (*jsonutils.JSONDict, e
}
log.Infof("getUserInfo modules.Hosts.Get")
s2 := auth.GetSession(ctx, token, FetchRegion(req), "v2")
// s2 := auth.GetSession(ctx, token, FetchRegion(req), "v2")
params := jsonutils.NewDict()
params.Add(jsonutils.NewString("host_type"), "field")
params.Add(jsonutils.NewString("system"), "scope")
params.Add(jsonutils.JSONTrue, "usable")
params.Add(jsonutils.JSONTrue, "show_emulated")
cap, err := modules.Hosts.Get(s2, "distinct-field", params)
cap, err := modules.Hosts.Get(s, "distinct-field", params)
if err != nil {
log.Errorf("modules.Servers.Get distinct-field fail %s", err)
} else {
+88 -20
View File
@@ -45,7 +45,10 @@ import (
)
const (
// OIDC code expires in 5 minutes
OIDC_CODE_EXPIRE_SECONDS = 300
// OIDC token expires in 2 hours
OIDC_TOKEN_EXPIRE_SECONDS = 7200
)
func getLoginCallbackParam() string {
@@ -72,7 +75,7 @@ func addQuery(urlstr string, qs jsonutils.JSONObject) string {
func handleOIDCAuth(ctx context.Context, w http.ResponseWriter, req *http.Request) {
ctx, err := fetchAndSetAuthContext(ctx, w, req)
if err != nil {
// redirect to login page
// not login redirect to login page
qs := jsonutils.NewDict()
oUrl := req.URL.String()
if !strings.HasPrefix(oUrl, "http") {
@@ -135,8 +138,10 @@ func doOIDCAuth(ctx context.Context, req *http.Request, query jsonutils.JSONObje
return oidcAuth, "", errors.Wrap(httperrors.ErrInvalidCredential, "redirect uri not match")
}
token := AppContextToken(ctx)
cliIp := netutils2.GetHttpRequestIp(req)
codeInfo := newOIDCClientInfo(cliIp)
codeInfo := newOIDCClientInfo(token, cliIp, FetchRegion(req))
code := clientman.EncryptString(codeInfo.toBytes())
return oidcAuth, code, nil
@@ -155,12 +160,20 @@ func handleOIDCToken(ctx context.Context, w http.ResponseWriter, req *http.Reque
type SOIDCClientInfo struct {
Timestamp int64
Ip netutils.IPV4Addr
UserId string
ProjectId string
Region string
}
func (i SOIDCClientInfo) toBytes() []byte {
enc := make([]byte, 12)
enc := make([]byte, 12+1+len(i.UserId)+1+len(i.ProjectId)+len(i.Region))
binary.LittleEndian.PutUint64(enc, uint64(i.Timestamp))
binary.LittleEndian.PutUint32(enc[8:], uint32(i.Ip))
enc[12] = byte(len(i.UserId))
enc[13] = byte(len(i.ProjectId))
copy(enc[14:], i.UserId)
copy(enc[14+len(i.UserId):], i.ProjectId)
copy(enc[14+len(i.UserId)+len(i.ProjectId):], i.Region)
return enc
}
@@ -171,23 +184,67 @@ func (i SOIDCClientInfo) isExpired() bool {
return false
}
func (i SOIDCClientInfo) expiresAt(secs int) time.Time {
expires := i.Timestamp + int64(secs)*int64(time.Second)
esecs := expires / int64(time.Second)
nsecs := expires - esecs*int64(time.Second)
return time.Unix(esecs, nsecs)
}
func decodeOIDCClientInfo(enc []byte) (SOIDCClientInfo, error) {
info := SOIDCClientInfo{}
if len(enc) != 8+4 {
if len(enc) < 8+4+1 {
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:]))
info.UserId = string(enc[14 : 14+int(enc[12])])
info.ProjectId = string(enc[14+int(enc[12]) : 14+int(enc[12])+int(enc[13])])
info.Region = string(enc[14+int(enc[12])+int(enc[13]):])
return info, nil
}
func newOIDCClientInfo(ipstr string) SOIDCClientInfo {
func newOIDCClientInfo(token mcclient.TokenCredential, ipstr string, region string) SOIDCClientInfo {
info := SOIDCClientInfo{}
info.Timestamp = time.Now().UnixNano()
info.Ip, _ = netutils.NewIPV4Addr(ipstr)
info.UserId = token.GetUserId()
info.ProjectId = token.GetProjectId()
info.Region = region
return info
}
type SOIDCClientToken struct {
Info SOIDCClientInfo
}
func (t SOIDCClientToken) encode() string {
json := jsonutils.NewDict()
json.Add(jsonutils.NewString(string(t.Info.toBytes())), "info")
return clientman.EncryptString([]byte(json.String()))
}
func decodeOIDCClientToken(token string) (SOIDCClientToken, error) {
ret := SOIDCClientToken{}
tBytes, err := clientman.DecryptString(token)
if err != nil {
return ret, errors.Wrap(err, "DecryptString")
}
json, err := jsonutils.Parse(tBytes)
if err != nil {
return ret, errors.Wrap(err, "json.Parse")
}
info, err := json.GetString("info")
if err != nil {
return ret, errors.Wrap(err, "getString(info)")
}
ret.Info, err = decodeOIDCClientInfo([]byte(info))
if err != nil {
return ret, errors.Wrap(err, "decodeOIDCClientInfo")
}
return ret, nil
}
func validateOIDCToken(ctx context.Context, req *http.Request) (oidcutils.SOIDCAccessTokenResponse, error) {
var tokenResp oidcutils.SOIDCAccessTokenResponse
bodyBytes, err := appsrv.Fetch(req)
@@ -253,35 +310,29 @@ func validateOIDCToken(ctx context.Context, req *http.Request) (oidcutils.SOIDCA
return tokenResp, errors.Wrap(httperrors.ErrInvalidCredential, "client secret not match")
}
token, err := auth.Client().AuthenticateByAccessKey(clientId, clientSecret, codeInfo.Ip.String())
if err != nil {
return tokenResp, errors.Wrap(err, "invalid client_id/client_secret")
token := SOIDCClientToken{
Info: codeInfo,
}
tokenResp = token2AccessTokenResponse(token, clientId)
return tokenResp, nil
}
func token2AccessTokenResponse(token mcclient.TokenCredential, clientId string) oidcutils.SOIDCAccessTokenResponse {
func token2AccessTokenResponse(token SOIDCClientToken, clientId string) oidcutils.SOIDCAccessTokenResponse {
resp := oidcutils.SOIDCAccessTokenResponse{}
resp.AccessToken = token2AccessToken(token)
resp.AccessToken = token.encode()
resp.TokenType = oidcutils.OIDC_BEARER_TOKEN_TYPE
resp.IdToken, _ = token2IdToken(token, clientId)
resp.ExpiresIn = int(token.GetExpires().Unix() - time.Now().Unix())
resp.ExpiresIn = int(token.Info.expiresAt(OIDC_TOKEN_EXPIRE_SECONDS).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) {
func token2IdToken(token SOIDCClientToken, clientId string) (string, error) {
jwtToken := jwt.New()
jwtToken.Set(jwt.IssuerKey, options.Options.ApiServer)
jwtToken.Set(jwt.SubjectKey, token.GetUserId())
jwtToken.Set(jwt.SubjectKey, token.Info.UserId)
jwtToken.Set(jwt.AudienceKey, clientId)
jwtToken.Set(jwt.ExpirationKey, token.GetExpires().Unix())
jwtToken.Set(jwt.ExpirationKey, token.Info.expiresAt(OIDC_TOKEN_EXPIRE_SECONDS).Unix())
jwtToken.Set(jwt.IssuedAtKey, time.Now().Unix())
return clientman.SignJWT(jwtToken)
}
@@ -334,7 +385,24 @@ func handleOIDCJWKeys(ctx context.Context, w http.ResponseWriter, req *http.Requ
}
func handleOIDCUserInfo(ctx context.Context, w http.ResponseWriter, req *http.Request) {
data, err := getUserInfo(ctx, req)
tokenHdr := getAuthToken(req)
if len(tokenHdr) == 0 {
httperrors.InvalidCredentialError(ctx, w, "No token in header")
return
}
token, err := decodeOIDCClientToken(tokenHdr)
if err != nil {
log.Errorf("decodeOIDCClientToken %s fail %s", tokenHdr, err)
httperrors.InvalidCredentialError(ctx, w, "Token in header invalid")
return
}
if token.Info.expiresAt(OIDC_TOKEN_EXPIRE_SECONDS).Before(time.Now()) {
httperrors.InvalidCredentialError(ctx, w, "Token expired")
return
}
s := auth.GetAdminSession(ctx, token.Info.Region, "")
data, err := getUserInfo2(s, token.Info.UserId, token.Info.ProjectId, token.Info.Ip.String())
if err != nil {
httperrors.NotFoundError(ctx, w, "%v", err)
return
+93
View File
@@ -0,0 +1,93 @@
// 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 (
"reflect"
"testing"
"time"
"yunion.io/x/pkg/util/netutils"
"yunion.io/x/onecloud/pkg/apigateway/clientman"
)
func TestClientInfo(t *testing.T) {
clientman.SetupTest()
cases := []struct {
ip string
user string
project string
}{
{
ip: "0.0.0.0",
user: "sysadmin",
project: "system",
},
{
ip: "10.168.26.253",
user: "ab9502de-c6b6-4150-880b-d0e3e6ba8ec8",
project: "a2049cfadf4c40888b9da136faba5cc8",
},
}
for _, c := range cases {
info := SOIDCClientInfo{}
info.Timestamp = time.Now().UnixNano()
info.Ip, _ = netutils.NewIPV4Addr(c.ip)
info.UserId = c.user
info.ProjectId = c.project
msg := info.toBytes()
if len(msg) != 14+len(info.UserId)+len(info.ProjectId)+len(info.Region) {
t.Fatalf("incorrect msg size")
}
info2, err := decodeOIDCClientInfo(msg)
if err != nil {
t.Fatalf("decode error %s", err)
}
if info2.Timestamp != info.Timestamp {
t.Fatalf("incorrect timestamp")
}
if info2.Ip.String() != info.Ip.String() {
t.Fatalf("incorrect ip")
}
if info2.UserId != info.UserId {
t.Fatalf("incorrect user id")
}
if info2.ProjectId != info.ProjectId {
t.Fatalf("incorrect project id")
}
if info2.Region != info.Region {
t.Fatalf("incorrect region id")
}
token := SOIDCClientToken{
Info: info,
}
tokenStr := token.encode()
token2, err := decodeOIDCClientToken(tokenStr)
if err != nil {
t.Fatalf("decodeOIDCClientToken fail %s", err)
}
if !reflect.DeepEqual(token2.Info, info2) {
t.Fatalf("token2 info not equal to info2")
}
}
}
+7 -1
View File
@@ -655,6 +655,7 @@ type sSimpleRbacIdentity struct {
domainId string
projectName string
roleNames []string
loginIp string
}
func (id sSimpleRbacIdentity) GetProjectDomainId() string {
@@ -670,7 +671,7 @@ func (id sSimpleRbacIdentity) GetProjectName() string {
}
func (id sSimpleRbacIdentity) GetLoginIp() string {
return ""
return id.loginIp
}
func (id sSimpleRbacIdentity) GetTokenString() string {
@@ -678,10 +679,15 @@ func (id sSimpleRbacIdentity) GetTokenString() string {
}
func NewRbacIdentity(domainId, projectName string, roleNames []string) IRbacIdentity {
return NewRbacIdentity2(domainId, projectName, roleNames, "")
}
func NewRbacIdentity2(domainId, projectName string, roleNames []string, loginIp string) IRbacIdentity {
return sSimpleRbacIdentity{
domainId: domainId,
projectName: projectName,
roleNames: roleNames,
loginIp: loginIp,
}
}