Improve oauth2 client "preferred username field" logic and the error handling (#30622)
Follow #30454 And fix #24957 When using "preferred_username", if no such field, `extractUserNameFromOAuth2` (old `getUserName`) shouldn't return an error. All other USERNAME options do not return such error. And fine tune some logic and error messages, make code more stable and more friendly to end users.
This commit is contained in:
parent
d0bfc978de
commit
bffbbf5470
@ -1558,8 +1558,8 @@ LEVEL = Info
|
|||||||
;; email = use the username part of the email attribute
|
;; email = use the username part of the email attribute
|
||||||
;; Note: `nickname`, `preferred_username` and `email` options will normalize input strings using the following criteria:
|
;; Note: `nickname`, `preferred_username` and `email` options will normalize input strings using the following criteria:
|
||||||
;; - diacritics are removed
|
;; - diacritics are removed
|
||||||
;; - the characters in the set `['´\x60]` are removed
|
;; - the characters in the set ['´`] are removed
|
||||||
;; - the characters in the set `[\s~+]` are replaced with `-`
|
;; - 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.
|
||||||
|
@ -612,7 +612,7 @@ And the following unique queues:
|
|||||||
- `email` - use the username part of the email attribute
|
- `email` - use the username part of the email attribute
|
||||||
- Note: `nickname`, `preferred_username` and `email` options will normalize input strings using the following criteria:
|
- Note: `nickname`, `preferred_username` and `email` options will normalize input strings using the following criteria:
|
||||||
- diacritics are removed
|
- diacritics are removed
|
||||||
- the characters in the set `['´\x60]` are removed
|
- the characters in the set ```['´`]``` are removed
|
||||||
- the characters in the set `[\s~+]` are replaced with `-`
|
- 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:
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
"code.gitea.io/gitea/models/system"
|
"code.gitea.io/gitea/models/system"
|
||||||
"code.gitea.io/gitea/modules/auth/password/hash"
|
"code.gitea.io/gitea/modules/auth/password/hash"
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
|
"code.gitea.io/gitea/modules/cache"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/setting/config"
|
"code.gitea.io/gitea/modules/setting/config"
|
||||||
@ -106,6 +107,7 @@ func MainTest(m *testing.M, testOpts ...*TestOptions) {
|
|||||||
fatalTestError("Error creating test engine: %v\n", err)
|
fatalTestError("Error creating test engine: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setting.IsInTesting = true
|
||||||
setting.AppURL = "https://try.gitea.io/"
|
setting.AppURL = "https://try.gitea.io/"
|
||||||
setting.RunUser = "runuser"
|
setting.RunUser = "runuser"
|
||||||
setting.SSH.User = "sshuser"
|
setting.SSH.User = "sshuser"
|
||||||
@ -148,6 +150,9 @@ func MainTest(m *testing.M, testOpts ...*TestOptions) {
|
|||||||
|
|
||||||
config.SetDynGetter(system.NewDatabaseDynKeyGetter())
|
config.SetDynGetter(system.NewDatabaseDynKeyGetter())
|
||||||
|
|
||||||
|
if err = cache.Init(); err != nil {
|
||||||
|
fatalTestError("cache.Init: %v\n", err)
|
||||||
|
}
|
||||||
if err = storage.Init(); err != nil {
|
if err = storage.Init(); err != nil {
|
||||||
fatalTestError("storage.Init: %v\n", err)
|
fatalTestError("storage.Init: %v\n", err)
|
||||||
}
|
}
|
||||||
|
@ -502,18 +502,18 @@ func GetUserSalt() (string, error) {
|
|||||||
// but characters removed from this set can cause user account linking to break
|
// but characters removed from this set can cause user account linking to break
|
||||||
var (
|
var (
|
||||||
customCharsReplacement = strings.NewReplacer("Æ", "AE")
|
customCharsReplacement = strings.NewReplacer("Æ", "AE")
|
||||||
removeCharsRE = regexp.MustCompile(`['´\x60]`)
|
removeCharsRE = regexp.MustCompile("['`´]")
|
||||||
removeDiacriticsTransform = transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
|
transformDiacritics = transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
|
||||||
replaceCharsHyphenRE = regexp.MustCompile(`[\s~+]`)
|
replaceCharsHyphenRE = regexp.MustCompile(`[\s~+]`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// normalizeUserName returns a string with single-quotes and diacritics
|
// NormalizeUserName only takes the name part if it is an email address, transforms it diacritics to ASCII characters.
|
||||||
// removed, and any other non-supported username characters replaced with
|
// It returns a string with the single-quotes removed, and any other non-supported username characters are replaced with a `-` character
|
||||||
// a `-` character
|
|
||||||
func NormalizeUserName(s string) (string, error) {
|
func NormalizeUserName(s string) (string, error) {
|
||||||
strDiacriticsRemoved, n, err := transform.String(removeDiacriticsTransform, customCharsReplacement.Replace(s))
|
s, _, _ = strings.Cut(s, "@")
|
||||||
|
strDiacriticsRemoved, n, err := transform.String(transformDiacritics, customCharsReplacement.Replace(s))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("Failed to normalize character `%v` in provided username `%v`", s[n], s)
|
return "", fmt.Errorf("failed to normalize the string of provided username %q at position %d", s, n)
|
||||||
}
|
}
|
||||||
return replaceCharsHyphenRE.ReplaceAllLiteralString(removeCharsRE.ReplaceAllLiteralString(strDiacriticsRemoved, ""), "-"), nil
|
return replaceCharsHyphenRE.ReplaceAllLiteralString(removeCharsRE.ReplaceAllLiteralString(strDiacriticsRemoved, ""), "-"), nil
|
||||||
}
|
}
|
||||||
|
@ -506,15 +506,16 @@ func Test_NormalizeUserFromEmail(t *testing.T) {
|
|||||||
Expected string
|
Expected string
|
||||||
IsNormalizedValid bool
|
IsNormalizedValid bool
|
||||||
}{
|
}{
|
||||||
{"test", "test", true},
|
{"name@example.com", "name", true},
|
||||||
|
{"test'`´name", "testname", true},
|
||||||
{"Sinéad.O'Connor", "Sinead.OConnor", true},
|
{"Sinéad.O'Connor", "Sinead.OConnor", true},
|
||||||
{"Æsir", "AEsir", true},
|
{"Æsir", "AEsir", true},
|
||||||
// \u00e9\u0065\u0301
|
{"éé", "ee", true}, // \u00e9\u0065\u0301
|
||||||
{"éé", "ee", true},
|
|
||||||
{"Awareness Hub", "Awareness-Hub", true},
|
{"Awareness Hub", "Awareness-Hub", true},
|
||||||
{"double__underscore", "double__underscore", false}, // We should consider squashing double non-alpha characters
|
{"double__underscore", "double__underscore", false}, // We should consider squashing double non-alpha characters
|
||||||
{".bad.", ".bad.", false},
|
{".bad.", ".bad.", false},
|
||||||
{"new😀user", "new😀user", false}, // No plans to support
|
{"new😀user", "new😀user", false}, // No plans to support
|
||||||
|
{`"quoted"`, `"quoted"`, false}, // No plans to support
|
||||||
}
|
}
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
normalizedName, err := user_model.NormalizeUserName(testCase.Input)
|
normalizedName, err := user_model.NormalizeUserName(testCase.Input)
|
||||||
|
26
modules/session/mock.go
Normal file
26
modules/session/mock.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"gitea.com/go-chi/session"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockStore struct {
|
||||||
|
*session.MemStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockStore) Destroy(writer http.ResponseWriter, request *http.Request) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockStoreContextKeyStruct struct{}
|
||||||
|
|
||||||
|
var MockStoreContextKey = mockStoreContextKeyStruct{}
|
||||||
|
|
||||||
|
func NewMockStore(sid string) *MockStore {
|
||||||
|
return &MockStore{session.NewMemStore(sid)}
|
||||||
|
}
|
@ -6,6 +6,8 @@ package session
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
"gitea.com/go-chi/session"
|
"gitea.com/go-chi/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -14,6 +16,10 @@ type Store interface {
|
|||||||
Get(any) any
|
Get(any) any
|
||||||
Set(any, any) error
|
Set(any, any) error
|
||||||
Delete(any) error
|
Delete(any) error
|
||||||
|
ID() string
|
||||||
|
Release() error
|
||||||
|
Flush() error
|
||||||
|
Destroy(http.ResponseWriter, *http.Request) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegenerateSession regenerates the underlying session and returns the new store
|
// RegenerateSession regenerates the underlying session and returns the new store
|
||||||
@ -21,8 +27,21 @@ func RegenerateSession(resp http.ResponseWriter, req *http.Request) (Store, erro
|
|||||||
for _, f := range BeforeRegenerateSession {
|
for _, f := range BeforeRegenerateSession {
|
||||||
f(resp, req)
|
f(resp, req)
|
||||||
}
|
}
|
||||||
s, err := session.RegenerateSession(resp, req)
|
if setting.IsInTesting {
|
||||||
return s, err
|
if store, ok := req.Context().Value(MockStoreContextKey).(*MockStore); ok {
|
||||||
|
return store, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return session.RegenerateSession(resp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetContextSession(req *http.Request) Store {
|
||||||
|
if setting.IsInTesting {
|
||||||
|
if store, ok := req.Context().Value(MockStoreContextKey).(*MockStore); ok {
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return session.GetSession(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BeforeRegenerateSession is a list of functions that are called before a session is regenerated.
|
// BeforeRegenerateSession is a list of functions that are called before a session is regenerated.
|
||||||
|
@ -16,14 +16,10 @@ import (
|
|||||||
type OAuth2UsernameType string
|
type OAuth2UsernameType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// OAuth2UsernameUserid oauth2 userid field will be used as gitea name
|
OAuth2UsernameUserid OAuth2UsernameType = "userid" // use user id (sub) field as gitea's username
|
||||||
OAuth2UsernameUserid OAuth2UsernameType = "userid"
|
OAuth2UsernameNickname OAuth2UsernameType = "nickname" // use nickname field
|
||||||
// OAuth2UsernameNickname oauth2 nickname field will be used as gitea name
|
OAuth2UsernameEmail OAuth2UsernameType = "email" // use email field
|
||||||
OAuth2UsernameNickname OAuth2UsernameType = "nickname"
|
OAuth2UsernamePreferredUsername OAuth2UsernameType = "preferred_username" // use preferred_username field
|
||||||
// OAuth2UsernameEmail username of oauth2 email field will be used as gitea name
|
|
||||||
OAuth2UsernameEmail OAuth2UsernameType = "email"
|
|
||||||
// OAuth2UsernameEmail username of oauth2 preferred_username field will be used as gitea name
|
|
||||||
OAuth2UsernamePreferredUsername OAuth2UsernameType = "preferred_username"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (username OAuth2UsernameType) isValid() bool {
|
func (username OAuth2UsernameType) isValid() bool {
|
||||||
@ -71,8 +67,8 @@ func loadOAuth2ClientFrom(rootCfg ConfigProvider) {
|
|||||||
OAuth2Client.EnableAutoRegistration = sec.Key("ENABLE_AUTO_REGISTRATION").MustBool()
|
OAuth2Client.EnableAutoRegistration = sec.Key("ENABLE_AUTO_REGISTRATION").MustBool()
|
||||||
OAuth2Client.Username = OAuth2UsernameType(sec.Key("USERNAME").MustString(string(OAuth2UsernameNickname)))
|
OAuth2Client.Username = OAuth2UsernameType(sec.Key("USERNAME").MustString(string(OAuth2UsernameNickname)))
|
||||||
if !OAuth2Client.Username.isValid() {
|
if !OAuth2Client.Username.isValid() {
|
||||||
log.Warn("Username setting is not valid: '%s', will fallback to '%s'", OAuth2Client.Username, OAuth2UsernameNickname)
|
|
||||||
OAuth2Client.Username = OAuth2UsernameNickname
|
OAuth2Client.Username = OAuth2UsernameNickname
|
||||||
|
log.Warn("[oauth2_client].USERNAME setting is invalid, falls back to %q", OAuth2Client.Username)
|
||||||
}
|
}
|
||||||
OAuth2Client.UpdateAvatar = sec.Key("UPDATE_AVATAR").MustBool()
|
OAuth2Client.UpdateAvatar = sec.Key("UPDATE_AVATAR").MustBool()
|
||||||
OAuth2Client.AccountLinking = OAuth2AccountLinkingType(sec.Key("ACCOUNT_LINKING").MustString(string(OAuth2AccountLinkingLogin)))
|
OAuth2Client.AccountLinking = OAuth2AccountLinkingType(sec.Key("ACCOUNT_LINKING").MustString(string(OAuth2AccountLinkingLogin)))
|
||||||
|
@ -436,6 +436,7 @@ oauth_signin_submit = Link Account
|
|||||||
oauth.signin.error = There was an error processing the authorization request. If this error persists, please contact the site administrator.
|
oauth.signin.error = There was an error processing the authorization request. If this error persists, please contact the site administrator.
|
||||||
oauth.signin.error.access_denied = The authorization request was denied.
|
oauth.signin.error.access_denied = The authorization request was denied.
|
||||||
oauth.signin.error.temporarily_unavailable = Authorization failed because the authentication server is temporarily unavailable. Please try again later.
|
oauth.signin.error.temporarily_unavailable = Authorization failed because the authentication server is temporarily unavailable. Please try again later.
|
||||||
|
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 create or link to an account, or contact the site administrator.
|
||||||
openid_connect_submit = Connect
|
openid_connect_submit = Connect
|
||||||
openid_connect_title = Connect to an existing account
|
openid_connect_title = Connect to an existing account
|
||||||
openid_connect_desc = The chosen OpenID URI is unknown. Associate it with a new account here.
|
openid_connect_desc = The chosen OpenID URI is unknown. Associate it with a new account here.
|
||||||
|
@ -382,17 +382,17 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
|
|||||||
return setting.AppSubURL + "/"
|
return setting.AppSubURL + "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserName(gothUser *goth.User) (string, error) {
|
// extractUserNameFromOAuth2 tries to extract a normalized username from the given OAuth2 user.
|
||||||
|
// It returns ("", nil) if the required field doesn't exist.
|
||||||
|
func extractUserNameFromOAuth2(gothUser *goth.User) (string, error) {
|
||||||
switch setting.OAuth2Client.Username {
|
switch setting.OAuth2Client.Username {
|
||||||
case setting.OAuth2UsernameEmail:
|
case setting.OAuth2UsernameEmail:
|
||||||
return user_model.NormalizeUserName(strings.Split(gothUser.Email, "@")[0])
|
return user_model.NormalizeUserName(gothUser.Email)
|
||||||
case setting.OAuth2UsernamePreferredUsername:
|
case setting.OAuth2UsernamePreferredUsername:
|
||||||
preferredUsername, exists := gothUser.RawData["preferred_username"]
|
if preferredUsername, ok := gothUser.RawData["preferred_username"].(string); ok {
|
||||||
if exists {
|
return user_model.NormalizeUserName(preferredUsername)
|
||||||
return user_model.NormalizeUserName(preferredUsername.(string))
|
|
||||||
} else {
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
return "", nil
|
||||||
case setting.OAuth2UsernameNickname:
|
case setting.OAuth2UsernameNickname:
|
||||||
return user_model.NormalizeUserName(gothUser.NickName)
|
return user_model.NormalizeUserName(gothUser.NickName)
|
||||||
default: // OAuth2UsernameUserid
|
default: // OAuth2UsernameUserid
|
||||||
|
@ -8,12 +8,31 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/modules/session"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/test"
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||||
"code.gitea.io/gitea/services/contexttest"
|
"code.gitea.io/gitea/services/contexttest"
|
||||||
|
|
||||||
|
"github.com/markbates/goth"
|
||||||
|
"github.com/markbates/goth/gothic"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func addOAuth2Source(t *testing.T, authName string, cfg oauth2.Source) {
|
||||||
|
cfg.Provider = util.IfZero(cfg.Provider, "gitea")
|
||||||
|
err := auth_model.CreateSource(db.DefaultContext, &auth_model.Source{
|
||||||
|
Type: auth_model.OAuth2,
|
||||||
|
Name: authName,
|
||||||
|
IsActive: true,
|
||||||
|
Cfg: &cfg,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestUserLogin(t *testing.T) {
|
func TestUserLogin(t *testing.T) {
|
||||||
ctx, resp := contexttest.MockContext(t, "/user/login")
|
ctx, resp := contexttest.MockContext(t, "/user/login")
|
||||||
SignIn(ctx)
|
SignIn(ctx)
|
||||||
@ -41,3 +60,24 @@ func TestUserLogin(t *testing.T) {
|
|||||||
SignIn(ctx)
|
SignIn(ctx)
|
||||||
assert.Equal(t, "/", test.RedirectURL(resp))
|
assert.Equal(t, "/", test.RedirectURL(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSignUpOAuth2ButMissingFields(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)()
|
||||||
|
defer test.MockVariableValue(&gothic.CompleteUserAuth, func(res http.ResponseWriter, req *http.Request) (goth.User, error) {
|
||||||
|
return goth.User{Provider: "dummy-auth-source", UserID: "dummy-user"}, nil
|
||||||
|
})()
|
||||||
|
|
||||||
|
addOAuth2Source(t, "dummy-auth-source", oauth2.Source{})
|
||||||
|
|
||||||
|
mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockStore("dummy-sid")}
|
||||||
|
ctx, resp := contexttest.MockContext(t, "/user/oauth2/dummy-auth-source/callback?code=dummy-code", mockOpt)
|
||||||
|
ctx.SetParams("provider", "dummy-auth-source")
|
||||||
|
SignInOAuthCallback(ctx)
|
||||||
|
assert.Equal(t, http.StatusSeeOther, resp.Code)
|
||||||
|
assert.Equal(t, "/user/link_account", test.RedirectURL(resp))
|
||||||
|
|
||||||
|
// then the user will be redirected to the link account page, and see a message about the missing fields
|
||||||
|
ctx, _ = contexttest.MockContext(t, "/user/link_account", mockOpt)
|
||||||
|
LinkAccount(ctx)
|
||||||
|
assert.EqualValues(t, "auth.oauth_callback_unable_auto_reg:dummy-auth-source,email", ctx.Data["AutoRegistrationFailedPrompt"])
|
||||||
|
}
|
||||||
|
@ -48,23 +48,27 @@ func LinkAccount(ctx *context.Context) {
|
|||||||
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
|
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
|
||||||
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
|
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
|
||||||
|
|
||||||
gothUser := ctx.Session.Get("linkAccountGothUser")
|
gothUser, ok := ctx.Session.Get("linkAccountGothUser").(goth.User)
|
||||||
if gothUser == nil {
|
if !ok {
|
||||||
ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
|
// no account in session, so just redirect to the login page, then the user could restart the process
|
||||||
|
ctx.Redirect(setting.AppSubURL + "/user/login")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gu, _ := gothUser.(goth.User)
|
if missingFields, ok := gothUser.RawData["__giteaAutoRegMissingFields"].([]string); ok {
|
||||||
uname, err := getUserName(&gu)
|
ctx.Data["AutoRegistrationFailedPrompt"] = ctx.Tr("auth.oauth_callback_unable_auto_reg", gothUser.Provider, strings.Join(missingFields, ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
uname, err := extractUserNameFromOAuth2(&gothUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("UserSignIn", err)
|
ctx.ServerError("UserSignIn", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
email := gu.Email
|
email := gothUser.Email
|
||||||
ctx.Data["user_name"] = uname
|
ctx.Data["user_name"] = uname
|
||||||
ctx.Data["email"] = email
|
ctx.Data["email"] = email
|
||||||
|
|
||||||
if len(email) != 0 {
|
if email != "" {
|
||||||
u, err := user_model.GetUserByEmail(ctx, email)
|
u, err := user_model.GetUserByEmail(ctx, email)
|
||||||
if err != nil && !user_model.IsErrUserNotExist(err) {
|
if err != nil && !user_model.IsErrUserNotExist(err) {
|
||||||
ctx.ServerError("UserSignIn", err)
|
ctx.ServerError("UserSignIn", err)
|
||||||
@ -73,7 +77,7 @@ func LinkAccount(ctx *context.Context) {
|
|||||||
if u != nil {
|
if u != nil {
|
||||||
ctx.Data["user_exists"] = true
|
ctx.Data["user_exists"] = true
|
||||||
}
|
}
|
||||||
} else if len(uname) != 0 {
|
} else if uname != "" {
|
||||||
u, err := user_model.GetUserByName(ctx, uname)
|
u, err := user_model.GetUserByName(ctx, uname)
|
||||||
if err != nil && !user_model.IsErrUserNotExist(err) {
|
if err != nil && !user_model.IsErrUserNotExist(err) {
|
||||||
ctx.ServerError("UserSignIn", err)
|
ctx.ServerError("UserSignIn", err)
|
||||||
|
@ -934,7 +934,7 @@ func SignInOAuthCallback(ctx *context.Context) {
|
|||||||
|
|
||||||
if u == nil {
|
if u == nil {
|
||||||
if ctx.Doer != nil {
|
if ctx.Doer != nil {
|
||||||
// attach user to already logged in user
|
// attach user to the current signed-in user
|
||||||
err = externalaccount.LinkAccountToUser(ctx, ctx.Doer, gothUser)
|
err = externalaccount.LinkAccountToUser(ctx, ctx.Doer, gothUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("UserLinkAccount", err)
|
ctx.ServerError("UserLinkAccount", err)
|
||||||
@ -952,23 +952,32 @@ func SignInOAuthCallback(ctx *context.Context) {
|
|||||||
if gothUser.Email == "" {
|
if gothUser.Email == "" {
|
||||||
missingFields = append(missingFields, "email")
|
missingFields = append(missingFields, "email")
|
||||||
}
|
}
|
||||||
if setting.OAuth2Client.Username == setting.OAuth2UsernameNickname && gothUser.NickName == "" {
|
uname, err := extractUserNameFromOAuth2(&gothUser)
|
||||||
missingFields = append(missingFields, "nickname")
|
|
||||||
}
|
|
||||||
if len(missingFields) > 0 {
|
|
||||||
log.Error("OAuth2 Provider %s returned empty or missing fields: %s", authSource.Name, missingFields)
|
|
||||||
if authSource.IsOAuth2() && authSource.Cfg.(*oauth2.Source).Provider == "openidConnect" {
|
|
||||||
log.Error("You may need to change the 'OPENID_CONNECT_SCOPES' setting to request all required fields")
|
|
||||||
}
|
|
||||||
err = fmt.Errorf("OAuth2 Provider %s returned empty or missing fields: %s", authSource.Name, missingFields)
|
|
||||||
ctx.ServerError("CreateUser", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
uname, err := getUserName(&gothUser)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("UserSignIn", err)
|
ctx.ServerError("UserSignIn", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if uname == "" {
|
||||||
|
if setting.OAuth2Client.Username == setting.OAuth2UsernameNickname {
|
||||||
|
missingFields = append(missingFields, "nickname")
|
||||||
|
} else if setting.OAuth2Client.Username == setting.OAuth2UsernamePreferredUsername {
|
||||||
|
missingFields = append(missingFields, "preferred_username")
|
||||||
|
} // else: "UserID" and "Email" have been handled above separately
|
||||||
|
}
|
||||||
|
if len(missingFields) > 0 {
|
||||||
|
log.Error(`OAuth2 auto registration (ENABLE_AUTO_REGISTRATION) is enabled but OAuth2 provider %q doesn't return required fields: %s. `+
|
||||||
|
`Suggest to: disable auto registration, or make OPENID_CONNECT_SCOPES (for OpenIDConnect) / Authentication Source Scopes (for Admin panel) to request all required fields, and the fields shouldn't be empty.`,
|
||||||
|
authSource.Name, strings.Join(missingFields, ","))
|
||||||
|
// The RawData is the only way to pass the missing fields to the another page at the moment, other ways all have various problems:
|
||||||
|
// by session or cookie: difficult to clean or reset; by URL: could be injected with uncontrollable content; by ctx.Flash: the link_account page is a mess ...
|
||||||
|
// Since the RawData is for the provider's data, so we need to use our own prefix here to avoid conflict.
|
||||||
|
if gothUser.RawData == nil {
|
||||||
|
gothUser.RawData = make(map[string]any)
|
||||||
|
}
|
||||||
|
gothUser.RawData["__giteaAutoRegMissingFields"] = missingFields
|
||||||
|
showLinkingLogin(ctx, gothUser)
|
||||||
|
return
|
||||||
|
}
|
||||||
u = &user_model.User{
|
u = &user_model.User{
|
||||||
Name: uname,
|
Name: uname,
|
||||||
FullName: gothUser.Name,
|
FullName: gothUser.Name,
|
||||||
|
@ -20,14 +20,13 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/cache"
|
"code.gitea.io/gitea/modules/cache"
|
||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
"code.gitea.io/gitea/modules/httpcache"
|
"code.gitea.io/gitea/modules/httpcache"
|
||||||
|
"code.gitea.io/gitea/modules/session"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
"code.gitea.io/gitea/modules/translation"
|
"code.gitea.io/gitea/modules/translation"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/modules/web/middleware"
|
"code.gitea.io/gitea/modules/web/middleware"
|
||||||
web_types "code.gitea.io/gitea/modules/web/types"
|
web_types "code.gitea.io/gitea/modules/web/types"
|
||||||
|
|
||||||
"gitea.com/go-chi/session"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Render represents a template render
|
// Render represents a template render
|
||||||
@ -154,7 +153,7 @@ func Contexter() func(next http.Handler) http.Handler {
|
|||||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||||
base, baseCleanUp := NewBaseContext(resp, req)
|
base, baseCleanUp := NewBaseContext(resp, req)
|
||||||
defer baseCleanUp()
|
defer baseCleanUp()
|
||||||
ctx := NewWebContext(base, rnd, session.GetSession(req))
|
ctx := NewWebContext(base, rnd, session.GetContextSession(req))
|
||||||
|
|
||||||
ctx.Data.MergeFrom(middleware.CommonTemplateContextData())
|
ctx.Data.MergeFrom(middleware.CommonTemplateContextData())
|
||||||
ctx.Data["Context"] = ctx // TODO: use "ctx" in template and remove this
|
ctx.Data["Context"] = ctx // TODO: use "ctx" in template and remove this
|
||||||
|
@ -19,7 +19,9 @@ import (
|
|||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/cache"
|
||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
|
"code.gitea.io/gitea/modules/session"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
"code.gitea.io/gitea/modules/translation"
|
"code.gitea.io/gitea/modules/translation"
|
||||||
"code.gitea.io/gitea/modules/web/middleware"
|
"code.gitea.io/gitea/modules/web/middleware"
|
||||||
@ -44,6 +46,7 @@ func mockRequest(t *testing.T, reqPath string) *http.Request {
|
|||||||
|
|
||||||
type MockContextOption struct {
|
type MockContextOption struct {
|
||||||
Render context.Render
|
Render context.Render
|
||||||
|
SessionStore *session.MockStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// MockContext mock context for unit tests
|
// MockContext mock context for unit tests
|
||||||
@ -62,12 +65,17 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont
|
|||||||
base.Data = middleware.GetContextData(req.Context())
|
base.Data = middleware.GetContextData(req.Context())
|
||||||
base.Locale = &translation.MockLocale{}
|
base.Locale = &translation.MockLocale{}
|
||||||
|
|
||||||
|
chiCtx := chi.NewRouteContext()
|
||||||
ctx := context.NewWebContext(base, opt.Render, nil)
|
ctx := context.NewWebContext(base, opt.Render, nil)
|
||||||
ctx.AppendContextValue(context.WebContextKey, ctx)
|
ctx.AppendContextValue(context.WebContextKey, ctx)
|
||||||
|
ctx.AppendContextValue(chi.RouteCtxKey, chiCtx)
|
||||||
|
if opt.SessionStore != nil {
|
||||||
|
ctx.AppendContextValue(session.MockStoreContextKey, opt.SessionStore)
|
||||||
|
ctx.Session = opt.SessionStore
|
||||||
|
}
|
||||||
|
ctx.Cache = cache.GetCache()
|
||||||
ctx.PageData = map[string]any{}
|
ctx.PageData = map[string]any{}
|
||||||
ctx.Data["PageStartTime"] = time.Now()
|
ctx.Data["PageStartTime"] = time.Now()
|
||||||
chiCtx := chi.NewRouteContext()
|
|
||||||
ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx)
|
|
||||||
return ctx, resp
|
return ctx, resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,18 +17,15 @@
|
|||||||
</overflow-menu>
|
</overflow-menu>
|
||||||
<div class="ui middle very relaxed page grid">
|
<div class="ui middle very relaxed page grid">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="ui tab {{if not .user_exists}}active{{end}}"
|
<div class="ui tab {{if not .user_exists}}active{{end}}" data-tab="auth-link-signup-tab">
|
||||||
data-tab="auth-link-signup-tab">
|
{{if .AutoRegistrationFailedPrompt}}<div class="ui message">{{.AutoRegistrationFailedPrompt}}</div>{{end}}
|
||||||
{{template "user/auth/signup_inner" .}}
|
{{template "user/auth/signup_inner" .}}
|
||||||
</div>
|
</div>
|
||||||
<div class="ui tab {{if .user_exists}}active{{end}}"
|
<div class="ui tab {{if .user_exists}}active{{end}}" data-tab="auth-link-signin-tab">
|
||||||
data-tab="auth-link-signin-tab">
|
|
||||||
<div class="ui user signin container icon">
|
|
||||||
{{template "user/auth/signin_inner" .}}
|
{{template "user/auth/signin_inner" .}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{{template "base/footer" .}}
|
{{template "base/footer" .}}
|
||||||
|
Loading…
Reference in New Issue
Block a user