Add Yao member profile support in OIDC user info
- Enhanced the OIDC user info structure to include Yao member profile information, such as member ID, display name, bio, avatar, and email. - Updated the SignIDToken and MakeOIDCUserInfo functions to incorporate member details if available, improving team context handling. - Refactored the issueTokens function to accept a new IssueTokensParams structure, streamlining token issuance with team and member context. - Adjusted login functions to retrieve and pass member profile data during token issuance, ensuring comprehensive user context in authentication flows.
This commit is contained in:
parent
bf452ba1d3
commit
376befc21b
5 changed files with 183 additions and 39 deletions
|
|
@ -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{})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue