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 }