Normalize oauth email username (#28561)

This commit is contained in:
Kyle D 2024-01-03 16:48:20 -08:00 committed by GitHub
parent 657b23d635
commit 54acf7b0d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 80 additions and 10 deletions

View file

@ -1529,6 +1529,10 @@ LEVEL = Info
;; userid = use the userid / sub attribute ;; userid = use the userid / sub attribute
;; nickname = use the nickname attribute ;; nickname = use the nickname attribute
;; email = use the username part of the email attribute ;; email = use the username part of the email attribute
;; Note: `nickname` and `email` options will normalize input strings using the following criteria:
;; - diacritics are removed
;; - the characters in the set `['´\x60]` are removed
;; - the characters in the set `[\s~+]` are replaced with `-`
;USERNAME = nickname ;USERNAME = nickname
;; ;;
;; Update avatar if available from oauth2 provider. ;; Update avatar if available from oauth2 provider.

View file

@ -596,9 +596,13 @@ And the following unique queues:
- `OPENID_CONNECT_SCOPES`: **_empty_**: List of additional openid connect scopes. (`openid` is implicitly added) - `OPENID_CONNECT_SCOPES`: **_empty_**: List of additional openid connect scopes. (`openid` is implicitly added)
- `ENABLE_AUTO_REGISTRATION`: **false**: Automatically create user accounts for new oauth2 users. - `ENABLE_AUTO_REGISTRATION`: **false**: Automatically create user accounts for new oauth2 users.
- `USERNAME`: **nickname**: The source of the username for new oauth2 accounts: - `USERNAME`: **nickname**: The source of the username for new oauth2 accounts:
- userid - use the userid / sub attribute - `userid` - use the userid / sub attribute
- nickname - use the nickname attribute - `nickname` - use the nickname attribute
- email - use the username part of the email attribute - `email` - use the username part of the email attribute
- Note: `nickname` and `email` options will normalize input strings using the following criteria:
- diacritics are removed
- the characters in the set `['´\x60]` are removed
- the characters in the set `[\s~+]` are replaced with `-`
- `UPDATE_AVATAR`: **false**: Update avatar if available from oauth2 provider. Update will be performed on each login. - `UPDATE_AVATAR`: **false**: Update avatar if available from oauth2 provider. Update will be performed on each login.
- `ACCOUNT_LINKING`: **login**: How to handle if an account / email already exists: - `ACCOUNT_LINKING`: **login**: How to handle if an account / email already exists:
- disabled - show an error - disabled - show an error

View file

@ -10,8 +10,10 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"time" "time"
"unicode"
_ "image/jpeg" // Needed for jpeg support _ "image/jpeg" // Needed for jpeg support
@ -29,6 +31,9 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/validation"
"golang.org/x/text/runes"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
"xorm.io/builder" "xorm.io/builder"
) )
@ -515,6 +520,26 @@ func GetUserSalt() (string, error) {
return hex.EncodeToString(rBytes), nil return hex.EncodeToString(rBytes), nil
} }
// Note: The set of characters here can safely expand without a breaking change,
// but characters removed from this set can cause user account linking to break
var (
customCharsReplacement = strings.NewReplacer("Æ", "AE")
removeCharsRE = regexp.MustCompile(`['´\x60]`)
removeDiacriticsTransform = transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
replaceCharsHyphenRE = regexp.MustCompile(`[\s~+]`)
)
// normalizeUserName returns a string with single-quotes and diacritics
// removed, and any other non-supported username characters replaced with
// a `-` character
func NormalizeUserName(s string) (string, error) {
strDiacriticsRemoved, n, err := transform.String(removeDiacriticsTransform, customCharsReplacement.Replace(s))
if err != nil {
return "", fmt.Errorf("Failed to normalize character `%v` in provided username `%v`", s[n], s)
}
return replaceCharsHyphenRE.ReplaceAllLiteralString(removeCharsRE.ReplaceAllLiteralString(strDiacriticsRemoved, ""), "-"), nil
}
var ( var (
reservedUsernames = []string{ reservedUsernames = []string{
".", ".",

View file

@ -544,3 +544,31 @@ func Test_ValidateUser(t *testing.T) {
assert.EqualValues(t, expected, err == nil, fmt.Sprintf("case: %+v", kase)) assert.EqualValues(t, expected, err == nil, fmt.Sprintf("case: %+v", kase))
} }
} }
func Test_NormalizeUserFromEmail(t *testing.T) {
testCases := []struct {
Input string
Expected string
IsNormalizedValid bool
}{
{"test", "test", true},
{"Sinéad.O'Connor", "Sinead.OConnor", true},
{"Æsir", "AEsir", true},
// \u00e9\u0065\u0301
{"éé", "ee", true},
{"Awareness Hub", "Awareness-Hub", true},
{"double__underscore", "double__underscore", false}, // We should consider squashing double non-alpha characters
{".bad.", ".bad.", false},
{"new😀user", "new😀user", false}, // No plans to support
}
for _, testCase := range testCases {
normalizedName, err := user_model.NormalizeUserName(testCase.Input)
assert.NoError(t, err)
assert.EqualValues(t, testCase.Expected, normalizedName)
if testCase.IsNormalizedValid {
assert.NoError(t, user_model.IsUsableUsername(normalizedName))
} else {
assert.Error(t, user_model.IsUsableUsername(normalizedName))
}
}
}

View file

@ -21,7 +21,7 @@ const (
OAuth2UsernameUserid OAuth2UsernameType = "userid" OAuth2UsernameUserid OAuth2UsernameType = "userid"
// OAuth2UsernameNickname oauth2 nickname field will be used as gitea name // OAuth2UsernameNickname oauth2 nickname field will be used as gitea name
OAuth2UsernameNickname OAuth2UsernameType = "nickname" OAuth2UsernameNickname OAuth2UsernameType = "nickname"
// OAuth2UsernameEmail username of oauth2 email filed will be used as gitea name // OAuth2UsernameEmail username of oauth2 email field will be used as gitea name
OAuth2UsernameEmail OAuth2UsernameType = "email" OAuth2UsernameEmail OAuth2UsernameType = "email"
) )

