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:
zeripath 2021-12-14 08:37:11 +00:00 committed by GitHub
parent b4782e24d2
commit 0981ec30c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 344 additions and 88 deletions

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -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)
}
}