Commit f53bf2d3 authored by Andrei Mihu's avatar Andrei Mihu
Browse files

Add new Go runtime logger interface, username and password auth. (#280)

parent bac1fc86
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -13,8 +13,12 @@ The format is based on [keep a changelog](http://keepachangelog.com) and this pr
- New runtime function to kick users from a group.
- Clients sending data to an invalid match ID will now receive an uncollated error.
- Optional log file rotation.
- Go runtime authoritative matches now also print Match IDs in log lines generated within the match.
- Allow client email authentication requests to optionally authenticate with username/password instead of email/password.
- Allow runtime email authentication calls to authenticate with username/password instead of email/password.

### Changed
- Replace standard logger supplied to the Go runtime with a more powerful interface.
- Rename stream 'descriptor' field to 'subcontext' to avoid protocol naming conflict.
- Rename Facebook authentication and link 'import' field to avoid language keyword conflict.
- Rejoining a match the user is already part of will now return the match label.
+203 −131

File changed.

Preview size limit exceeded, changes collapsed.

+31 −8
Original line number Diff line number Diff line
@@ -217,22 +217,34 @@ func (s *ApiServer) AuthenticateEmail(ctx context.Context, in *api.AuthenticateE
	}

	email := in.Account
	if email == nil || email.Email == "" || email.Password == "" {
	if email == nil {
		return nil, status.Error(codes.InvalidArgument, "Email address and password is required.")
	}

	var attemptUsernameLogin bool
	if email.Email == "" {
		// Password was supplied, but no email. Perhaps the user is attempting to login with username/password.
		attemptUsernameLogin = true
	} else if invalidCharsRegex.MatchString(email.Email) {
		return nil, status.Error(codes.InvalidArgument, "Invalid email address, no spaces or control characters allowed.")
	} else if len(email.Password) < 8 {
		return nil, status.Error(codes.InvalidArgument, "Password must be longer than 8 characters.")
	} else if !emailRegex.MatchString(email.Email) {
		return nil, status.Error(codes.InvalidArgument, "Invalid email address format.")
	} else if len(email.Email) < 10 || len(email.Email) > 255 {
		return nil, status.Error(codes.InvalidArgument, "Invalid email address, must be 10-255 bytes.")
	}

	cleanEmail := strings.ToLower(email.Email)
	if len(email.Password) < 8 {
		return nil, status.Error(codes.InvalidArgument, "Password must be longer than 8 characters.")
	}

	username := in.Username
	if username == "" {
		// If no username was supplied and the email was missing.
		if attemptUsernameLogin {
			return nil, status.Error(codes.InvalidArgument, "Username is required when email address is not supplied.")
		}

		// Email address was supplied, we are allowed to generate a username.
		username = generateUsername()
	} else if invalidCharsRegex.MatchString(username) {
		return nil, status.Error(codes.InvalidArgument, "Username invalid, no spaces or control characters allowed.")
@@ -240,14 +252,25 @@ func (s *ApiServer) AuthenticateEmail(ctx context.Context, in *api.AuthenticateE
		return nil, status.Error(codes.InvalidArgument, "Username invalid, must be 1-128 bytes.")
	}

	var dbUserID string
	var created bool
	var err error

	if attemptUsernameLogin {
		// Attempting to log in with username/password. Create flag is ignored, creation is not possible here.
		dbUserID, err = AuthenticateUsername(ctx, s.logger, s.db, username, email.Password)
	} else {
		// Attempting email authentication, may or may not create.
		cleanEmail := strings.ToLower(email.Email)
		create := in.Create == nil || in.Create.Value

	dbUserID, dbUsername, created, err := AuthenticateEmail(ctx, s.logger, s.db, cleanEmail, email.Password, username, create)
		dbUserID, username, created, err = AuthenticateEmail(ctx, s.logger, s.db, cleanEmail, email.Password, username, create)
	}
	if err != nil {
		return nil, err
	}

	token, exp := generateToken(s.config, dbUserID, dbUsername)
	token, exp := generateToken(s.config, dbUserID, username)
	session := &api.Session{Created: created, Token: token}

	// After hook.
@@ -260,7 +283,7 @@ func (s *ApiServer) AuthenticateEmail(ctx context.Context, in *api.AuthenticateE

		// Extract request information and execute the hook.
		clientIP, clientPort := extractClientAddress(s.logger, ctx)
		fn(ctx, s.logger, dbUserID, dbUsername, exp, clientIP, clientPort, session, in)
		fn(ctx, s.logger, dbUserID, username, exp, clientIP, clientPort, session, in)

		// Stats measurement end boundary.
		span.End()
+39 −0
Original line number Diff line number Diff line
@@ -234,6 +234,7 @@ func AuthenticateEmail(ctx context.Context, logger *zap.Logger, db *sql.DB, emai
			return "", "", false, status.Error(codes.Unauthenticated, "Error finding or creating user account.")
		}

		// Check if password matches.
		err = bcrypt.CompareHashAndPassword(dbPassword, []byte(password))
		if err != nil {
			return "", "", false, status.Error(codes.Unauthenticated, "Invalid credentials.")
@@ -275,6 +276,44 @@ func AuthenticateEmail(ctx context.Context, logger *zap.Logger, db *sql.DB, emai
	return userID, username, true, nil
}

func AuthenticateUsername(ctx context.Context, logger *zap.Logger, db *sql.DB, username, password string) (string, error) {
	// Look for an existing account.
	query := "SELECT id, password, disable_time FROM users WHERE username = $1"
	var dbUserID string
	var dbPassword []byte
	var dbDisableTime pq.NullTime
	err := db.QueryRowContext(ctx, query, username).Scan(&dbUserID, &dbPassword, &dbDisableTime)
	if err != nil {
		if err == sql.ErrNoRows {
			// Account not found and creation is never allowed for this type.
			return "", status.Error(codes.NotFound, "User account not found.")
		} else {
			logger.Error("Error looking up user by username.", zap.Error(err), zap.String("username", username))
			return "", status.Error(codes.Internal, "Error finding user account.")
		}
	}

	// Check if it's disabled.
	if dbDisableTime.Valid && dbDisableTime.Time.Unix() != 0 {
		logger.Info("User account is disabled.", zap.String("username", username))
		return "", status.Error(codes.Unauthenticated, "Error finding or creating user account.")
	}

	// Check if the account has a password.
	if len(dbPassword) == 0 {
		// Do not disambiguate between bad password and password login not possible at all in client-facing error messages.
		return "", status.Error(codes.Unauthenticated, "Invalid credentials.")
	}

	// Check if password matches.
	err = bcrypt.CompareHashAndPassword(dbPassword, []byte(password))
	if err != nil {
		return "", status.Error(codes.Unauthenticated, "Invalid credentials.")
	}

	return dbUserID, nil
}

func AuthenticateFacebook(ctx context.Context, logger *zap.Logger, db *sql.DB, client *social.Client, accessToken, username string, create bool) (string, string, bool, error) {
	facebookProfile, err := client.GetFacebookProfile(ctx, accessToken)
	if err != nil {
+131 −132

File changed.

Preview size limit exceeded, changes collapsed.

Loading