mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	Add Option to synchronize Admin & Restricted states from OIDC/OAuth2 along with Setting Scopes (#16766)
* Add setting to OAuth handlers to override local 2FA settings This PR adds a setting to OAuth and OpenID login sources to allow the source to override local 2FA requirements. Fix #13939 Signed-off-by: Andrew Thornton <art27@cantab.net> * Fix regression from #16544 Signed-off-by: Andrew Thornton <art27@cantab.net> * Add scopes settings Signed-off-by: Andrew Thornton <art27@cantab.net> * fix trace logging in auth_openid Signed-off-by: Andrew Thornton <art27@cantab.net> * add required claim options Signed-off-by: Andrew Thornton <art27@cantab.net> * Move UpdateExternalUser to externalaccount Signed-off-by: Andrew Thornton <art27@cantab.net> * Allow OAuth2/OIDC to set Admin/Restricted status Signed-off-by: Andrew Thornton <art27@cantab.net> * Allow use of the same group claim name for the prohibit login value Signed-off-by: Andrew Thornton <art27@cantab.net> * fixup! Move UpdateExternalUser to externalaccount * as per wxiaoguang Signed-off-by: Andrew Thornton <art27@cantab.net> * add label back in Signed-off-by: Andrew Thornton <art27@cantab.net> * adjust localisation Signed-off-by: Andrew Thornton <art27@cantab.net> * placate lint Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
This commit is contained in:
		
							
								
								
									
										58
									
								
								cmd/admin.go
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								cmd/admin.go
									
									
									
									
									
								
							| @@ -299,6 +299,36 @@ var ( | ||||
| 			Name:  "skip-local-2fa", | ||||
| 			Usage: "Set to true to skip local 2fa for users authenticated by this source", | ||||
| 		}, | ||||
| 		cli.StringSliceFlag{ | ||||
| 			Name:  "scopes", | ||||
| 			Value: nil, | ||||
| 			Usage: "Scopes to request when to authenticate against this OAuth2 source", | ||||
| 		}, | ||||
| 		cli.StringFlag{ | ||||
| 			Name:  "required-claim-name", | ||||
| 			Value: "", | ||||
| 			Usage: "Claim name that has to be set to allow users to login with this source", | ||||
| 		}, | ||||
| 		cli.StringFlag{ | ||||
| 			Name:  "required-claim-value", | ||||
| 			Value: "", | ||||
| 			Usage: "Claim value that has to be set to allow users to login with this source", | ||||
| 		}, | ||||
| 		cli.StringFlag{ | ||||
| 			Name:  "group-claim-name", | ||||
| 			Value: "", | ||||
| 			Usage: "Claim name providing group names for this source", | ||||
| 		}, | ||||
| 		cli.StringFlag{ | ||||
| 			Name:  "admin-group", | ||||
| 			Value: "", | ||||
| 			Usage: "Group Claim value for administrator users", | ||||
| 		}, | ||||
| 		cli.StringFlag{ | ||||
| 			Name:  "restricted-group", | ||||
| 			Value: "", | ||||
| 			Usage: "Group Claim value for restricted users", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	microcmdAuthUpdateOauth = cli.Command{ | ||||
| @@ -649,6 +679,12 @@ func parseOAuth2Config(c *cli.Context) *oauth2.Source { | ||||
| 		CustomURLMapping:              customURLMapping, | ||||
| 		IconURL:                       c.String("icon-url"), | ||||
| 		SkipLocalTwoFA:                c.Bool("skip-local-2fa"), | ||||
| 		Scopes:                        c.StringSlice("scopes"), | ||||
| 		RequiredClaimName:             c.String("required-claim-name"), | ||||
| 		RequiredClaimValue:            c.String("required-claim-value"), | ||||
| 		GroupClaimName:                c.String("group-claim-name"), | ||||
| 		AdminGroup:                    c.String("admin-group"), | ||||
| 		RestrictedGroup:               c.String("restricted-group"), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -711,6 +747,28 @@ func runUpdateOauth(c *cli.Context) error { | ||||
| 		oAuth2Config.IconURL = c.String("icon-url") | ||||
| 	} | ||||
|  | ||||
| 	if c.IsSet("scopes") { | ||||
| 		oAuth2Config.Scopes = c.StringSlice("scopes") | ||||
| 	} | ||||
|  | ||||
| 	if c.IsSet("required-claim-name") { | ||||
| 		oAuth2Config.RequiredClaimName = c.String("required-claim-name") | ||||
|  | ||||
| 	} | ||||
| 	if c.IsSet("required-claim-value") { | ||||
| 		oAuth2Config.RequiredClaimValue = c.String("required-claim-value") | ||||
| 	} | ||||
|  | ||||
| 	if c.IsSet("group-claim-name") { | ||||
| 		oAuth2Config.GroupClaimName = c.String("group-claim-name") | ||||
| 	} | ||||
| 	if c.IsSet("admin-group") { | ||||
| 		oAuth2Config.AdminGroup = c.String("admin-group") | ||||
| 	} | ||||
| 	if c.IsSet("restricted-group") { | ||||
| 		oAuth2Config.RestrictedGroup = c.String("restricted-group") | ||||
| 	} | ||||
|  | ||||
| 	// update custom URL mapping | ||||
| 	var customURLMapping = &oauth2.CustomURLMapping{} | ||||
|  | ||||
|   | ||||
| @@ -129,6 +129,13 @@ Admin operations: | ||||
|         - `--custom-profile-url`: Use a custom Profile URL (option for GitLab/GitHub). | ||||
|         - `--custom-email-url`: Use a custom Email URL (option for GitHub). | ||||
|         - `--icon-url`: Custom icon URL for OAuth2 login source. | ||||
|         - `--override-local-2fa`: Allow source to override local 2fa. (Optional) | ||||
|         - `--scopes`: Addtional scopes to request for this OAuth2 source. (Optional) | ||||
|         - `--required-claim-name`: Claim name that has to be set to allow users to login with this source. (Optional) | ||||
|         - `--required-claim-value`: Claim value that has to be set to allow users to login with this source. (Optional) | ||||
|         - `--group-claim-name`: Claim name providing group names for this source. (Optional) | ||||
|         - `--admin-group`: Group Claim value for administrator users. (Optional) | ||||
|         - `--restricted-group`: Group Claim value for restricted users. (Optional) | ||||
|       - Examples: | ||||
|         - `gitea admin auth add-oauth --name external-github --provider github --key OBTAIN_FROM_SOURCE --secret OBTAIN_FROM_SOURCE` | ||||
|     - `update-oauth`: | ||||
| @@ -145,6 +152,13 @@ Admin operations: | ||||
|         - `--custom-profile-url`: Use a custom Profile URL (option for GitLab/GitHub). | ||||
|         - `--custom-email-url`: Use a custom Email URL (option for GitHub). | ||||
|         - `--icon-url`: Custom icon URL for OAuth2 login source. | ||||
|         - `--override-local-2fa`: Allow source to override local 2fa. (Optional) | ||||
|         - `--scopes`: Addtional scopes to request for this OAuth2 source. | ||||
|         - `--required-claim-name`: Claim name that has to be set to allow users to login with this source. (Optional) | ||||
|         - `--required-claim-value`: Claim value that has to be set to allow users to login with this source. (Optional) | ||||
|         - `--group-claim-name`: Claim name providing group names for this source. (Optional) | ||||
|         - `--admin-group`: Group Claim value for administrator users. (Optional) | ||||
|         - `--restricted-group`: Group Claim value for restricted users. (Optional) | ||||
|       - Examples: | ||||
|         - `gitea admin auth update-oauth --id 1 --name external-github-updated` | ||||
|     - `add-ldap`: Add new LDAP (via Bind DN) authentication source | ||||
|   | ||||
| @@ -10,9 +10,7 @@ import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	"code.gitea.io/gitea/models/login" | ||||
|  | ||||
| 	"github.com/markbates/goth" | ||||
| 	"xorm.io/builder" | ||||
| ) | ||||
|  | ||||
| @@ -139,42 +137,18 @@ func GetUserIDByExternalUserID(provider, userID string) (int64, error) { | ||||
| 	return id, nil | ||||
| } | ||||
|  | ||||
| // UpdateExternalUser updates external user's information | ||||
| func UpdateExternalUser(user *User, gothUser goth.User) error { | ||||
| 	loginSource, err := login.GetActiveOAuth2LoginSourceByName(gothUser.Provider) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	externalLoginUser := &ExternalLoginUser{ | ||||
| 		ExternalID:        gothUser.UserID, | ||||
| 		UserID:            user.ID, | ||||
| 		LoginSourceID:     loginSource.ID, | ||||
| 		RawData:           gothUser.RawData, | ||||
| 		Provider:          gothUser.Provider, | ||||
| 		Email:             gothUser.Email, | ||||
| 		Name:              gothUser.Name, | ||||
| 		FirstName:         gothUser.FirstName, | ||||
| 		LastName:          gothUser.LastName, | ||||
| 		NickName:          gothUser.NickName, | ||||
| 		Description:       gothUser.Description, | ||||
| 		AvatarURL:         gothUser.AvatarURL, | ||||
| 		Location:          gothUser.Location, | ||||
| 		AccessToken:       gothUser.AccessToken, | ||||
| 		AccessTokenSecret: gothUser.AccessTokenSecret, | ||||
| 		RefreshToken:      gothUser.RefreshToken, | ||||
| 		ExpiresAt:         gothUser.ExpiresAt, | ||||
| 	} | ||||
|  | ||||
| 	has, err := db.GetEngine(db.DefaultContext).Where("external_id=? AND login_source_id=?", gothUser.UserID, loginSource.ID). | ||||
| // UpdateExternalUserByExternalID updates an external user's information | ||||
| func UpdateExternalUserByExternalID(external *ExternalLoginUser) error { | ||||
| 	has, err := db.GetEngine(db.DefaultContext).Where("external_id=? AND login_source_id=?", external.ExternalID, external.LoginSourceID). | ||||
| 		NoAutoCondition(). | ||||
| 		Exist(externalLoginUser) | ||||
| 		Exist(external) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} else if !has { | ||||
| 		return ErrExternalLoginUserNotExist{user.ID, loginSource.ID} | ||||
| 		return ErrExternalLoginUserNotExist{external.UserID, external.LoginSourceID} | ||||
| 	} | ||||
|  | ||||
| 	_, err = db.GetEngine(db.DefaultContext).Where("external_id=? AND login_source_id=?", gothUser.UserID, loginSource.ID).AllCols().Update(externalLoginUser) | ||||
| 	_, err = db.GetEngine(db.DefaultContext).Where("external_id=? AND login_source_id=?", external.ExternalID, external.LoginSourceID).AllCols().Update(external) | ||||
| 	return err | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -377,6 +377,7 @@ func NewFuncMap() []template.FuncMap { | ||||
| 		"MermaidMaxSourceCharacters": func() int { | ||||
| 			return setting.MermaidMaxSourceCharacters | ||||
| 		}, | ||||
| 		"Join":        strings.Join, | ||||
| 		"QueryEscape": url.QueryEscape, | ||||
| 	}} | ||||
| } | ||||
|   | ||||
| @@ -2521,6 +2521,11 @@ auths.oauth2_emailURL = Email URL | ||||
| auths.skip_local_two_fa = Skip local 2FA | ||||
| auths.skip_local_two_fa_helper = Leaving unset means local users with 2FA set will still have to pass 2FA to log on | ||||
| auths.oauth2_tenant = Tenant | ||||
| auths.oauth2_scopes = Additional Scopes | ||||
| auths.oauth2_required_claim_name = Required Claim Name | ||||
| auths.oauth2_required_claim_name_helper = Set this name to restrict login from this source to users with a claim with this name | ||||
| 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.enable_auto_register = Enable Auto Registration | ||||
| auths.sspi_auto_create_users = Automatically create users | ||||
| auths.sspi_auto_create_users_helper = Allow SSPI auth method to automatically create new accounts for users that login for the first time | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import ( | ||||
| 	"net/url" | ||||
| 	"regexp" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models/login" | ||||
| 	"code.gitea.io/gitea/modules/auth/pam" | ||||
| @@ -187,6 +188,9 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source { | ||||
| 		OpenIDConnectAutoDiscoveryURL: form.OpenIDConnectAutoDiscoveryURL, | ||||
| 		CustomURLMapping:              customURLMapping, | ||||
| 		IconURL:                       form.Oauth2IconURL, | ||||
| 		Scopes:                        strings.Split(form.Oauth2Scopes, ","), | ||||
| 		RequiredClaimName:             form.Oauth2RequiredClaimName, | ||||
| 		RequiredClaimValue:            form.Oauth2RequiredClaimValue, | ||||
| 		SkipLocalTwoFA:                form.SkipLocalTwoFA, | ||||
| 	} | ||||
| } | ||||
| @@ -329,8 +333,8 @@ func EditAuthSource(ctx *context.Context) { | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	ctx.HTML(http.StatusOK, tplAuthEdit) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -320,16 +320,8 @@ func TwoFactorPost(ctx *context.Context) { | ||||
| 		} | ||||
|  | ||||
| 		if ctx.Session.Get("linkAccount") != nil { | ||||
| 			gothUser := ctx.Session.Get("linkAccountGothUser") | ||||
| 			if gothUser == nil { | ||||
| 				ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session")) | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			err = externalaccount.LinkAccountToUser(u, gothUser.(goth.User)) | ||||
| 			if err != nil { | ||||
| 			if err := externalaccount.LinkAccountFromStore(ctx.Session, u); err != nil { | ||||
| 				ctx.ServerError("UserSignIn", err) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| @@ -506,16 +498,8 @@ func U2FSign(ctx *context.Context) { | ||||
| 			} | ||||
|  | ||||
| 			if ctx.Session.Get("linkAccount") != nil { | ||||
| 				gothUser := ctx.Session.Get("linkAccountGothUser") | ||||
| 				if gothUser == nil { | ||||
| 					ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session")) | ||||
| 					return | ||||
| 				} | ||||
|  | ||||
| 				err = externalaccount.LinkAccountToUser(user, gothUser.(goth.User)) | ||||
| 				if err != nil { | ||||
| 				if err := externalaccount.LinkAccountFromStore(ctx.Session, user); err != nil { | ||||
| 					ctx.ServerError("UserSignIn", err) | ||||
| 					return | ||||
| 				} | ||||
| 			} | ||||
| 			redirect := handleSignInFull(ctx, user, remember, false) | ||||
| @@ -653,6 +637,13 @@ func SignInOAuthCallback(ctx *context.Context) { | ||||
| 	u, gothUser, err := oAuth2UserLoginCallback(loginSource, ctx.Req, ctx.Resp) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		if user_model.IsErrUserProhibitLogin(err) { | ||||
| 			uplerr := err.(*user_model.ErrUserProhibitLogin) | ||||
| 			log.Info("Failed authentication attempt for %s from %s: %v", uplerr.Name, ctx.RemoteAddr(), err) | ||||
| 			ctx.Data["Title"] = ctx.Tr("auth.prohibit_login") | ||||
| 			ctx.HTML(http.StatusOK, "user/auth/prohibit_login") | ||||
| 			return | ||||
| 		} | ||||
| 		ctx.ServerError("UserSignIn", err) | ||||
| 		return | ||||
| 	} | ||||
| @@ -690,6 +681,8 @@ func SignInOAuthCallback(ctx *context.Context) { | ||||
| 				IsRestricted: setting.Service.DefaultUserIsRestricted, | ||||
| 			} | ||||
|  | ||||
| 			setUserGroupClaims(loginSource, u, &gothUser) | ||||
|  | ||||
| 			if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) { | ||||
| 				// error already handled | ||||
| 				return | ||||
| @@ -704,6 +697,53 @@ func SignInOAuthCallback(ctx *context.Context) { | ||||
| 	handleOAuth2SignIn(ctx, loginSource, u, gothUser) | ||||
| } | ||||
|  | ||||
| func claimValueToStringSlice(claimValue interface{}) []string { | ||||
| 	var groups []string | ||||
|  | ||||
| 	switch rawGroup := claimValue.(type) { | ||||
| 	case []string: | ||||
| 		groups = rawGroup | ||||
| 	default: | ||||
| 		str := fmt.Sprintf("%s", rawGroup) | ||||
| 		groups = strings.Split(str, ",") | ||||
| 	} | ||||
| 	return groups | ||||
| } | ||||
|  | ||||
| func setUserGroupClaims(loginSource *login.Source, u *user_model.User, gothUser *goth.User) bool { | ||||
|  | ||||
| 	source := loginSource.Cfg.(*oauth2.Source) | ||||
| 	if source.GroupClaimName == "" || (source.AdminGroup == "" && source.RestrictedGroup == "") { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	groupClaims, has := gothUser.RawData[source.GroupClaimName] | ||||
| 	if !has { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	groups := claimValueToStringSlice(groupClaims) | ||||
|  | ||||
| 	wasAdmin, wasRestricted := u.IsAdmin, u.IsRestricted | ||||
|  | ||||
| 	if source.AdminGroup != "" { | ||||
| 		u.IsAdmin = false | ||||
| 	} | ||||
| 	if source.RestrictedGroup != "" { | ||||
| 		u.IsRestricted = false | ||||
| 	} | ||||
|  | ||||
| 	for _, g := range groups { | ||||
| 		if source.AdminGroup != "" && g == source.AdminGroup { | ||||
| 			u.IsAdmin = true | ||||
| 		} else if source.RestrictedGroup != "" && g == source.RestrictedGroup { | ||||
| 			u.IsRestricted = true | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return wasAdmin != u.IsAdmin || wasRestricted != u.IsRestricted | ||||
| } | ||||
|  | ||||
| func getUserName(gothUser *goth.User) string { | ||||
| 	switch setting.OAuth2Client.Username { | ||||
| 	case setting.OAuth2UsernameEmail: | ||||
| @@ -774,13 +814,21 @@ func handleOAuth2SignIn(ctx *context.Context, source *login.Source, u *user_mode | ||||
|  | ||||
| 		// Register last login | ||||
| 		u.SetLastLogin() | ||||
| 		if err := user_model.UpdateUserCols(db.DefaultContext, u, "last_login_unix"); err != nil { | ||||
|  | ||||
| 		// Update GroupClaims | ||||
| 		changed := setUserGroupClaims(source, u, &gothUser) | ||||
| 		cols := []string{"last_login_unix"} | ||||
| 		if changed { | ||||
| 			cols = append(cols, "is_admin", "is_restricted") | ||||
| 		} | ||||
|  | ||||
| 		if err := user_model.UpdateUserCols(db.DefaultContext, u, cols...); err != nil { | ||||
| 			ctx.ServerError("UpdateUserCols", err) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// update external user information | ||||
| 		if err := user_model.UpdateExternalUser(u, gothUser); err != nil { | ||||
| 		if err := externalaccount.UpdateExternalUser(u, gothUser); err != nil { | ||||
| 			log.Error("UpdateExternalUser failed: %v", err) | ||||
| 		} | ||||
|  | ||||
| @@ -794,6 +842,14 @@ func handleOAuth2SignIn(ctx *context.Context, source *login.Source, u *user_mode | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	changed := setUserGroupClaims(source, u, &gothUser) | ||||
| 	if changed { | ||||
| 		if err := user_model.UpdateUserCols(db.DefaultContext, u, "is_admin", "is_restricted"); err != nil { | ||||
| 			ctx.ServerError("UpdateUserCols", err) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// User needs to use 2FA, save data and redirect to 2FA page. | ||||
| 	if err := ctx.Session.Set("twofaUid", u.ID); err != nil { | ||||
| 		log.Error("Error setting twofaUid in session: %v", err) | ||||
| @@ -818,7 +874,9 @@ func handleOAuth2SignIn(ctx *context.Context, source *login.Source, u *user_mode | ||||
| // OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful | ||||
| // login the user | ||||
| func oAuth2UserLoginCallback(loginSource *login.Source, request *http.Request, response http.ResponseWriter) (*user_model.User, goth.User, error) { | ||||
| 	gothUser, err := loginSource.Cfg.(*oauth2.Source).Callback(request, response) | ||||
| 	oauth2Source := loginSource.Cfg.(*oauth2.Source) | ||||
|  | ||||
| 	gothUser, err := oauth2Source.Callback(request, response) | ||||
| 	if err != nil { | ||||
| 		if err.Error() == "securecookie: the value is too long" || strings.Contains(err.Error(), "Data too long") { | ||||
| 			log.Error("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", loginSource.Name, setting.OAuth2.MaxTokenLength) | ||||
| @@ -827,6 +885,27 @@ func oAuth2UserLoginCallback(loginSource *login.Source, request *http.Request, r | ||||
| 		return nil, goth.User{}, err | ||||
| 	} | ||||
|  | ||||
| 	if oauth2Source.RequiredClaimName != "" { | ||||
| 		claimInterface, has := gothUser.RawData[oauth2Source.RequiredClaimName] | ||||
| 		if !has { | ||||
| 			return nil, goth.User{}, user_model.ErrUserProhibitLogin{Name: gothUser.UserID} | ||||
| 		} | ||||
|  | ||||
| 		if oauth2Source.RequiredClaimValue != "" { | ||||
| 			groups := claimValueToStringSlice(claimInterface) | ||||
| 			found := false | ||||
| 			for _, group := range groups { | ||||
| 				if group == oauth2Source.RequiredClaimValue { | ||||
| 					found = true | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
| 			if !found { | ||||
| 				return nil, goth.User{}, user_model.ErrUserProhibitLogin{Name: gothUser.UserID} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	user := &user_model.User{ | ||||
| 		LoginName:   gothUser.UserID, | ||||
| 		LoginType:   login.OAuth2, | ||||
| @@ -1354,7 +1433,7 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth. | ||||
|  | ||||
| 	// update external user information | ||||
| 	if gothUser != nil { | ||||
| 		if err := user_model.UpdateExternalUser(u, *gothUser); err != nil { | ||||
| 		if err := externalaccount.UpdateExternalUser(u, *gothUser); err != nil { | ||||
| 			log.Error("UpdateExternalUser failed: %v", err) | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -144,10 +144,10 @@ func SignInOpenIDPost(ctx *context.Context) { | ||||
| // signInOpenIDVerify handles response from OpenID provider | ||||
| func signInOpenIDVerify(ctx *context.Context) { | ||||
|  | ||||
| 	log.Trace("Incoming call to: " + ctx.Req.URL.String()) | ||||
| 	log.Trace("Incoming call to: %s", ctx.Req.URL.String()) | ||||
|  | ||||
| 	fullURL := setting.AppURL + ctx.Req.URL.String()[1:] | ||||
| 	log.Trace("Full URL: " + fullURL) | ||||
| 	log.Trace("Full URL: %s", fullURL) | ||||
|  | ||||
| 	var id, err = openid.Verify(fullURL) | ||||
| 	if err != nil { | ||||
| @@ -157,7 +157,7 @@ func signInOpenIDVerify(ctx *context.Context) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	log.Trace("Verified ID: " + id) | ||||
| 	log.Trace("Verified ID: %s", id) | ||||
|  | ||||
| 	/* Now we should seek for the user and log him in, or prompt | ||||
| 	 * to register if not found */ | ||||
| @@ -180,7 +180,7 @@ func signInOpenIDVerify(ctx *context.Context) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	log.Trace("User with openid " + id + " does not exist, should connect or register") | ||||
| 	log.Trace("User with openid: %s does not exist, should connect or register", id) | ||||
|  | ||||
| 	parsedURL, err := url.Parse(fullURL) | ||||
| 	if err != nil { | ||||
| @@ -199,7 +199,7 @@ func signInOpenIDVerify(ctx *context.Context) { | ||||
| 	email := values.Get("openid.sreg.email") | ||||
| 	nickname := values.Get("openid.sreg.nickname") | ||||
|  | ||||
| 	log.Trace("User has email=" + email + " and nickname=" + nickname) | ||||
| 	log.Trace("User has email=%s and nickname=%s", email, nickname) | ||||
|  | ||||
| 	if email != "" { | ||||
| 		u, err = user_model.GetUserByEmail(email) | ||||
| @@ -213,7 +213,7 @@ func signInOpenIDVerify(ctx *context.Context) { | ||||
| 			log.Error("signInOpenIDVerify: %v", err) | ||||
| 		} | ||||
| 		if u != nil { | ||||
| 			log.Trace("Local user " + u.LowerName + " has OpenID provided email " + email) | ||||
| 			log.Trace("Local user %s has OpenID provided email %s", u.LowerName, email) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -228,7 +228,7 @@ func signInOpenIDVerify(ctx *context.Context) { | ||||
| 			} | ||||
| 		} | ||||
| 		if u != nil { | ||||
| 			log.Trace("Local user " + u.LowerName + " has OpenID provided nickname " + nickname) | ||||
| 			log.Trace("Local user %s has OpenID provided nickname %s", u.LowerName, nickname) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -17,7 +17,7 @@ import ( | ||||
| ) | ||||
|  | ||||
| // CustomProviderNewFn creates a goth.Provider using a custom url mapping | ||||
| type CustomProviderNewFn func(clientID, secret, callbackURL string, custom *CustomURLMapping) (goth.Provider, error) | ||||
| type CustomProviderNewFn func(clientID, secret, callbackURL string, custom *CustomURLMapping, scopes []string) (goth.Provider, error) | ||||
|  | ||||
| // CustomProvider is a GothProvider that has CustomURL features | ||||
| type CustomProvider struct { | ||||
| @@ -35,7 +35,7 @@ func (c *CustomProvider) CustomURLSettings() *CustomURLSettings { | ||||
| func (c *CustomProvider) CreateGothProvider(providerName, callbackURL string, source *Source) (goth.Provider, error) { | ||||
| 	custom := c.customURLSettings.OverrideWith(source.CustomURLMapping) | ||||
|  | ||||
| 	return c.newFn(source.ClientID, source.ClientSecret, callbackURL, custom) | ||||
| 	return c.newFn(source.ClientID, source.ClientSecret, callbackURL, custom, source.Scopes) | ||||
| } | ||||
|  | ||||
| // NewCustomProvider is a constructor function for custom providers | ||||
| @@ -60,8 +60,7 @@ func init() { | ||||
| 			ProfileURL: availableAttribute(github.ProfileURL), | ||||
| 			EmailURL:   availableAttribute(github.EmailURL), | ||||
| 		}, | ||||
| 		func(clientID, secret, callbackURL string, custom *CustomURLMapping) (goth.Provider, error) { | ||||
| 			scopes := []string{} | ||||
| 		func(clientID, secret, callbackURL string, custom *CustomURLMapping, scopes []string) (goth.Provider, error) { | ||||
| 			if setting.OAuth2Client.EnableAutoRegistration { | ||||
| 				scopes = append(scopes, "user:email") | ||||
| 			} | ||||
| @@ -73,8 +72,9 @@ func init() { | ||||
| 			AuthURL:    availableAttribute(gitlab.AuthURL), | ||||
| 			TokenURL:   availableAttribute(gitlab.TokenURL), | ||||
| 			ProfileURL: availableAttribute(gitlab.ProfileURL), | ||||
| 		}, func(clientID, secret, callbackURL string, custom *CustomURLMapping) (goth.Provider, error) { | ||||
| 			return gitlab.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL, custom.TokenURL, custom.ProfileURL, "read_user"), nil | ||||
| 		}, func(clientID, secret, callbackURL string, custom *CustomURLMapping, scopes []string) (goth.Provider, error) { | ||||
| 			scopes = append(scopes, "read_user") | ||||
| 			return gitlab.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL, custom.TokenURL, custom.ProfileURL, scopes...), nil | ||||
| 		})) | ||||
|  | ||||
| 	RegisterGothProvider(NewCustomProvider( | ||||
| @@ -83,8 +83,8 @@ func init() { | ||||
| 			AuthURL:    requiredAttribute(gitea.AuthURL), | ||||
| 			ProfileURL: requiredAttribute(gitea.ProfileURL), | ||||
| 		}, | ||||
| 		func(clientID, secret, callbackURL string, custom *CustomURLMapping) (goth.Provider, error) { | ||||
| 			return gitea.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL, custom.TokenURL, custom.ProfileURL), nil | ||||
| 		func(clientID, secret, callbackURL string, custom *CustomURLMapping, scopes []string) (goth.Provider, error) { | ||||
| 			return gitea.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL, custom.TokenURL, custom.ProfileURL, scopes...), nil | ||||
| 		})) | ||||
|  | ||||
| 	RegisterGothProvider(NewCustomProvider( | ||||
| @@ -93,25 +93,31 @@ func init() { | ||||
| 			AuthURL:    requiredAttribute(nextcloud.AuthURL), | ||||
| 			ProfileURL: requiredAttribute(nextcloud.ProfileURL), | ||||
| 		}, | ||||
| 		func(clientID, secret, callbackURL string, custom *CustomURLMapping) (goth.Provider, error) { | ||||
| 			return nextcloud.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL, custom.TokenURL, custom.ProfileURL), nil | ||||
| 		func(clientID, secret, callbackURL string, custom *CustomURLMapping, scopes []string) (goth.Provider, error) { | ||||
| 			return nextcloud.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL, custom.TokenURL, custom.ProfileURL, scopes...), nil | ||||
| 		})) | ||||
|  | ||||
| 	RegisterGothProvider(NewCustomProvider( | ||||
| 		"mastodon", "Mastodon", &CustomURLSettings{ | ||||
| 			AuthURL: requiredAttribute(mastodon.InstanceURL), | ||||
| 		}, | ||||
| 		func(clientID, secret, callbackURL string, custom *CustomURLMapping) (goth.Provider, error) { | ||||
| 			return mastodon.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL), nil | ||||
| 		func(clientID, secret, callbackURL string, custom *CustomURLMapping, scopes []string) (goth.Provider, error) { | ||||
| 			return mastodon.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL, scopes...), nil | ||||
| 		})) | ||||
|  | ||||
| 	RegisterGothProvider(NewCustomProvider( | ||||
| 		"azureadv2", "Azure AD v2", &CustomURLSettings{ | ||||
| 			Tenant: requiredAttribute("organizations"), | ||||
| 		}, | ||||
| 		func(clientID, secret, callbackURL string, custom *CustomURLMapping) (goth.Provider, error) { | ||||
| 		func(clientID, secret, callbackURL string, custom *CustomURLMapping, scopes []string) (goth.Provider, error) { | ||||
| 			azureScopes := make([]azureadv2.ScopeType, len(scopes)) | ||||
| 			for i, scope := range scopes { | ||||
| 				azureScopes[i] = azureadv2.ScopeType(scope) | ||||
| 			} | ||||
|  | ||||
| 			return azureadv2.New(clientID, secret, callbackURL, azureadv2.ProviderOptions{ | ||||
| 				Tenant: azureadv2.TenantType(custom.Tenant), | ||||
| 				Scopes: azureScopes, | ||||
| 			}), nil | ||||
| 		}, | ||||
| 	)) | ||||
|   | ||||
| @@ -33,7 +33,12 @@ func (o *OpenIDProvider) Image() string { | ||||
|  | ||||
| // CreateGothProvider creates a GothProvider from this Provider | ||||
| func (o *OpenIDProvider) CreateGothProvider(providerName, callbackURL string, source *Source) (goth.Provider, error) { | ||||
| 	provider, err := openidConnect.New(source.ClientID, source.ClientSecret, callbackURL, source.OpenIDConnectAutoDiscoveryURL, setting.OAuth2Client.OpenIDConnectScopes...) | ||||
| 	scopes := setting.OAuth2Client.OpenIDConnectScopes | ||||
| 	if len(scopes) == 0 { | ||||
| 		scopes = append(scopes, source.Scopes...) | ||||
| 	} | ||||
|  | ||||
| 	provider, err := openidConnect.New(source.ClientID, source.ClientSecret, callbackURL, source.OpenIDConnectAutoDiscoveryURL, scopes...) | ||||
| 	if err != nil { | ||||
| 		log.Warn("Failed to create OpenID Connect Provider with name '%s' with url '%s': %v", providerName, source.OpenIDConnectAutoDiscoveryURL, err) | ||||
| 	} | ||||
|   | ||||
| @@ -31,7 +31,10 @@ type SimpleProvider struct { | ||||
|  | ||||
| // CreateGothProvider creates a GothProvider from this Provider | ||||
| func (c *SimpleProvider) CreateGothProvider(providerName, callbackURL string, source *Source) (goth.Provider, error) { | ||||
| 	return c.newFn(source.ClientID, source.ClientSecret, callbackURL, c.scopes...), nil | ||||
| 	scopes := make([]string, len(c.scopes)+len(source.Scopes)) | ||||
| 	copy(scopes, c.scopes) | ||||
| 	copy(scopes[len(c.scopes):], source.Scopes) | ||||
| 	return c.newFn(source.ClientID, source.ClientSecret, callbackURL, scopes...), nil | ||||
| } | ||||
|  | ||||
| // NewSimpleProvider is a constructor function for simple providers | ||||
|   | ||||
| @@ -24,7 +24,14 @@ type Source struct { | ||||
| 	OpenIDConnectAutoDiscoveryURL string | ||||
| 	CustomURLMapping              *CustomURLMapping | ||||
| 	IconURL                       string | ||||
| 	SkipLocalTwoFA                bool `json:",omitempty"` | ||||
|  | ||||
| 	Scopes             []string | ||||
| 	RequiredClaimName  string | ||||
| 	RequiredClaimValue string | ||||
| 	GroupClaimName     string | ||||
| 	AdminGroup         string | ||||
| 	RestrictedGroup    string | ||||
| 	SkipLocalTwoFA     bool `json:",omitempty"` | ||||
|  | ||||
| 	// reference to the loginSource | ||||
| 	loginSource *login.Source | ||||
|   | ||||
							
								
								
									
										29
									
								
								services/externalaccount/link.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								services/externalaccount/link.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| // Copyright 2021 The Gitea Authors. All rights reserved. | ||||
| // Use of this source code is governed by a MIT-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| package externalaccount | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	"github.com/markbates/goth" | ||||
| ) | ||||
|  | ||||
| // Store represents a thing that stores things | ||||
| type Store interface { | ||||
| 	Get(interface{}) interface{} | ||||
| 	Set(interface{}, interface{}) error | ||||
| 	Release() error | ||||
| } | ||||
|  | ||||
| // LinkAccountFromStore links the provided user with a stored external user | ||||
| func LinkAccountFromStore(store Store, user *user_model.User) error { | ||||
| 	gothUser := store.Get("linkAccountGothUser") | ||||
| 	if gothUser == nil { | ||||
| 		return fmt.Errorf("not in LinkAccount session") | ||||
| 	} | ||||
|  | ||||
| 	return LinkAccountToUser(user, gothUser.(goth.User)) | ||||
| } | ||||
| @@ -15,14 +15,12 @@ import ( | ||||
| 	"github.com/markbates/goth" | ||||
| ) | ||||
|  | ||||
| // LinkAccountToUser link the gothUser to the user | ||||
| func LinkAccountToUser(user *user_model.User, gothUser goth.User) error { | ||||
| func toExternalLoginUser(user *user_model.User, gothUser goth.User) (*user_model.ExternalLoginUser, error) { | ||||
| 	loginSource, err := login.GetActiveOAuth2LoginSourceByName(gothUser.Provider) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	externalLoginUser := &user_model.ExternalLoginUser{ | ||||
| 	return &user_model.ExternalLoginUser{ | ||||
| 		ExternalID:        gothUser.UserID, | ||||
| 		UserID:            user.ID, | ||||
| 		LoginSourceID:     loginSource.ID, | ||||
| @@ -40,6 +38,14 @@ func LinkAccountToUser(user *user_model.User, gothUser goth.User) error { | ||||
| 		AccessTokenSecret: gothUser.AccessTokenSecret, | ||||
| 		RefreshToken:      gothUser.RefreshToken, | ||||
| 		ExpiresAt:         gothUser.ExpiresAt, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // LinkAccountToUser link the gothUser to the user | ||||
| func LinkAccountToUser(user *user_model.User, gothUser goth.User) error { | ||||
| 	externalLoginUser, err := toExternalLoginUser(user, gothUser) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if err := user_model.LinkExternalToUser(user, externalLoginUser); err != nil { | ||||
| @@ -62,3 +68,13 @@ func LinkAccountToUser(user *user_model.User, gothUser goth.User) error { | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // UpdateExternalUser updates external user's information | ||||
| func UpdateExternalUser(user *user_model.User, gothUser goth.User) error { | ||||
| 	externalLoginUser, err := toExternalLoginUser(user, gothUser) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return user_model.UpdateExternalUserByExternalID(externalLoginUser) | ||||
| } | ||||
|   | ||||
| @@ -67,6 +67,12 @@ type AuthenticationForm struct { | ||||
| 	Oauth2EmailURL                string | ||||
| 	Oauth2IconURL                 string | ||||
| 	Oauth2Tenant                  string | ||||
| 	Oauth2Scopes                  string | ||||
| 	Oauth2RequiredClaimName       string | ||||
| 	Oauth2RequiredClaimValue      string | ||||
| 	Oauth2GroupClaimName          string | ||||
| 	Oauth2AdminGroup              string | ||||
| 	Oauth2RestrictedGroup         string | ||||
| 	SkipLocalTwoFA                bool | ||||
| 	SSPIAutoCreateUsers           bool | ||||
| 	SSPIAutoActivateUsers         bool | ||||
|   | ||||
| @@ -286,11 +286,6 @@ | ||||
| 							<input id="skip_local_two_fa" name="skip_local_two_fa" type="checkbox" {{if $cfg.SkipLocalTwoFA}}checked{{end}}> | ||||
| 							<p class="help">{{.i18n.Tr "admin.auths.skip_local_two_fa_helper"}}</p> | ||||
| 						</div> | ||||
| 					</div> | ||||
|  | ||||
| 					<div class="oauth2_use_custom_url inline field"> | ||||
| 						<div class="ui checkbox"> | ||||
| 							<label><strong>{{.i18n.Tr "admin.auths.oauth2_use_custom_url"}}</strong></label> | ||||
| 							<input id="oauth2_use_custom_url" name="oauth2_use_custom_url" type="checkbox" {{if $cfg.CustomURLMapping}}checked{{end}}> | ||||
| 						</div> | ||||
| 					</div> | ||||
| @@ -323,6 +318,33 @@ | ||||
| 						<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" /> | ||||
| 					{{end}}{{end}} | ||||
|  | ||||
| 					<div class="field"> | ||||
| 						<label for="oauth2_scopes">{{.i18n.Tr "admin.auths.oauth2_scopes"}}</label> | ||||
| 						<input id="oauth2_scopes" name="oauth2_scopes" value="{{if $cfg.Scopes}}{{Join $cfg.Scopes "," }}{{end}}"> | ||||
| 					</div> | ||||
| 					<div class="field"> | ||||
| 						<label for="oauth2_required_claim_name">{{.i18n.Tr "admin.auths.oauth2_required_claim_name"}}</label> | ||||
| 						<input id="oauth2_required_claim_name" name="oauth2_required_claim_name" values="{{$cfg.RequiredClaimName}}"> | ||||
| 						<p class="help">{{.i18n.Tr "admin.auths.oauth2_required_claim_name_helper"}}</p> | ||||
| 					</div> | ||||
| 					<div class="field"> | ||||
| 						<label for="oauth2_required_claim_value">{{.i18n.Tr "admin.auths.oauth2_required_claim_value"}}</label> | ||||
| 						<input id="oauth2_required_claim_value" name="oauth2_required_claim_value" values="{{$cfg.RequiredClaimValue}}"> | ||||
| 						<p class="help">{{.i18n.Tr "admin.auths.oauth2_required_claim_value_helper"}}</p> | ||||
| 					</div> | ||||
| 					<div class="field"> | ||||
| 						<label for="oauth2_group_claim_name">{{.i18n.Tr "admin.auths.oauth2_group_claim_name"}}</label> | ||||
| 						<input id="oauth2_group_claim_name" name="oauth2_group_claim_name" value="{{$cfg.GroupClaimName}}"> | ||||
| 					</div> | ||||
| 					<div class="field"> | ||||
| 						<label for="oauth2_admin_group">{{.i18n.Tr "admin.auths.oauth2_admin_group"}}</label> | ||||
| 						<input id="oauth2_admin_group" name="oauth2_admin_group" value="{{$cfg.AdminGroup}}"> | ||||
| 					</div> | ||||
| 					<div class="field"> | ||||
| 						<label for="oauth2_restricted_group">{{.i18n.Tr "admin.auths.oauth2_restricted_group"}}</label> | ||||
| 						<input id="oauth2_restricted_group" name="oauth2_restricted_group" value="{{$cfg.RestrictedGroup}}"> | ||||
| 					</div> | ||||
| 				{{end}} | ||||
|  | ||||
| 				<!-- SSPI --> | ||||
|   | ||||
| @@ -71,4 +71,31 @@ | ||||
| 		<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" /> | ||||
| 	{{end}}{{end}} | ||||
|  | ||||
| 	<div class="field"> | ||||
| 		<label for="oauth2_scopes">{{.i18n.Tr "admin.auths.oauth2_scopes"}}</label> | ||||
| 		<input id="oauth2_scopes" name="oauth2_scopes" values="{{.oauth2_scopes}}"> | ||||
| 	</div> | ||||
| 	<div class="field"> | ||||
| 		<label for="oauth2_required_claim_name">{{.i18n.Tr "admin.auths.oauth2_required_claim_name"}}</label> | ||||
| 		<input id="oauth2_required_claim_name" name="oauth2_required_claim_name" values="{{.oauth2_required_claim_name}}"> | ||||
| 		<p class="help">{{.i18n.Tr "admin.auths.oauth2_required_claim_name_helper"}}</p> | ||||
| 	</div> | ||||
| 	<div class="field"> | ||||
| 		<label for="oauth2_required_claim_value">{{.i18n.Tr "admin.auths.oauth2_required_claim_value"}}</label> | ||||
| 		<input id="oauth2_required_claim_value" name="oauth2_required_claim_value" values="{{.oauth2_required_claim_value}}"> | ||||
| 		<p class="help">{{.i18n.Tr "admin.auths.oauth2_required_claim_value_helper"}}</p> | ||||
| 	</div> | ||||
| 	<div class="field"> | ||||
| 		<label for="oauth2_group_claim_name">{{.i18n.Tr "admin.auths.oauth2_group_claim_name"}}</label> | ||||
| 		<input id="oauth2_group_claim_name" name="oauth2_group_claim_name" value="{{.oauth2_group_claim_name}}"> | ||||
| 	</div> | ||||
| 	<div class="field"> | ||||
| 		<label for="oauth2_admin_group">{{.i18n.Tr "admin.auths.oauth2_admin_group"}}</label> | ||||
| 		<input id="oauth2_admin_group" name="oauth2_admin_group" value="{{.oauth2_group_claim_name}}"> | ||||
| 	</div> | ||||
| 	<div class="field"> | ||||
| 		<label for="oauth2_restricted_group">{{.i18n.Tr "admin.auths.oauth2_restricted_group"}}</label> | ||||
| 		<input id="oauth2_restricted_group" name="oauth2_restricted_group" value="{{.oauth2_group_claim_name}}"> | ||||
| 	</div> | ||||
| </div> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user