Skip to content

Commit f28aa56

Browse files
committed
follow #30454
1 parent e865de1 commit f28aa56

File tree

9 files changed

+69
-60
lines changed

9 files changed

+69
-60
lines changed

models/user/user.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -501,19 +501,19 @@ func GetUserSalt() (string, error) {
501501
// Note: The set of characters here can safely expand without a breaking change,
502502
// but characters removed from this set can cause user account linking to break
503503
var (
504-
customCharsReplacement = strings.NewReplacer("Æ", "AE")
505-
removeCharsRE = regexp.MustCompile(`['´\x60]`)
506-
removeDiacriticsTransform = transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
507-
replaceCharsHyphenRE = regexp.MustCompile(`[\s~+]`)
504+
customCharsReplacement = strings.NewReplacer("Æ", "AE")
505+
removeCharsRE = regexp.MustCompile("['`´]")
506+
transformDiacritics = transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
507+
replaceCharsHyphenRE = regexp.MustCompile(`[\s~+]`)
508508
)
509509

510-
// normalizeUserName returns a string with single-quotes and diacritics
511-
// removed, and any other non-supported username characters replaced with
512-
// a `-` character
510+
// NormalizeUserName only takes the name part if it is an email address, transforms it diacritics to ASCII characters.
511+
// It returns a string with the single-quotes removed, and any other non-supported username characters are replaced with a `-` character
513512
func NormalizeUserName(s string) (string, error) {
514-
strDiacriticsRemoved, n, err := transform.String(removeDiacriticsTransform, customCharsReplacement.Replace(s))
513+
s, _, _ = strings.Cut(s, "@")
514+
strDiacriticsRemoved, n, err := transform.String(transformDiacritics, customCharsReplacement.Replace(s))
515515
if err != nil {
516-
return "", fmt.Errorf("Failed to normalize character `%v` in provided username `%v`", s[n], s)
516+
return "", fmt.Errorf("failed to normalize the string of provided username %q at position %d", s, n)
517517
}
518518
return replaceCharsHyphenRE.ReplaceAllLiteralString(removeCharsRE.ReplaceAllLiteralString(strDiacriticsRemoved, ""), "-"), nil
519519
}

models/user/user_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -506,15 +506,16 @@ func Test_NormalizeUserFromEmail(t *testing.T) {
506506
Expected string
507507
IsNormalizedValid bool
508508
}{
509-
{"test", "test", true},
509+
{"[email protected]", "name", true},
510+
{"test'`´name", "testname", true},
510511
{"Sinéad.O'Connor", "Sinead.OConnor", true},
511512
{"Æsir", "AEsir", true},
512-
// \u00e9\u0065\u0301
513-
{"éé", "ee", true},
513+
{"éé", "ee", true}, // \u00e9\u0065\u0301
514514
{"Awareness Hub", "Awareness-Hub", true},
515515
{"double__underscore", "double__underscore", false}, // We should consider squashing double non-alpha characters
516516
{".bad.", ".bad.", false},
517517
{"new😀user", "new😀user", false}, // No plans to support
518+
{`"quoted"`, `"quoted"`, false}, // No plans to support
518519
}
519520
for _, testCase := range testCases {
520521
normalizedName, err := user_model.NormalizeUserName(testCase.Input)

modules/setting/oauth2.go

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,10 @@ import (
1616
type OAuth2UsernameType string
1717

1818
const (
19-
// OAuth2UsernameUserid oauth2 userid field will be used as gitea name
20-
OAuth2UsernameUserid OAuth2UsernameType = "userid"
21-
// OAuth2UsernameNickname oauth2 nickname field will be used as gitea name
22-
OAuth2UsernameNickname OAuth2UsernameType = "nickname"
23-
// OAuth2UsernameEmail username of oauth2 email field will be used as gitea name
24-
OAuth2UsernameEmail OAuth2UsernameType = "email"
25-
// OAuth2UsernameEmail username of oauth2 preferred_username field will be used as gitea name
26-
OAuth2UsernamePreferredUsername OAuth2UsernameType = "preferred_username"
19+
OAuth2UsernameUserid OAuth2UsernameType = "userid" // use user id (sub) field as gitea's username
20+
OAuth2UsernameNickname OAuth2UsernameType = "nickname" // use nickname field
21+
OAuth2UsernameEmail OAuth2UsernameType = "email" // use email field
22+
OAuth2UsernamePreferredUsername OAuth2UsernameType = "preferred_username" // use preferred_username field
2723
)
2824

2925
func (username OAuth2UsernameType) isValid() bool {
@@ -71,8 +67,8 @@ func loadOAuth2ClientFrom(rootCfg ConfigProvider) {
7167
OAuth2Client.EnableAutoRegistration = sec.Key("ENABLE_AUTO_REGISTRATION").MustBool()
7268
OAuth2Client.Username = OAuth2UsernameType(sec.Key("USERNAME").MustString(string(OAuth2UsernameNickname)))
7369
if !OAuth2Client.Username.isValid() {
74-
log.Warn("Username setting is not valid: '%s', will fallback to '%s'", OAuth2Client.Username, OAuth2UsernameNickname)
7570
OAuth2Client.Username = OAuth2UsernameNickname
71+
log.Warn("[oauth2_client].USERNAME setting is invalid, falls back to %q", OAuth2Client.Username)
7672
}
7773
OAuth2Client.UpdateAvatar = sec.Key("UPDATE_AVATAR").MustBool()
7874
OAuth2Client.AccountLinking = OAuth2AccountLinkingType(sec.Key("ACCOUNT_LINKING").MustString(string(OAuth2AccountLinkingLogin)))

modules/util/util.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,13 @@ func IfZero[T comparable](v, def T) T {
230230
return v
231231
}
232232

233+
func ArgDefault[T any](args []T, def T) T {
234+
if len(args) >= 1 {
235+
return args[0]
236+
}
237+
return def
238+
}
239+
233240
func ReserveLineBreakForTextarea(input string) string {
234241
// Since the content is from a form which is a textarea, the line endings are \r\n.
235242
// It's a standard behavior of HTML.

options/locale/locale_en-US.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ oauth_signin_submit = Link Account
436436
oauth.signin.error = There was an error processing the authorization request. If this error persists, please contact the site administrator.
437437
oauth.signin.error.access_denied = The authorization request was denied.
438438
oauth.signin.error.temporarily_unavailable = Authorization failed because the authentication server is temporarily unavailable. Please try again later.
439+
oauth_callback_unable_auto_reg = Auto Registration is enabled, but OAuth2 Provider %[1]s returned missing fields: %[2]s, unable to create an account automatically, please link to an account or contact the site administrator.
439440
openid_connect_submit = Connect
440441
openid_connect_title = Connect to an existing account
441442
openid_connect_desc = The chosen OpenID URI is unknown. Associate it with a new account here.

routers/web/auth/auth.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -382,17 +382,17 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
382382
return setting.AppSubURL + "/"
383383
}
384384

385-
func getUserName(gothUser *goth.User) (string, error) {
385+
// extractUserNameFromOAuth2 tries to extract a normalized username from the given OAuth2 user.
386+
// It returns ("", nil) if the required field doesn't exist.
387+
func extractUserNameFromOAuth2(gothUser *goth.User) (string, error) {
386388
switch setting.OAuth2Client.Username {
387389
case setting.OAuth2UsernameEmail:
388-
return user_model.NormalizeUserName(strings.Split(gothUser.Email, "@")[0])
390+
return user_model.NormalizeUserName(gothUser.Email)
389391
case setting.OAuth2UsernamePreferredUsername:
390-
preferredUsername, exists := gothUser.RawData["preferred_username"]
391-
if exists {
392-
return user_model.NormalizeUserName(preferredUsername.(string))
393-
} else {
394-
return "", fmt.Errorf("preferred_username is missing in received user data but configured as username source for user_id %q. Check if OPENID_CONNECT_SCOPES contains profile", gothUser.UserID)
392+
if preferredUsername, ok := gothUser.RawData["preferred_username"].(string); ok {
393+
return user_model.NormalizeUserName(preferredUsername)
395394
}
395+
return "", nil
396396
case setting.OAuth2UsernameNickname:
397397
return user_model.NormalizeUserName(gothUser.NickName)
398398
default: // OAuth2UsernameUserid

routers/web/auth/linkaccount.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,23 +48,28 @@ func LinkAccount(ctx *context.Context) {
4848
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
4949
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
5050

51-
gothUser := ctx.Session.Get("linkAccountGothUser")
52-
if gothUser == nil {
53-
ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
51+
gothUser, ok := ctx.Session.Get("linkAccountGothUser").(goth.User)
52+
if !ok {
53+
// no account in session, so just redirect to the login page, then the user could restart the process
54+
ctx.Redirect(setting.AppSubURL + "/user/login")
5455
return
5556
}
5657

57-
gu, _ := gothUser.(goth.User)
58-
uname, err := getUserName(&gu)
58+
autoRegistrationMissingFields := ctx.FormString("auto_reg_missing_fields")
59+
if autoRegistrationMissingFields != "" {
60+
ctx.Data["AutoRegistrationFailedPrompt"] = ctx.Tr("auth.oauth_callback_unable_auto_reg", gothUser.Provider, autoRegistrationMissingFields)
61+
}
62+
63+
uname, err := extractUserNameFromOAuth2(&gothUser)
5964
if err != nil {
6065
ctx.ServerError("UserSignIn", err)
6166
return
6267
}
63-
email := gu.Email
68+
email := gothUser.Email
6469
ctx.Data["user_name"] = uname
6570
ctx.Data["email"] = email
6671

67-
if len(email) != 0 {
72+
if email != "" {
6873
u, err := user_model.GetUserByEmail(ctx, email)
6974
if err != nil && !user_model.IsErrUserNotExist(err) {
7075
ctx.ServerError("UserSignIn", err)
@@ -73,7 +78,7 @@ func LinkAccount(ctx *context.Context) {
7378
if u != nil {
7479
ctx.Data["user_exists"] = true
7580
}
76-
} else if len(uname) != 0 {
81+
} else if uname != "" {
7782
u, err := user_model.GetUserByName(ctx, uname)
7883
if err != nil && !user_model.IsErrUserNotExist(err) {
7984
ctx.ServerError("UserSignIn", err)

routers/web/auth/oauth.go

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ import (
4646
)
4747

4848
const (
49-
tplGrantAccess base.TplName = "user/auth/grant"
50-
tplGrantError base.TplName = "user/auth/grant_error"
49+
tplGrantAccess base.TplName = "user/auth/grant"
50+
tplGrantError base.TplName = "user/auth/grant_error"
5151
)
5252

5353
// TODO move error and responses to SDK or models
@@ -952,23 +952,25 @@ func SignInOAuthCallback(ctx *context.Context) {
952952
if gothUser.Email == "" {
953953
missingFields = append(missingFields, "email")
954954
}
955-
if setting.OAuth2Client.Username == setting.OAuth2UsernameNickname && gothUser.NickName == "" {
956-
missingFields = append(missingFields, "nickname")
957-
}
958-
if len(missingFields) > 0 {
959-
log.Error("OAuth2 Provider %s returned empty or missing fields: %s", authSource.Name, missingFields)
960-
if authSource.IsOAuth2() && authSource.Cfg.(*oauth2.Source).Provider == "openidConnect" {
961-
log.Error("You may need to change the 'OPENID_CONNECT_SCOPES' setting to request all required fields")
962-
}
963-
err = fmt.Errorf("OAuth2 Provider %s returned empty or missing fields: %s", authSource.Name, missingFields)
964-
ctx.ServerError("CreateUser", err)
965-
return
966-
}
967-
uname, err := getUserName(&gothUser)
955+
uname, err := extractUserNameFromOAuth2(&gothUser)
968956
if err != nil {
969957
ctx.ServerError("UserSignIn", err)
970958
return
971959
}
960+
if uname == "" {
961+
if setting.OAuth2Client.Username == setting.OAuth2UsernameNickname {
962+
missingFields = append(missingFields, "nickname")
963+
} else if setting.OAuth2Client.Username == setting.OAuth2UsernamePreferredUsername {
964+
missingFields = append(missingFields, "preferred_username")
965+
} // else: "UserID" and "Email" have been handled above separately
966+
}
967+
if len(missingFields) > 0 {
968+
log.Error(`OAuth2 auto registration (ENABLE_AUTO_REGISTRATION) is enabled but OAuth2 provider %q doesn't return necessary fields: %s. `+
969+
`Suggest to: disable auto registration, or make OPENID_CONNECT_SCOPES (for OpenIDConnect) / Authentication Source Scopes (for Admin panel) request all required fields.`,
970+
authSource.Name, strings.Join(missingFields, ","))
971+
showLinkingLogin(ctx, gothUser, setting.AppSubURL+"/user/link_account?auto_reg_missing_fields="+url.QueryEscape(strings.Join(missingFields, ",")))
972+
return
973+
}
972974
u = &user_model.User{
973975
Name: uname,
974976
FullName: gothUser.Name,
@@ -1063,14 +1065,14 @@ func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *g
10631065
return isAdmin, isRestricted
10641066
}
10651067

1066-
func showLinkingLogin(ctx *context.Context, gothUser goth.User) {
1068+
func showLinkingLogin(ctx *context.Context, gothUser goth.User, link ...string) {
10671069
if err := updateSession(ctx, nil, map[string]any{
10681070
"linkAccountGothUser": gothUser,
10691071
}); err != nil {
10701072
ctx.ServerError("updateSession", err)
10711073
return
10721074
}
1073-
ctx.Redirect(setting.AppSubURL + "/user/link_account")
1075+
ctx.Redirect(util.ArgDefault(link, setting.AppSubURL+"/user/link_account"))
10741076
}
10751077

10761078
func updateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) {

templates/user/auth/link_account.tmpl

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,12 @@
1717
</overflow-menu>
1818
<div class="ui middle very relaxed page grid">
1919
<div class="column">
20-
<div class="ui tab {{if not .user_exists}}active{{end}}"
21-
data-tab="auth-link-signup-tab">
20+
<div class="ui tab {{if not .user_exists}}active{{end}}" data-tab="auth-link-signup-tab">
21+
{{if .AutoRegistrationFailedPrompt}}<div class="ui message">{{.AutoRegistrationFailedPrompt}}</div>{{end}}
2222
{{template "user/auth/signup_inner" .}}
2323
</div>
24-
<div class="ui tab {{if .user_exists}}active{{end}}"
25-
data-tab="auth-link-signin-tab">
26-
<div class="ui user signin container icon">
27-
{{template "user/auth/signin_inner" .}}
28-
</div>
24+
<div class="ui tab {{if .user_exists}}active{{end}}" data-tab="auth-link-signin-tab">
25+
{{template "user/auth/signin_inner" .}}
2926
</div>
3027
</div>
3128
</div>

0 commit comments

Comments
 (0)