mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-04 01:34:27 +00:00 
			
		
		
		
	Refactor OpenIDConnect to support SSH/FullName sync (#34978)
* Fix #26585 * Fix #28327 * Fix #34932
This commit is contained in:
		@@ -87,6 +87,14 @@ func oauthCLIFlags() []cli.Flag {
 | 
				
			|||||||
			Value: nil,
 | 
								Value: nil,
 | 
				
			||||||
			Usage: "Scopes to request when to authenticate against this OAuth2 source",
 | 
								Usage: "Scopes to request when to authenticate against this OAuth2 source",
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
							&cli.StringFlag{
 | 
				
			||||||
 | 
								Name:  "ssh-public-key-claim-name",
 | 
				
			||||||
 | 
								Usage: "Claim name that provides SSH public keys",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							&cli.StringFlag{
 | 
				
			||||||
 | 
								Name:  "full-name-claim-name",
 | 
				
			||||||
 | 
								Usage: "Claim name that provides user's full name",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		&cli.StringFlag{
 | 
							&cli.StringFlag{
 | 
				
			||||||
			Name:  "required-claim-name",
 | 
								Name:  "required-claim-name",
 | 
				
			||||||
			Value: "",
 | 
								Value: "",
 | 
				
			||||||
@@ -177,6 +185,8 @@ func parseOAuth2Config(c *cli.Command) *oauth2.Source {
 | 
				
			|||||||
		RestrictedGroup:               c.String("restricted-group"),
 | 
							RestrictedGroup:               c.String("restricted-group"),
 | 
				
			||||||
		GroupTeamMap:                  c.String("group-team-map"),
 | 
							GroupTeamMap:                  c.String("group-team-map"),
 | 
				
			||||||
		GroupTeamMapRemoval:           c.Bool("group-team-map-removal"),
 | 
							GroupTeamMapRemoval:           c.Bool("group-team-map-removal"),
 | 
				
			||||||
 | 
							SSHPublicKeyClaimName:         c.String("ssh-public-key-claim-name"),
 | 
				
			||||||
 | 
							FullNameClaimName:             c.String("full-name-claim-name"),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -268,6 +278,12 @@ func (a *authService) runUpdateOauth(ctx context.Context, c *cli.Command) error
 | 
				
			|||||||
	if c.IsSet("group-team-map-removal") {
 | 
						if c.IsSet("group-team-map-removal") {
 | 
				
			||||||
		oAuth2Config.GroupTeamMapRemoval = c.Bool("group-team-map-removal")
 | 
							oAuth2Config.GroupTeamMapRemoval = c.Bool("group-team-map-removal")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						if c.IsSet("ssh-public-key-claim-name") {
 | 
				
			||||||
 | 
							oAuth2Config.SSHPublicKeyClaimName = c.String("ssh-public-key-claim-name")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if c.IsSet("full-name-claim-name") {
 | 
				
			||||||
 | 
							oAuth2Config.FullNameClaimName = c.String("full-name-claim-name")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// update custom URL mapping
 | 
						// update custom URL mapping
 | 
				
			||||||
	customURLMapping := &oauth2.CustomURLMapping{}
 | 
						customURLMapping := &oauth2.CustomURLMapping{}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -88,6 +88,8 @@ func TestAddOauth(t *testing.T) {
 | 
				
			|||||||
				"--restricted-group", "restricted",
 | 
									"--restricted-group", "restricted",
 | 
				
			||||||
				"--group-team-map", `{"group1": [1,2]}`,
 | 
									"--group-team-map", `{"group1": [1,2]}`,
 | 
				
			||||||
				"--group-team-map-removal=true",
 | 
									"--group-team-map-removal=true",
 | 
				
			||||||
 | 
									"--ssh-public-key-claim-name", "attr_ssh_pub_key",
 | 
				
			||||||
 | 
									"--full-name-claim-name", "attr_full_name",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			source: &auth_model.Source{
 | 
								source: &auth_model.Source{
 | 
				
			||||||
				Type:     auth_model.OAuth2,
 | 
									Type:     auth_model.OAuth2,
 | 
				
			||||||
@@ -104,15 +106,17 @@ func TestAddOauth(t *testing.T) {
 | 
				
			|||||||
						EmailURL:   "https://example.com/email",
 | 
											EmailURL:   "https://example.com/email",
 | 
				
			||||||
						Tenant:     "some_tenant",
 | 
											Tenant:     "some_tenant",
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
					IconURL:             "https://example.com/icon",
 | 
										IconURL:               "https://example.com/icon",
 | 
				
			||||||
					Scopes:              []string{"scope1", "scope2"},
 | 
										Scopes:                []string{"scope1", "scope2"},
 | 
				
			||||||
					RequiredClaimName:   "claim_name",
 | 
										RequiredClaimName:     "claim_name",
 | 
				
			||||||
					RequiredClaimValue:  "claim_value",
 | 
										RequiredClaimValue:    "claim_value",
 | 
				
			||||||
					GroupClaimName:      "group_name",
 | 
										GroupClaimName:        "group_name",
 | 
				
			||||||
					AdminGroup:          "admin",
 | 
										AdminGroup:            "admin",
 | 
				
			||||||
					RestrictedGroup:     "restricted",
 | 
										RestrictedGroup:       "restricted",
 | 
				
			||||||
					GroupTeamMap:        `{"group1": [1,2]}`,
 | 
										GroupTeamMap:          `{"group1": [1,2]}`,
 | 
				
			||||||
					GroupTeamMapRemoval: true,
 | 
										GroupTeamMapRemoval:   true,
 | 
				
			||||||
 | 
										SSHPublicKeyClaimName: "attr_ssh_pub_key",
 | 
				
			||||||
 | 
										FullNameClaimName:     "attr_full_name",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
				TwoFactorPolicy: "skip",
 | 
									TwoFactorPolicy: "skip",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
@@ -223,15 +227,17 @@ func TestUpdateOauth(t *testing.T) {
 | 
				
			|||||||
						EmailURL:   "https://old.example.com/email",
 | 
											EmailURL:   "https://old.example.com/email",
 | 
				
			||||||
						Tenant:     "old_tenant",
 | 
											Tenant:     "old_tenant",
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
					IconURL:             "https://old.example.com/icon",
 | 
										IconURL:               "https://old.example.com/icon",
 | 
				
			||||||
					Scopes:              []string{"old_scope1", "old_scope2"},
 | 
										Scopes:                []string{"old_scope1", "old_scope2"},
 | 
				
			||||||
					RequiredClaimName:   "old_claim_name",
 | 
										RequiredClaimName:     "old_claim_name",
 | 
				
			||||||
					RequiredClaimValue:  "old_claim_value",
 | 
										RequiredClaimValue:    "old_claim_value",
 | 
				
			||||||
					GroupClaimName:      "old_group_name",
 | 
										GroupClaimName:        "old_group_name",
 | 
				
			||||||
					AdminGroup:          "old_admin",
 | 
										AdminGroup:            "old_admin",
 | 
				
			||||||
					RestrictedGroup:     "old_restricted",
 | 
										RestrictedGroup:       "old_restricted",
 | 
				
			||||||
					GroupTeamMap:        `{"old_group1": [1,2]}`,
 | 
										GroupTeamMap:          `{"old_group1": [1,2]}`,
 | 
				
			||||||
					GroupTeamMapRemoval: true,
 | 
										GroupTeamMapRemoval:   true,
 | 
				
			||||||
 | 
										SSHPublicKeyClaimName: "old_ssh_pub_key",
 | 
				
			||||||
 | 
										FullNameClaimName:     "old_full_name",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
				TwoFactorPolicy: "",
 | 
									TwoFactorPolicy: "",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
@@ -257,6 +263,8 @@ func TestUpdateOauth(t *testing.T) {
 | 
				
			|||||||
				"--restricted-group", "restricted",
 | 
									"--restricted-group", "restricted",
 | 
				
			||||||
				"--group-team-map", `{"group1": [1,2]}`,
 | 
									"--group-team-map", `{"group1": [1,2]}`,
 | 
				
			||||||
				"--group-team-map-removal=false",
 | 
									"--group-team-map-removal=false",
 | 
				
			||||||
 | 
									"--ssh-public-key-claim-name", "new_ssh_pub_key",
 | 
				
			||||||
 | 
									"--full-name-claim-name", "new_full_name",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			authSource: &auth_model.Source{
 | 
								authSource: &auth_model.Source{
 | 
				
			||||||
				ID:       1,
 | 
									ID:       1,
 | 
				
			||||||
@@ -274,15 +282,17 @@ func TestUpdateOauth(t *testing.T) {
 | 
				
			|||||||
						EmailURL:   "https://example.com/email",
 | 
											EmailURL:   "https://example.com/email",
 | 
				
			||||||
						Tenant:     "new_tenant",
 | 
											Tenant:     "new_tenant",
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
					IconURL:             "https://example.com/icon",
 | 
										IconURL:               "https://example.com/icon",
 | 
				
			||||||
					Scopes:              []string{"scope1", "scope2"},
 | 
										Scopes:                []string{"scope1", "scope2"},
 | 
				
			||||||
					RequiredClaimName:   "claim_name",
 | 
										RequiredClaimName:     "claim_name",
 | 
				
			||||||
					RequiredClaimValue:  "claim_value",
 | 
										RequiredClaimValue:    "claim_value",
 | 
				
			||||||
					GroupClaimName:      "group_name",
 | 
										GroupClaimName:        "group_name",
 | 
				
			||||||
					AdminGroup:          "admin",
 | 
										AdminGroup:            "admin",
 | 
				
			||||||
					RestrictedGroup:     "restricted",
 | 
										RestrictedGroup:       "restricted",
 | 
				
			||||||
					GroupTeamMap:        `{"group1": [1,2]}`,
 | 
										GroupTeamMap:          `{"group1": [1,2]}`,
 | 
				
			||||||
					GroupTeamMapRemoval: false,
 | 
										GroupTeamMapRemoval:   false,
 | 
				
			||||||
 | 
										SSHPublicKeyClaimName: "new_ssh_pub_key",
 | 
				
			||||||
 | 
										FullNameClaimName:     "new_full_name",
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
				TwoFactorPolicy: "skip",
 | 
									TwoFactorPolicy: "skip",
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -355,13 +355,13 @@ func AddPublicKeysBySource(ctx context.Context, usr *user_model.User, s *auth.So
 | 
				
			|||||||
	return sshKeysNeedUpdate
 | 
						return sshKeysNeedUpdate
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SynchronizePublicKeys updates a users public keys. Returns true if there are changes.
 | 
					// SynchronizePublicKeys updates a user's public keys. Returns true if there are changes.
 | 
				
			||||||
func SynchronizePublicKeys(ctx context.Context, usr *user_model.User, s *auth.Source, sshPublicKeys []string) bool {
 | 
					func SynchronizePublicKeys(ctx context.Context, usr *user_model.User, s *auth.Source, sshPublicKeys []string) bool {
 | 
				
			||||||
	var sshKeysNeedUpdate bool
 | 
						var sshKeysNeedUpdate bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log.Trace("synchronizePublicKeys[%s]: Handling Public SSH Key synchronization for user %s", s.Name, usr.Name)
 | 
						log.Trace("synchronizePublicKeys[%s]: Handling Public SSH Key synchronization for user %s", s.Name, usr.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Get Public Keys from DB with current LDAP source
 | 
						// Get Public Keys from DB with the current auth source
 | 
				
			||||||
	var giteaKeys []string
 | 
						var giteaKeys []string
 | 
				
			||||||
	keys, err := db.Find[PublicKey](ctx, FindPublicKeyOptions{
 | 
						keys, err := db.Find[PublicKey](ctx, FindPublicKeyOptions{
 | 
				
			||||||
		OwnerID:       usr.ID,
 | 
							OwnerID:       usr.ID,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -612,8 +612,8 @@ func (err ErrOAuthApplicationNotFound) Unwrap() error {
 | 
				
			|||||||
	return util.ErrNotExist
 | 
						return util.ErrNotExist
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetActiveOAuth2SourceByName returns a OAuth2 AuthSource based on the given name
 | 
					// GetActiveOAuth2SourceByAuthName returns a OAuth2 AuthSource based on the given name
 | 
				
			||||||
func GetActiveOAuth2SourceByName(ctx context.Context, name string) (*Source, error) {
 | 
					func GetActiveOAuth2SourceByAuthName(ctx context.Context, name string) (*Source, error) {
 | 
				
			||||||
	authSource := new(Source)
 | 
						authSource := new(Source)
 | 
				
			||||||
	has, err := db.GetEngine(ctx).Where("name = ? and type = ? and is_active = ?", name, OAuth2, true).Get(authSource)
 | 
						has, err := db.GetEngine(ctx).Where("name = ? and type = ? and is_active = ?", name, OAuth2, true).Get(authSource)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -334,7 +334,7 @@ func UpdateSource(ctx context.Context, source *Source) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	err = registerableSource.RegisterSource()
 | 
						err = registerableSource.RegisterSource()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		// restore original values since we cannot update the provider it self
 | 
							// restore original values since we cannot update the provider itself
 | 
				
			||||||
		if _, err := db.GetEngine(ctx).ID(source.ID).AllCols().Update(originalSource); err != nil {
 | 
							if _, err := db.GetEngine(ctx).ID(source.ID).AllCols().Update(originalSource); err != nil {
 | 
				
			||||||
			log.Error("UpdateSource: Error while wrapOpenIDConnectInitializeError: %v", err)
 | 
								log.Error("UpdateSource: Error while wrapOpenIDConnectInitializeError: %v", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,7 +12,7 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/log"
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// OAuth2UsernameType is enum describing the way gitea 'name' should be generated from oauth2 data
 | 
					// OAuth2UsernameType is enum describing the way gitea generates its 'username' from oauth2 data
 | 
				
			||||||
type OAuth2UsernameType string
 | 
					type OAuth2UsernameType string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3251,6 +3251,8 @@ auths.oauth2_required_claim_name_helper = Set this name to restrict login from t
 | 
				
			|||||||
auths.oauth2_required_claim_value = Required Claim Value
 | 
					auths.oauth2_required_claim_value = Required Claim Value
 | 
				
			||||||
auths.oauth2_required_claim_value_helper = Set this value to restrict login from this source to users with a claim with this name and value
 | 
					auths.oauth2_required_claim_value_helper = Set this value to restrict login from this source to users with a claim with this name and value
 | 
				
			||||||
auths.oauth2_group_claim_name = Claim name providing group names for this source. (Optional)
 | 
					auths.oauth2_group_claim_name = Claim name providing group names for this source. (Optional)
 | 
				
			||||||
 | 
					auths.oauth2_full_name_claim_name = Full Name Claim Name. (Optional, if set, the user's full name will always be synchronized with this claim)
 | 
				
			||||||
 | 
					auths.oauth2_ssh_public_key_claim_name = SSH Public Key Claim Name
 | 
				
			||||||
auths.oauth2_admin_group = Group Claim value for administrator users. (Optional - requires claim name above)
 | 
					auths.oauth2_admin_group = Group Claim value for administrator users. (Optional - requires claim name above)
 | 
				
			||||||
auths.oauth2_restricted_group = Group Claim value for restricted users. (Optional - requires claim name above)
 | 
					auths.oauth2_restricted_group = Group Claim value for restricted users. (Optional - requires claim name above)
 | 
				
			||||||
auths.oauth2_map_group_to_team = Map claimed groups to Organization teams. (Optional - requires claim name above)
 | 
					auths.oauth2_map_group_to_team = Map claimed groups to Organization teams. (Optional - requires claim name above)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -199,6 +199,9 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
 | 
				
			|||||||
		AdminGroup:                    form.Oauth2AdminGroup,
 | 
							AdminGroup:                    form.Oauth2AdminGroup,
 | 
				
			||||||
		GroupTeamMap:                  form.Oauth2GroupTeamMap,
 | 
							GroupTeamMap:                  form.Oauth2GroupTeamMap,
 | 
				
			||||||
		GroupTeamMapRemoval:           form.Oauth2GroupTeamMapRemoval,
 | 
							GroupTeamMapRemoval:           form.Oauth2GroupTeamMapRemoval,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							SSHPublicKeyClaimName: form.Oauth2SSHPublicKeyClaimName,
 | 
				
			||||||
 | 
							FullNameClaimName:     form.Oauth2FullNameClaimName,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,6 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/templates"
 | 
						"code.gitea.io/gitea/modules/templates"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/web"
 | 
						"code.gitea.io/gitea/modules/web"
 | 
				
			||||||
	"code.gitea.io/gitea/services/context"
 | 
						"code.gitea.io/gitea/services/context"
 | 
				
			||||||
	"code.gitea.io/gitea/services/externalaccount"
 | 
					 | 
				
			||||||
	"code.gitea.io/gitea/services/forms"
 | 
						"code.gitea.io/gitea/services/forms"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -75,7 +74,7 @@ func TwoFactorPost(ctx *context.Context) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if ctx.Session.Get("linkAccount") != nil {
 | 
							if ctx.Session.Get("linkAccount") != nil {
 | 
				
			||||||
			err = externalaccount.LinkAccountFromStore(ctx, ctx.Session, u)
 | 
								err = linkAccountFromContext(ctx, u)
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				ctx.ServerError("UserSignIn", err)
 | 
									ctx.ServerError("UserSignIn", err)
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -329,6 +329,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
 | 
				
			|||||||
		"twofaUid",
 | 
							"twofaUid",
 | 
				
			||||||
		"twofaRemember",
 | 
							"twofaRemember",
 | 
				
			||||||
		"linkAccount",
 | 
							"linkAccount",
 | 
				
			||||||
 | 
							"linkAccountData",
 | 
				
			||||||
	}, map[string]any{
 | 
						}, map[string]any{
 | 
				
			||||||
		session.KeyUID:                  u.ID,
 | 
							session.KeyUID:                  u.ID,
 | 
				
			||||||
		session.KeyUname:                u.Name,
 | 
							session.KeyUname:                u.Name,
 | 
				
			||||||
@@ -519,7 +520,7 @@ func SignUpPost(ctx *context.Context) {
 | 
				
			|||||||
		Passwd: form.Password,
 | 
							Passwd: form.Password,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if !createAndHandleCreatedUser(ctx, tplSignUp, form, u, nil, nil, false) {
 | 
						if !createAndHandleCreatedUser(ctx, tplSignUp, form, u, nil, nil) {
 | 
				
			||||||
		// error already handled
 | 
							// error already handled
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -530,22 +531,22 @@ func SignUpPost(ctx *context.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// createAndHandleCreatedUser calls createUserInContext and
 | 
					// createAndHandleCreatedUser calls createUserInContext and
 | 
				
			||||||
// then handleUserCreated.
 | 
					// then handleUserCreated.
 | 
				
			||||||
func createAndHandleCreatedUser(ctx *context.Context, tpl templates.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool) bool {
 | 
					func createAndHandleCreatedUser(ctx *context.Context, tpl templates.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, possibleLinkAccountData *LinkAccountData) bool {
 | 
				
			||||||
	if !createUserInContext(ctx, tpl, form, u, overwrites, gothUser, allowLink) {
 | 
						if !createUserInContext(ctx, tpl, form, u, overwrites, possibleLinkAccountData) {
 | 
				
			||||||
		return false
 | 
							return false
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return handleUserCreated(ctx, u, gothUser)
 | 
						return handleUserCreated(ctx, u, possibleLinkAccountData)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// createUserInContext creates a user and handles errors within a given context.
 | 
					// createUserInContext creates a user and handles errors within a given context.
 | 
				
			||||||
// Optionally a template can be specified.
 | 
					// Optionally, a template can be specified.
 | 
				
			||||||
func createUserInContext(ctx *context.Context, tpl templates.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, gothUser *goth.User, allowLink bool) (ok bool) {
 | 
					func createUserInContext(ctx *context.Context, tpl templates.TplName, form any, u *user_model.User, overwrites *user_model.CreateUserOverwriteOptions, possibleLinkAccountData *LinkAccountData) (ok bool) {
 | 
				
			||||||
	meta := &user_model.Meta{
 | 
						meta := &user_model.Meta{
 | 
				
			||||||
		InitialIP:        ctx.RemoteAddr(),
 | 
							InitialIP:        ctx.RemoteAddr(),
 | 
				
			||||||
		InitialUserAgent: ctx.Req.UserAgent(),
 | 
							InitialUserAgent: ctx.Req.UserAgent(),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if err := user_model.CreateUser(ctx, u, meta, overwrites); err != nil {
 | 
						if err := user_model.CreateUser(ctx, u, meta, overwrites); err != nil {
 | 
				
			||||||
		if allowLink && (user_model.IsErrUserAlreadyExist(err) || user_model.IsErrEmailAlreadyUsed(err)) {
 | 
							if possibleLinkAccountData != nil && (user_model.IsErrUserAlreadyExist(err) || user_model.IsErrEmailAlreadyUsed(err)) {
 | 
				
			||||||
			switch setting.OAuth2Client.AccountLinking {
 | 
								switch setting.OAuth2Client.AccountLinking {
 | 
				
			||||||
			case setting.OAuth2AccountLinkingAuto:
 | 
								case setting.OAuth2AccountLinkingAuto:
 | 
				
			||||||
				var user *user_model.User
 | 
									var user *user_model.User
 | 
				
			||||||
@@ -561,15 +562,15 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any,
 | 
				
			|||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				// TODO: probably we should respect 'remember' user's choice...
 | 
									// TODO: probably we should respect 'remember' user's choice...
 | 
				
			||||||
				linkAccount(ctx, user, *gothUser, true)
 | 
									oauth2LinkAccount(ctx, user, possibleLinkAccountData, true)
 | 
				
			||||||
				return false // user is already created here, all redirects are handled
 | 
									return false // user is already created here, all redirects are handled
 | 
				
			||||||
			case setting.OAuth2AccountLinkingLogin:
 | 
								case setting.OAuth2AccountLinkingLogin:
 | 
				
			||||||
				showLinkingLogin(ctx, *gothUser)
 | 
									showLinkingLogin(ctx, &possibleLinkAccountData.AuthSource, possibleLinkAccountData.GothUser)
 | 
				
			||||||
				return false // user will be created only after linking login
 | 
									return false // user will be created only after linking login
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// handle error without template
 | 
							// handle error without a template
 | 
				
			||||||
		if len(tpl) == 0 {
 | 
							if len(tpl) == 0 {
 | 
				
			||||||
			ctx.ServerError("CreateUser", err)
 | 
								ctx.ServerError("CreateUser", err)
 | 
				
			||||||
			return false
 | 
								return false
 | 
				
			||||||
@@ -610,7 +611,7 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any,
 | 
				
			|||||||
// handleUserCreated does additional steps after a new user is created.
 | 
					// handleUserCreated does additional steps after a new user is created.
 | 
				
			||||||
// It auto-sets admin for the only user, updates the optional external user and
 | 
					// It auto-sets admin for the only user, updates the optional external user and
 | 
				
			||||||
// sends a confirmation email if required.
 | 
					// sends a confirmation email if required.
 | 
				
			||||||
func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.User) (ok bool) {
 | 
					func handleUserCreated(ctx *context.Context, u *user_model.User, possibleLinkAccountData *LinkAccountData) (ok bool) {
 | 
				
			||||||
	// Auto-set admin for the only user.
 | 
						// Auto-set admin for the only user.
 | 
				
			||||||
	hasUsers, err := user_model.HasUsers(ctx)
 | 
						hasUsers, err := user_model.HasUsers(ctx)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -631,8 +632,8 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// update external user information
 | 
						// update external user information
 | 
				
			||||||
	if gothUser != nil {
 | 
						if possibleLinkAccountData != nil {
 | 
				
			||||||
		if err := externalaccount.EnsureLinkExternalToUser(ctx, u, *gothUser); err != nil {
 | 
							if err := externalaccount.EnsureLinkExternalToUser(ctx, possibleLinkAccountData.AuthSource.ID, u, possibleLinkAccountData.GothUser); err != nil {
 | 
				
			||||||
			log.Error("EnsureLinkExternalToUser failed: %v", err)
 | 
								log.Error("EnsureLinkExternalToUser failed: %v", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,6 @@ package auth
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -21,8 +20,6 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/services/context"
 | 
						"code.gitea.io/gitea/services/context"
 | 
				
			||||||
	"code.gitea.io/gitea/services/externalaccount"
 | 
						"code.gitea.io/gitea/services/externalaccount"
 | 
				
			||||||
	"code.gitea.io/gitea/services/forms"
 | 
						"code.gitea.io/gitea/services/forms"
 | 
				
			||||||
 | 
					 | 
				
			||||||
	"github.com/markbates/goth"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var tplLinkAccount templates.TplName = "user/auth/link_account"
 | 
					var tplLinkAccount templates.TplName = "user/auth/link_account"
 | 
				
			||||||
@@ -52,28 +49,28 @@ 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, ok := ctx.Session.Get("linkAccountGothUser").(goth.User)
 | 
						linkAccountData := oauth2GetLinkAccountData(ctx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// If you'd like to quickly debug the "link account" page layout, just uncomment the blow line
 | 
						// If you'd like to quickly debug the "link account" page layout, just uncomment the blow line
 | 
				
			||||||
	// Don't worry, when the below line exists, the lint won't pass: ineffectual assignment to gothUser (ineffassign)
 | 
						// Don't worry, when the below line exists, the lint won't pass: ineffectual assignment to gothUser (ineffassign)
 | 
				
			||||||
	// gothUser, ok = goth.User{Email: "invalid-email", Name: "."}, true // intentionally use invalid data to avoid pass the registration check
 | 
						// linkAccountData = &LinkAccountData{authSource, gothUser} // intentionally use invalid data to avoid pass the registration check
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if !ok {
 | 
						if linkAccountData == nil {
 | 
				
			||||||
		// no account in session, so just redirect to the login page, then the user could restart the process
 | 
							// no account in session, so just redirect to the login page, then the user could restart the process
 | 
				
			||||||
		ctx.Redirect(setting.AppSubURL + "/user/login")
 | 
							ctx.Redirect(setting.AppSubURL + "/user/login")
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if missingFields, ok := gothUser.RawData["__giteaAutoRegMissingFields"].([]string); ok {
 | 
						if missingFields, ok := linkAccountData.GothUser.RawData["__giteaAutoRegMissingFields"].([]string); ok {
 | 
				
			||||||
		ctx.Data["AutoRegistrationFailedPrompt"] = ctx.Tr("auth.oauth_callback_unable_auto_reg", gothUser.Provider, strings.Join(missingFields, ","))
 | 
							ctx.Data["AutoRegistrationFailedPrompt"] = ctx.Tr("auth.oauth_callback_unable_auto_reg", linkAccountData.GothUser.Provider, strings.Join(missingFields, ","))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	uname, err := extractUserNameFromOAuth2(&gothUser)
 | 
						uname, err := extractUserNameFromOAuth2(&linkAccountData.GothUser)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.ServerError("UserSignIn", err)
 | 
							ctx.ServerError("UserSignIn", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	email := gothUser.Email
 | 
						email := linkAccountData.GothUser.Email
 | 
				
			||||||
	ctx.Data["user_name"] = uname
 | 
						ctx.Data["user_name"] = uname
 | 
				
			||||||
	ctx.Data["email"] = email
 | 
						ctx.Data["email"] = email
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -152,8 +149,8 @@ func LinkAccountPostSignIn(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")
 | 
						linkAccountData := oauth2GetLinkAccountData(ctx)
 | 
				
			||||||
	if gothUser == nil {
 | 
						if linkAccountData == nil {
 | 
				
			||||||
		ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
 | 
							ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session"))
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -169,11 +166,14 @@ func LinkAccountPostSignIn(ctx *context.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	linkAccount(ctx, u, gothUser.(goth.User), signInForm.Remember)
 | 
						oauth2LinkAccount(ctx, u, linkAccountData, signInForm.Remember)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func linkAccount(ctx *context.Context, u *user_model.User, gothUser goth.User, remember bool) {
 | 
					func oauth2LinkAccount(ctx *context.Context, u *user_model.User, linkAccountData *LinkAccountData, remember bool) {
 | 
				
			||||||
	updateAvatarIfNeed(ctx, gothUser.AvatarURL, u)
 | 
						oauth2SignInSync(ctx, &linkAccountData.AuthSource, u, linkAccountData.GothUser)
 | 
				
			||||||
 | 
						if ctx.Written() {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// If this user is enrolled in 2FA, we can't sign the user in just yet.
 | 
						// If this user is enrolled in 2FA, we can't sign the user in just yet.
 | 
				
			||||||
	// Instead, redirect them to the 2FA authentication page.
 | 
						// Instead, redirect them to the 2FA authentication page.
 | 
				
			||||||
@@ -185,7 +185,7 @@ func linkAccount(ctx *context.Context, u *user_model.User, gothUser goth.User, r
 | 
				
			|||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		err = externalaccount.LinkAccountToUser(ctx, u, gothUser)
 | 
							err = externalaccount.LinkAccountToUser(ctx, linkAccountData.AuthSource.ID, u, linkAccountData.GothUser)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			ctx.ServerError("UserLinkAccount", err)
 | 
								ctx.ServerError("UserLinkAccount", err)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
@@ -243,17 +243,11 @@ func LinkAccountPostRegister(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"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	gothUserInterface := ctx.Session.Get("linkAccountGothUser")
 | 
						linkAccountData := oauth2GetLinkAccountData(ctx)
 | 
				
			||||||
	if gothUserInterface == nil {
 | 
						if linkAccountData == nil {
 | 
				
			||||||
		ctx.ServerError("UserSignUp", errors.New("not in LinkAccount session"))
 | 
							ctx.ServerError("UserSignUp", errors.New("not in LinkAccount session"))
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	gothUser, ok := gothUserInterface.(goth.User)
 | 
					 | 
				
			||||||
	if !ok {
 | 
					 | 
				
			||||||
		ctx.ServerError("UserSignUp", fmt.Errorf("session linkAccountGothUser type is %t but not goth.User", gothUserInterface))
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if ctx.HasError() {
 | 
						if ctx.HasError() {
 | 
				
			||||||
		ctx.HTML(http.StatusOK, tplLinkAccount)
 | 
							ctx.HTML(http.StatusOK, tplLinkAccount)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
@@ -296,31 +290,33 @@ func LinkAccountPostRegister(ctx *context.Context) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	authSource, err := auth.GetActiveOAuth2SourceByName(ctx, gothUser.Provider)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		ctx.ServerError("CreateUser", err)
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	u := &user_model.User{
 | 
						u := &user_model.User{
 | 
				
			||||||
		Name:        form.UserName,
 | 
							Name:        form.UserName,
 | 
				
			||||||
		Email:       form.Email,
 | 
							Email:       form.Email,
 | 
				
			||||||
		Passwd:      form.Password,
 | 
							Passwd:      form.Password,
 | 
				
			||||||
		LoginType:   auth.OAuth2,
 | 
							LoginType:   auth.OAuth2,
 | 
				
			||||||
		LoginSource: authSource.ID,
 | 
							LoginSource: linkAccountData.AuthSource.ID,
 | 
				
			||||||
		LoginName:   gothUser.UserID,
 | 
							LoginName:   linkAccountData.GothUser.UserID,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if !createAndHandleCreatedUser(ctx, tplLinkAccount, form, u, nil, &gothUser, false) {
 | 
						if !createAndHandleCreatedUser(ctx, tplLinkAccount, form, u, nil, linkAccountData) {
 | 
				
			||||||
		// error already handled
 | 
							// error already handled
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	source := authSource.Cfg.(*oauth2.Source)
 | 
						source := linkAccountData.AuthSource.Cfg.(*oauth2.Source)
 | 
				
			||||||
	if err := syncGroupsToTeams(ctx, source, &gothUser, u); err != nil {
 | 
						if err := syncGroupsToTeams(ctx, source, &linkAccountData.GothUser, u); err != nil {
 | 
				
			||||||
		ctx.ServerError("SyncGroupsToTeams", err)
 | 
							ctx.ServerError("SyncGroupsToTeams", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	handleSignIn(ctx, u, false)
 | 
						handleSignIn(ctx, u, false)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func linkAccountFromContext(ctx *context.Context, user *user_model.User) error {
 | 
				
			||||||
 | 
						linkAccountData := oauth2GetLinkAccountData(ctx)
 | 
				
			||||||
 | 
						if linkAccountData == nil {
 | 
				
			||||||
 | 
							return errors.New("not in LinkAccount session")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return externalaccount.LinkAccountToUser(ctx, linkAccountData.AuthSource.ID, user, linkAccountData.GothUser)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,7 +20,6 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/optional"
 | 
						"code.gitea.io/gitea/modules/optional"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/session"
 | 
						"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/web/middleware"
 | 
						"code.gitea.io/gitea/modules/web/middleware"
 | 
				
			||||||
	source_service "code.gitea.io/gitea/services/auth/source"
 | 
						source_service "code.gitea.io/gitea/services/auth/source"
 | 
				
			||||||
	"code.gitea.io/gitea/services/auth/source/oauth2"
 | 
						"code.gitea.io/gitea/services/auth/source/oauth2"
 | 
				
			||||||
@@ -35,9 +34,8 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// SignInOAuth handles the OAuth2 login buttons
 | 
					// SignInOAuth handles the OAuth2 login buttons
 | 
				
			||||||
func SignInOAuth(ctx *context.Context) {
 | 
					func SignInOAuth(ctx *context.Context) {
 | 
				
			||||||
	provider := ctx.PathParam("provider")
 | 
						authName := ctx.PathParam("provider")
 | 
				
			||||||
 | 
						authSource, err := auth.GetActiveOAuth2SourceByAuthName(ctx, authName)
 | 
				
			||||||
	authSource, err := auth.GetActiveOAuth2SourceByName(ctx, provider)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.ServerError("SignIn", err)
 | 
							ctx.ServerError("SignIn", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
@@ -74,8 +72,6 @@ func SignInOAuth(ctx *context.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// SignInOAuthCallback handles the callback from the given provider
 | 
					// SignInOAuthCallback handles the callback from the given provider
 | 
				
			||||||
func SignInOAuthCallback(ctx *context.Context) {
 | 
					func SignInOAuthCallback(ctx *context.Context) {
 | 
				
			||||||
	provider := ctx.PathParam("provider")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if ctx.Req.FormValue("error") != "" {
 | 
						if ctx.Req.FormValue("error") != "" {
 | 
				
			||||||
		var errorKeyValues []string
 | 
							var errorKeyValues []string
 | 
				
			||||||
		for k, vv := range ctx.Req.Form {
 | 
							for k, vv := range ctx.Req.Form {
 | 
				
			||||||
@@ -88,7 +84,8 @@ func SignInOAuthCallback(ctx *context.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// first look if the provider is still active
 | 
						// first look if the provider is still active
 | 
				
			||||||
	authSource, err := auth.GetActiveOAuth2SourceByName(ctx, provider)
 | 
						authName := ctx.PathParam("provider")
 | 
				
			||||||
 | 
						authSource, err := auth.GetActiveOAuth2SourceByAuthName(ctx, authName)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.ServerError("SignIn", err)
 | 
							ctx.ServerError("SignIn", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
@@ -133,7 +130,7 @@ func SignInOAuthCallback(ctx *context.Context) {
 | 
				
			|||||||
	if u == nil {
 | 
						if u == nil {
 | 
				
			||||||
		if ctx.Doer != nil {
 | 
							if ctx.Doer != nil {
 | 
				
			||||||
			// attach user to the current signed-in user
 | 
								// attach user to the current signed-in user
 | 
				
			||||||
			err = externalaccount.LinkAccountToUser(ctx, ctx.Doer, gothUser)
 | 
								err = externalaccount.LinkAccountToUser(ctx, authSource.ID, ctx.Doer, gothUser)
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				ctx.ServerError("UserLinkAccount", err)
 | 
									ctx.ServerError("UserLinkAccount", err)
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
@@ -174,12 +171,11 @@ func SignInOAuthCallback(ctx *context.Context) {
 | 
				
			|||||||
					gothUser.RawData = make(map[string]any)
 | 
										gothUser.RawData = make(map[string]any)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				gothUser.RawData["__giteaAutoRegMissingFields"] = missingFields
 | 
									gothUser.RawData["__giteaAutoRegMissingFields"] = missingFields
 | 
				
			||||||
				showLinkingLogin(ctx, gothUser)
 | 
									showLinkingLogin(ctx, authSource, gothUser)
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			u = &user_model.User{
 | 
								u = &user_model.User{
 | 
				
			||||||
				Name:        uname,
 | 
									Name:        uname,
 | 
				
			||||||
				FullName:    gothUser.Name,
 | 
					 | 
				
			||||||
				Email:       gothUser.Email,
 | 
									Email:       gothUser.Email,
 | 
				
			||||||
				LoginType:   auth.OAuth2,
 | 
									LoginType:   auth.OAuth2,
 | 
				
			||||||
				LoginSource: authSource.ID,
 | 
									LoginSource: authSource.ID,
 | 
				
			||||||
@@ -196,7 +192,11 @@ func SignInOAuthCallback(ctx *context.Context) {
 | 
				
			|||||||
			u.IsAdmin = isAdmin.ValueOrDefault(user_service.UpdateOptionField[bool]{FieldValue: false}).FieldValue
 | 
								u.IsAdmin = isAdmin.ValueOrDefault(user_service.UpdateOptionField[bool]{FieldValue: false}).FieldValue
 | 
				
			||||||
			u.IsRestricted = isRestricted.ValueOrDefault(setting.Service.DefaultUserIsRestricted)
 | 
								u.IsRestricted = isRestricted.ValueOrDefault(setting.Service.DefaultUserIsRestricted)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if !createAndHandleCreatedUser(ctx, templates.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
 | 
								linkAccountData := &LinkAccountData{*authSource, gothUser}
 | 
				
			||||||
 | 
								if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingDisabled {
 | 
				
			||||||
 | 
									linkAccountData = nil
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if !createAndHandleCreatedUser(ctx, "", nil, u, overwriteDefault, linkAccountData) {
 | 
				
			||||||
				// error already handled
 | 
									// error already handled
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@@ -207,7 +207,7 @@ func SignInOAuthCallback(ctx *context.Context) {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			// no existing user is found, request attach or new account
 | 
								// no existing user is found, request attach or new account
 | 
				
			||||||
			showLinkingLogin(ctx, gothUser)
 | 
								showLinkingLogin(ctx, authSource, gothUser)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -271,9 +271,22 @@ func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *g
 | 
				
			|||||||
	return isAdmin, isRestricted
 | 
						return isAdmin, isRestricted
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func showLinkingLogin(ctx *context.Context, gothUser goth.User) {
 | 
					type LinkAccountData struct {
 | 
				
			||||||
 | 
						AuthSource auth.Source
 | 
				
			||||||
 | 
						GothUser   goth.User
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func oauth2GetLinkAccountData(ctx *context.Context) *LinkAccountData {
 | 
				
			||||||
 | 
						v, ok := ctx.Session.Get("linkAccountData").(LinkAccountData)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &v
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func showLinkingLogin(ctx *context.Context, authSource *auth.Source, gothUser goth.User) {
 | 
				
			||||||
	if err := updateSession(ctx, nil, map[string]any{
 | 
						if err := updateSession(ctx, nil, map[string]any{
 | 
				
			||||||
		"linkAccountGothUser": gothUser,
 | 
							"linkAccountData": LinkAccountData{*authSource, gothUser},
 | 
				
			||||||
	}); err != nil {
 | 
						}); err != nil {
 | 
				
			||||||
		ctx.ServerError("updateSession", err)
 | 
							ctx.ServerError("updateSession", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
@@ -281,7 +294,7 @@ func showLinkingLogin(ctx *context.Context, gothUser goth.User) {
 | 
				
			|||||||
	ctx.Redirect(setting.AppSubURL + "/user/link_account")
 | 
						ctx.Redirect(setting.AppSubURL + "/user/link_account")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func updateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) {
 | 
					func oauth2UpdateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) {
 | 
				
			||||||
	if setting.OAuth2Client.UpdateAvatar && len(url) > 0 {
 | 
						if setting.OAuth2Client.UpdateAvatar && len(url) > 0 {
 | 
				
			||||||
		resp, err := http.Get(url)
 | 
							resp, err := http.Get(url)
 | 
				
			||||||
		if err == nil {
 | 
							if err == nil {
 | 
				
			||||||
@@ -299,11 +312,14 @@ func updateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model.User, gothUser goth.User) {
 | 
					func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_model.User, gothUser goth.User) {
 | 
				
			||||||
	updateAvatarIfNeed(ctx, gothUser.AvatarURL, u)
 | 
						oauth2SignInSync(ctx, authSource, u, gothUser)
 | 
				
			||||||
 | 
						if ctx.Written() {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	needs2FA := false
 | 
						needs2FA := false
 | 
				
			||||||
	if !source.TwoFactorShouldSkip() {
 | 
						if !authSource.TwoFactorShouldSkip() {
 | 
				
			||||||
		_, err := auth.GetTwoFactorByUID(ctx, u.ID)
 | 
							_, err := auth.GetTwoFactorByUID(ctx, u.ID)
 | 
				
			||||||
		if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
 | 
							if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
 | 
				
			||||||
			ctx.ServerError("UserSignIn", err)
 | 
								ctx.ServerError("UserSignIn", err)
 | 
				
			||||||
@@ -312,7 +328,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
 | 
				
			|||||||
		needs2FA = err == nil
 | 
							needs2FA = err == nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	oauth2Source := source.Cfg.(*oauth2.Source)
 | 
						oauth2Source := authSource.Cfg.(*oauth2.Source)
 | 
				
			||||||
	groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(oauth2Source.GroupTeamMap)
 | 
						groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(oauth2Source.GroupTeamMap)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		ctx.ServerError("UnmarshalGroupTeamMapping", err)
 | 
							ctx.ServerError("UnmarshalGroupTeamMapping", err)
 | 
				
			||||||
@@ -338,7 +354,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := externalaccount.EnsureLinkExternalToUser(ctx, u, gothUser); err != nil {
 | 
						if err := externalaccount.EnsureLinkExternalToUser(ctx, authSource.ID, u, gothUser); err != nil {
 | 
				
			||||||
		ctx.ServerError("EnsureLinkExternalToUser", err)
 | 
							ctx.ServerError("EnsureLinkExternalToUser", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										88
									
								
								routers/web/auth/oauth_signin_sync.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								routers/web/auth/oauth_signin_sync.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,88 @@
 | 
				
			|||||||
 | 
					// Copyright 2025 The Gitea Authors. All rights reserved.
 | 
				
			||||||
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package auth
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						asymkey_model "code.gitea.io/gitea/models/asymkey"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/models/auth"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
 | 
						asymkey_service "code.gitea.io/gitea/services/asymkey"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/auth/source/oauth2"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/services/context"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/markbates/goth"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func oauth2SignInSync(ctx *context.Context, authSource *auth.Source, u *user_model.User, gothUser goth.User) {
 | 
				
			||||||
 | 
						oauth2UpdateAvatarIfNeed(ctx, gothUser.AvatarURL, u)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						oauth2Source, _ := authSource.Cfg.(*oauth2.Source)
 | 
				
			||||||
 | 
						if !authSource.IsOAuth2() || oauth2Source == nil {
 | 
				
			||||||
 | 
							ctx.ServerError("oauth2SignInSync", fmt.Errorf("source %s is not an OAuth2 source", gothUser.Provider))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// sync full name
 | 
				
			||||||
 | 
						fullNameKey := util.IfZero(oauth2Source.FullNameClaimName, "name")
 | 
				
			||||||
 | 
						fullName, _ := gothUser.RawData[fullNameKey].(string)
 | 
				
			||||||
 | 
						fullName = util.IfZero(fullName, gothUser.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// need to update if the user has no full name set
 | 
				
			||||||
 | 
						shouldUpdateFullName := u.FullName == ""
 | 
				
			||||||
 | 
						// force to update if the attribute is set
 | 
				
			||||||
 | 
						shouldUpdateFullName = shouldUpdateFullName || oauth2Source.FullNameClaimName != ""
 | 
				
			||||||
 | 
						// only update if the full name is different
 | 
				
			||||||
 | 
						shouldUpdateFullName = shouldUpdateFullName && u.FullName != fullName
 | 
				
			||||||
 | 
						if shouldUpdateFullName {
 | 
				
			||||||
 | 
							u.FullName = fullName
 | 
				
			||||||
 | 
							if err := user_model.UpdateUserCols(ctx, u, "full_name"); err != nil {
 | 
				
			||||||
 | 
								log.Error("Unable to sync OAuth2 user full name %s: %v", gothUser.Provider, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := oauth2UpdateSSHPubIfNeed(ctx, authSource, &gothUser, u)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Error("Unable to sync OAuth2 SSH public key %s: %v", gothUser.Provider, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func oauth2SyncGetSSHKeys(source *oauth2.Source, gothUser *goth.User) ([]string, error) {
 | 
				
			||||||
 | 
						value, exists := gothUser.RawData[source.SSHPublicKeyClaimName]
 | 
				
			||||||
 | 
						if !exists {
 | 
				
			||||||
 | 
							return []string{}, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						rawSlice, ok := value.([]any)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("invalid SSH public key value type: %T", value)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sshKeys := make([]string, 0, len(rawSlice))
 | 
				
			||||||
 | 
						for _, v := range rawSlice {
 | 
				
			||||||
 | 
							str, ok := v.(string)
 | 
				
			||||||
 | 
							if !ok {
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("invalid SSH public key value item type: %T", v)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							sshKeys = append(sshKeys, str)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return sshKeys, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func oauth2UpdateSSHPubIfNeed(ctx *context.Context, authSource *auth.Source, gothUser *goth.User, user *user_model.User) error {
 | 
				
			||||||
 | 
						oauth2Source, _ := authSource.Cfg.(*oauth2.Source)
 | 
				
			||||||
 | 
						if oauth2Source == nil || oauth2Source.SSHPublicKeyClaimName == "" {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						sshKeys, err := oauth2SyncGetSSHKeys(oauth2Source, gothUser)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !asymkey_model.SynchronizePublicKeys(ctx, user, authSource, sshKeys) {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return asymkey_service.RewriteAllPublicKeys(ctx)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -361,7 +361,7 @@ func RegisterOpenIDPost(ctx *context.Context) {
 | 
				
			|||||||
		Email:  form.Email,
 | 
							Email:  form.Email,
 | 
				
			||||||
		Passwd: password,
 | 
							Passwd: password,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if !createUserInContext(ctx, tplSignUpOID, form, u, nil, nil, false) {
 | 
						if !createUserInContext(ctx, tplSignUpOID, form, u, nil, nil) {
 | 
				
			||||||
		// error already handled
 | 
							// error already handled
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,7 +15,6 @@ import (
 | 
				
			|||||||
	"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/services/context"
 | 
						"code.gitea.io/gitea/services/context"
 | 
				
			||||||
	"code.gitea.io/gitea/services/externalaccount"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/go-webauthn/webauthn/protocol"
 | 
						"github.com/go-webauthn/webauthn/protocol"
 | 
				
			||||||
	"github.com/go-webauthn/webauthn/webauthn"
 | 
						"github.com/go-webauthn/webauthn/webauthn"
 | 
				
			||||||
@@ -150,7 +149,7 @@ func WebAuthnPasskeyLogin(ctx *context.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// Now handle account linking if that's requested
 | 
						// Now handle account linking if that's requested
 | 
				
			||||||
	if ctx.Session.Get("linkAccount") != nil {
 | 
						if ctx.Session.Get("linkAccount") != nil {
 | 
				
			||||||
		if err := externalaccount.LinkAccountFromStore(ctx, ctx.Session, user); err != nil {
 | 
							if err := linkAccountFromContext(ctx, user); err != nil {
 | 
				
			||||||
			ctx.ServerError("LinkAccountFromStore", err)
 | 
								ctx.ServerError("LinkAccountFromStore", err)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -268,7 +267,7 @@ func WebAuthnLoginAssertionPost(ctx *context.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// Now handle account linking if that's requested
 | 
						// Now handle account linking if that's requested
 | 
				
			||||||
	if ctx.Session.Get("linkAccount") != nil {
 | 
						if ctx.Session.Get("linkAccount") != nil {
 | 
				
			||||||
		if err := externalaccount.LinkAccountFromStore(ctx, ctx.Session, user); err != nil {
 | 
							if err := linkAccountFromContext(ctx, user); err != nil {
 | 
				
			||||||
			ctx.ServerError("LinkAccountFromStore", err)
 | 
								ctx.ServerError("LinkAccountFromStore", err)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,6 +27,7 @@ type Provider interface {
 | 
				
			|||||||
	DisplayName() string
 | 
						DisplayName() string
 | 
				
			||||||
	IconHTML(size int) template.HTML
 | 
						IconHTML(size int) template.HTML
 | 
				
			||||||
	CustomURLSettings() *CustomURLSettings
 | 
						CustomURLSettings() *CustomURLSettings
 | 
				
			||||||
 | 
						SupportSSHPublicKey() bool
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GothProviderCreator provides a function to create a goth.Provider
 | 
					// GothProviderCreator provides a function to create a goth.Provider
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,13 @@ import (
 | 
				
			|||||||
type BaseProvider struct {
 | 
					type BaseProvider struct {
 | 
				
			||||||
	name        string
 | 
						name        string
 | 
				
			||||||
	displayName string
 | 
						displayName string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO: maybe some providers also support SSH public keys, then they can set this to true
 | 
				
			||||||
 | 
						supportSSHPublicKey bool
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (b *BaseProvider) SupportSSHPublicKey() bool {
 | 
				
			||||||
 | 
						return b.supportSSHPublicKey
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Name provides the technical name for this provider
 | 
					// Name provides the technical name for this provider
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,6 +17,10 @@ import (
 | 
				
			|||||||
// OpenIDProvider is a GothProvider for OpenID
 | 
					// OpenIDProvider is a GothProvider for OpenID
 | 
				
			||||||
type OpenIDProvider struct{}
 | 
					type OpenIDProvider struct{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (o *OpenIDProvider) SupportSSHPublicKey() bool {
 | 
				
			||||||
 | 
						return true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Name provides the technical name for this provider
 | 
					// Name provides the technical name for this provider
 | 
				
			||||||
func (o *OpenIDProvider) Name() string {
 | 
					func (o *OpenIDProvider) Name() string {
 | 
				
			||||||
	return "openidConnect"
 | 
						return "openidConnect"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,6 +27,9 @@ type Source struct {
 | 
				
			|||||||
	GroupTeamMap        string
 | 
						GroupTeamMap        string
 | 
				
			||||||
	GroupTeamMapRemoval bool
 | 
						GroupTeamMapRemoval bool
 | 
				
			||||||
	RestrictedGroup     string
 | 
						RestrictedGroup     string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						SSHPublicKeyClaimName string
 | 
				
			||||||
 | 
						FullNameClaimName     string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// FromDB fills up an OAuth2Config from serialized format.
 | 
					// FromDB fills up an OAuth2Config from serialized format.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,30 +0,0 @@
 | 
				
			|||||||
// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
					 | 
				
			||||||
// SPDX-License-Identifier: MIT
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
package externalaccount
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"context"
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	user_model "code.gitea.io/gitea/models/user"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"github.com/markbates/goth"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Store represents a thing that stores things
 | 
					 | 
				
			||||||
type Store interface {
 | 
					 | 
				
			||||||
	Get(any) any
 | 
					 | 
				
			||||||
	Set(any, any) error
 | 
					 | 
				
			||||||
	Release() error
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// LinkAccountFromStore links the provided user with a stored external user
 | 
					 | 
				
			||||||
func LinkAccountFromStore(ctx context.Context, store Store, user *user_model.User) error {
 | 
					 | 
				
			||||||
	gothUser := store.Get("linkAccountGothUser")
 | 
					 | 
				
			||||||
	if gothUser == nil {
 | 
					 | 
				
			||||||
		return errors.New("not in LinkAccount session")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return LinkAccountToUser(ctx, user, gothUser.(goth.User))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -8,7 +8,6 @@ import (
 | 
				
			|||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models/auth"
 | 
					 | 
				
			||||||
	issues_model "code.gitea.io/gitea/models/issues"
 | 
						issues_model "code.gitea.io/gitea/models/issues"
 | 
				
			||||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
						repo_model "code.gitea.io/gitea/models/repo"
 | 
				
			||||||
	user_model "code.gitea.io/gitea/models/user"
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
@@ -17,15 +16,11 @@ import (
 | 
				
			|||||||
	"github.com/markbates/goth"
 | 
						"github.com/markbates/goth"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func toExternalLoginUser(ctx context.Context, user *user_model.User, gothUser goth.User) (*user_model.ExternalLoginUser, error) {
 | 
					func toExternalLoginUser(authSourceID int64, user *user_model.User, gothUser goth.User) *user_model.ExternalLoginUser {
 | 
				
			||||||
	authSource, err := auth.GetActiveOAuth2SourceByName(ctx, gothUser.Provider)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return &user_model.ExternalLoginUser{
 | 
						return &user_model.ExternalLoginUser{
 | 
				
			||||||
		ExternalID:        gothUser.UserID,
 | 
							ExternalID:        gothUser.UserID,
 | 
				
			||||||
		UserID:            user.ID,
 | 
							UserID:            user.ID,
 | 
				
			||||||
		LoginSourceID:     authSource.ID,
 | 
							LoginSourceID:     authSourceID,
 | 
				
			||||||
		RawData:           gothUser.RawData,
 | 
							RawData:           gothUser.RawData,
 | 
				
			||||||
		Provider:          gothUser.Provider,
 | 
							Provider:          gothUser.Provider,
 | 
				
			||||||
		Email:             gothUser.Email,
 | 
							Email:             gothUser.Email,
 | 
				
			||||||
@@ -40,15 +35,12 @@ func toExternalLoginUser(ctx context.Context, user *user_model.User, gothUser go
 | 
				
			|||||||
		AccessTokenSecret: gothUser.AccessTokenSecret,
 | 
							AccessTokenSecret: gothUser.AccessTokenSecret,
 | 
				
			||||||
		RefreshToken:      gothUser.RefreshToken,
 | 
							RefreshToken:      gothUser.RefreshToken,
 | 
				
			||||||
		ExpiresAt:         gothUser.ExpiresAt,
 | 
							ExpiresAt:         gothUser.ExpiresAt,
 | 
				
			||||||
	}, nil
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// LinkAccountToUser link the gothUser to the user
 | 
					// LinkAccountToUser link the gothUser to the user
 | 
				
			||||||
func LinkAccountToUser(ctx context.Context, user *user_model.User, gothUser goth.User) error {
 | 
					func LinkAccountToUser(ctx context.Context, authSourceID int64, user *user_model.User, gothUser goth.User) error {
 | 
				
			||||||
	externalLoginUser, err := toExternalLoginUser(ctx, user, gothUser)
 | 
						externalLoginUser := toExternalLoginUser(authSourceID, user, gothUser)
 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := user_model.LinkExternalToUser(ctx, user, externalLoginUser); err != nil {
 | 
						if err := user_model.LinkExternalToUser(ctx, user, externalLoginUser); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
@@ -72,12 +64,8 @@ func LinkAccountToUser(ctx context.Context, user *user_model.User, gothUser goth
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// EnsureLinkExternalToUser link the gothUser to the user
 | 
					// EnsureLinkExternalToUser link the gothUser to the user
 | 
				
			||||||
func EnsureLinkExternalToUser(ctx context.Context, user *user_model.User, gothUser goth.User) error {
 | 
					func EnsureLinkExternalToUser(ctx context.Context, authSourceID int64, user *user_model.User, gothUser goth.User) error {
 | 
				
			||||||
	externalLoginUser, err := toExternalLoginUser(ctx, user, gothUser)
 | 
						externalLoginUser := toExternalLoginUser(authSourceID, user, gothUser)
 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return user_model.EnsureLinkExternalToUser(ctx, externalLoginUser)
 | 
						return user_model.EnsureLinkExternalToUser(ctx, externalLoginUser)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,45 +18,54 @@ type AuthenticationForm struct {
 | 
				
			|||||||
	Type            int    `binding:"Range(2,7)"`
 | 
						Type            int    `binding:"Range(2,7)"`
 | 
				
			||||||
	Name            string `binding:"Required;MaxSize(30)"`
 | 
						Name            string `binding:"Required;MaxSize(30)"`
 | 
				
			||||||
	TwoFactorPolicy string
 | 
						TwoFactorPolicy string
 | 
				
			||||||
 | 
						IsActive        bool
 | 
				
			||||||
 | 
						IsSyncEnabled   bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	Host                          string
 | 
						// LDAP
 | 
				
			||||||
	Port                          int
 | 
						Host                  string
 | 
				
			||||||
	BindDN                        string
 | 
						Port                  int
 | 
				
			||||||
	BindPassword                  string
 | 
						BindDN                string
 | 
				
			||||||
	UserBase                      string
 | 
						BindPassword          string
 | 
				
			||||||
	UserDN                        string
 | 
						UserBase              string
 | 
				
			||||||
	AttributeUsername             string
 | 
						UserDN                string
 | 
				
			||||||
	AttributeName                 string
 | 
						AttributeUsername     string
 | 
				
			||||||
	AttributeSurname              string
 | 
						AttributeName         string
 | 
				
			||||||
	AttributeMail                 string
 | 
						AttributeSurname      string
 | 
				
			||||||
	AttributeSSHPublicKey         string
 | 
						AttributeMail         string
 | 
				
			||||||
	AttributeAvatar               string
 | 
						AttributeSSHPublicKey string
 | 
				
			||||||
	AttributesInBind              bool
 | 
						AttributeAvatar       string
 | 
				
			||||||
	UsePagedSearch                bool
 | 
						AttributesInBind      bool
 | 
				
			||||||
	SearchPageSize                int
 | 
						UsePagedSearch        bool
 | 
				
			||||||
	Filter                        string
 | 
						SearchPageSize        int
 | 
				
			||||||
	AdminFilter                   string
 | 
						Filter                string
 | 
				
			||||||
	GroupsEnabled                 bool
 | 
						AdminFilter           string
 | 
				
			||||||
	GroupDN                       string
 | 
						GroupsEnabled         bool
 | 
				
			||||||
	GroupFilter                   string
 | 
						GroupDN               string
 | 
				
			||||||
	GroupMemberUID                string
 | 
						GroupFilter           string
 | 
				
			||||||
	UserUID                       string
 | 
						GroupMemberUID        string
 | 
				
			||||||
	RestrictedFilter              string
 | 
						UserUID               string
 | 
				
			||||||
	AllowDeactivateAll            bool
 | 
						RestrictedFilter      string
 | 
				
			||||||
	IsActive                      bool
 | 
						AllowDeactivateAll    bool
 | 
				
			||||||
	IsSyncEnabled                 bool
 | 
						GroupTeamMap          string `binding:"ValidGroupTeamMap"`
 | 
				
			||||||
	SMTPAuth                      string
 | 
						GroupTeamMapRemoval   bool
 | 
				
			||||||
	SMTPHost                      string
 | 
					
 | 
				
			||||||
	SMTPPort                      int
 | 
						// SMTP
 | 
				
			||||||
	AllowedDomains                string
 | 
						SMTPAuth         string
 | 
				
			||||||
	SecurityProtocol              int `binding:"Range(0,2)"`
 | 
						SMTPHost         string
 | 
				
			||||||
	TLS                           bool
 | 
						SMTPPort         int
 | 
				
			||||||
	SkipVerify                    bool
 | 
						AllowedDomains   string
 | 
				
			||||||
	HeloHostname                  string
 | 
						SecurityProtocol int `binding:"Range(0,2)"`
 | 
				
			||||||
	DisableHelo                   bool
 | 
						TLS              bool
 | 
				
			||||||
	ForceSMTPS                    bool
 | 
						SkipVerify       bool
 | 
				
			||||||
	PAMServiceName                string
 | 
						HeloHostname     string
 | 
				
			||||||
	PAMEmailDomain                string
 | 
						DisableHelo      bool
 | 
				
			||||||
 | 
						ForceSMTPS       bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// PAM
 | 
				
			||||||
 | 
						PAMServiceName string
 | 
				
			||||||
 | 
						PAMEmailDomain string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Oauth2 & OIDC
 | 
				
			||||||
	Oauth2Provider                string
 | 
						Oauth2Provider                string
 | 
				
			||||||
	Oauth2Key                     string
 | 
						Oauth2Key                     string
 | 
				
			||||||
	Oauth2Secret                  string
 | 
						Oauth2Secret                  string
 | 
				
			||||||
@@ -76,13 +85,15 @@ type AuthenticationForm struct {
 | 
				
			|||||||
	Oauth2RestrictedGroup         string
 | 
						Oauth2RestrictedGroup         string
 | 
				
			||||||
	Oauth2GroupTeamMap            string `binding:"ValidGroupTeamMap"`
 | 
						Oauth2GroupTeamMap            string `binding:"ValidGroupTeamMap"`
 | 
				
			||||||
	Oauth2GroupTeamMapRemoval     bool
 | 
						Oauth2GroupTeamMapRemoval     bool
 | 
				
			||||||
	SSPIAutoCreateUsers           bool
 | 
						Oauth2SSHPublicKeyClaimName   string
 | 
				
			||||||
	SSPIAutoActivateUsers         bool
 | 
						Oauth2FullNameClaimName       string
 | 
				
			||||||
	SSPIStripDomainNames          bool
 | 
					
 | 
				
			||||||
	SSPISeparatorReplacement      string `binding:"AlphaDashDot;MaxSize(5)"`
 | 
						// SSPI
 | 
				
			||||||
	SSPIDefaultLanguage           string
 | 
						SSPIAutoCreateUsers      bool
 | 
				
			||||||
	GroupTeamMap                  string `binding:"ValidGroupTeamMap"`
 | 
						SSPIAutoActivateUsers    bool
 | 
				
			||||||
	GroupTeamMapRemoval           bool
 | 
						SSPIStripDomainNames     bool
 | 
				
			||||||
 | 
						SSPISeparatorReplacement string `binding:"AlphaDashDot;MaxSize(5)"`
 | 
				
			||||||
 | 
						SSPIDefaultLanguage      string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Validate validates fields
 | 
					// Validate validates fields
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -301,19 +301,30 @@
 | 
				
			|||||||
						<input id="oauth2_tenant" name="oauth2_tenant" value="{{if $cfg.CustomURLMapping}}{{$cfg.CustomURLMapping.Tenant}}{{end}}">
 | 
											<input id="oauth2_tenant" name="oauth2_tenant" value="{{if $cfg.CustomURLMapping}}{{$cfg.CustomURLMapping.Tenant}}{{end}}">
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					{{range .OAuth2Providers}}{{if .CustomURLSettings}}
 | 
										{{range .OAuth2Providers}}
 | 
				
			||||||
 | 
											<input id="{{.Name}}_SupportSSHPublicKey" value="{{.SupportSSHPublicKey}}" type="hidden">
 | 
				
			||||||
 | 
											{{if .CustomURLSettings}}
 | 
				
			||||||
						<input id="{{.Name}}_customURLSettings" type="hidden" data-required="{{.CustomURLSettings.Required}}" data-available="true">
 | 
											<input id="{{.Name}}_customURLSettings" type="hidden" data-required="{{.CustomURLSettings.Required}}" data-available="true">
 | 
				
			||||||
						<input id="{{.Name}}_token_url" value="{{.CustomURLSettings.TokenURL.Value}}" data-available="{{.CustomURLSettings.TokenURL.Available}}" data-required="{{.CustomURLSettings.TokenURL.Required}}" type="hidden">
 | 
											<input id="{{.Name}}_token_url" value="{{.CustomURLSettings.TokenURL.Value}}" data-available="{{.CustomURLSettings.TokenURL.Available}}" data-required="{{.CustomURLSettings.TokenURL.Required}}" type="hidden">
 | 
				
			||||||
						<input id="{{.Name}}_auth_url" value="{{.CustomURLSettings.AuthURL.Value}}" data-available="{{.CustomURLSettings.AuthURL.Available}}" data-required="{{.CustomURLSettings.AuthURL.Required}}" type="hidden">
 | 
											<input id="{{.Name}}_auth_url" value="{{.CustomURLSettings.AuthURL.Value}}" data-available="{{.CustomURLSettings.AuthURL.Available}}" data-required="{{.CustomURLSettings.AuthURL.Required}}" type="hidden">
 | 
				
			||||||
						<input id="{{.Name}}_profile_url" value="{{.CustomURLSettings.ProfileURL.Value}}" data-available="{{.CustomURLSettings.ProfileURL.Available}}" data-required="{{.CustomURLSettings.ProfileURL.Required}}" type="hidden">
 | 
											<input id="{{.Name}}_profile_url" value="{{.CustomURLSettings.ProfileURL.Value}}" data-available="{{.CustomURLSettings.ProfileURL.Available}}" data-required="{{.CustomURLSettings.ProfileURL.Required}}" type="hidden">
 | 
				
			||||||
						<input id="{{.Name}}_email_url" value="{{.CustomURLSettings.EmailURL.Value}}" data-available="{{.CustomURLSettings.EmailURL.Available}}" data-required="{{.CustomURLSettings.EmailURL.Required}}" type="hidden">
 | 
											<input id="{{.Name}}_email_url" value="{{.CustomURLSettings.EmailURL.Value}}" data-available="{{.CustomURLSettings.EmailURL.Available}}" data-required="{{.CustomURLSettings.EmailURL.Required}}" type="hidden">
 | 
				
			||||||
						<input id="{{.Name}}_tenant" value="{{.CustomURLSettings.Tenant.Value}}" data-available="{{.CustomURLSettings.Tenant.Available}}" data-required="{{.CustomURLSettings.Tenant.Required}}" type="hidden">
 | 
											<input id="{{.Name}}_tenant" value="{{.CustomURLSettings.Tenant.Value}}" data-available="{{.CustomURLSettings.Tenant.Available}}" data-required="{{.CustomURLSettings.Tenant.Required}}" type="hidden">
 | 
				
			||||||
					{{end}}{{end}}
 | 
											{{end}}
 | 
				
			||||||
 | 
									{{end}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					<div class="field">
 | 
										<div class="field">
 | 
				
			||||||
						<label for="oauth2_scopes">{{ctx.Locale.Tr "admin.auths.oauth2_scopes"}}</label>
 | 
											<label for="oauth2_scopes">{{ctx.Locale.Tr "admin.auths.oauth2_scopes"}}</label>
 | 
				
			||||||
						<input id="oauth2_scopes" name="oauth2_scopes" value="{{if $cfg.Scopes}}{{StringUtils.Join $cfg.Scopes ","}}{{end}}">
 | 
											<input id="oauth2_scopes" name="oauth2_scopes" value="{{if $cfg.Scopes}}{{StringUtils.Join $cfg.Scopes ","}}{{end}}">
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
 | 
										<div class="field">
 | 
				
			||||||
 | 
											<label>{{ctx.Locale.Tr "admin.auths.oauth2_full_name_claim_name"}}</label>
 | 
				
			||||||
 | 
											<input name="oauth2_full_name_claim_name" value="{{$cfg.FullNameClaimName}}" placeholder="name">
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
										<div class="field oauth2_ssh_public_key_claim_name">
 | 
				
			||||||
 | 
											<label>{{ctx.Locale.Tr "admin.auths.oauth2_ssh_public_key_claim_name"}}</label>
 | 
				
			||||||
 | 
											<input name="oauth2_ssh_public_key_claim_name" value="{{$cfg.SSHPublicKeyClaimName}}" placeholder="sshpubkey">
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
					<div class="field">
 | 
										<div class="field">
 | 
				
			||||||
						<label for="oauth2_required_claim_name">{{ctx.Locale.Tr "admin.auths.oauth2_required_claim_name"}}</label>
 | 
											<label for="oauth2_required_claim_name">{{ctx.Locale.Tr "admin.auths.oauth2_required_claim_name"}}</label>
 | 
				
			||||||
						<input id="oauth2_required_claim_name" name="oauth2_required_claim_name" value="{{$cfg.RequiredClaimName}}">
 | 
											<input id="oauth2_required_claim_name" name="oauth2_required_claim_name" value="{{$cfg.RequiredClaimName}}">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -63,19 +63,31 @@
 | 
				
			|||||||
		<input id="oauth2_tenant" name="oauth2_tenant" value="{{.oauth2_tenant}}">
 | 
							<input id="oauth2_tenant" name="oauth2_tenant" value="{{.oauth2_tenant}}">
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	{{range .OAuth2Providers}}{{if .CustomURLSettings}}
 | 
						{{range .OAuth2Providers}}
 | 
				
			||||||
 | 
							<input id="{{.Name}}_SupportSSHPublicKey" value="{{.SupportSSHPublicKey}}" type="hidden">
 | 
				
			||||||
 | 
							{{if .CustomURLSettings}}
 | 
				
			||||||
		<input id="{{.Name}}_customURLSettings" type="hidden" data-required="{{.CustomURLSettings.Required}}" data-available="true">
 | 
							<input id="{{.Name}}_customURLSettings" type="hidden" data-required="{{.CustomURLSettings.Required}}" data-available="true">
 | 
				
			||||||
		<input id="{{.Name}}_token_url" value="{{.CustomURLSettings.TokenURL.Value}}" data-available="{{.CustomURLSettings.TokenURL.Available}}" data-required="{{.CustomURLSettings.TokenURL.Required}}" type="hidden">
 | 
							<input id="{{.Name}}_token_url" value="{{.CustomURLSettings.TokenURL.Value}}" data-available="{{.CustomURLSettings.TokenURL.Available}}" data-required="{{.CustomURLSettings.TokenURL.Required}}" type="hidden">
 | 
				
			||||||
		<input id="{{.Name}}_auth_url" value="{{.CustomURLSettings.AuthURL.Value}}" data-available="{{.CustomURLSettings.AuthURL.Available}}" data-required="{{.CustomURLSettings.AuthURL.Required}}" type="hidden">
 | 
							<input id="{{.Name}}_auth_url" value="{{.CustomURLSettings.AuthURL.Value}}" data-available="{{.CustomURLSettings.AuthURL.Available}}" data-required="{{.CustomURLSettings.AuthURL.Required}}" type="hidden">
 | 
				
			||||||
		<input id="{{.Name}}_profile_url" value="{{.CustomURLSettings.ProfileURL.Value}}" data-available="{{.CustomURLSettings.ProfileURL.Available}}" data-required="{{.CustomURLSettings.ProfileURL.Required}}" type="hidden">
 | 
							<input id="{{.Name}}_profile_url" value="{{.CustomURLSettings.ProfileURL.Value}}" data-available="{{.CustomURLSettings.ProfileURL.Available}}" data-required="{{.CustomURLSettings.ProfileURL.Required}}" type="hidden">
 | 
				
			||||||
		<input id="{{.Name}}_email_url" value="{{.CustomURLSettings.EmailURL.Value}}" data-available="{{.CustomURLSettings.EmailURL.Available}}" data-required="{{.CustomURLSettings.EmailURL.Required}}" type="hidden">
 | 
							<input id="{{.Name}}_email_url" value="{{.CustomURLSettings.EmailURL.Value}}" data-available="{{.CustomURLSettings.EmailURL.Available}}" data-required="{{.CustomURLSettings.EmailURL.Required}}" type="hidden">
 | 
				
			||||||
		<input id="{{.Name}}_tenant" value="{{.CustomURLSettings.Tenant.Value}}" data-available="{{.CustomURLSettings.Tenant.Available}}" data-required="{{.CustomURLSettings.Tenant.Required}}" type="hidden">
 | 
							<input id="{{.Name}}_tenant" value="{{.CustomURLSettings.Tenant.Value}}" data-available="{{.CustomURLSettings.Tenant.Available}}" data-required="{{.CustomURLSettings.Tenant.Required}}" type="hidden">
 | 
				
			||||||
	{{end}}{{end}}
 | 
							{{end}}
 | 
				
			||||||
 | 
						{{end}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	<div class="field">
 | 
						<div class="field">
 | 
				
			||||||
		<label for="oauth2_scopes">{{ctx.Locale.Tr "admin.auths.oauth2_scopes"}}</label>
 | 
							<label for="oauth2_scopes">{{ctx.Locale.Tr "admin.auths.oauth2_scopes"}}</label>
 | 
				
			||||||
		<input id="oauth2_scopes" name="oauth2_scopes" value="{{.oauth2_scopes}}">
 | 
							<input id="oauth2_scopes" name="oauth2_scopes" value="{{.oauth2_scopes}}">
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<div class="field">
 | 
				
			||||||
 | 
							<label>{{ctx.Locale.Tr "admin.auths.oauth2_full_name_claim_name"}}</label>
 | 
				
			||||||
 | 
							<input name="oauth2_full_name_claim_name" value="{{.oauth2_full_name_claim_name}}" placeholder="name">
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<div class="field oauth2_ssh_public_key_claim_name">
 | 
				
			||||||
 | 
							<label>{{ctx.Locale.Tr "admin.auths.oauth2_ssh_public_key_claim_name"}}</label>
 | 
				
			||||||
 | 
							<input name="oauth2_ssh_public_key_claim_name" value="{{.oauth2_ssh_public_key_claim_name}}" placeholder="sshpubkey">
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
	<div class="field">
 | 
						<div class="field">
 | 
				
			||||||
		<label for="oauth2_required_claim_name">{{ctx.Locale.Tr "admin.auths.oauth2_required_claim_name"}}</label>
 | 
							<label for="oauth2_required_claim_name">{{ctx.Locale.Tr "admin.auths.oauth2_required_claim_name"}}</label>
 | 
				
			||||||
		<input id="oauth2_required_claim_name" name="oauth2_required_claim_name" value="{{.oauth2_required_claim_name}}">
 | 
							<input id="oauth2_required_claim_name" name="oauth2_required_claim_name" value="{{.oauth2_required_claim_name}}">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,9 +9,11 @@ import (
 | 
				
			|||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/http/httptest"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						asymkey_model "code.gitea.io/gitea/models/asymkey"
 | 
				
			||||||
	auth_model "code.gitea.io/gitea/models/auth"
 | 
						auth_model "code.gitea.io/gitea/models/auth"
 | 
				
			||||||
	"code.gitea.io/gitea/models/db"
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
	"code.gitea.io/gitea/models/unittest"
 | 
						"code.gitea.io/gitea/models/unittest"
 | 
				
			||||||
@@ -20,9 +22,13 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
	api "code.gitea.io/gitea/modules/structs"
 | 
						api "code.gitea.io/gitea/modules/structs"
 | 
				
			||||||
	"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/oauth2_provider"
 | 
						"code.gitea.io/gitea/services/oauth2_provider"
 | 
				
			||||||
	"code.gitea.io/gitea/tests"
 | 
						"code.gitea.io/gitea/tests"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/markbates/goth"
 | 
				
			||||||
 | 
						"github.com/markbates/goth/gothic"
 | 
				
			||||||
	"github.com/stretchr/testify/assert"
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
	"github.com/stretchr/testify/require"
 | 
						"github.com/stretchr/testify/require"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -931,3 +937,107 @@ func testOAuth2WellKnown(t *testing.T) {
 | 
				
			|||||||
	defer test.MockVariableValue(&setting.OAuth2.Enabled, false)()
 | 
						defer test.MockVariableValue(&setting.OAuth2.Enabled, false)()
 | 
				
			||||||
	MakeRequest(t, NewRequest(t, "GET", urlOpenidConfiguration), http.StatusNotFound)
 | 
						MakeRequest(t, NewRequest(t, "GET", urlOpenidConfiguration), http.StatusNotFound)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) {
 | 
				
			||||||
 | 
						defer tests.PrepareTestEnv(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var mockServer *httptest.Server
 | 
				
			||||||
 | 
						mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
							switch r.URL.Path {
 | 
				
			||||||
 | 
							case "/.well-known/openid-configuration":
 | 
				
			||||||
 | 
								_, _ = w.Write([]byte(`{
 | 
				
			||||||
 | 
									"issuer": "` + mockServer.URL + `",
 | 
				
			||||||
 | 
									"authorization_endpoint": "` + mockServer.URL + `/authorize",
 | 
				
			||||||
 | 
									"token_endpoint": "` + mockServer.URL + `/token",
 | 
				
			||||||
 | 
									"userinfo_endpoint": "` + mockServer.URL + `/userinfo"
 | 
				
			||||||
 | 
								}`))
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								http.NotFound(w, r)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}))
 | 
				
			||||||
 | 
						defer mockServer.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx := t.Context()
 | 
				
			||||||
 | 
						oauth2Source := oauth2.Source{
 | 
				
			||||||
 | 
							Provider:                      "openidConnect",
 | 
				
			||||||
 | 
							ClientID:                      "test-client-id",
 | 
				
			||||||
 | 
							SSHPublicKeyClaimName:         "sshpubkey",
 | 
				
			||||||
 | 
							FullNameClaimName:             "name",
 | 
				
			||||||
 | 
							OpenIDConnectAutoDiscoveryURL: mockServer.URL + "/.well-known/openid-configuration",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						addOAuth2Source(t, "test-oidc-source", oauth2Source)
 | 
				
			||||||
 | 
						authSource, err := auth_model.GetActiveOAuth2SourceByAuthName(ctx, "test-oidc-source")
 | 
				
			||||||
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sshKey1 := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf"
 | 
				
			||||||
 | 
						sshKey2 := "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIE7kM1R02+4ertDKGKEDcKG0s+2vyDDcIvceJ0Gqv5f1AAAABHNzaDo="
 | 
				
			||||||
 | 
						sshKey3 := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEHjnNEfE88W1pvBLdV3otv28x760gdmPao3lVD5uAt9"
 | 
				
			||||||
 | 
						cases := []struct {
 | 
				
			||||||
 | 
							testName           string
 | 
				
			||||||
 | 
							mockFullName       string
 | 
				
			||||||
 | 
							mockRawData        map[string]any
 | 
				
			||||||
 | 
							expectedSSHPubKeys []string
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								testName:           "Login1",
 | 
				
			||||||
 | 
								mockFullName:       "FullName1",
 | 
				
			||||||
 | 
								mockRawData:        map[string]any{"sshpubkey": []any{sshKey1 + " any-comment"}},
 | 
				
			||||||
 | 
								expectedSSHPubKeys: []string{sshKey1},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								testName:           "Login2",
 | 
				
			||||||
 | 
								mockFullName:       "FullName2",
 | 
				
			||||||
 | 
								mockRawData:        map[string]any{"sshpubkey": []any{sshKey2 + " any-comment", sshKey3}},
 | 
				
			||||||
 | 
								expectedSSHPubKeys: []string{sshKey2, sshKey3},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								testName:           "Login3",
 | 
				
			||||||
 | 
								mockFullName:       "FullName3",
 | 
				
			||||||
 | 
								mockRawData:        map[string]any{},
 | 
				
			||||||
 | 
								expectedSSHPubKeys: []string{},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						session := emptyTestSession(t)
 | 
				
			||||||
 | 
						for _, c := range cases {
 | 
				
			||||||
 | 
							t.Run(c.testName, func(t *testing.T) {
 | 
				
			||||||
 | 
								defer test.MockVariableValue(&setting.OAuth2Client.Username, "")()
 | 
				
			||||||
 | 
								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: authSource.Cfg.(*oauth2.Source).Provider,
 | 
				
			||||||
 | 
										UserID:   "oidc-userid",
 | 
				
			||||||
 | 
										Email:    "oidc-email@example.com",
 | 
				
			||||||
 | 
										RawData:  c.mockRawData,
 | 
				
			||||||
 | 
										Name:     c.mockFullName,
 | 
				
			||||||
 | 
									}, nil
 | 
				
			||||||
 | 
								})()
 | 
				
			||||||
 | 
								req := NewRequest(t, "GET", "/user/oauth2/test-oidc-source/callback?code=XYZ&state=XYZ")
 | 
				
			||||||
 | 
								session.MakeRequest(t, req, http.StatusSeeOther)
 | 
				
			||||||
 | 
								user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "oidc-userid"})
 | 
				
			||||||
 | 
								keys, _, err := db.FindAndCount[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{
 | 
				
			||||||
 | 
									ListOptions:   db.ListOptionsAll,
 | 
				
			||||||
 | 
									OwnerID:       user.ID,
 | 
				
			||||||
 | 
									LoginSourceID: authSource.ID,
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								require.NoError(t, err)
 | 
				
			||||||
 | 
								var sshPubKeys []string
 | 
				
			||||||
 | 
								for _, key := range keys {
 | 
				
			||||||
 | 
									sshPubKeys = append(sshPubKeys, key.Content)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								assert.ElementsMatch(t, c.expectedSSHPubKeys, sshPubKeys)
 | 
				
			||||||
 | 
								assert.Equal(t, c.mockFullName, user.FullName)
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,7 @@ import (
 | 
				
			|||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						auth_model "code.gitea.io/gitea/models/auth"
 | 
				
			||||||
	"code.gitea.io/gitea/models/db"
 | 
						"code.gitea.io/gitea/models/db"
 | 
				
			||||||
	"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"
 | 
				
			||||||
@@ -17,6 +18,7 @@ import (
 | 
				
			|||||||
	"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/routers"
 | 
						"code.gitea.io/gitea/routers"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/routers/web/auth"
 | 
				
			||||||
	"code.gitea.io/gitea/services/context"
 | 
						"code.gitea.io/gitea/services/context"
 | 
				
			||||||
	"code.gitea.io/gitea/tests"
 | 
						"code.gitea.io/gitea/tests"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -103,8 +105,9 @@ func TestEnablePasswordSignInFormAndEnablePasskeyAuth(t *testing.T) {
 | 
				
			|||||||
	defer tests.PrepareTestEnv(t)()
 | 
						defer tests.PrepareTestEnv(t)()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mockLinkAccount := func(ctx *context.Context) {
 | 
						mockLinkAccount := func(ctx *context.Context) {
 | 
				
			||||||
 | 
							authSource := auth_model.Source{ID: 1}
 | 
				
			||||||
		gothUser := goth.User{Email: "invalid-email", Name: "."}
 | 
							gothUser := goth.User{Email: "invalid-email", Name: "."}
 | 
				
			||||||
		_ = ctx.Session.Set("linkAccountGothUser", gothUser)
 | 
							_ = ctx.Session.Set("linkAccountData", auth.LinkAccountData{AuthSource: authSource, GothUser: gothUser})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	t.Run("EnablePasswordSignInForm=false", func(t *testing.T) {
 | 
						t.Run("EnablePasswordSignInForm=false", func(t *testing.T) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -102,6 +102,9 @@ function initAdminAuthentication() {
 | 
				
			|||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const supportSshPublicKey = document.querySelector<HTMLInputElement>(`#${provider}_SupportSSHPublicKey`)?.value === 'true';
 | 
				
			||||||
 | 
					    toggleElem('.field.oauth2_ssh_public_key_claim_name', supportSshPublicKey);
 | 
				
			||||||
    onOAuth2UseCustomURLChange(applyDefaultValues);
 | 
					    onOAuth2UseCustomURLChange(applyDefaultValues);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user