fix: make user creation errors helpful (#5382)

* fix: make user creation errors helpful

* fix linting and unit testing errors

* fix linting

* make zitadel config reusable

* fix human validations

* translate ssr errors

* make zitadel config reusable

* cover more translations for ssr

* handle email validation message centrally

* fix unit tests

* fix linting

* align signatures

* use more precise wording

* handle phone validation message centrally

* fix: return specific profile errors

* docs: edit comments

* fix unit tests

---------

Co-authored-by: Silvan <silvan.reusser@gmail.com>
This commit is contained in:
Elio Bischof
2023-03-14 20:20:38 +01:00
committed by GitHub
parent 9ff810eb92
commit e00cc187fa
79 changed files with 610 additions and 485 deletions

View File

@@ -7,6 +7,7 @@ import (
"golang.org/x/oauth2"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
)
@@ -120,13 +121,13 @@ func newConfig(tenant TenantType, clientID, secret, callbackURL string, scopes [
// AzureAD does not return an `email_verified` claim.
// The verification can be automatically activated on the provider ([WithEmailVerified])
type User struct {
Sub string `json:"sub"`
FamilyName string `json:"family_name"`
GivenName string `json:"given_name"`
Name string `json:"name"`
PreferredUsername string `json:"preferred_username"`
Email string `json:"email"`
Picture string `json:"picture"`
Sub string `json:"sub"`
FamilyName string `json:"family_name"`
GivenName string `json:"given_name"`
Name string `json:"name"`
PreferredUsername string `json:"preferred_username"`
Email domain.EmailAddress `json:"email"`
Picture string `json:"picture"`
isEmailVerified bool
}
@@ -162,7 +163,7 @@ func (u *User) GetPreferredUsername() string {
}
// GetEmail is an implementation of the [idp.User] interface.
func (u *User) GetEmail() string {
func (u *User) GetEmail() domain.EmailAddress {
return u.Email
}
@@ -176,7 +177,7 @@ func (u *User) IsEmailVerified() bool {
// GetPhone is an implementation of the [idp.User] interface.
// It returns an empty string because AzureAD does not provide the user's phone.
func (u *User) GetPhone() string {
func (u *User) GetPhone() domain.PhoneNumber {
return ""
}

View File

@@ -14,6 +14,7 @@ import (
"golang.org/x/oauth2"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
)
@@ -259,9 +260,9 @@ func TestSession_FetchUser(t *testing.T) {
a.Equal(tt.want.displayName, user.GetDisplayName())
a.Equal(tt.want.nickName, user.GetNickname())
a.Equal(tt.want.preferredUsername, user.GetPreferredUsername())
a.Equal(tt.want.email, user.GetEmail())
a.Equal(domain.EmailAddress(tt.want.email), user.GetEmail())
a.Equal(tt.want.isEmailVerified, user.IsEmailVerified())
a.Equal(tt.want.phone, user.GetPhone())
a.Equal(domain.PhoneNumber(tt.want.phone), user.GetPhone())
a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified())
a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage())
a.Equal(tt.want.avatarURL, user.GetAvatarURL())

View File

@@ -4,6 +4,8 @@ import (
"strconv"
"time"
"github.com/zitadel/zitadel/internal/domain"
"golang.org/x/oauth2"
"golang.org/x/text/language"
@@ -68,44 +70,44 @@ func newConfig(clientID, secret, callbackURL, authURL, tokenURL string, scopes [
// User is a representation of the authenticated GitHub user and implements the [idp.User] interface
// https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user
type User struct {
Login string `json:"login"`
ID int `json:"id"`
NodeId string `json:"node_id"`
AvatarUrl string `json:"avatar_url"`
GravatarId string `json:"gravatar_id"`
Url string `json:"url"`
HtmlUrl string `json:"html_url"`
FollowersUrl string `json:"followers_url"`
FollowingUrl string `json:"following_url"`
GistsUrl string `json:"gists_url"`
StarredUrl string `json:"starred_url"`
SubscriptionsUrl string `json:"subscriptions_url"`
OrganizationsUrl string `json:"organizations_url"`
ReposUrl string `json:"repos_url"`
EventsUrl string `json:"events_url"`
ReceivedEventsUrl string `json:"received_events_url"`
Type string `json:"type"`
SiteAdmin bool `json:"site_admin"`
Name string `json:"name"`
Company string `json:"company"`
Blog string `json:"blog"`
Location string `json:"location"`
Email string `json:"email"`
Hireable bool `json:"hireable"`
Bio string `json:"bio"`
TwitterUsername string `json:"twitter_username"`
PublicRepos int `json:"public_repos"`
PublicGists int `json:"public_gists"`
Followers int `json:"followers"`
Following int `json:"following"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
PrivateGists int `json:"private_gists"`
TotalPrivateRepos int `json:"total_private_repos"`
OwnedPrivateRepos int `json:"owned_private_repos"`
DiskUsage int `json:"disk_usage"`
Collaborators int `json:"collaborators"`
TwoFactorAuthentication bool `json:"two_factor_authentication"`
Login string `json:"login"`
ID int `json:"id"`
NodeId string `json:"node_id"`
AvatarUrl string `json:"avatar_url"`
GravatarId string `json:"gravatar_id"`
Url string `json:"url"`
HtmlUrl string `json:"html_url"`
FollowersUrl string `json:"followers_url"`
FollowingUrl string `json:"following_url"`
GistsUrl string `json:"gists_url"`
StarredUrl string `json:"starred_url"`
SubscriptionsUrl string `json:"subscriptions_url"`
OrganizationsUrl string `json:"organizations_url"`
ReposUrl string `json:"repos_url"`
EventsUrl string `json:"events_url"`
ReceivedEventsUrl string `json:"received_events_url"`
Type string `json:"type"`
SiteAdmin bool `json:"site_admin"`
Name string `json:"name"`
Company string `json:"company"`
Blog string `json:"blog"`
Location string `json:"location"`
Email domain.EmailAddress `json:"email"`
Hireable bool `json:"hireable"`
Bio string `json:"bio"`
TwitterUsername string `json:"twitter_username"`
PublicRepos int `json:"public_repos"`
PublicGists int `json:"public_gists"`
Followers int `json:"followers"`
Following int `json:"following"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
PrivateGists int `json:"private_gists"`
TotalPrivateRepos int `json:"total_private_repos"`
OwnedPrivateRepos int `json:"owned_private_repos"`
DiskUsage int `json:"disk_usage"`
Collaborators int `json:"collaborators"`
TwoFactorAuthentication bool `json:"two_factor_authentication"`
Plan struct {
Name string `json:"name"`
Space int `json:"space"`
@@ -150,7 +152,7 @@ func (u *User) GetPreferredUsername() string {
}
// GetEmail is an implementation of the [idp.User] interface.
func (u *User) GetEmail() string {
func (u *User) GetEmail() domain.EmailAddress {
return u.Email
}
@@ -162,7 +164,7 @@ func (u *User) IsEmailVerified() bool {
// GetPhone is an implementation of the [idp.User] interface.
// It returns an empty string because GitHub does not provide the user's phone.
func (u *User) GetPhone() string {
func (u *User) GetPhone() domain.PhoneNumber {
return ""
}

View File

@@ -14,6 +14,7 @@ import (
"golang.org/x/oauth2"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
)
@@ -188,9 +189,9 @@ func TestSession_FetchUser(t *testing.T) {
a.Equal(tt.want.displayName, user.GetDisplayName())
a.Equal(tt.want.nickName, user.GetNickname())
a.Equal(tt.want.preferredUsername, user.GetPreferredUsername())
a.Equal(tt.want.email, user.GetEmail())
a.Equal(domain.EmailAddress(tt.want.email), user.GetEmail())
a.Equal(tt.want.isEmailVerified, user.IsEmailVerified())
a.Equal(tt.want.phone, user.GetPhone())
a.Equal(domain.PhoneNumber(tt.want.phone), user.GetPhone())
a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified())
a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage())
a.Equal(tt.want.avatarURL, user.GetAvatarURL())

View File

@@ -14,6 +14,7 @@ import (
"golang.org/x/oauth2"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp/providers/oidc"
)
@@ -187,9 +188,9 @@ func TestProvider_FetchUser(t *testing.T) {
a.Equal(tt.want.displayName, user.GetDisplayName())
a.Equal(tt.want.nickName, user.GetNickname())
a.Equal(tt.want.preferredUsername, user.GetPreferredUsername())
a.Equal(tt.want.email, user.GetEmail())
a.Equal(domain.EmailAddress(tt.want.email), user.GetEmail())
a.Equal(tt.want.isEmailVerified, user.IsEmailVerified())
a.Equal(tt.want.phone, user.GetPhone())
a.Equal(domain.PhoneNumber(tt.want.phone), user.GetPhone())
a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified())
a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage())
a.Equal(tt.want.avatarURL, user.GetAvatarURL())

View File

@@ -43,5 +43,5 @@ type User struct {
// GetPreferredUsername implements the [idp.User] interface.
// It returns the email, because Google does not return a username.
func (u *User) GetPreferredUsername() string {
return u.GetEmail()
return string(u.GetEmail())
}

View File

@@ -14,6 +14,7 @@ import (
"golang.org/x/oauth2"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp/providers/oidc"
)
@@ -188,9 +189,9 @@ func TestSession_FetchUser(t *testing.T) {
a.Equal(tt.want.displayName, user.GetDisplayName())
a.Equal(tt.want.nickName, user.GetNickname())
a.Equal(tt.want.preferredUsername, user.GetPreferredUsername())
a.Equal(tt.want.email, user.GetEmail())
a.Equal(domain.EmailAddress(tt.want.email), user.GetEmail())
a.Equal(tt.want.isEmailVerified, user.IsEmailVerified())
a.Equal(tt.want.phone, user.GetPhone())
a.Equal(domain.PhoneNumber(tt.want.phone), user.GetPhone())
a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified())
a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage())
a.Equal(tt.want.avatarURL, user.GetAvatarURL())

View File

@@ -12,6 +12,7 @@ import (
"github.com/zitadel/oidc/v2/pkg/oidc"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp"
)
@@ -106,8 +107,8 @@ func (u *User) GetNickname() string {
return u.IDTokenClaims.GetNickname()
}
func (u *User) GetPhone() string {
return u.IDTokenClaims.GetPhoneNumber()
func (u *User) GetPhone() domain.PhoneNumber {
return domain.PhoneNumber(u.IDTokenClaims.GetPhoneNumber())
}
func (u *User) IsPhoneVerified() bool {
@@ -121,3 +122,7 @@ func (u *User) GetPreferredLanguage() language.Tag {
func (u *User) GetAvatarURL() string {
return u.IDTokenClaims.GetPicture()
}
func (u *User) GetEmail() domain.EmailAddress {
return domain.EmailAddress(u.IDTokenClaims.GetEmail())
}

View File

@@ -17,6 +17,7 @@ import (
"gopkg.in/square/go-jose.v2"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
)
func TestSession_FetchUser(t *testing.T) {
@@ -193,9 +194,9 @@ func TestSession_FetchUser(t *testing.T) {
a.Equal(tt.want.displayName, user.GetDisplayName())
a.Equal(tt.want.nickName, user.GetNickname())
a.Equal(tt.want.preferredUsername, user.GetPreferredUsername())
a.Equal(tt.want.email, user.GetEmail())
a.Equal(domain.EmailAddress(tt.want.email), user.GetEmail())
a.Equal(tt.want.isEmailVerified, user.IsEmailVerified())
a.Equal(tt.want.phone, user.GetPhone())
a.Equal(domain.PhoneNumber(tt.want.phone), user.GetPhone())
a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified())
a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage())
a.Equal(tt.want.avatarURL, user.GetAvatarURL())

View File

@@ -10,6 +10,7 @@ import (
"github.com/go-ldap/ldap/v3"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp"
)
@@ -87,9 +88,9 @@ func (s *Session) FetchUser(_ context.Context) (idp.User, error) {
user.GetAttributeValue(s.Provider.displayNameAttribute),
user.GetAttributeValue(s.Provider.nickNameAttribute),
user.GetAttributeValue(s.Provider.preferredUsernameAttribute),
user.GetAttributeValue(s.Provider.emailAttribute),
domain.EmailAddress(user.GetAttributeValue(s.Provider.emailAttribute)),
emailVerified,
user.GetAttributeValue(s.Provider.phoneAttribute),
domain.PhoneNumber(user.GetAttributeValue(s.Provider.phoneAttribute)),
phoneVerified,
language.Make(user.GetAttributeValue(s.Provider.preferredLanguageAttribute)),
user.GetAttributeValue(s.Provider.avatarURLAttribute),

View File

@@ -1,6 +1,10 @@
package ldap
import "golang.org/x/text/language"
import (
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
)
type User struct {
id string
@@ -9,9 +13,9 @@ type User struct {
displayName string
nickName string
preferredUsername string
email string
email domain.EmailAddress
emailVerified bool
phone string
phone domain.PhoneNumber
phoneVerified bool
preferredLanguage language.Tag
avatarURL string
@@ -25,9 +29,9 @@ func NewUser(
displayName string,
nickName string,
preferredUsername string,
email string,
email domain.EmailAddress,
emailVerified bool,
phone string,
phone domain.PhoneNumber,
phoneVerified bool,
preferredLanguage language.Tag,
avatarURL string,
@@ -68,13 +72,13 @@ func (u *User) GetNickname() string {
func (u *User) GetPreferredUsername() string {
return u.preferredUsername
}
func (u *User) GetEmail() string {
func (u *User) GetEmail() domain.EmailAddress {
return u.email
}
func (u *User) IsEmailVerified() bool {
return u.emailVerified
}
func (u *User) GetPhone() string {
func (u *User) GetPhone() domain.PhoneNumber {
return u.phone
}
func (u *User) IsPhoneVerified() bool {

View File

@@ -7,6 +7,7 @@ import (
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp"
)
@@ -74,7 +75,7 @@ func (u *UserMapper) GetPreferredUsername() string {
}
// GetEmail is an implementation of the [idp.User] interface.
func (u *UserMapper) GetEmail() string {
func (u *UserMapper) GetEmail() domain.EmailAddress {
return ""
}
@@ -84,7 +85,7 @@ func (u *UserMapper) IsEmailVerified() bool {
}
// GetPhone is an implementation of the [idp.User] interface.
func (u *UserMapper) GetPhone() string {
func (u *UserMapper) GetPhone() domain.PhoneNumber {
return ""
}

View File

@@ -13,6 +13,7 @@ import (
"golang.org/x/oauth2"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp"
)
@@ -260,9 +261,9 @@ func TestProvider_FetchUser(t *testing.T) {
a.Equal(tt.want.displayName, user.GetDisplayName())
a.Equal(tt.want.nickName, user.GetNickname())
a.Equal(tt.want.preferredUsername, user.GetPreferredUsername())
a.Equal(tt.want.email, user.GetEmail())
a.Equal(domain.EmailAddress(tt.want.email), user.GetEmail())
a.Equal(tt.want.isEmailVerified, user.IsEmailVerified())
a.Equal(tt.want.phone, user.GetPhone())
a.Equal(domain.PhoneNumber(tt.want.phone), user.GetPhone())
a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified())
a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage())
a.Equal(tt.want.avatarURL, user.GetAvatarURL())

View File

@@ -8,6 +8,7 @@ import (
"github.com/zitadel/oidc/v2/pkg/oidc"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp"
)
@@ -82,8 +83,8 @@ func (u *User) GetDisplayName() string {
return u.GetName()
}
func (u *User) GetPhone() string {
return u.GetPhoneNumber()
func (u *User) GetPhone() domain.PhoneNumber {
return domain.PhoneNumber(u.GetPhoneNumber())
}
func (u *User) IsPhoneVerified() bool {
@@ -97,3 +98,7 @@ func (u *User) GetPreferredLanguage() language.Tag {
func (u *User) GetAvatarURL() string {
return u.GetPicture()
}
func (u *User) GetEmail() domain.EmailAddress {
return domain.EmailAddress(u.UserInfo.GetEmail())
}

View File

@@ -17,6 +17,7 @@ import (
"gopkg.in/square/go-jose.v2"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp"
)
@@ -281,9 +282,9 @@ func TestSession_FetchUser(t *testing.T) {
a.Equal(tt.want.displayName, user.GetDisplayName())
a.Equal(tt.want.nickName, user.GetNickname())
a.Equal(tt.want.preferredUsername, user.GetPreferredUsername())
a.Equal(tt.want.email, user.GetEmail())
a.Equal(domain.EmailAddress(tt.want.email), user.GetEmail())
a.Equal(tt.want.isEmailVerified, user.IsEmailVerified())
a.Equal(tt.want.phone, user.GetPhone())
a.Equal(domain.PhoneNumber(tt.want.phone), user.GetPhone())
a.Equal(tt.want.isPhoneVerified, user.IsPhoneVerified())
a.Equal(tt.want.preferredLanguage, user.GetPreferredLanguage())
a.Equal(tt.want.avatarURL, user.GetAvatarURL())