Example #1
0
    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)
Example #2
0
    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)
Example #3
0
    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()
Example #4
0
    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)
Example #5
0
    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,
                )