code.gitea.io/gitea@v1.21.7/services/auth/sspi.go (about) 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package auth 5 6 import ( 7 "context" 8 "errors" 9 "net/http" 10 "strings" 11 "sync" 12 13 "code.gitea.io/gitea/models/auth" 14 user_model "code.gitea.io/gitea/models/user" 15 "code.gitea.io/gitea/modules/base" 16 gitea_context "code.gitea.io/gitea/modules/context" 17 "code.gitea.io/gitea/modules/log" 18 "code.gitea.io/gitea/modules/setting" 19 "code.gitea.io/gitea/modules/util" 20 "code.gitea.io/gitea/modules/web/middleware" 21 "code.gitea.io/gitea/services/auth/source/sspi" 22 23 gouuid "github.com/google/uuid" 24 ) 25 26 const ( 27 tplSignIn base.TplName = "user/auth/signin" 28 ) 29 30 type SSPIAuth interface { 31 AppendAuthenticateHeader(w http.ResponseWriter, data string) 32 Authenticate(r *http.Request, w http.ResponseWriter) (userInfo *SSPIUserInfo, outToken string, err error) 33 } 34 35 var ( 36 sspiAuth SSPIAuth // a global instance of the websspi authenticator to avoid acquiring the server credential handle on every request 37 sspiAuthOnce sync.Once 38 sspiAuthErrInit error 39 40 // Ensure the struct implements the interface. 41 _ Method = &SSPI{} 42 ) 43 44 // SSPI implements the SingleSignOn interface and authenticates requests 45 // via the built-in SSPI module in Windows for SPNEGO authentication. 46 // The SSPI plugin is expected to be executed last, as it returns 401 status code if negotiation 47 // fails (or if negotiation should continue), which would prevent other authentication methods 48 // to execute at all. 49 type SSPI struct{} 50 51 // Name represents the name of auth method 52 func (s *SSPI) Name() string { 53 return "sspi" 54 } 55 56 // Verify uses SSPI (Windows implementation of SPNEGO) to authenticate the request. 57 // If authentication is successful, returns the corresponding user object. 58 // If negotiation should continue or authentication fails, immediately returns a 401 HTTP 59 // response code, as required by the SPNEGO protocol. 60 func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore, sess SessionStore) (*user_model.User, error) { 61 sspiAuthOnce.Do(func() { sspiAuthErrInit = sspiAuthInit() }) 62 if sspiAuthErrInit != nil { 63 return nil, sspiAuthErrInit 64 } 65 if !s.shouldAuthenticate(req) { 66 return nil, nil 67 } 68 69 cfg, err := s.getConfig() 70 if err != nil { 71 log.Error("could not get SSPI config: %v", err) 72 return nil, err 73 } 74 75 log.Trace("SSPI Authorization: Attempting to authenticate") 76 userInfo, outToken, err := sspiAuth.Authenticate(req, w) 77 if err != nil { 78 log.Warn("Authentication failed with error: %v\n", err) 79 sspiAuth.AppendAuthenticateHeader(w, outToken) 80 81 // Include the user login page in the 401 response to allow the user 82 // to login with another authentication method if SSPI authentication 83 // fails 84 store.GetData()["Flash"] = map[string]string{ 85 "ErrorMsg": err.Error(), 86 } 87 store.GetData()["EnableOpenIDSignIn"] = setting.Service.EnableOpenIDSignIn 88 store.GetData()["EnableSSPI"] = true 89 // in this case, the Verify function is called in Gitea's web context 90 // FIXME: it doesn't look good to render the page here, why not redirect? 91 gitea_context.GetWebContext(req).HTML(http.StatusUnauthorized, tplSignIn) 92 return nil, err 93 } 94 if outToken != "" { 95 sspiAuth.AppendAuthenticateHeader(w, outToken) 96 } 97 98 username := sanitizeUsername(userInfo.Username, cfg) 99 if len(username) == 0 { 100 return nil, nil 101 } 102 log.Info("Authenticated as %s\n", username) 103 104 user, err := user_model.GetUserByName(req.Context(), username) 105 if err != nil { 106 if !user_model.IsErrUserNotExist(err) { 107 log.Error("GetUserByName: %v", err) 108 return nil, err 109 } 110 if !cfg.AutoCreateUsers { 111 log.Error("User '%s' not found", username) 112 return nil, nil 113 } 114 user, err = s.newUser(req.Context(), username, cfg) 115 if err != nil { 116 log.Error("CreateUser: %v", err) 117 return nil, err 118 } 119 } 120 121 // Make sure requests to API paths and PWA resources do not create a new session 122 if !middleware.IsAPIPath(req) && !isAttachmentDownload(req) { 123 handleSignIn(w, req, sess, user) 124 } 125 126 log.Trace("SSPI Authorization: Logged in user %-v", user) 127 return user, nil 128 } 129 130 // getConfig retrieves the SSPI configuration from login sources 131 func (s *SSPI) getConfig() (*sspi.Source, error) { 132 sources, err := auth.ActiveSources(auth.SSPI) 133 if err != nil { 134 return nil, err 135 } 136 if len(sources) == 0 { 137 return nil, errors.New("no active login sources of type SSPI found") 138 } 139 if len(sources) > 1 { 140 return nil, errors.New("more than one active login source of type SSPI found") 141 } 142 return sources[0].Cfg.(*sspi.Source), nil 143 } 144 145 func (s *SSPI) shouldAuthenticate(req *http.Request) (shouldAuth bool) { 146 shouldAuth = false 147 path := strings.TrimSuffix(req.URL.Path, "/") 148 if path == "/user/login" { 149 if req.FormValue("user_name") != "" && req.FormValue("password") != "" { 150 shouldAuth = false 151 } else if req.FormValue("auth_with_sspi") == "1" { 152 shouldAuth = true 153 } 154 } else if middleware.IsAPIPath(req) || isAttachmentDownload(req) { 155 shouldAuth = true 156 } 157 return shouldAuth 158 } 159 160 // newUser creates a new user object for the purpose of automatic registration 161 // and populates its name and email with the information present in request headers. 162 func (s *SSPI) newUser(ctx context.Context, username string, cfg *sspi.Source) (*user_model.User, error) { 163 email := gouuid.New().String() + "@localhost.localdomain" 164 user := &user_model.User{ 165 Name: username, 166 Email: email, 167 Language: cfg.DefaultLanguage, 168 } 169 emailNotificationPreference := user_model.EmailNotificationsDisabled 170 overwriteDefault := &user_model.CreateUserOverwriteOptions{ 171 IsActive: util.OptionalBoolOf(cfg.AutoActivateUsers), 172 KeepEmailPrivate: util.OptionalBoolTrue, 173 EmailNotificationsPreference: &emailNotificationPreference, 174 } 175 if err := user_model.CreateUser(ctx, user, overwriteDefault); err != nil { 176 return nil, err 177 } 178 179 return user, nil 180 } 181 182 // stripDomainNames removes NETBIOS domain name and separator from down-level logon names 183 // (eg. "DOMAIN\user" becomes "user"), and removes the UPN suffix (domain name) and separator 184 // from UPNs (eg. "user@domain.local" becomes "user") 185 func stripDomainNames(username string) string { 186 if strings.Contains(username, "\\") { 187 parts := strings.SplitN(username, "\\", 2) 188 if len(parts) > 1 { 189 username = parts[1] 190 } 191 } else if strings.Contains(username, "@") { 192 parts := strings.Split(username, "@") 193 if len(parts) > 1 { 194 username = parts[0] 195 } 196 } 197 return username 198 } 199 200 func replaceSeparators(username string, cfg *sspi.Source) string { 201 newSep := cfg.SeparatorReplacement 202 username = strings.ReplaceAll(username, "\\", newSep) 203 username = strings.ReplaceAll(username, "/", newSep) 204 username = strings.ReplaceAll(username, "@", newSep) 205 return username 206 } 207 208 func sanitizeUsername(username string, cfg *sspi.Source) string { 209 if len(username) == 0 { 210 return "" 211 } 212 if cfg.StripDomainNames { 213 username = stripDomainNames(username) 214 } 215 // Replace separators even if we have already stripped the domain name part, 216 // as the username can contain several separators: eg. "MICROSOFT\useremail@live.com" 217 username = replaceSeparators(username, cfg) 218 return username 219 }