fix(oauth): add Microsoft Graph API field mapping support

Support Microsoft-specific user info fields:
  - email: check 'mail' and 'userPrincipalName' as fallbacks
  - name: check 'displayName' (camelCase) for Microsoft Graph API
This commit is contained in:
Benjamin
2025-12-01 12:19:42 +01:00
parent bd46d6b706
commit 5261dce49e
2 changed files with 64 additions and 4 deletions

View File

@@ -344,12 +344,21 @@ func (p *OAuthProvider) parseUserInfo(resp *http.Response) (*models.User, error)
return nil, fmt.Errorf("missing user ID in response")
}
if email, ok := rawUser["email"].(string); ok {
// Check for email in various provider-specific fields
// - "email": Standard OIDC claim (Google, GitHub, GitLab, etc.)
// - "mail": Microsoft Graph API
// - "userPrincipalName": Microsoft fallback (UPN format)
if email, ok := rawUser["email"].(string); ok && email != "" {
user.Email = email
} else if mail, ok := rawUser["mail"].(string); ok && mail != "" {
user.Email = mail
} else if upn, ok := rawUser["userPrincipalName"].(string); ok && upn != "" {
user.Email = upn
} else {
return nil, fmt.Errorf("missing email in user info response")
return nil, fmt.Errorf("missing email in user info response (checked: email, mail, userPrincipalName)")
}
// Extract display name from various provider-specific fields
var name string
if fullName, ok := rawUser["name"].(string); ok && fullName != "" {
name = fullName
@@ -359,10 +368,12 @@ func (p *OAuthProvider) parseUserInfo(resp *http.Response) (*models.User, error)
} else {
name = firstName
}
} else if displayName, ok := rawUser["displayName"].(string); ok && displayName != "" {
name = displayName
} else if cn, ok := rawUser["cn"].(string); ok && cn != "" {
name = cn
} else if displayName, ok := rawUser["display_name"].(string); ok && displayName != "" {
name = displayName
} else if displayNameSnake, ok := rawUser["display_name"].(string); ok && displayNameSnake != "" {
name = displayNameSnake
} else if preferredName, ok := rawUser["preferred_username"].(string); ok && preferredName != "" {
name = preferredName
}

View File

@@ -272,6 +272,55 @@ func TestOAuthProvider_parseUserInfo(t *testing.T) {
}
},
},
{
name: "Microsoft Graph API - mail field",
responseObj: map[string]interface{}{
"id": "microsoft-id-12345",
"mail": "user@company.com",
"displayName": "Microsoft User",
"userPrincipalName": "user@company.onmicrosoft.com",
},
wantErr: false,
checkUser: func(t *testing.T, user *models.User) {
if user.Sub != "microsoft-id-12345" {
t.Errorf("Sub = %v, expected microsoft-id-12345", user.Sub)
}
if user.Email != "user@company.com" {
t.Errorf("Email = %v, expected user@company.com (from mail field)", user.Email)
}
if user.Name != "Microsoft User" {
t.Errorf("Name = %v, expected Microsoft User (from displayName)", user.Name)
}
},
},
{
name: "Microsoft Graph API - userPrincipalName fallback",
responseObj: map[string]interface{}{
"id": "microsoft-id-67890",
"displayName": "UPN User",
"userPrincipalName": "user@company.onmicrosoft.com",
},
wantErr: false,
checkUser: func(t *testing.T, user *models.User) {
if user.Email != "user@company.onmicrosoft.com" {
t.Errorf("Email = %v, expected user@company.onmicrosoft.com (from userPrincipalName)", user.Email)
}
},
},
{
name: "email field takes priority over mail",
responseObj: map[string]interface{}{
"sub": "12345",
"email": "primary@example.com",
"mail": "secondary@example.com",
},
wantErr: false,
checkUser: func(t *testing.T, user *models.User) {
if user.Email != "primary@example.com" {
t.Errorf("Email = %v, expected primary@example.com (email should take priority)", user.Email)
}
},
},
{
name: "missing email",
responseObj: map[string]interface{}{