def Authenticate(self, request, context): """ Authenticates a classic password based login request. request.user can be any of id/username/email """ logger.debug(f"Logging in with {request.user=}, password=*******") with session_scope() as session: user = get_user_by_field(session, request.user) if user: logger.debug(f"Found user") if not user.hashed_password: logger.debug(f"User doesn't have a password!") context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.NO_PASSWORD) if verify_password(user.hashed_password, request.password): logger.debug(f"Right password") # correct password token, expiry = self._create_session(context, session, user, request.remember_device) context.send_initial_metadata( [ ("set-cookie", create_session_cookie(token, expiry)), ] ) return auth_pb2.AuthRes(jailed=user.is_jailed) else: logger.debug(f"Wrong password") # wrong password context.abort(grpc.StatusCode.NOT_FOUND, errors.INVALID_USERNAME_OR_PASSWORD) else: # user not found logger.debug(f"Didn't find user") # do about as much work as if the user was found, reduces timing based username enumeration attacks hash_password(request.password) context.abort(grpc.StatusCode.NOT_FOUND, errors.INVALID_USERNAME_OR_PASSWORD)
def CompleteTokenLogin(self, request, context): """ Second step of email-based login. Validates the given LoginToken (sent in email), creates a new session and returns bearer token. Or fails with grpc.NOT_FOUND if LoginToken is invalid. """ with session_scope() as session: res = ( session.query(LoginToken, User) .join(User, User.id == LoginToken.user_id) .filter(LoginToken.token == request.login_token) .filter(LoginToken.is_valid) .one_or_none() ) if res: login_token, user = res # delete the login token so it can't be reused session.delete(login_token) session.commit() # create a session token, expiry = self._create_session(context, session, user, False) context.send_initial_metadata( [ ("set-cookie", create_session_cookie(token, expiry)), ] ) return auth_pb2.AuthRes(jailed=user.is_jailed) else: context.abort(grpc.StatusCode.NOT_FOUND, errors.INVALID_TOKEN)
def Deauthenticate(self, request, context): """ Removes an active cookie session. """ token = parse_session_cookie(dict(context.invocation_metadata())) logger.info(f"Deauthenticate(token={token})") # if we had a token, try to remove the session if token: delete_session(token) # set the cookie to an empty string and expire immediately, should remove it from the browser context.send_initial_metadata([ ("set-cookie", create_session_cookie("", now())), ]) return empty_pb2.Empty()
def CompleteSignup(self, request, context): """ Completes user sign up by creating the user in question, then logs them in. TODO: nice error handling for dupe username/email? """ with session_scope() as session: signup_token = ( session.query(SignupToken) .filter(SignupToken.token == request.signup_token) .filter(SignupToken.is_valid) .one_or_none() ) if not signup_token: context.abort(grpc.StatusCode.NOT_FOUND, errors.INVALID_TOKEN) # check birthdate validity (YYYY-MM-DD format and in the past) try: birthdate = datetime.fromisoformat(request.birthdate) except ValueError: context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_BIRTHDATE) if pytz.UTC.localize(birthdate) >= now(): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_BIRTHDATE) # check email again if not is_valid_email(signup_token.email): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_EMAIL) # check username validity if not is_valid_username(request.username): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_USERNAME) # check name validity if not is_valid_name(request.name): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_NAME) if not request.hosting_status: context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.HOSTING_STATUS_REQUIRED) if not self._username_available(request.username): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.USERNAME_NOT_AVAILABLE) if request.lat == 0 and request.lng == 0: context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_COORDINATE) user = User( email=signup_token.email, username=request.username, name=request.name, gender=request.gender, birthdate=birthdate, hosting_status=hostingstatus2sql[request.hosting_status], city=request.city, geom=create_coordinate(request.lat, request.lng), geom_radius=request.radius, accepted_tos=1 if request.accept_tos else 0, ) # happens in same transaction session.delete(signup_token) # enforces email/username uniqueness session.add(user) session.commit() token, expiry = self._create_session(context, session, user, False) context.send_initial_metadata( [ ("set-cookie", create_session_cookie(token, expiry)), ] ) return auth_pb2.AuthRes(jailed=user.is_jailed)
def SignupFlow(self, request, context): with session_scope() as session: if request.email_token: # the email token can either be for verification or just to find an existing signup flow = session.execute( select(SignupFlow). where(SignupFlow.email_verified == False).where( SignupFlow.email_token == request.email_token).where( SignupFlow.token_is_valid)).scalar_one_or_none() if flow: # find flow by email verification token and mark it as verified flow.email_verified = True flow.email_token = None flow.email_token_expiry = None session.flush() else: # just try to find the flow by flow token, no verification is done flow = session.execute( select(SignupFlow).where( SignupFlow.flow_token == request.email_token)).scalar_one_or_none() if not flow: context.abort(grpc.StatusCode.NOT_FOUND, errors.INVALID_TOKEN) else: if not request.flow_token: # fresh signup if not request.HasField("basic"): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.SIGNUP_FLOW_BASIC_NEEDED) # TODO: unique across both tables existing_user = session.execute( select(User).where(User.email == request.basic.email) ).scalar_one_or_none() if existing_user: context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.SIGNUP_FLOW_EMAIL_TAKEN) existing_flow = session.execute( select(SignupFlow).where( SignupFlow.email == request.basic.email)).scalar_one_or_none() if existing_flow: send_signup_email(existing_flow) session.commit() context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.SIGNUP_FLOW_EMAIL_STARTED_SIGNUP) if not is_valid_email(request.basic.email): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_EMAIL) if not is_valid_name(request.basic.name): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_NAME) flow_token = cookiesafe_secure_token() flow = SignupFlow( flow_token=flow_token, name=request.basic.name, email=request.basic.email, ) session.add(flow) session.flush() else: # not fresh signup flow = session.execute( select(SignupFlow).where( SignupFlow.flow_token == request.flow_token)).scalar_one_or_none() if not flow: context.abort(grpc.StatusCode.NOT_FOUND, errors.INVALID_TOKEN) if request.HasField("basic"): context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.SIGNUP_FLOW_BASIC_FILLED) # we've found and/or created a new flow, now sort out other parts if request.HasField("account"): if flow.account_is_filled: context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.SIGNUP_FLOW_ACCOUNT_FILLED) # check username validity if not is_valid_username(request.account.username): context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_USERNAME) if not self._username_available(request.account.username): context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.USERNAME_NOT_AVAILABLE) abort_on_invalid_password(request.account.password, context) hashed_password = hash_password(request.account.password) birthdate = parse_date(request.account.birthdate) if not birthdate or birthdate >= minimum_allowed_birthdate( ): context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.INVALID_BIRTHDATE) if not request.account.hosting_status: context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.HOSTING_STATUS_REQUIRED) if request.account.lat == 0 and request.account.lng == 0: context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_COORDINATE) if not request.account.accept_tos: context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.MUST_ACCEPT_TOS) flow.username = request.account.username flow.hashed_password = hashed_password flow.birthdate = birthdate flow.gender = request.account.gender flow.hosting_status = hostingstatus2sql[ request.account.hosting_status] flow.city = request.account.city flow.geom = create_coordinate(request.account.lat, request.account.lng) flow.geom_radius = request.account.radius flow.accepted_tos = TOS_VERSION session.flush() if request.HasField("feedback"): if flow.filled_feedback: context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.SIGNUP_FLOW_FEEDBACK_FILLED) form = request.feedback flow.filled_feedback = True flow.ideas = form.ideas flow.features = form.features flow.experience = form.experience flow.contribute = contributeoption2sql[form.contribute] flow.contribute_ways = form.contribute_ways flow.expertise = form.expertise session.flush() if request.HasField("accept_community_guidelines"): if not request.accept_community_guidelines.value: context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.MUST_ACCEPT_COMMUNITY_GUIDELINES) flow.accepted_community_guidelines = GUIDELINES_VERSION session.flush() # send verification email if needed if not flow.email_sent: send_signup_email(flow) session.flush() # finish the signup if done if flow.is_completed: user = User( name=flow.name, email=flow.email, username=flow.username, hashed_password=flow.hashed_password, birthdate=flow.birthdate, gender=flow.gender, hosting_status=flow.hosting_status, city=flow.city, geom=flow.geom, geom_radius=flow.geom_radius, accepted_tos=flow.accepted_tos, accepted_community_guidelines=flow. accepted_community_guidelines, onboarding_emails_sent=1, last_onboarding_email_sent=func.now(), ) session.add(user) form = ContributorForm( user=user, ideas=flow.ideas or None, features=flow.features or None, experience=flow.experience or None, contribute=flow.contribute or None, contribute_ways=flow.contribute_ways, expertise=flow.expertise or None, ) session.add(form) user.filled_contributor_form = form.is_filled session.delete(flow) session.commit() enforce_community_memberships_for_user(session, user) if form.is_filled: user.filled_contributor_form = True maybe_send_contributor_form_email(form) send_onboarding_email(user, email_number=1) token, expiry = create_session(context, session, user, False) context.send_initial_metadata([ ("set-cookie", create_session_cookie(token, expiry)), ]) return auth_pb2.SignupFlowRes(auth_res=_auth_res(user), ) else: return auth_pb2.SignupFlowRes( flow_token=flow.flow_token, need_account=not flow.account_is_filled, need_feedback=not flow.filled_feedback, need_verify_email=not flow.email_verified, need_accept_community_guidelines=flow. accepted_community_guidelines < GUIDELINES_VERSION, )