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  }