mirror of
https://github.com/zitadel/zitadel.git
synced 2025-10-23 20:41:56 +00:00
get oidc user info from projections and add actions
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
@@ -19,7 +20,8 @@ type Server struct {
|
||||
storage *OPStorage
|
||||
*op.LegacyServer
|
||||
|
||||
query *query.Queries
|
||||
query *query.Queries
|
||||
command *command.Commands
|
||||
|
||||
fallbackLogger *slog.Logger
|
||||
hashAlg crypto.HashAlgorithm
|
||||
|
||||
@@ -2,11 +2,18 @@ package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/actions"
|
||||
"github.com/zitadel/zitadel/internal/actions/object"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
@@ -19,7 +26,7 @@ func (s *Server) getUserInfoWithRoles(ctx context.Context, userID, projectID str
|
||||
defer cancel()
|
||||
|
||||
userInfoChan := make(chan *userInfoResult)
|
||||
go s.getUserInfo(ctx, userID, scope, roleAudience, userInfoChan)
|
||||
go s.getUserInfo(ctx, userID, userInfoChan)
|
||||
|
||||
rolesChan := make(chan *assertRolesResult)
|
||||
go s.assertRoles(ctx, userID, projectID, scope, roleAudience, rolesChan)
|
||||
@@ -56,7 +63,7 @@ func (s *Server) getUserInfoWithRoles(ctx context.Context, userID, projectID str
|
||||
userInfo := userInfoToOIDC(userInfoResult.userInfo, scope)
|
||||
setUserInfoRoleClaims(userInfo, assertRolesResult.projectsRoles)
|
||||
|
||||
return userInfo, nil
|
||||
return userInfo, s.userinfoFlows(ctx, userInfoResult.userInfo, assertRolesResult.userGrants, userInfo)
|
||||
}
|
||||
|
||||
type userInfoResult struct {
|
||||
@@ -64,8 +71,8 @@ type userInfoResult struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *Server) getUserInfo(ctx context.Context, userID string, scope, roleAudience []string, rc chan<- *userInfoResult) {
|
||||
userInfo, err := s.storage.query.GetOIDCUserInfo(ctx, userID, scope, roleAudience)
|
||||
func (s *Server) getUserInfo(ctx context.Context, userID string, rc chan<- *userInfoResult) {
|
||||
userInfo, err := s.storage.query.GetOIDCUserInfo(ctx, userID)
|
||||
rc <- &userInfoResult{
|
||||
userInfo: userInfo,
|
||||
err: err,
|
||||
@@ -81,7 +88,7 @@ type assertRolesResult struct {
|
||||
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
|
||||
if slices.Contains(scope, domain.ScopeProjectsRoles) {
|
||||
if slices.Contains(scope, ScopeProjectsRoles) {
|
||||
roleAudience = domain.AddAudScopeToAudience(ctx, roleAudience, scope)
|
||||
}
|
||||
|
||||
@@ -148,19 +155,17 @@ func userInfoToOIDC(user *query.OIDCUserInfo, scope []string) *oidc.UserInfo {
|
||||
for _, s := range scope {
|
||||
switch s {
|
||||
case oidc.ScopeOpenID:
|
||||
out.Subject = user.ID
|
||||
out.Subject = user.User.ID
|
||||
case oidc.ScopeEmail:
|
||||
out.UserInfoEmail = userInfoEmailToOIDC(user)
|
||||
out.UserInfoEmail = userInfoEmailToOIDC(user.User)
|
||||
case oidc.ScopeProfile:
|
||||
out.UserInfoProfile = userInfoProfileToOidc(user)
|
||||
out.UserInfoProfile = userInfoProfileToOidc(user.User)
|
||||
case oidc.ScopePhone:
|
||||
out.UserInfoPhone = userInfoPhoneToOIDC(user)
|
||||
out.UserInfoPhone = userInfoPhoneToOIDC(user.User)
|
||||
case oidc.ScopeAddress:
|
||||
out.Address = userInfoAddressToOIDC(user)
|
||||
//TODO: handle address for human users as soon as implemented
|
||||
case ScopeUserMetaData:
|
||||
if len(user.Metadata) > 0 {
|
||||
out.AppendClaims(ClaimUserMetaData, user.Metadata)
|
||||
}
|
||||
setUserInfoMetadata(user.Metadata, out)
|
||||
case ScopeResourceOwner:
|
||||
setUserInfoOrgClaims(user, out)
|
||||
default:
|
||||
@@ -177,47 +182,173 @@ func userInfoToOIDC(user *query.OIDCUserInfo, scope []string) *oidc.UserInfo {
|
||||
return out
|
||||
}
|
||||
|
||||
func userInfoEmailToOIDC(user *query.OIDCUserInfo) oidc.UserInfoEmail {
|
||||
return oidc.UserInfoEmail{
|
||||
Email: string(user.Email),
|
||||
EmailVerified: oidc.Bool(user.IsEmailVerified),
|
||||
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),
|
||||
}
|
||||
}
|
||||
return oidc.UserInfoEmail{}
|
||||
}
|
||||
|
||||
func userInfoProfileToOidc(user *query.OIDCUserInfo) oidc.UserInfoProfile {
|
||||
func userInfoProfileToOidc(user *query.User) oidc.UserInfoProfile {
|
||||
if human := user.Human; human != nil {
|
||||
return oidc.UserInfoProfile{
|
||||
Name: human.DisplayName,
|
||||
GivenName: human.FirstName,
|
||||
FamilyName: human.LastName,
|
||||
Nickname: human.NickName,
|
||||
// Picture: domain.AvatarURL(o.assetAPIPrefix(ctx), user.ResourceOwner, user.Human.AvatarKey),
|
||||
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,
|
||||
}
|
||||
}
|
||||
return oidc.UserInfoProfile{
|
||||
Name: user.Name,
|
||||
GivenName: user.FirstName,
|
||||
FamilyName: user.LastName,
|
||||
Nickname: user.NickName,
|
||||
// Picture: domain.AvatarURL(o.assetAPIPrefix(ctx), user.ResourceOwner, user.Human.AvatarKey),
|
||||
Gender: getGender(user.Gender),
|
||||
Locale: oidc.NewLocale(user.PreferredLanguage),
|
||||
UpdatedAt: oidc.FromTime(user.UpdatedAt),
|
||||
// PreferredUsername: user.PreferredLoginName,
|
||||
UpdatedAt: oidc.FromTime(user.ChangeDate),
|
||||
PreferredUsername: user.PreferredLoginName,
|
||||
}
|
||||
}
|
||||
|
||||
func userInfoPhoneToOIDC(user *query.OIDCUserInfo) oidc.UserInfoPhone {
|
||||
return oidc.UserInfoPhone{
|
||||
PhoneNumber: string(user.Phone),
|
||||
PhoneNumberVerified: user.IsPhoneVerified,
|
||||
func userInfoPhoneToOIDC(user *query.User) oidc.UserInfoPhone {
|
||||
if human := user.Human; human != nil {
|
||||
return oidc.UserInfoPhone{
|
||||
PhoneNumber: string(human.Phone),
|
||||
PhoneNumberVerified: human.IsPhoneVerified,
|
||||
}
|
||||
}
|
||||
return oidc.UserInfoPhone{}
|
||||
}
|
||||
|
||||
func userInfoAddressToOIDC(user *query.OIDCUserInfo) *oidc.UserInfoAddress {
|
||||
return &oidc.UserInfoAddress{
|
||||
// Formatted: ??,
|
||||
StreetAddress: user.StreetAddress,
|
||||
Locality: user.Locality,
|
||||
Region: user.Region,
|
||||
PostalCode: user.PostalCode,
|
||||
Country: user.Country,
|
||||
func setUserInfoMetadata(metadata []query.UserMetadata, out *oidc.UserInfo) {
|
||||
if len(metadata) == 0 {
|
||||
return
|
||||
}
|
||||
mdmap := make(map[string]string, len(metadata))
|
||||
for _, md := range metadata {
|
||||
mdmap[md.Key] = base64.RawURLEncoding.EncodeToString(md.Value)
|
||||
}
|
||||
out.AppendClaims(ClaimUserMetaData, mdmap)
|
||||
}
|
||||
|
||||
func setUserInfoOrgClaims(user *query.OIDCUserInfo, out *oidc.UserInfo) {
|
||||
out.AppendClaims(ClaimResourceOwner+"id", user.OrgID)
|
||||
out.AppendClaims(ClaimResourceOwner+"name", user.OrgName)
|
||||
out.AppendClaims(ClaimResourceOwner+"primary_domain", user.OrgPrimaryDomain)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user