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