Merge pull request #1249 from trheyi/main

Add Yao member profile support in OIDC user info
This commit is contained in:
Max 2025-10-28 16:57:06 +08:00 committed by GitHub
commit 5e438cde94
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 183 additions and 39 deletions

View file

@ -607,6 +607,28 @@ func (s *Service) SignIDToken(clientID, scope string, expiresIn int, userdata *t
claims["yao:team"] = teamMap
}
}
// Add Yao member info if present (for team context)
if userdata.YaoMember != nil {
memberMap := make(map[string]interface{})
if userdata.YaoMember.MemberID != "" {
memberMap["member_id"] = userdata.YaoMember.MemberID
}
if userdata.YaoMember.DisplayName != "" {
memberMap["display_name"] = userdata.YaoMember.DisplayName
}
if userdata.YaoMember.Bio != "" {
memberMap["bio"] = userdata.YaoMember.Bio
}
if userdata.YaoMember.Avatar != "" {
memberMap["avatar"] = userdata.YaoMember.Avatar
}
if userdata.YaoMember.Email != "" {
memberMap["email"] = userdata.YaoMember.Email
}
if len(memberMap) > 0 {
claims["yao:member"] = memberMap
}
}
// Add Yao type info if present
if userdata.YaoType != nil {
typeMap := make(map[string]interface{})

View file

@ -157,6 +157,29 @@ func (user OIDCUserInfo) Map() map[string]interface{} {
}
}
// Add Yao member info if present and has content (for team context)
if user.YaoMember != nil {
memberMap := make(map[string]interface{})
if user.YaoMember.MemberID != "" {
memberMap["member_id"] = user.YaoMember.MemberID
}
if user.YaoMember.DisplayName != "" {
memberMap["display_name"] = user.YaoMember.DisplayName
}
if user.YaoMember.Bio != "" {
memberMap["bio"] = user.YaoMember.Bio
}
if user.YaoMember.Avatar != "" {
memberMap["avatar"] = user.YaoMember.Avatar
}
if user.YaoMember.Email != "" {
memberMap["email"] = user.YaoMember.Email
}
if len(memberMap) > 0 {
result["yao:member"] = memberMap
}
}
// Include raw data if available
// if user.Raw != nil {
// // Merge raw data, but let structured fields take precedence
@ -327,6 +350,27 @@ func MakeOIDCUserInfo(user map[string]interface{}) *OIDCUserInfo {
userInfo.YaoType = typeInfo
}
// Yao member info (nested object, for team context)
if memberData, ok := user["yao:member"].(map[string]interface{}); ok {
member := &OIDCMemberInfo{}
if memberID, ok := memberData["member_id"].(string); ok {
member.MemberID = memberID
}
if displayName, ok := memberData["display_name"].(string); ok {
member.DisplayName = displayName
}
if bio, ok := memberData["bio"].(string); ok {
member.Bio = bio
}
if avatar, ok := memberData["avatar"].(string); ok {
member.Avatar = avatar
}
if email, ok := memberData["email"].(string); ok {
member.Email = email
}
userInfo.YaoMember = member
}
return userInfo
}

View file

