diff options
author | zeripath <art27@cantab.net> | 2021-12-14 08:37:11 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-12-14 16:37:11 +0800 |
commit | 0981ec30c3d5218939d44fc2f40725b0b4a03684 (patch) | |
tree | 5479fb309f9800310cf2268d493e1cd33abfeac6 /routers | |
parent | b4782e24d2821bbb5647eff2eaf5c338e92324db (diff) | |
download | gitea-0981ec30c3d5218939d44fc2f40725b0b4a03684.tar.gz gitea-0981ec30c3d5218939d44fc2f40725b0b4a03684.zip |
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>
Diffstat (limited to 'routers')
-rw-r--r-- | routers/web/admin/auths.go | 6 | ||||
-rw-r--r-- | routers/web/user/auth.go | 123 | ||||
-rw-r--r-- | routers/web/user/auth_openid.go | 14 |
3 files changed, 113 insertions, 30 deletions
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index 5fd15b5c5a..b288273871 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -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) } diff --git a/routers/web/user/auth.go b/routers/web/user/auth.go index 42cd977b54..55a4b11007 100644 --- a/routers/web/user/auth.go +++ b/routers/web/user/auth.go @@ -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) } } diff --git a/routers/web/user/auth_openid.go b/routers/web/user/auth_openid.go index 68e166d12c..884ec29c21 100644 --- a/routers/web/user/auth_openid.go +++ b/routers/web/user/auth_openid.go @@ -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) } } |