mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 00:27:31 +00:00
fix(oidc): respect role assertion and idTokenInfo flags and trigger preAccessToken trigger (#8046)
# Which Problems Are Solved After deployment of 2.53.x, customers noted that the roles claims where always present in the tokens even if the corresponding option on the client (accessTokenRoleAssertion, idTokenRoleAsseriton) was disabled. Only the project flag (assertRolesOnAuthentication) would be considered. Further it was noted, that the action on the preAccessTokenCreation trigger would not be executed. Additionally, while testing those issues we found out, that the user information (name, givenname, family name, ...) where always present in the id_token even if the option (idTokenUserInfo) was not enabled. # How the Problems Are Solved - The `getUserinfoOnce` which was used for access and id_tokens is refactored to `getUserInfo` and no longer only queries the info once from the database, but still provides a mechanism to be reused for access and id_token where the corresponding `roleAssertion` and action `triggerType` can be passed. - `userInfo` on the other hand now directly makes sure the information is only queried once from the database. Role claims are asserted every time and action triggers are executed on every call. - `userInfo` now also checks if the profile information need to be returned. # Additional Changes None. # Additional Context - relates to #7822 - reported by customers
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/zitadel/logging"
|
||||
@@ -55,7 +56,14 @@ func (s *Server) UserInfo(ctx context.Context, r *op.Request[oidc.UserInfoReques
|
||||
}
|
||||
}
|
||||
|
||||
userInfo, err := s.userInfo(ctx, token.userID, token.scope, projectID, assertion, false)
|
||||
userInfo, err := s.userInfo(
|
||||
token.userID,
|
||||
token.scope,
|
||||
projectID,
|
||||
assertion,
|
||||
true,
|
||||
false,
|
||||
)(ctx, true, domain.TriggerTypePreUserinfoCreation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -66,24 +74,44 @@ func (s *Server) UserInfo(ctx context.Context, r *op.Request[oidc.UserInfoReques
|
||||
// The returned UserInfo contains standard and reserved claims, documented
|
||||
// here: https://zitadel.com/docs/apis/openidoauth/claims.
|
||||
//
|
||||
// User information is only retrieved once from the database.
|
||||
// However, each time, role claims are asserted and also action flows will trigger.
|
||||
//
|
||||
// projectID is an optional parameter which defines the default audience when there are any (or all) role claims requested.
|
||||
// projectRoleAssertion sets the default of returning all project roles, only if no specific roles were requested in the scope.
|
||||
// roleAssertion decides whether the roles will be returned (in the token or response)
|
||||
// userInfoAssertion decides whether the user information (profile data like name, email, ...) are returned
|
||||
//
|
||||
// currentProjectOnly can be set to use the current project ID only and ignore the audience from the scope.
|
||||
// It should be set in cases where the client doesn't need to know roles outside its own project,
|
||||
// for example an introspection client.
|
||||
func (s *Server) userInfo(ctx context.Context, userID string, scope []string, projectID string, projectRoleAssertion, currentProjectOnly bool) (_ *oidc.UserInfo, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
func (s *Server) userInfo(
|
||||
userID string,
|
||||
scope []string,
|
||||
projectID string,
|
||||
projectRoleAssertion, userInfoAssertion, currentProjectOnly bool,
|
||||
) func(ctx context.Context, roleAssertion bool, triggerType domain.TriggerType) (_ *oidc.UserInfo, err error) {
|
||||
var (
|
||||
once sync.Once
|
||||
userInfo *oidc.UserInfo
|
||||
qu *query.OIDCUserInfo
|
||||
roleAudience, requestedRoles []string
|
||||
)
|
||||
return func(ctx context.Context, roleAssertion bool, triggerType domain.TriggerType) (_ *oidc.UserInfo, err error) {
|
||||
once.Do(func() {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
roleAudience, requestedRoles := prepareRoles(ctx, scope, projectID, projectRoleAssertion, currentProjectOnly)
|
||||
qu, err := s.query.GetOIDCUserInfo(ctx, userID, roleAudience)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
roleAudience, requestedRoles = prepareRoles(ctx, scope, projectID, projectRoleAssertion, currentProjectOnly)
|
||||
qu, err = s.query.GetOIDCUserInfo(ctx, userID, roleAudience)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
userInfo = userInfoToOIDC(qu, userInfoAssertion, scope, s.assetAPIPrefix(ctx))
|
||||
})
|
||||
userInfoWithRoles := assertRoles(projectID, qu, roleAudience, requestedRoles, roleAssertion, userInfo)
|
||||
return userInfoWithRoles, s.userinfoFlows(ctx, qu, userInfoWithRoles, triggerType)
|
||||
}
|
||||
|
||||
userInfo := userInfoToOIDC(projectID, qu, scope, roleAudience, requestedRoles, s.assetAPIPrefix(ctx))
|
||||
return userInfo, s.userinfoFlows(ctx, qu, userInfo)
|
||||
}
|
||||
|
||||
// prepareRoles scans the requested scopes and builds the requested roles
|
||||
@@ -120,20 +148,32 @@ func prepareRoles(ctx context.Context, scope []string, projectID string, project
|
||||
return roleAudience, requestedRoles
|
||||
}
|
||||
|
||||
func userInfoToOIDC(projectID string, user *query.OIDCUserInfo, scope, roleAudience, requestedRoles []string, assetPrefix string) *oidc.UserInfo {
|
||||
func userInfoToOIDC(user *query.OIDCUserInfo, userInfoAssertion bool, scope []string, assetPrefix string) *oidc.UserInfo {
|
||||
out := new(oidc.UserInfo)
|
||||
for _, s := range scope {
|
||||
switch s {
|
||||
case oidc.ScopeOpenID:
|
||||
out.Subject = user.User.ID
|
||||
case oidc.ScopeEmail:
|
||||
if !userInfoAssertion {
|
||||
continue
|
||||
}
|
||||
out.UserInfoEmail = userInfoEmailToOIDC(user.User)
|
||||
case oidc.ScopeProfile:
|
||||
if !userInfoAssertion {
|
||||
continue
|
||||
}
|
||||
out.UserInfoProfile = userInfoProfileToOidc(user.User, assetPrefix)
|
||||
case oidc.ScopePhone:
|
||||
if !userInfoAssertion {
|
||||
continue
|
||||
}
|
||||
out.UserInfoPhone = userInfoPhoneToOIDC(user.User)
|
||||
case oidc.ScopeAddress:
|
||||
//TODO: handle address for human users as soon as implemented
|
||||
if !userInfoAssertion {
|
||||
continue
|
||||
}
|
||||
// TODO: handle address for human users as soon as implemented
|
||||
case ScopeUserMetaData:
|
||||
setUserInfoMetadata(user.Metadata, out)
|
||||
case ScopeResourceOwner:
|
||||
@@ -148,12 +188,19 @@ func userInfoToOIDC(projectID string, user *query.OIDCUserInfo, scope, roleAudie
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func assertRoles(projectID string, user *query.OIDCUserInfo, roleAudience, requestedRoles []string, assertion bool, info *oidc.UserInfo) *oidc.UserInfo {
|
||||
if !assertion {
|
||||
return info
|
||||
}
|
||||
userInfo := *info
|
||||
// prevent returning obtained grants if none where requested
|
||||
if (projectID != "" && len(requestedRoles) > 0) || len(roleAudience) > 0 {
|
||||
setUserInfoRoleClaims(out, newProjectRoles(projectID, user.UserGrants, requestedRoles))
|
||||
setUserInfoRoleClaims(&userInfo, newProjectRoles(projectID, user.UserGrants, requestedRoles))
|
||||
}
|
||||
return out
|
||||
return &userInfo
|
||||
}
|
||||
|
||||
func userInfoEmailToOIDC(user *query.User) oidc.UserInfoEmail {
|
||||
@@ -230,11 +277,11 @@ func setUserInfoRoleClaims(userInfo *oidc.UserInfo, roles *projectsRoles) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, userInfo *oidc.UserInfo) (err error) {
|
||||
func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, userInfo *oidc.UserInfo, triggerType domain.TriggerType) (err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
queriedActions, err := s.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeCustomiseToken, domain.TriggerTypePreUserinfoCreation, qu.User.ResourceOwner)
|
||||
queriedActions, err := s.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeCustomiseToken, triggerType, qu.User.ResourceOwner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
Reference in New Issue
Block a user