@ -714,13 +714,14 @@ 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
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
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
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
YaoMember *OIDCMemberInfo `json:"yao:member,omitempty"` // Yao member profile info (for team context)
// Raw response for debugging and custom processing
Raw map[string]interface{} `json:"raw,omitempty"` // Original provider response
@ -743,6 +744,15 @@ type OIDCTypeInfo struct {
Locale string `json:"locale,omitempty"` // User type locale
}
// OIDCMemberInfo represents team member profile information
type OIDCMemberInfo struct {
MemberID string `json:"member_id,omitempty"` // Member's unique identifier in team
DisplayName string `json:"display_name,omitempty"` // Member's display name in team
Bio string `json:"bio,omitempty"` // Member's bio in team
Avatar string `json:"avatar,omitempty"` // Member's avatar in team
Email string `json:"email,omitempty"` // Member's email in team
}
// OIDCAddress represents the OIDC address claim structure
type OIDCAddress struct {
Formatted string `json:"formatted,omitempty"` // Full mailing address

View file

@ -267,7 +267,16 @@ func LoginByUserID(userid string, loginCtx *LoginContext) (*LoginResponse, error
}
// Issue tokens without team context
return issueTokens(ctx, userid, "", nil, user, subject, scopes, loginCtx)
return issueTokens(ctx, &IssueTokensParams{
UserID: userid,
TeamID: "",
Team: nil,
Member: nil,
User: user,
Subject: subject,
Scopes: scopes,
LoginCtx: loginCtx,
})
}
// LoginByTeamID is the handler for login by team ID (after team selection)
@ -301,7 +310,16 @@ func LoginByTeamID(userid string, teamID string, loginCtx *LoginContext) (*Login
// Handle personal account (no team)
if teamID == "" || teamID == "personal" {
return issueTokens(ctx, userid, "", nil, user, subject, scopes, loginCtx)
return issueTokens(ctx, &IssueTokensParams{
UserID: userid,
TeamID: "",
Team: nil,
Member: nil,
User: user,
Subject: subject,
Scopes: scopes,
LoginCtx: loginCtx,
})
}
// Verify user is a member of the team and get team details
@ -310,6 +328,14 @@ func LoginByTeamID(userid string, teamID string, loginCtx *LoginContext) (*Login
return nil, fmt.Errorf("access denied: you are not a member of this team")
}
// Get member profile information for team context
member, err := userProvider.GetMember(ctx, teamID, userid)
if err != nil {
log.Warn("Failed to get member profile: %s", err.Error())
// Continue without member profile if it fails
member = nil
}
// Update Last Login
if loginCtx != nil {
err = userProvider.UpdateUserLastLogin(ctx, userid, loginCtx)
@ -318,12 +344,21 @@ func LoginByTeamID(userid string, teamID string, loginCtx *LoginContext) (*Login
}
}
// Issue tokens with team context
return issueTokens(ctx, userid, teamID, team, user, subject, scopes, loginCtx)
// Issue tokens with team context and member profile
return issueTokens(ctx, &IssueTokensParams{
UserID: userid,
TeamID: teamID,
Team: team,
Member: member,
User: user,
Subject: subject,
Scopes: scopes,
LoginCtx: loginCtx,
})
}
// issueTokens is the core function that issues all necessary tokens (ID token, access token, refresh token)
func issueTokens(ctx context.Context, userid string, teamID string, team map[string]interface{}, user map[string]interface{}, subject string, scopes []string, loginCtx *LoginContext) (*LoginResponse, error) {
func issueTokens(ctx context.Context, params *IssueTokensParams) (*LoginResponse, error) {
yaoClientConfig := GetYaoClientConfig()
// Determine token expiration times based on Remember Me setting
@ -333,7 +368,7 @@ func issueTokens(ctx context.Context, userid string, teamID string, team map[str
locale := ""
entryConfig := GetEntryConfig(locale)
if loginCtx != nil && loginCtx.RememberMe {
if params.LoginCtx != nil && params.LoginCtx.RememberMe {
// Remember Me mode: use extended token durations
if entryConfig != nil && entryConfig.Token != nil {
// Parse Remember Me access token expires_in
@ -412,62 +447,83 @@ 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
oidcUserInfo := oauthtypes.MakeOIDCUserInfo(params.User)
oidcUserInfo.Sub = params.Subject
oidcUserInfo.YaoUserID = params.UserID // Add original user ID
// Prepare extra claims for access token
extraClaims := make(map[string]interface{})
// Add team context if available
if teamID != "" && team != nil {
extraClaims["team_id"] = teamID
if params.TeamID != "" && params.Team != nil {
extraClaims["team_id"] = params.TeamID
// Add tenant_id if available from the team
if tenantID := toString(team["tenant_id"]); tenantID != "" {
if tenantID := toString(params.Team["tenant_id"]); tenantID != "" {
extraClaims["tenant_id"] = tenantID
oidcUserInfo.YaoTenantID = tenantID
}
// Add team info to OIDC user info
oidcUserInfo.YaoTeamID = teamID
oidcUserInfo.YaoTeamID = params.TeamID
teamInfo := &oauthtypes.OIDCTeamInfo{}
if teamIDVal := toString(team["team_id"]); teamIDVal != "" {
if teamIDVal := toString(params.Team["team_id"]); teamIDVal != "" {
teamInfo.TeamID = teamIDVal
}
if logo := toString(team["logo"]); logo != "" {
if logo := toString(params.Team["logo"]); logo != "" {
teamInfo.Logo = logo
}
if name := toString(team["name"]); name != "" {
if name := toString(params.Team["name"]); name != "" {
teamInfo.Name = name
}
if description := toString(team["description"]); description != "" {
if description := toString(params.Team["description"]); description != "" {
teamInfo.Description = description
}
// Add owner_id if available from the team (only check once)
if ownerID := toString(team["owner_id"]); ownerID != "" {
if ownerID := toString(params.Team["owner_id"]); ownerID != "" {
extraClaims["owner_id"] = ownerID
teamInfo.OwnerID = ownerID
// Check if user is owner
if ownerID == userid {
if ownerID == params.UserID {
isOwner := true
oidcUserInfo.YaoIsOwner = &isOwner
}
}
oidcUserInfo.YaoTeam = teamInfo
// Add member profile information if available
if params.Member != nil {
memberInfo := &oauthtypes.OIDCMemberInfo{}
if memberID := toString(params.Member["member_id"]); memberID != "" {
memberInfo.MemberID = memberID
}
if displayName := toString(params.Member["display_name"]); displayName != "" {
memberInfo.DisplayName = displayName
}
if bio := toString(params.Member["bio"]); bio != "" {
memberInfo.Bio = bio
}
if avatar := toString(params.Member["avatar"]); avatar != "" {
memberInfo.Avatar = avatar
}
if email := toString(params.Member["email"]); email != "" {
memberInfo.Email = email
}
oidcUserInfo.YaoMember = memberInfo
}
}
// Add type information (use team type if in team context, otherwise use user type)
var typeID string
if teamID != "" && team != nil {
if params.TeamID != "" && params.Team != nil {
// Team context - use team's type
typeID = toString(team["type_id"])
typeID = toString(params.Team["type_id"])
} else {
// Personal context - use user's type
typeID = toString(user["type_id"])
typeID = toString(params.User["type_id"])
}
if typeID != "" {
@ -500,9 +556,9 @@ func issueTokens(ctx context.Context, userid string, teamID string, team map[str
var oidcToken string
var err error
if len(extraClaims) > 0 {
oidcToken, err = oauth.OAuth.SignIDToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), expiresIn, oidcUserInfo, extraClaims)
oidcToken, err = oauth.OAuth.SignIDToken(yaoClientConfig.ClientID, strings.Join(params.Scopes, " "), expiresIn, oidcUserInfo, extraClaims)
} else {
oidcToken, err = oauth.OAuth.SignIDToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), expiresIn, oidcUserInfo)
oidcToken, err = oauth.OAuth.SignIDToken(yaoClientConfig.ClientID, strings.Join(params.Scopes, " "), expiresIn, oidcUserInfo)
}
if err != nil {
return nil, fmt.Errorf("failed to sign OIDC token: %w", err)
@ -511,9 +567,9 @@ func issueTokens(ctx context.Context, userid string, teamID string, team map[str
// Sign Access Token
var accessToken string
if len(extraClaims) > 0 {
accessToken, err = oauth.OAuth.MakeAccessToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), subject, expiresIn, extraClaims)
accessToken, err = oauth.OAuth.MakeAccessToken(yaoClientConfig.ClientID, strings.Join(params.Scopes, " "), params.Subject, expiresIn, extraClaims)
} else {
accessToken, err = oauth.OAuth.MakeAccessToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), subject, expiresIn)
accessToken, err = oauth.OAuth.MakeAccessToken(yaoClientConfig.ClientID, strings.Join(params.Scopes, " "), params.Subject, expiresIn)
}
if err != nil {
return nil, fmt.Errorf("failed to sign access token: %w", err)
@ -522,25 +578,25 @@ func issueTokens(ctx context.Context, userid string, teamID string, team map[str
// Sign Refresh Token
var refreshToken string
if len(extraClaims) > 0 {
refreshToken, err = oauth.OAuth.MakeRefreshToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), subject, refreshTokenExpiresIn, extraClaims)
refreshToken, err = oauth.OAuth.MakeRefreshToken(yaoClientConfig.ClientID, strings.Join(params.Scopes, " "), params.Subject, refreshTokenExpiresIn, extraClaims)
} else {
refreshToken, err = oauth.OAuth.MakeRefreshToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), subject, refreshTokenExpiresIn)
refreshToken, err = oauth.OAuth.MakeRefreshToken(yaoClientConfig.ClientID, strings.Join(params.Scopes, " "), params.Subject, refreshTokenExpiresIn)
}
if err != nil {
return nil, fmt.Errorf("failed to sign refresh token: %w", err)
}
return &LoginResponse{
UserID: userid,
Subject: subject,
UserID: params.UserID,
Subject: params.Subject,
AccessToken: accessToken,
IDToken: oidcToken,
RefreshToken: refreshToken,
ExpiresIn: expiresIn,
RefreshTokenExpiresIn: refreshTokenExpiresIn,
TokenType: "Bearer",
MFAEnabled: toBool(user["mfa_enabled"]),
Scope: strings.Join(scopes, " "),
MFAEnabled: toBool(params.User["mfa_enabled"]),
Scope: strings.Join(params.Scopes, " "),
Status: LoginStatusSuccess,
}, nil
}

View file

@ -265,6 +265,18 @@ type LoginSuccessResponse struct {
// LoginContext is an alias for the oauth types LoginContext
type LoginContext = oauthtypes.LoginContext
// IssueTokensParams represents parameters for issueTokens function
type IssueTokensParams struct {
UserID string // User ID
TeamID string // Team ID (empty for personal account)
Team map[string]interface{} // Team data (nil for personal account)
Member map[string]interface{} // Member profile data (nil for personal account or if not available)
User map[string]interface{} // User data
Subject string // Token subject
Scopes []string // Token scopes
LoginCtx *LoginContext // Login context (IP, user agent, etc.)
}
// ==== Entry Verification Types ====
// EntryVerifyRequest represents the request to verify entry (login/register)