Add Yao user ID support in OIDC and token generation

- Enhanced the SignIDToken method to include the original Yao user ID in the token claims, improving user identification.
- Updated the MakeOIDCUserInfo function to extract and set the Yao user ID from the user map, ensuring consistency in user information.
- Modified the OIDCUserInfo structure to include a field for Yao user ID, facilitating better integration with Yao-specific features.
- Adjusted the team invitation response to include the inviter's user ID, enhancing the invitation context.
This commit is contained in:
Max 2025-10-14 09:56:29 +08:00
parent 5e67a9e5c0
commit 0d83faeeca
9 changed files with 165 additions and 3 deletions

View file

@ -291,7 +291,7 @@ func (u *DefaultUser) GetTeamsByMember(ctx context.Context, memberID string) ([]
teams, err := u.GetTeams(ctx, model.QueryParam{
Select: u.teamFields,
Wheres: []model.QueryWhere{
{Column: "team_id", Value: teamIDs, Method: "in"},
{Column: "team_id", Value: teamIDs, OP: "in"},
},
})
if err != nil {

View file

@ -567,6 +567,9 @@ func (s *Service) SignIDToken(clientID, scope string, expiresIn int, userdata *t
}
// Add Yao custom fields with namespace
if userdata.YaoUserID != "" {
claims["yao:user_id"] = userdata.YaoUserID
}
if userdata.YaoTenantID != "" {
claims["yao:tenant_id"] = userdata.YaoTenantID
}

View file

@ -265,6 +265,9 @@ func MakeOIDCUserInfo(user map[string]interface{}) *OIDCUserInfo {
}
// Yao custom fields with namespace
if userID, ok := user["yao:user_id"].(string); ok {
userInfo.YaoUserID = userID
}
if tenantID, ok := user["yao:tenant_id"].(string); ok {
userInfo.YaoTenantID = tenantID
}

View file

@ -690,6 +690,7 @@ type OIDCUserInfo struct {
Address *OIDCAddress `json:"address,omitempty"` // Physical mailing address
// Additional custom claims with namespace
YaoUserID string `json:"yao:user_id,omitempty"` // Yao user ID (original user ID)
YaoTenantID string `json:"yao:tenant_id,omitempty"` // Yao tenant ID
YaoTeamID string `json:"yao:team_id,omitempty"` // Yao team ID
YaoTeam *OIDCTeamInfo `json:"yao:team,omitempty"` // Yao team info

View file

@ -299,6 +299,7 @@ func issueTokens(ctx context.Context, userid string, teamID string, team map[str
// Prepare OIDC user info
oidcUserInfo := oauthtypes.MakeOIDCUserInfo(user)
oidcUserInfo.Sub = subject
oidcUserInfo.YaoUserID = userid // Add original user ID
// Prepare extra claims for access token
extraClaims := make(map[string]interface{})

152
openapi/user/profile.go Normal file
View file

@ -0,0 +1,152 @@
package user
import (
"context"
"net/http"
"github.com/gin-gonic/gin"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/openapi/oauth"
oauthtypes "github.com/yaoapp/yao/openapi/oauth/types"
"github.com/yaoapp/yao/openapi/response"
)
// User Profile Management Handlers
// GinProfileGet handles GET /profile - Get current user profile
func GinProfileGet(c *gin.Context) {
// Get authorized user info
authInfo := oauth.GetAuthorizedInfo(c)
if authInfo == nil || authInfo.UserID == "" {
errorResp := &response.ErrorResponse{
Code: response.ErrInvalidClient.Code,
ErrorDescription: "User not authenticated",
}
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
return
}
// Get user provider
userProvider, err := oauth.OAuth.GetUserProvider()
if err != nil {
log.Error("Failed to get user provider: %v", err)
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Failed to get user provider",
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
// Get user data with scopes
ctx := c.Request.Context()
if ctx == nil {
ctx = context.Background()
}
user, err := userProvider.GetUserWithScopes(ctx, authInfo.UserID)
if err != nil {
log.Error("Failed to get user profile: %v", err)
errorResp := &response.ErrorResponse{
Code: response.ErrServerError.Code,
ErrorDescription: "Failed to retrieve user profile",
}
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
return
}
// Get Yao client config
yaoClientConfig := GetYaoClientConfig()
// Get or create subject
subject, err := oauth.OAuth.Subject(yaoClientConfig.ClientID, authInfo.UserID)
if err != nil {
log.Warn("Failed to get user subject: %s", err.Error())
subject = authInfo.UserID // Fallback to user ID
}
// Prepare OIDC user info (same format as login response)
oidcUserInfo := oauthtypes.MakeOIDCUserInfo(user)
oidcUserInfo.Sub = subject
oidcUserInfo.YaoUserID = authInfo.UserID
// Add team context if available from token
if authInfo.TeamID != "" {
// Get team details
team, err := userProvider.GetTeamByMember(ctx, authInfo.TeamID, authInfo.UserID)
if err == nil && team != nil {
// Add team info to OIDC user info
oidcUserInfo.YaoTeamID = authInfo.TeamID
teamInfo := &oauthtypes.OIDCTeamInfo{}
if teamIDVal := toString(team["team_id"]); teamIDVal != "" {
teamInfo.TeamID = teamIDVal
}
if logo := toString(team["logo"]); logo != "" {
teamInfo.Logo = logo
}
if name := toString(team["name"]); name != "" {
teamInfo.Name = name
}
if description := toString(team["description"]); description != "" {
teamInfo.Description = description
}
if ownerID := toString(team["owner_id"]); ownerID != "" {
teamInfo.OwnerID = ownerID
// Check if user is owner
if ownerID == authInfo.UserID {
isOwner := true
oidcUserInfo.YaoIsOwner = &isOwner
}
}
oidcUserInfo.YaoTeam = teamInfo
// Add tenant_id if available from the team
if tenantID := toString(team["tenant_id"]); tenantID != "" {
oidcUserInfo.YaoTenantID = tenantID
}
}
}
// Add type information
var typeID string
if authInfo.TeamID != "" {
// Team context - try to get team's type first
team, err := userProvider.GetTeamByMember(ctx, authInfo.TeamID, authInfo.UserID)
if err == nil && team != nil {
typeID = toString(team["type_id"])
}
}
// Fallback to user's type if no team type
if typeID == "" {
typeID = toString(user["type_id"])
}
if typeID != "" {
oidcUserInfo.YaoTypeID = typeID
// Get type details
typeInfo, err := userProvider.GetType(ctx, typeID)
if err == nil && typeInfo != nil {
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
}
}
// Convert to map for response
profileData := oidcUserInfo.Map()
// Return user profile
response.RespondWithSuccess(c, http.StatusOK, profileData)
}

View file

@ -817,6 +817,7 @@ func teamInvitationGetPublic(ctx context.Context, invitationID, locale string) (
inviter, err := provider.GetUser(ctx, inviterID)
if err == nil {
inviterInfo = &InviterInfo{
UserID: inviterID,
Name: toString(inviter["name"]),
Picture: toString(inviter["picture"]),
}

View file

@ -392,6 +392,7 @@ type PublicInvitationResponse struct {
// InviterInfo represents public information about the person who sent the invitation
type InviterInfo struct {
UserID string `json:"user_id"` // Inviter's user ID
Name string `json:"name,omitempty"`
Picture string `json:"picture"` // Always return, empty string if not set
}

View file

@ -189,8 +189,8 @@ func attachProfile(group *gin.RouterGroup, oauth types.OAuth) {
profile := group.Group("/profile")
profile.Use(oauth.Guard)
profile.GET("/", placeholder) // Get user profile
profile.PUT("/", placeholder) // Update user profile
profile.GET("/", GinProfileGet) // Get user profile
profile.PUT("/", placeholder) // Update user profile
}
// User management (CRUD)