From 5e67a9e5c00f39684502c8c5c5c8bb15d69957b3 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 13 Oct 2025 18:26:52 +0800 Subject: [PATCH] Add Yao type information to user and token claims - Enhanced the OIDCUserInfo structure to include YaoTypeID and YaoType fields for better user type management. - Updated SignIDToken method to incorporate YaoTypeID and YaoType claims, ensuring comprehensive user type information in ID tokens. - Modified team creation logic to set default type_id based on team configuration, improving team management capabilities. - Refactored token generation methods to support additional type information in access and refresh tokens, enhancing overall token customization. --- openapi/oauth/signing.go | 19 +++++++++++++ openapi/oauth/types/oidc.go | 38 ++++++++++++++++++++++++++ openapi/oauth/types/types.go | 9 +++++++ openapi/user/login.go | 52 ++++++++++++++++++++++++++++++------ openapi/user/team.go | 31 +++++++++++++++++++++ openapi/user/types.go | 1 + 6 files changed, 142 insertions(+), 8 deletions(-) diff --git a/openapi/oauth/signing.go b/openapi/oauth/signing.go index 385ac21f..e422da2d 100644 --- a/openapi/oauth/signing.go +++ b/openapi/oauth/signing.go @@ -576,6 +576,9 @@ func (s *Service) SignIDToken(clientID, scope string, expiresIn int, userdata *t if userdata.YaoIsOwner != nil { claims["yao:is_owner"] = *userdata.YaoIsOwner } + if userdata.YaoTypeID != "" { + claims["yao:type_id"] = userdata.YaoTypeID + } // Add Yao team info if present if userdata.YaoTeam != nil { teamMap := make(map[string]interface{}) @@ -601,6 +604,22 @@ func (s *Service) SignIDToken(clientID, scope string, expiresIn int, userdata *t claims["yao:team"] = teamMap } } + // Add Yao type info if present + if userdata.YaoType != nil { + typeMap := make(map[string]interface{}) + if userdata.YaoType.TypeID != "" { + typeMap["type_id"] = userdata.YaoType.TypeID + } + if userdata.YaoType.Name != "" { + typeMap["name"] = userdata.YaoType.Name + } + if userdata.YaoType.Locale != "" { + typeMap["locale"] = userdata.YaoType.Locale + } + if len(typeMap) > 0 { + claims["yao:type"] = typeMap + } + } // Add scope if provided (useful for determining which claims to include) if scope != "" { diff --git a/openapi/oauth/types/oidc.go b/openapi/oauth/types/oidc.go index 037e00b1..64f5c448 100644 --- a/openapi/oauth/types/oidc.go +++ b/openapi/oauth/types/oidc.go @@ -107,6 +107,9 @@ func (user OIDCUserInfo) Map() map[string]interface{} { if user.YaoIsOwner != nil { result["yao:is_owner"] = user.YaoIsOwner } + if user.YaoTypeID != "" { + result["yao:type_id"] = user.YaoTypeID + } // Add Yao team info if present and has content if user.YaoTeam != nil { @@ -134,6 +137,23 @@ func (user OIDCUserInfo) Map() map[string]interface{} { } } + // Add Yao type info if present and has content + if user.YaoType != nil { + typeMap := make(map[string]interface{}) + if user.YaoType.TypeID != "" { + typeMap["type_id"] = user.YaoType.TypeID + } + if user.YaoType.Name != "" { + typeMap["name"] = user.YaoType.Name + } + if user.YaoType.Locale != "" { + typeMap["locale"] = user.YaoType.Locale + } + if len(typeMap) > 0 { + result["yao:type"] = typeMap + } + } + // Include raw data if available // if user.Raw != nil { // // Merge raw data, but let structured fields take precedence @@ -254,6 +274,9 @@ func MakeOIDCUserInfo(user map[string]interface{}) *OIDCUserInfo { if isOwner, ok := user["yao:is_owner"].(bool); ok { userInfo.YaoIsOwner = &isOwner } + if typeID, ok := user["yao:type_id"].(string); ok { + userInfo.YaoTypeID = typeID + } // Yao team info (nested object) if teamData, ok := user["yao:team"].(map[string]interface{}); ok { @@ -283,6 +306,21 @@ func MakeOIDCUserInfo(user map[string]interface{}) *OIDCUserInfo { userInfo.YaoTeam = team } + // Yao type info (nested object) + if typeData, ok := user["yao:type"].(map[string]interface{}); ok { + typeInfo := &OIDCTypeInfo{} + if typeID, ok := typeData["type_id"].(string); ok { + typeInfo.TypeID = typeID + } + if name, ok := typeData["name"].(string); ok { + typeInfo.Name = name + } + if locale, ok := typeData["locale"].(string); ok { + typeInfo.Locale = locale + } + userInfo.YaoType = typeInfo + } + return userInfo } diff --git a/openapi/oauth/types/types.go b/openapi/oauth/types/types.go index 6fb1113a..91c48c8b 100644 --- a/openapi/oauth/types/types.go +++ b/openapi/oauth/types/types.go @@ -694,6 +694,8 @@ type OIDCUserInfo struct { YaoTeamID string `json:"yao:team_id,omitempty"` // Yao team ID YaoTeam *OIDCTeamInfo `json:"yao:team,omitempty"` // Yao team info YaoIsOwner *bool `json:"yao:is_owner,omitempty"` // Yao is owner + YaoTypeID string `json:"yao:type_id,omitempty"` // Yao user type ID + YaoType *OIDCTypeInfo `json:"yao:type,omitempty"` // Yao user type info // Raw response for debugging and custom processing Raw map[string]interface{} `json:"raw,omitempty"` // Original provider response @@ -709,6 +711,13 @@ type OIDCTeamInfo struct { UpdatedAt *int64 `json:"updated_at,omitempty"` // Team updated at (seconds since epoch) } +// OIDCTypeInfo represents user type information based on OIDC standard +type OIDCTypeInfo struct { + TypeID string `json:"type_id,omitempty"` // User type identifier + Name string `json:"name,omitempty"` // User type name + Locale string `json:"locale,omitempty"` // User type locale +} + // OIDCAddress represents the OIDC address claim structure type OIDCAddress struct { Formatted string `json:"formatted,omitempty"` // Full mailing address diff --git a/openapi/user/login.go b/openapi/user/login.go index 39a250ca..434541ab 100644 --- a/openapi/user/login.go +++ b/openapi/user/login.go @@ -300,12 +300,12 @@ func issueTokens(ctx context.Context, userid string, teamID string, team map[str oidcUserInfo := oauthtypes.MakeOIDCUserInfo(user) oidcUserInfo.Sub = subject - // Prepare extra claims for team context - var extraClaims map[string]interface{} + // Prepare extra claims for access token + extraClaims := make(map[string]interface{}) + + // Add team context if available if teamID != "" && team != nil { - extraClaims = map[string]interface{}{ - "team_id": teamID, - } + extraClaims["team_id"] = teamID // Add tenant_id if available from the team if tenantID := toString(team["tenant_id"]); tenantID != "" { @@ -344,10 +344,46 @@ func issueTokens(ctx context.Context, userid string, teamID string, team map[str oidcUserInfo.YaoTeam = teamInfo } + // Add type information (use team type if in team context, otherwise use user type) + var typeID string + if teamID != "" && team != nil { + // Team context - use team's type + typeID = toString(team["type_id"]) + } else { + // Personal context - use user's type + typeID = toString(user["type_id"]) + } + + if typeID != "" { + // Add type_id to extra claims for access token + extraClaims["type_id"] = typeID + oidcUserInfo.YaoTypeID = typeID + + // Get type details + userProvider, err := oauth.OAuth.GetUserProvider() + if err == nil { + typeInfo, err := userProvider.GetType(ctx, typeID) + if err == nil && typeInfo != nil { + // Add type info to OIDC user info + typeDetails := &oauthtypes.OIDCTypeInfo{} + if typeIDVal := toString(typeInfo["type_id"]); typeIDVal != "" { + typeDetails.TypeID = typeIDVal + } + if name := toString(typeInfo["name"]); name != "" { + typeDetails.Name = name + } + if locale := toString(typeInfo["locale"]); locale != "" { + typeDetails.Locale = locale + } + oidcUserInfo.YaoType = typeDetails + } + } + } + // Sign OIDC Token var oidcToken string var err error - if extraClaims != nil { + if len(extraClaims) > 0 { oidcToken, err = oauth.OAuth.SignIDToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), yaoClientConfig.ExpiresIn, oidcUserInfo, extraClaims) } else { oidcToken, err = oauth.OAuth.SignIDToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), yaoClientConfig.ExpiresIn, oidcUserInfo) @@ -358,7 +394,7 @@ func issueTokens(ctx context.Context, userid string, teamID string, team map[str // Sign Access Token var accessToken string - if extraClaims != nil { + if len(extraClaims) > 0 { accessToken, err = oauth.OAuth.MakeAccessToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), subject, yaoClientConfig.ExpiresIn, extraClaims) } else { accessToken, err = oauth.OAuth.MakeAccessToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), subject, yaoClientConfig.ExpiresIn) @@ -369,7 +405,7 @@ func issueTokens(ctx context.Context, userid string, teamID string, team map[str // Sign Refresh Token var refreshToken string - if extraClaims != nil { + if len(extraClaims) > 0 { refreshToken, err = oauth.OAuth.MakeRefreshToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), subject, yaoClientConfig.RefreshTokenExpiresIn, extraClaims) } else { refreshToken, err = oauth.OAuth.MakeRefreshToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), subject, yaoClientConfig.RefreshTokenExpiresIn) diff --git a/openapi/user/team.go b/openapi/user/team.go index 7756d38f..80323fee 100644 --- a/openapi/user/team.go +++ b/openapi/user/team.go @@ -689,6 +689,37 @@ func teamCreate(ctx context.Context, userID string, teamData maps.MapStrAny) (st teamData["created_at"] = time.Now() teamData["updated_at"] = time.Now() + // Set default type_id from team config if not provided + if _, hasType := teamData["type_id"]; !hasType { + // Try to get locale from team data + locale := "" + if localeVal, ok := teamData["locale"].(string); ok && localeVal != "" { + locale = strings.TrimSpace(strings.ToLower(localeVal)) + } + + // Fallback: try common locale variations or use "en" as final fallback + // This ensures we always get a valid config even if locale is invalid + teamConfig := GetTeamConfig(locale) + if teamConfig == nil { + // Try fallback locales in order + fallbackLocales := []string{"en", "zh-cn"} + for _, fallback := range fallbackLocales { + teamConfig = GetTeamConfig(fallback) + if teamConfig != nil { + break + } + } + } + + // Apply default type from config if available + if teamConfig != nil && teamConfig.Type != "" { + teamData["type_id"] = teamConfig.Type + } + } + + // Clean up: remove locale from team data as it's not stored in database + delete(teamData, "locale") + // Create team teamID, err := provider.CreateTeam(ctx, teamData) if err != nil { diff --git a/openapi/user/types.go b/openapi/user/types.go index 4f41fd8a..16918ce2 100644 --- a/openapi/user/types.go +++ b/openapi/user/types.go @@ -415,6 +415,7 @@ type CreateInvitationRequest struct { type TeamConfig struct { Roles []*TeamRole `json:"roles,omitempty"` Invite *InviteConfig `json:"invite,omitempty"` + Type string `json:"type,omitempty"` // Default type for new teams } // TeamRole represents a team role configuration