github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/server/authentication.go (about)

     1  // Copyright 2017 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package server
    12  
    13  import (
    14  	"bytes"
    15  	"context"
    16  	"crypto/rand"
    17  	"crypto/sha256"
    18  	"encoding/base64"
    19  	"fmt"
    20  	"net/http"
    21  	"strconv"
    22  	"time"
    23  
    24  	"github.com/cockroachdb/cockroach/pkg/security"
    25  	"github.com/cockroachdb/cockroach/pkg/server/serverpb"
    26  	"github.com/cockroachdb/cockroach/pkg/settings"
    27  	"github.com/cockroachdb/cockroach/pkg/sql"
    28  	"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
    29  	"github.com/cockroachdb/cockroach/pkg/sql/sqlbase"
    30  	"github.com/cockroachdb/cockroach/pkg/sql/types"
    31  	"github.com/cockroachdb/cockroach/pkg/util/log"
    32  	"github.com/cockroachdb/cockroach/pkg/util/protoutil"
    33  	"github.com/cockroachdb/cockroach/pkg/util/timeutil"
    34  	"github.com/cockroachdb/errors"
    35  	gwruntime "github.com/grpc-ecosystem/grpc-gateway/runtime"
    36  	"google.golang.org/grpc"
    37  	"google.golang.org/grpc/codes"
    38  	"google.golang.org/grpc/metadata"
    39  	"google.golang.org/grpc/status"
    40  )
    41  
    42  const (
    43  	// authPrefix is the prefix for RESTful endpoints used to provide
    44  	// authentication methods.
    45  	loginPath  = "/login"
    46  	logoutPath = "/logout"
    47  	// secretLength is the number of random bytes generated for session secrets.
    48  	secretLength = 16
    49  	// SessionCookieName is the name of the cookie used for HTTP auth.
    50  	SessionCookieName = "session"
    51  )
    52  
    53  var webSessionTimeout = settings.RegisterPublicNonNegativeDurationSetting(
    54  	"server.web_session_timeout",
    55  	"the duration that a newly created web session will be valid",
    56  	7*24*time.Hour,
    57  )
    58  
    59  type authenticationServer struct {
    60  	server *Server
    61  }
    62  
    63  // newAuthenticationServer allocates and returns a new REST server for
    64  // authentication APIs.
    65  func newAuthenticationServer(s *Server) *authenticationServer {
    66  	return &authenticationServer{
    67  		server: s,
    68  	}
    69  }
    70  
    71  // RegisterService registers the GRPC service.
    72  func (s *authenticationServer) RegisterService(g *grpc.Server) {
    73  	serverpb.RegisterLogInServer(g, s)
    74  	serverpb.RegisterLogOutServer(g, s)
    75  }
    76  
    77  // RegisterGateway starts the gateway (i.e. reverse proxy) that proxies HTTP requests
    78  // to the appropriate gRPC endpoints.
    79  func (s *authenticationServer) RegisterGateway(
    80  	ctx context.Context, mux *gwruntime.ServeMux, conn *grpc.ClientConn,
    81  ) error {
    82  	if err := serverpb.RegisterLogInHandler(ctx, mux, conn); err != nil {
    83  		return err
    84  	}
    85  	return serverpb.RegisterLogOutHandler(ctx, mux, conn)
    86  }
    87  
    88  // UserLogin verifies an incoming request by a user to create an web
    89  // authentication session. It checks the provided credentials against the
    90  // system.users table, and if successful creates a new authentication session.
    91  // The session's ID and secret are returned to the caller as an HTTP cookie,
    92  // added via a "Set-Cookie" header.
    93  func (s *authenticationServer) UserLogin(
    94  	ctx context.Context, req *serverpb.UserLoginRequest,
    95  ) (*serverpb.UserLoginResponse, error) {
    96  	username := req.Username
    97  	if username == "" {
    98  		return nil, status.Errorf(
    99  			codes.Unauthenticated,
   100  			"no username was provided",
   101  		)
   102  	}
   103  
   104  	// Verify the provided username/password pair.
   105  	verified, expired, err := s.verifyPassword(ctx, username, req.Password)
   106  	if err != nil {
   107  		return nil, apiInternalError(ctx, err)
   108  	}
   109  	if expired {
   110  		return nil, status.Errorf(
   111  			codes.Unauthenticated,
   112  			"the password for %s has expired",
   113  			username,
   114  		)
   115  	}
   116  	if !verified {
   117  		return nil, status.Errorf(
   118  			codes.Unauthenticated,
   119  			"the provided username and password did not match any credentials on the server",
   120  		)
   121  	}
   122  
   123  	// Create a new database session, generating an ID and secret key.
   124  	id, secret, err := s.newAuthSession(ctx, username)
   125  	if err != nil {
   126  		return nil, apiInternalError(ctx, err)
   127  	}
   128  
   129  	// Generate and set a session cookie for the response. Because HTTP cookies
   130  	// must be strings, the cookie value (a marshaled protobuf) is encoded in
   131  	// base64.
   132  	cookieValue := &serverpb.SessionCookie{
   133  		ID:     id,
   134  		Secret: secret,
   135  	}
   136  	cookie, err := EncodeSessionCookie(cookieValue, !s.server.cfg.DisableTLSForHTTP)
   137  	if err != nil {
   138  		return nil, apiInternalError(ctx, err)
   139  	}
   140  
   141  	// Set the cookie header on the outgoing response.
   142  	if err := grpc.SetHeader(ctx, metadata.Pairs("set-cookie", cookie.String())); err != nil {
   143  		return nil, apiInternalError(ctx, err)
   144  	}
   145  
   146  	return &serverpb.UserLoginResponse{}, nil
   147  }
   148  
   149  // UserLogout allows a user to terminate their currently active session.
   150  func (s *authenticationServer) UserLogout(
   151  	ctx context.Context, req *serverpb.UserLogoutRequest,
   152  ) (*serverpb.UserLogoutResponse, error) {
   153  	md, ok := metadata.FromIncomingContext(ctx)
   154  	if !ok {
   155  		return nil, apiInternalError(ctx, fmt.Errorf("couldn't get incoming context"))
   156  	}
   157  	sessionIDs := md.Get(webSessionIDKeyStr)
   158  	if len(sessionIDs) != 1 {
   159  		return nil, apiInternalError(ctx, fmt.Errorf("couldn't get incoming context"))
   160  	}
   161  
   162  	sessionID, err := strconv.Atoi(sessionIDs[0])
   163  	if err != nil {
   164  		return nil, fmt.Errorf("invalid session id: %d", sessionID)
   165  	}
   166  
   167  	// Revoke the session.
   168  	if n, err := s.server.sqlServer.internalExecutor.ExecEx(
   169  		ctx,
   170  		"revoke-auth-session",
   171  		nil, /* txn */
   172  		sqlbase.InternalExecutorSessionDataOverride{User: security.RootUser},
   173  		`UPDATE system.web_sessions SET "revokedAt" = now() WHERE id = $1`,
   174  		sessionID,
   175  	); err != nil {
   176  		return nil, apiInternalError(ctx, err)
   177  	} else if n == 0 {
   178  		err := errors.Newf("session with id %d nonexistent", sessionID)
   179  		log.Infof(ctx, "%v", err)
   180  		return nil, err
   181  	}
   182  
   183  	// Send back a header which will cause the browser to destroy the cookie.
   184  	// See https://tools.ietf.org/search/rfc6265, page 7.
   185  	cookie := makeCookieWithValue("", false /* forHTTPSOnly */)
   186  	cookie.MaxAge = -1
   187  
   188  	// Set the cookie header on the outgoing response.
   189  	if err := grpc.SetHeader(ctx, metadata.Pairs("set-cookie", cookie.String())); err != nil {
   190  		return nil, apiInternalError(ctx, err)
   191  	}
   192  
   193  	return &serverpb.UserLogoutResponse{}, nil
   194  }
   195  
   196  // verifySession verifies the existence and validity of the session claimed by
   197  // the supplied SessionCookie. Returns three parameters: a boolean indicating if
   198  // the session was valid, the username associated with the session (if
   199  // validated), and an error for any internal errors which prevented validation.
   200  func (s *authenticationServer) verifySession(
   201  	ctx context.Context, cookie *serverpb.SessionCookie,
   202  ) (bool, string, error) {
   203  	// Look up session in database and verify hashed secret value.
   204  	const sessionQuery = `
   205  SELECT "hashedSecret", "username", "expiresAt", "revokedAt"
   206  FROM system.web_sessions
   207  WHERE id = $1`
   208  
   209  	var (
   210  		hashedSecret []byte
   211  		username     string
   212  		expiresAt    time.Time
   213  		isRevoked    bool
   214  	)
   215  
   216  	row, err := s.server.sqlServer.internalExecutor.QueryRowEx(
   217  		ctx,
   218  		"lookup-auth-session",
   219  		nil, /* txn */
   220  		sqlbase.InternalExecutorSessionDataOverride{User: security.RootUser},
   221  		sessionQuery, cookie.ID)
   222  	if row == nil || err != nil {
   223  		return false, "", err
   224  	}
   225  
   226  	if row.Len() != 4 ||
   227  		row[0].ResolvedType().Family() != types.BytesFamily ||
   228  		row[1].ResolvedType().Family() != types.StringFamily ||
   229  		row[2].ResolvedType().Family() != types.TimestampFamily {
   230  		return false, "", errors.Errorf("values returned from auth session lookup do not match expectation")
   231  	}
   232  
   233  	// Extract datum values.
   234  	hashedSecret = []byte(*row[0].(*tree.DBytes))
   235  	username = string(*row[1].(*tree.DString))
   236  	expiresAt = row[2].(*tree.DTimestamp).Time
   237  	isRevoked = row[3].ResolvedType().Family() != types.UnknownFamily
   238  
   239  	if isRevoked {
   240  		return false, "", nil
   241  	}
   242  
   243  	if now := s.server.clock.PhysicalTime(); !now.Before(expiresAt) {
   244  		return false, "", nil
   245  	}
   246  
   247  	hasher := sha256.New()
   248  	_, _ = hasher.Write(cookie.Secret)
   249  	hashedCookieSecret := hasher.Sum(nil)
   250  	if !bytes.Equal(hashedSecret, hashedCookieSecret) {
   251  		return false, "", nil
   252  	}
   253  
   254  	return true, username, nil
   255  }
   256  
   257  // verifyPassword verifies the passed username/password pair against the
   258  // system.users table. The returned boolean indicates whether or not the
   259  // verification succeeded; an error is returned if the validation process could
   260  // not be completed.
   261  func (s *authenticationServer) verifyPassword(
   262  	ctx context.Context, username string, password string,
   263  ) (valid bool, expired bool, err error) {
   264  	exists, canLogin, pwRetrieveFn, validUntilFn, err := sql.GetUserHashedPassword(
   265  		ctx, s.server.sqlServer.execCfg.InternalExecutor, username,
   266  	)
   267  	if err != nil {
   268  		return false, false, err
   269  	}
   270  	if !exists || !canLogin {
   271  		return false, false, nil
   272  	}
   273  	hashedPassword, err := pwRetrieveFn(ctx)
   274  	if err != nil {
   275  		return false, false, err
   276  	}
   277  
   278  	validUntil, err := validUntilFn(ctx)
   279  	if err != nil {
   280  		return false, false, err
   281  	}
   282  	if validUntil != nil {
   283  		if validUntil.Time.Sub(timeutil.Now()) < 0 {
   284  			return false, true, nil
   285  		}
   286  	}
   287  
   288  	return security.CompareHashAndPassword(hashedPassword, password) == nil, false, nil
   289  }
   290  
   291  // CreateAuthSecret creates a secret, hash pair to populate a session auth token.
   292  func CreateAuthSecret() (secret, hashedSecret []byte, err error) {
   293  	secret = make([]byte, secretLength)
   294  	if _, err := rand.Read(secret); err != nil {
   295  		return nil, nil, err
   296  	}
   297  
   298  	hasher := sha256.New()
   299  	_, _ = hasher.Write(secret)
   300  	hashedSecret = hasher.Sum(nil)
   301  	return secret, hashedSecret, nil
   302  }
   303  
   304  // newAuthSession attempts to create a new authentication session for the given
   305  // user. If successful, returns the ID and secret value for the new session.
   306  func (s *authenticationServer) newAuthSession(
   307  	ctx context.Context, username string,
   308  ) (int64, []byte, error) {
   309  	secret, hashedSecret, err := CreateAuthSecret()
   310  	if err != nil {
   311  		return 0, nil, err
   312  	}
   313  
   314  	expiration := s.server.clock.PhysicalTime().Add(webSessionTimeout.Get(&s.server.st.SV))
   315  
   316  	insertSessionStmt := `
   317  INSERT INTO system.web_sessions ("hashedSecret", username, "expiresAt")
   318  VALUES($1, $2, $3)
   319  RETURNING id
   320  `
   321  	var id int64
   322  
   323  	row, err := s.server.sqlServer.internalExecutor.QueryRowEx(
   324  		ctx,
   325  		"create-auth-session",
   326  		nil, /* txn */
   327  		sqlbase.InternalExecutorSessionDataOverride{User: security.RootUser},
   328  		insertSessionStmt,
   329  		hashedSecret,
   330  		username,
   331  		expiration,
   332  	)
   333  	if err != nil {
   334  		return 0, nil, err
   335  	}
   336  	if row.Len() != 1 || row[0].ResolvedType().Family() != types.IntFamily {
   337  		return 0, nil, errors.Errorf(
   338  			"expected create auth session statement to return exactly one integer, returned %v",
   339  			row,
   340  		)
   341  	}
   342  
   343  	// Extract integer value from single datum.
   344  	id = int64(*row[0].(*tree.DInt))
   345  
   346  	return id, secret, nil
   347  }
   348  
   349  // authenticationMux implements http.Handler, and is used to provide session
   350  // authentication for an arbitrary "inner" handler.
   351  type authenticationMux struct {
   352  	server *authenticationServer
   353  	inner  http.Handler
   354  
   355  	// allowAnonymous, if true, indicates that the authentication mux should
   356  	// call its inner HTTP handler even if the request doesn't have a valid
   357  	// session. If there is a valid session, the mux calls its inner handler
   358  	// with a context containing the username and session ID.
   359  	//
   360  	// If allowAnonymous is false, the mux returns an error if there is no
   361  	// valid session.
   362  	allowAnonymous bool
   363  }
   364  
   365  func newAuthenticationMuxAllowAnonymous(
   366  	s *authenticationServer, inner http.Handler,
   367  ) *authenticationMux {
   368  	return &authenticationMux{
   369  		server:         s,
   370  		inner:          inner,
   371  		allowAnonymous: true,
   372  	}
   373  }
   374  
   375  func newAuthenticationMux(s *authenticationServer, inner http.Handler) *authenticationMux {
   376  	return &authenticationMux{
   377  		server:         s,
   378  		inner:          inner,
   379  		allowAnonymous: false,
   380  	}
   381  }
   382  
   383  type webSessionUserKey struct{}
   384  type webSessionIDKey struct{}
   385  
   386  const webSessionUserKeyStr = "websessionuser"
   387  const webSessionIDKeyStr = "websessionid"
   388  
   389  func (am *authenticationMux) ServeHTTP(w http.ResponseWriter, req *http.Request) {
   390  	username, cookie, err := am.getSession(w, req)
   391  	if err == nil {
   392  		ctx := req.Context()
   393  		ctx = context.WithValue(ctx, webSessionUserKey{}, username)
   394  		ctx = context.WithValue(ctx, webSessionIDKey{}, cookie.ID)
   395  		req = req.WithContext(ctx)
   396  	} else if !am.allowAnonymous {
   397  		log.Infof(req.Context(), "Web session error: %s", err)
   398  		http.Error(w, "a valid authentication cookie is required", http.StatusUnauthorized)
   399  		return
   400  	}
   401  	am.inner.ServeHTTP(w, req)
   402  }
   403  
   404  // EncodeSessionCookie encodes a SessionCookie proto into an http.Cookie.
   405  // The flag forHTTPSOnly, if set, produces the "Secure" flag on the
   406  // resulting HTTP cookie, which means the cookie should only be
   407  // transmitted over HTTPS channels. Note that a cookie without
   408  // the "Secure" flag can be transmitted over either HTTP or HTTPS channels.
   409  func EncodeSessionCookie(
   410  	sessionCookie *serverpb.SessionCookie, forHTTPSOnly bool,
   411  ) (*http.Cookie, error) {
   412  	cookieValueBytes, err := protoutil.Marshal(sessionCookie)
   413  	if err != nil {
   414  		return nil, errors.Wrap(err, "session cookie could not be encoded")
   415  	}
   416  	value := base64.StdEncoding.EncodeToString(cookieValueBytes)
   417  	return makeCookieWithValue(value, forHTTPSOnly), nil
   418  }
   419  
   420  func makeCookieWithValue(value string, forHTTPSOnly bool) *http.Cookie {
   421  	return &http.Cookie{
   422  		Name:     SessionCookieName,
   423  		Value:    value,
   424  		Path:     "/",
   425  		HttpOnly: true,
   426  		Secure:   forHTTPSOnly,
   427  	}
   428  }
   429  
   430  // getSession decodes the cookie from the request, looks up the corresponding session, and
   431  // returns the logged in user name. If there's an error, it returns an error value and the
   432  // HTTP error code.
   433  func (am *authenticationMux) getSession(
   434  	w http.ResponseWriter, req *http.Request,
   435  ) (string, *serverpb.SessionCookie, error) {
   436  	// Validate the returned cookie.
   437  	rawCookie, err := req.Cookie(SessionCookieName)
   438  	if err != nil {
   439  		return "", nil, err
   440  	}
   441  
   442  	cookie, err := decodeSessionCookie(rawCookie)
   443  	if err != nil {
   444  		err = errors.Wrap(err, "a valid authentication cookie is required")
   445  		return "", nil, err
   446  	}
   447  
   448  	valid, username, err := am.server.verifySession(req.Context(), cookie)
   449  	if err != nil {
   450  		err := apiInternalError(req.Context(), err)
   451  		return "", nil, err
   452  	}
   453  	if !valid {
   454  		err := errors.New("the provided authentication session could not be validated")
   455  		return "", nil, err
   456  	}
   457  
   458  	return username, cookie, nil
   459  }
   460  
   461  func decodeSessionCookie(encodedCookie *http.Cookie) (*serverpb.SessionCookie, error) {
   462  	// Cookie value should be a base64 encoded protobuf.
   463  	cookieBytes, err := base64.StdEncoding.DecodeString(encodedCookie.Value)
   464  	if err != nil {
   465  		return nil, errors.Wrap(err, "session cookie could not be decoded")
   466  	}
   467  	var sessionCookieValue serverpb.SessionCookie
   468  	if err := protoutil.Unmarshal(cookieBytes, &sessionCookieValue); err != nil {
   469  		return nil, errors.Wrap(err, "session cookie could not be unmarshaled")
   470  	}
   471  	return &sessionCookieValue, nil
   472  }
   473  
   474  // authenticationHeaderMatcher is a GRPC header matcher function, which provides
   475  // a conversion from GRPC headers to HTTP headers. This function is needed to
   476  // attach the "set-cookie" header to the response; by default, Grpc-Gateway
   477  // adds a prefix to all GRPC headers before adding them to the response.
   478  func authenticationHeaderMatcher(key string) (string, bool) {
   479  	// GRPC converts all headers to lower case.
   480  	if key == "set-cookie" {
   481  		return key, true
   482  	}
   483  	// This is the default behavior of GRPC Gateway when matching headers -
   484  	// it adds a constant prefix to the HTTP header so that by default they
   485  	// do not conflict with any HTTP headers that might be used by the
   486  	// browser.
   487  	// TODO(mrtracy): A function "DefaultOutgoingHeaderMatcher" should
   488  	// likely be added to GRPC Gateway so that the logic does not have to be
   489  	// duplicated here.
   490  	return fmt.Sprintf("%s%s", gwruntime.MetadataHeaderPrefix, key), true
   491  }
   492  
   493  func forwardAuthenticationMetadata(ctx context.Context, _ *http.Request) metadata.MD {
   494  	md := metadata.MD{}
   495  	if user := ctx.Value(webSessionUserKey{}); user != nil {
   496  		md.Set(webSessionUserKeyStr, user.(string))
   497  	}
   498  	if sessionID := ctx.Value(webSessionIDKey{}); sessionID != nil {
   499  		md.Set(webSessionIDKeyStr, fmt.Sprintf("%v", sessionID))
   500  	}
   501  	return md
   502  }