diff options
author | Lunny Xiao <xiaolunwen@gmail.com> | 2021-06-10 01:53:16 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-06-09 19:53:16 +0200 |
commit | fb3ffeb18df6bb94bb3f69348a93398b05259174 (patch) | |
tree | aa56433e062bc68d2a118581a715ee324f025594 /services/auth/sspi_windows.go | |
parent | da057996d584c633524406d69b424cbc3d4473eb (diff) | |
download | gitea-fb3ffeb18df6bb94bb3f69348a93398b05259174.tar.gz gitea-fb3ffeb18df6bb94bb3f69348a93398b05259174.zip |
Add sso.Group, context.Auth, context.APIAuth to allow auth special routes (#16086)
* Add sso.Group, context.Auth, context.APIAuth to allow auth special routes
* Remove unnecessary check
* Rename sso -> auth
* remove unused method of Auth interface
Diffstat (limited to 'services/auth/sspi_windows.go')
-rw-r--r-- | services/auth/sspi_windows.go | 246 |
1 files changed, 246 insertions, 0 deletions
diff --git a/services/auth/sspi_windows.go b/services/auth/sspi_windows.go new file mode 100644 index 0000000000..d1289a7617 --- /dev/null +++ b/services/auth/sspi_windows.go @@ -0,0 +1,246 @@ +// Copyright 2019 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 auth + +import ( + "errors" + "net/http" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/web/middleware" + + gouuid "github.com/google/uuid" + "github.com/quasoft/websspi" + "github.com/unrolled/render" +) + +const ( + tplSignIn base.TplName = "user/auth/signin" +) + +var ( + // sspiAuth is a global instance of the websspi authentication package, + // which is used to avoid acquiring the server credential handle on + // every request + sspiAuth *websspi.Authenticator + + // Ensure the struct implements the interface. + _ Auth = &SSPI{} +) + +// SSPI implements the SingleSignOn interface and authenticates requests +// via the built-in SSPI module in Windows for SPNEGO authentication. +// On successful authentication returns a valid user object. +// Returns nil if authentication fails. +type SSPI struct { + rnd *render.Render +} + +// Init creates a new global websspi.Authenticator object +func (s *SSPI) Init() error { + config := websspi.NewConfig() + var err error + sspiAuth, err = websspi.New(config) + if err != nil { + return err + } + s.rnd = render.New(render.Options{ + Extensions: []string{".tmpl"}, + Directory: "templates", + Funcs: templates.NewFuncMap(), + Asset: templates.GetAsset, + AssetNames: templates.GetAssetNames, + IsDevelopment: !setting.IsProd(), + }) + return nil +} + +// Name represents the name of auth method +func (s *SSPI) Name() string { + return "sspi" +} + +// Free releases resources used by the global websspi.Authenticator object +func (s *SSPI) Free() error { + return sspiAuth.Free() +} + +// Verify uses SSPI (Windows implementation of SPNEGO) to authenticate the request. +// If authentication is successful, returs the corresponding user object. +// If negotiation should continue or authentication fails, immediately returns a 401 HTTP +// response code, as required by the SPNEGO protocol. +func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) *models.User { + if !s.shouldAuthenticate(req) { + return nil + } + + cfg, err := s.getConfig() + if err != nil { + log.Error("could not get SSPI config: %v", err) + return nil + } + + log.Trace("SSPI Authorization: Attempting to authenticate") + userInfo, outToken, err := sspiAuth.Authenticate(req, w) + if err != nil { + log.Warn("Authentication failed with error: %v\n", err) + sspiAuth.AppendAuthenticateHeader(w, outToken) + + // Include the user login page in the 401 response to allow the user + // to login with another authentication method if SSPI authentication + // fails + store.GetData()["Flash"] = map[string]string{ + "ErrorMsg": err.Error(), + } + store.GetData()["EnableOpenIDSignIn"] = setting.Service.EnableOpenIDSignIn + store.GetData()["EnableSSPI"] = true + + err := s.rnd.HTML(w, 401, string(tplSignIn), templates.BaseVars().Merge(store.GetData())) + if err != nil { + log.Error("%v", err) + } + + return nil + } + if outToken != "" { + sspiAuth.AppendAuthenticateHeader(w, outToken) + } + + username := sanitizeUsername(userInfo.Username, cfg) + if len(username) == 0 { + return nil + } + log.Info("Authenticated as %s\n", username) + + user, err := models.GetUserByName(username) + if err != nil { + if !models.IsErrUserNotExist(err) { + log.Error("GetUserByName: %v", err) + return nil + } + if !cfg.AutoCreateUsers { + log.Error("User '%s' not found", username) + return nil + } + user, err = s.newUser(username, cfg) + if err != nil { + log.Error("CreateUser: %v", err) + return nil + } + } + + // Make sure requests to API paths and PWA resources do not create a new session + if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) { + handleSignIn(w, req, sess, user) + } + + log.Trace("SSPI Authorization: Logged in user %-v", user) + return user +} + +// getConfig retrieves the SSPI configuration from login sources +func (s *SSPI) getConfig() (*models.SSPIConfig, error) { + sources, err := models.ActiveLoginSources(models.LoginSSPI) + if err != nil { + return nil, err + } + if len(sources) == 0 { + return nil, errors.New("no active login sources of type SSPI found") + } + if len(sources) > 1 { + return nil, errors.New("more than one active login source of type SSPI found") + } + return sources[0].SSPI(), nil +} + +func (s *SSPI) shouldAuthenticate(req *http.Request) (shouldAuth bool) { + shouldAuth = false + path := strings.TrimSuffix(req.URL.Path, "/") + if path == "/user/login" { + if req.FormValue("user_name") != "" && req.FormValue("password") != "" { + shouldAuth = false + } else if req.FormValue("auth_with_sspi") == "1" { + shouldAuth = true + } + } else if middleware.IsAPIPath(req) || isAttachmentDownload(req) { + shouldAuth = true + } + return +} + +// newUser creates a new user object for the purpose of automatic registration +// and populates its name and email with the information present in request headers. +func (s *SSPI) newUser(username string, cfg *models.SSPIConfig) (*models.User, error) { + email := gouuid.New().String() + "@localhost.localdomain" + user := &models.User{ + Name: username, + Email: email, + KeepEmailPrivate: true, + Passwd: gouuid.New().String(), + IsActive: cfg.AutoActivateUsers, + Language: cfg.DefaultLanguage, + UseCustomAvatar: true, + Avatar: models.DefaultAvatarLink(), + EmailNotificationsPreference: models.EmailNotificationsDisabled, + } + if err := models.CreateUser(user); err != nil { + return nil, err + } + return user, nil +} + +// stripDomainNames removes NETBIOS domain name and separator from down-level logon names +// (eg. "DOMAIN\user" becomes "user"), and removes the UPN suffix (domain name) and separator +// from UPNs (eg. "user@domain.local" becomes "user") +func stripDomainNames(username string) string { + if strings.Contains(username, "\\") { + parts := strings.SplitN(username, "\\", 2) + if len(parts) > 1 { + username = parts[1] + } + } else if strings.Contains(username, "@") { + parts := strings.Split(username, "@") + if len(parts) > 1 { + username = parts[0] + } + } + return username +} + +func replaceSeparators(username string, cfg *models.SSPIConfig) string { + newSep := cfg.SeparatorReplacement + username = strings.ReplaceAll(username, "\\", newSep) + username = strings.ReplaceAll(username, "/", newSep) + username = strings.ReplaceAll(username, "@", newSep) + return username +} + +func sanitizeUsername(username string, cfg *models.SSPIConfig) string { + if len(username) == 0 { + return "" + } + if cfg.StripDomainNames { + username = stripDomainNames(username) + } + // Replace separators even if we have already stripped the domain name part, + // as the username can contain several separators: eg. "MICROSOFT\useremail@live.com" + username = replaceSeparators(username, cfg) + return username +} + +// specialInit registers the SSPI auth method as the last method in the list. +// The SSPI plugin is expected to be executed last, as it returns 401 status code if negotiation +// fails (or if negotiation should continue), which would prevent other authentication methods +// to execute at all. +func specialInit() { + if models.IsSSPIEnabled() { + Register(&SSPI{}) + } +} |