2023-11-02 17:27:30 +02:00
|
|
|
package oidc
|
|
|
|
|
|
|
|
|
|
import (
|
2023-11-06 20:27:25 +02:00
|
|
|
"context"
|
2023-11-13 18:13:34 +02:00
|
|
|
"encoding/base64"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
2023-11-06 20:27:25 +02:00
|
|
|
"slices"
|
2023-11-02 17:27:30 +02:00
|
|
|
"strings"
|
|
|
|
|
|
2023-11-13 18:13:34 +02:00
|
|
|
"github.com/dop251/goja"
|
|
|
|
|
"github.com/zitadel/logging"
|
2023-11-02 17:27:30 +02:00
|
|
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
2023-11-06 20:27:25 +02:00
|
|
|
|
2023-11-13 18:13:34 +02:00
|
|
|
"github.com/zitadel/zitadel/internal/actions"
|
|
|
|
|
"github.com/zitadel/zitadel/internal/actions/object"
|
2023-11-02 17:27:30 +02:00
|
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
|
|
|
"github.com/zitadel/zitadel/internal/query"
|
2023-11-06 20:27:25 +02:00
|
|
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
2023-11-02 17:27:30 +02:00
|
|
|
)
|
|
|
|
|
|
2023-11-06 20:27:25 +02:00
|
|
|
func (s *Server) getUserInfoWithRoles(ctx context.Context, userID, projectID string, scope, roleAudience []string) (_ *oidc.UserInfo, err error) {
|
|
|
|
|
ctx, span := tracing.NewSpan(ctx)
|
|
|
|
|
defer func() { span.EndWithError(err) }()
|
|
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
userInfoChan := make(chan *userInfoResult)
|
2023-11-13 18:13:34 +02:00
|
|
|
go s.getUserInfo(ctx, userID, userInfoChan)
|
2023-11-06 20:27:25 +02:00
|
|
|
|
|
|
|
|
rolesChan := make(chan *assertRolesResult)
|
|
|
|
|
go s.assertRoles(ctx, userID, projectID, scope, roleAudience, rolesChan)
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
userInfoResult *userInfoResult
|
|
|
|
|
assertRolesResult *assertRolesResult
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// make sure both channels are always read,
|
|
|
|
|
// and cancel the context on first error
|
|
|
|
|
for i := 0; i < 2; i++ {
|
|
|
|
|
var resErr error
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case userInfoResult = <-userInfoChan:
|
|
|
|
|
resErr = userInfoResult.err
|
|
|
|
|
case assertRolesResult = <-rolesChan:
|
|
|
|
|
resErr = assertRolesResult.err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if resErr == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
cancel()
|
|
|
|
|
|
|
|
|
|
// we only care for the first error that occured,
|
|
|
|
|
// as the next error is most probably a context error.
|
|
|
|
|
if err == nil {
|
|
|
|
|
err = resErr
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-13 18:27:09 +02:00
|
|
|
userInfo := userInfoToOIDC(userInfoResult.userInfo, scope, s.assetAPIPrefix(ctx))
|
2023-11-06 20:27:25 +02:00
|
|
|
setUserInfoRoleClaims(userInfo, assertRolesResult.projectsRoles)
|
|
|
|
|
|
2023-11-13 18:13:34 +02:00
|
|
|
return userInfo, s.userinfoFlows(ctx, userInfoResult.userInfo, assertRolesResult.userGrants, userInfo)
|
2023-11-06 20:27:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type userInfoResult struct {
|
|
|
|
|
userInfo *query.OIDCUserInfo
|
|
|
|
|
err error
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-13 18:13:34 +02:00
|
|
|
func (s *Server) getUserInfo(ctx context.Context, userID string, rc chan<- *userInfoResult) {
|
2023-11-13 19:38:52 +02:00
|
|
|
userInfo, err := s.query.GetOIDCUserInfo(ctx, userID)
|
2023-11-06 20:27:25 +02:00
|
|
|
rc <- &userInfoResult{
|
|
|
|
|
userInfo: userInfo,
|
|
|
|
|
err: err,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type assertRolesResult struct {
|
|
|
|
|
userGrants *query.UserGrants
|
|
|
|
|
projectsRoles *projectsRoles
|
|
|
|
|
err error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) assertRoles(ctx context.Context, userID, projectID string, scope, roleAudience []string, rc chan<- *assertRolesResult) {
|
|
|
|
|
userGrands, projectsRoles, err := func() (*query.UserGrants, *projectsRoles, error) {
|
|
|
|
|
// if all roles are requested take the audience for those from the scopes
|
2023-11-13 18:13:34 +02:00
|
|
|
if slices.Contains(scope, ScopeProjectsRoles) {
|
2023-11-06 20:27:25 +02:00
|
|
|
roleAudience = domain.AddAudScopeToAudience(ctx, roleAudience, scope)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
requestedRoles := make([]string, 0, len(scope))
|
|
|
|
|
for _, s := range scope {
|
|
|
|
|
if role, ok := strings.CutPrefix(s, ScopeProjectRolePrefix); ok {
|
|
|
|
|
requestedRoles = append(requestedRoles, role)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(requestedRoles) == 0 && len(roleAudience) == 0 {
|
|
|
|
|
return nil, nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ensure the projectID of the requesting is part of the roleAudience
|
|
|
|
|
if projectID != "" {
|
|
|
|
|
roleAudience = append(roleAudience, projectID)
|
|
|
|
|
}
|
|
|
|
|
queries := make([]query.SearchQuery, 0, 2)
|
|
|
|
|
projectQuery, err := query.NewUserGrantProjectIDsSearchQuery(roleAudience)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
queries = append(queries, projectQuery)
|
|
|
|
|
userIDQuery, err := query.NewUserGrantUserIDSearchQuery(userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
queries = append(queries, userIDQuery)
|
|
|
|
|
grants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{
|
|
|
|
|
Queries: queries,
|
|
|
|
|
}, false, false) // triggers disabled
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
roles := new(projectsRoles)
|
|
|
|
|
// if specific roles where requested, check if they are granted and append them in the roles list
|
|
|
|
|
if len(requestedRoles) > 0 {
|
|
|
|
|
for _, requestedRole := range requestedRoles {
|
|
|
|
|
for _, grant := range grants.UserGrants {
|
|
|
|
|
checkGrantedRoles(roles, grant, requestedRole, grant.ProjectID == projectID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return grants, roles, nil
|
|
|
|
|
}
|
|
|
|
|
// no specific roles were requested, so convert any grants into roles
|
|
|
|
|
for _, grant := range grants.UserGrants {
|
|
|
|
|
for _, role := range grant.Roles {
|
|
|
|
|
roles.Add(grant.ProjectID, role, grant.ResourceOwner, grant.OrgPrimaryDomain, grant.ProjectID == projectID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return grants, roles, nil
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
rc <- &assertRolesResult{
|
|
|
|
|
userGrants: userGrands,
|
|
|
|
|
projectsRoles: projectsRoles,
|
|
|
|
|
err: err,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-13 18:27:09 +02:00
|
|
|
func userInfoToOIDC(user *query.OIDCUserInfo, scope []string, assetPrefix string) *oidc.UserInfo {
|
2023-11-02 17:27:30 +02:00
|
|
|
out := new(oidc.UserInfo)
|
2023-11-06 20:27:25 +02:00
|
|
|
for _, s := range scope {
|
|
|
|
|
switch s {
|
2023-11-02 17:27:30 +02:00
|
|
|
case oidc.ScopeOpenID:
|
2023-11-13 18:13:34 +02:00
|
|
|
out.Subject = user.User.ID
|
2023-11-02 17:27:30 +02:00
|
|
|
case oidc.ScopeEmail:
|
2023-11-13 18:13:34 +02:00
|
|
|
out.UserInfoEmail = userInfoEmailToOIDC(user.User)
|
2023-11-02 17:27:30 +02:00
|
|
|
case oidc.ScopeProfile:
|
2023-11-13 18:27:09 +02:00
|
|
|
out.UserInfoProfile = userInfoProfileToOidc(user.User, assetPrefix)
|
2023-11-02 17:27:30 +02:00
|
|
|
case oidc.ScopePhone:
|
2023-11-13 18:13:34 +02:00
|
|
|
out.UserInfoPhone = userInfoPhoneToOIDC(user.User)
|
2023-11-02 17:27:30 +02:00
|
|
|
case oidc.ScopeAddress:
|
2023-11-13 18:13:34 +02:00
|
|
|
//TODO: handle address for human users as soon as implemented
|
2023-11-02 17:27:30 +02:00
|
|
|
case ScopeUserMetaData:
|
2023-11-13 18:13:34 +02:00
|
|
|
setUserInfoMetadata(user.Metadata, out)
|
2023-11-02 17:27:30 +02:00
|
|
|
case ScopeResourceOwner:
|
|
|
|
|
setUserInfoOrgClaims(user, out)
|
|
|
|
|
default:
|
2023-11-06 20:27:25 +02:00
|
|
|
if strings.HasPrefix(s, domain.OrgDomainPrimaryScope) {
|
|
|
|
|
out.AppendClaims(domain.OrgDomainPrimaryClaim, strings.TrimPrefix(s, domain.OrgDomainPrimaryScope))
|
2023-11-02 17:27:30 +02:00
|
|
|
}
|
2023-11-06 20:27:25 +02:00
|
|
|
if strings.HasPrefix(s, domain.OrgIDScope) {
|
|
|
|
|
out.AppendClaims(domain.OrgIDClaim, strings.TrimPrefix(s, domain.OrgIDScope))
|
2023-11-02 17:27:30 +02:00
|
|
|
setUserInfoOrgClaims(user, out)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-13 18:13:34 +02:00
|
|
|
func userInfoEmailToOIDC(user *query.User) oidc.UserInfoEmail {
|
|
|
|
|
if human := user.Human; human != nil {
|
|
|
|
|
return oidc.UserInfoEmail{
|
|
|
|
|
Email: string(human.Email),
|
|
|
|
|
EmailVerified: oidc.Bool(human.IsEmailVerified),
|
|
|
|
|
}
|
2023-11-02 17:27:30 +02:00
|
|
|
}
|
2023-11-13 18:13:34 +02:00
|
|
|
return oidc.UserInfoEmail{}
|
2023-11-02 17:27:30 +02:00
|
|
|
}
|
|
|
|
|
|
2023-11-13 18:27:09 +02:00
|
|
|
func userInfoProfileToOidc(user *query.User, assetPrefix string) oidc.UserInfoProfile {
|
2023-11-13 18:13:34 +02:00
|
|
|
if human := user.Human; human != nil {
|
|
|
|
|
return oidc.UserInfoProfile{
|
2023-11-13 18:27:09 +02:00
|
|
|
Name: human.DisplayName,
|
|
|
|
|
GivenName: human.FirstName,
|
|
|
|
|
FamilyName: human.LastName,
|
|
|
|
|
Nickname: human.NickName,
|
|
|
|
|
Picture: domain.AvatarURL(assetPrefix, user.ResourceOwner, user.Human.AvatarKey),
|
2023-11-13 18:13:34 +02:00
|
|
|
Gender: getGender(human.Gender),
|
|
|
|
|
Locale: oidc.NewLocale(human.PreferredLanguage),
|
|
|
|
|
UpdatedAt: oidc.FromTime(user.ChangeDate),
|
|
|
|
|
PreferredUsername: user.PreferredLoginName,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if machine := user.Machine; machine != nil {
|
|
|
|
|
return oidc.UserInfoProfile{
|
|
|
|
|
Name: machine.Name,
|
|
|
|
|
UpdatedAt: oidc.FromTime(user.ChangeDate),
|
|
|
|
|
PreferredUsername: user.PreferredLoginName,
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-11-02 17:27:30 +02:00
|
|
|
return oidc.UserInfoProfile{
|
2023-11-13 18:13:34 +02:00
|
|
|
UpdatedAt: oidc.FromTime(user.ChangeDate),
|
|
|
|
|
PreferredUsername: user.PreferredLoginName,
|
2023-11-02 17:27:30 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-13 18:13:34 +02:00
|
|
|
func userInfoPhoneToOIDC(user *query.User) oidc.UserInfoPhone {
|
|
|
|
|
if human := user.Human; human != nil {
|
|
|
|
|
return oidc.UserInfoPhone{
|
|
|
|
|
PhoneNumber: string(human.Phone),
|
|
|
|
|
PhoneNumberVerified: human.IsPhoneVerified,
|
|
|
|
|
}
|
2023-11-02 17:27:30 +02:00
|
|
|
}
|
2023-11-13 18:13:34 +02:00
|
|
|
return oidc.UserInfoPhone{}
|
2023-11-02 17:27:30 +02:00
|
|
|
}
|
|
|
|
|
|
2023-11-13 18:13:34 +02:00
|
|
|
func setUserInfoMetadata(metadata []query.UserMetadata, out *oidc.UserInfo) {
|
|
|
|
|
if len(metadata) == 0 {
|
|
|
|
|
return
|
2023-11-02 17:27:30 +02:00
|
|
|
}
|
2023-11-13 18:13:34 +02:00
|
|
|
mdmap := make(map[string]string, len(metadata))
|
|
|
|
|
for _, md := range metadata {
|
|
|
|
|
mdmap[md.Key] = base64.RawURLEncoding.EncodeToString(md.Value)
|
|
|
|
|
}
|
|
|
|
|
out.AppendClaims(ClaimUserMetaData, mdmap)
|
2023-11-02 17:27:30 +02:00
|
|
|
}
|
|
|
|
|
|
2023-11-06 20:27:25 +02:00
|
|
|
func setUserInfoOrgClaims(user *query.OIDCUserInfo, out *oidc.UserInfo) {
|
2023-11-13 18:13:34 +02:00
|
|
|
if org := user.Org; org != nil {
|
|
|
|
|
out.AppendClaims(ClaimResourceOwner+"id", org.ID)
|
|
|
|
|
out.AppendClaims(ClaimResourceOwner+"name", org.Name)
|
|
|
|
|
out.AppendClaims(ClaimResourceOwner+"primary_domain", org.PrimaryDomain)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-13 19:20:01 +02:00
|
|
|
func setUserInfoRoleClaims(userInfo *oidc.UserInfo, roles *projectsRoles) {
|
|
|
|
|
if roles != nil && len(roles.projects) > 0 {
|
|
|
|
|
if roles, ok := roles.projects[roles.requestProjectID]; ok {
|
|
|
|
|
userInfo.AppendClaims(ClaimProjectRoles, roles)
|
|
|
|
|
}
|
|
|
|
|
for projectID, roles := range roles.projects {
|
|
|
|
|
userInfo.AppendClaims(fmt.Sprintf(ClaimProjectRolesFormat, projectID), roles)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-13 18:13:34 +02:00
|
|
|
func (s *Server) userinfoFlows(ctx context.Context, user *query.OIDCUserInfo, userGrants *query.UserGrants, userInfo *oidc.UserInfo) error {
|
|
|
|
|
queriedActions, err := s.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeCustomiseToken, domain.TriggerTypePreUserinfoCreation, user.User.ResourceOwner, false)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctxFields := actions.SetContextFields(
|
|
|
|
|
actions.SetFields("v1",
|
|
|
|
|
actions.SetFields("claims", userinfoClaims(userInfo)),
|
|
|
|
|
actions.SetFields("getUser", func(c *actions.FieldConfig) interface{} {
|
|
|
|
|
return func(call goja.FunctionCall) goja.Value {
|
|
|
|
|
return object.UserFromQuery(c, user.User)
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
actions.SetFields("user",
|
|
|
|
|
actions.SetFields("getMetadata", func(c *actions.FieldConfig) interface{} {
|
|
|
|
|
return func(goja.FunctionCall) goja.Value {
|
|
|
|
|
return object.UserMetadataListFromSlice(c, user.Metadata)
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
actions.SetFields("grants", func(c *actions.FieldConfig) interface{} {
|
|
|
|
|
return object.UserGrantsFromQuery(c, userGrants)
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for _, action := range queriedActions {
|
|
|
|
|
actionCtx, cancel := context.WithTimeout(ctx, action.Timeout())
|
|
|
|
|
claimLogs := []string{}
|
|
|
|
|
|
|
|
|
|
apiFields := actions.WithAPIFields(
|
|
|
|
|
actions.SetFields("v1",
|
|
|
|
|
actions.SetFields("userinfo",
|
|
|
|
|
actions.SetFields("setClaim", func(key string, value interface{}) {
|
|
|
|
|
if userInfo.Claims[key] == nil {
|
|
|
|
|
userInfo.AppendClaims(key, value)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
claimLogs = append(claimLogs, fmt.Sprintf("key %q already exists", key))
|
|
|
|
|
}),
|
|
|
|
|
actions.SetFields("appendLogIntoClaims", func(entry string) {
|
|
|
|
|
claimLogs = append(claimLogs, entry)
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
actions.SetFields("claims",
|
|
|
|
|
actions.SetFields("setClaim", func(key string, value interface{}) {
|
|
|
|
|
if userInfo.Claims[key] == nil {
|
|
|
|
|
userInfo.AppendClaims(key, value)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
claimLogs = append(claimLogs, fmt.Sprintf("key %q already exists", key))
|
|
|
|
|
}),
|
|
|
|
|
actions.SetFields("appendLogIntoClaims", func(entry string) {
|
|
|
|
|
claimLogs = append(claimLogs, entry)
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
actions.SetFields("user",
|
|
|
|
|
actions.SetFields("setMetadata", func(call goja.FunctionCall) goja.Value {
|
|
|
|
|
if len(call.Arguments) != 2 {
|
|
|
|
|
panic("exactly 2 (key, value) arguments expected")
|
|
|
|
|
}
|
|
|
|
|
key := call.Arguments[0].Export().(string)
|
|
|
|
|
val := call.Arguments[1].Export()
|
|
|
|
|
|
|
|
|
|
value, err := json.Marshal(val)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logging.WithError(err).Debug("unable to marshal")
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
metadata := &domain.Metadata{
|
|
|
|
|
Key: key,
|
|
|
|
|
Value: value,
|
|
|
|
|
}
|
|
|
|
|
if _, err = s.command.SetUserMetadata(ctx, metadata, userInfo.Subject, user.User.ResourceOwner); err != nil {
|
|
|
|
|
logging.WithError(err).Info("unable to set md in action")
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
err = actions.Run(
|
|
|
|
|
actionCtx,
|
|
|
|
|
ctxFields,
|
|
|
|
|
apiFields,
|
|
|
|
|
action.Script,
|
|
|
|
|
action.Name,
|
|
|
|
|
append(actions.ActionToOptions(action), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))...,
|
|
|
|
|
)
|
|
|
|
|
cancel()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if len(claimLogs) > 0 {
|
|
|
|
|
userInfo.AppendClaims(fmt.Sprintf(ClaimActionLogFormat, action.Name), claimLogs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
2023-11-02 17:27:30 +02:00
|
|
|
}
|