View file

@ -368,14 +368,14 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
return setting.AppSubURL + "/" return setting.AppSubURL + "/"
} }
func getUserName(gothUser *goth.User) string { func getUserName(gothUser *goth.User) (string, error) {
switch setting.OAuth2Client.Username { switch setting.OAuth2Client.Username {
case setting.OAuth2UsernameEmail: case setting.OAuth2UsernameEmail:
return strings.Split(gothUser.Email, "@")[0] return user_model.NormalizeUserName(strings.Split(gothUser.Email, "@")[0])
case setting.OAuth2UsernameNickname: case setting.OAuth2UsernameNickname:
return gothUser.NickName return user_model.NormalizeUserName(gothUser.NickName)
default: // OAuth2UsernameUserid default: // OAuth2UsernameUserid
return gothUser.UserID return gothUser.UserID, nil
} }
} }

View file

@ -55,7 +55,11 @@ func LinkAccount(ctx *context.Context) {
} }
gu, _ := gothUser.(goth.User) gu, _ := gothUser.(goth.User)
uname := getUserName(&gu) uname, err := getUserName(&gu)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
email := gu.Email email := gu.Email
ctx.Data["user_name"] = uname ctx.Data["user_name"] = uname
ctx.Data["email"] = email ctx.Data["email"] = email

View file

@ -970,8 +970,13 @@ func SignInOAuthCallback(ctx *context.Context) {
ctx.ServerError("CreateUser", err) ctx.ServerError("CreateUser", err)
return return
} }
uname, err := getUserName(&gothUser)
if err != nil {
ctx.ServerError("UserSignIn", err)
return
}
u = &user_model.User{ u = &user_model.User{
Name: getUserName(&gothUser), Name: uname,
FullName: gothUser.Name, FullName: gothUser.Name,
Email: gothUser.Email, Email: gothUser.Email,
LoginType: auth.OAuth2, LoginType: auth.OAuth2,