Beispiel #1
0
def run(args):
    """This function is called by the user to log in and validate
       that a session is authorised to connect"""

    status = 0
    message = None
    provisioning_uri = None
    assigned_device_uid = None

    short_uid = args["short_uid"]
    username = args["username"]
    password = args["password"]
    otpcode = args["otpcode"]

    try:
        remember_device = args["remember_device"]
    except:
        remember_device = False

    try:
        device_uid = args["device_uid"]
    except:
        device_uid = None

    # create the user account for the user
    user_account = UserAccount(username)

    # log into the central identity account to query
    # the current status of this login session
    bucket = login_to_service_account()

    # locate the session referred to by this uid
    base_key = "requests/%s" % short_uid
    session_keys = ObjectStore.get_all_object_names(bucket, base_key)

    # try all of the sessions to find the one that the user
    # may be referring to...
    login_session_key = None
    request_session_key = None

    for session_key in session_keys:
        request_session_key = "%s/%s" % (base_key, session_key)
        session_user = ObjectStore.get_string_object(
            bucket, request_session_key)

        # did the right user request this session?
        if user_account.name() == session_user:
            if login_session_key:
                # this is an extremely unlikely edge case, whereby
                # two login requests within a 30 minute interval for the
                # same user result in the same short UID. This should be
                # signified as an error and the user asked to create a
                # new request
                raise LoginError(
                    "You have found an extremely rare edge-case "
                    "whereby two different login requests have randomly "
                    "obtained the same short UID. As we can't work out "
                    "which request is valid, the login is denied. Please "
                    "create a new login request, which will then have a "
                    "new login request UID")
            else:
                login_session_key = session_key

    if not login_session_key:
        raise LoginError(
            "There is no active login request with the "
            "short UID '%s' for user '%s'" % (short_uid, username))

    login_session_key = "sessions/%s/%s" % (user_account.sanitised_name(),
                                            login_session_key)

    # fully load the user account from the object store so that we
    # can validate the username and password
    try:
        account_key = "accounts/%s" % user_account.sanitised_name()
        user_account = UserAccount.from_data(
            ObjectStore.get_object_from_json(bucket, account_key))
    except:
        raise LoginError("No account available with username '%s'" %
                         username)

    if (not remember_device) and device_uid:
        # see if this device has been seen before
        device_key = "devices/%s/%s" % (user_account.sanitised_name(),
                                        device_uid)

        try:
            device_secret = ObjectStore.get_string_object(bucket,
                                                          device_key)
        except:
            device_secret = None

        if device_secret is None:
            raise LoginError(
                "The login device is not recognised. Please try to "
                "log in again using your master one-time-password.")
    else:
        device_secret = None

    # now try to log into this account using the supplied
    # password and one-time-code
    try:
        if device_secret:
            user_account.validate_password(password, otpcode,
                                           device_secret=device_secret)
        elif remember_device:
            (device_secret, provisioning_uri) = \
                        user_account.validate_password(
                                    password, otpcode,
                                    remember_device=True)

            device_uid = str(uuid.uuid4())
            device_key = "devices/%s/%s" % (user_account.sanitised_name(),
                                            device_uid)

            assigned_device_uid = device_uid
        else:
            user_account.validate_password(password, otpcode)
    except:
        # don't leak info about why validation failed
        raise LoginError("The password or OTP code is incorrect")

    # the user is valid - load up the actual login session
    login_session = LoginSession.from_data(
                        ObjectStore.get_object_from_json(bucket,
                                                         login_session_key))

    # we must record the session against which this otpcode has
    # been validated. This is to stop us validating an otpcode more than
    # once (e.g. if the password and code have been intercepted).
    # Any sessions validated using the same code should be treated
    # as immediately suspcious
    otproot = "otps/%s" % user_account.sanitised_name()
    sessions = ObjectStore.get_all_strings(bucket, otproot)

    utcnow = datetime.datetime.utcnow()

    for session in sessions:
        otpkey = "%s/%s" % (otproot, session)
        otpstring = ObjectStore.get_string_object(bucket, otpkey)

        (timestamp, code) = otpstring.split("|||")

        # remove all codes that are more than 10 minutes old. The
        # otp codes are only valid for 3 minutes, so no need to record
        # codes that have been used that are older than that...
        timedelta = utcnow - datetime.datetime.fromtimestamp(
                                                    float(timestamp))

        if timedelta.seconds > 600:
            try:
                ObjectStore.delete_object(bucket, otpkey)
            except:
                pass

        elif code == str(otpcode):
            # Low probability there is some recycling,
            # but very suspicious if the code was validated within the last
            # 10 minutes... (as 3 minute timeout of a code)
            suspect_key = "sessions/%s/%s" % (
                user_account.sanitised_name(), session)

            suspect_session = None

            try:
                suspect_session = LoginSession.from_data(
                        ObjectStore.get_object_from_json(bucket,
                                                         suspect_key))
            except:
                pass

            if suspect_session:
                suspect_session.set_suspicious()
                ObjectStore.set_object_from_json(bucket, suspect_key,
                                                 suspect_session.to_data())

            raise LoginError(
                "Cannot authorise the login as the one-time-code "
                "you supplied has already been used within the last 10 "
                "minutes. The chance of this happening is really low, so "
                "we are treating this as a suspicious event. You need to "
                "try another code. Meanwhile, the other login that used "
                "this code has been put into a 'suspicious' state.")

    # record the value and timestamp of when this otpcode was used
    otpkey = "%s/%s" % (otproot, login_session.uuid())
    otpstring = "%s|||%s" % (datetime.datetime.utcnow().timestamp(),
                             otpcode)

    ObjectStore.set_string_object(bucket, otpkey, otpstring)

    login_session.set_approved()

    # write this session back to the object store
    ObjectStore.set_object_from_json(bucket, login_session_key,
                                     login_session.to_data())

    # save the device secret as everything has now worked
    if assigned_device_uid:
        ObjectStore.set_string_object(bucket, device_key,
                                      device_secret)

    # finally, remove this from the list of requested logins
    try:
        ObjectStore.delete_object(bucket, request_session_key)
    except:
        pass

    status = 0
    message = "Success: Status = %s" % login_session.status()

    return_value = create_return_value(status, message)

    if provisioning_uri:
        return_value["provisioning_uri"] = provisioning_uri
        return_value["device_uid"] = assigned_device_uid

    return return_value
Beispiel #2
0
def run(args):
    """This function will allow a user to request a new session
       that will be validated by the passed public key and public
       signing certificate. This will return a URL that the user
       must connect to to then log in and validate that request.
    """

    status = 0
    message = None
    login_url = None
    login_uid = None
    user_uid = None

    username = args["username"]
    public_key = PublicKey.from_data(args["public_key"])
    public_cert = PublicKey.from_data(args["public_certificate"])

    ip_addr = None
    hostname = None
    login_message = None

    try:
        ip_addr = args["ipaddr"]
    except:
        pass

    try:
        hostname = args["hostname"]
    except:
        pass

    try:
        login_message = args["message"]
    except:
        pass

    # generate a sanitised version of the username
    user_account = UserAccount(username)

    # Now generate a login session for this request
    login_session = LoginSession(public_key, public_cert, ip_addr, hostname,
                                 login_message)

    # now log into the central identity account to record
    # that a request to open a login session has been opened
    bucket = login_to_service_account()

    # first, make sure that the user exists...
    account_key = "accounts/%s" % user_account.sanitised_name()

    try:
        existing_data = ObjectStore.get_object_from_json(bucket, account_key)
    except:
        existing_data = None

    if existing_data is None:
        raise InvalidLoginError("There is no user with name '%s'" % username)

    user_account = UserAccount.from_data(existing_data)
    user_uid = user_account.uid()

    # first, make sure that the user doens't have too many open
    # login sessions at once - this prevents denial of service
    user_session_root = "sessions/%s" % user_account.sanitised_name()

    open_sessions = ObjectStore.get_all_object_names(bucket, user_session_root)

    # take the opportunity to prune old user login sessions
    prune_expired_sessions(bucket, user_account, user_session_root,
                           open_sessions)

    # this is the key for the session in the object store
    user_session_key = "%s/%s" % (user_session_root, login_session.uuid())

    ObjectStore.set_object_from_json(bucket, user_session_key,
                                     login_session.to_data())

    # we will record a pointer to the request using the short
    # UUID. This way we can give a simple URL. If there is a clash,
    # then we will use the username provided at login to find the
    # correct request from a much smaller pool (likely < 3)
    request_key = "requests/%s/%s" % (login_session.short_uuid(),
                                      login_session.uuid())

    ObjectStore.set_string_object(bucket, request_key, user_account.name())

    status = 0
    # the login URL is the URL of this identity service plus the
    # short UID of the session
    login_url = "%s/s?id=%s" % (get_service_info().service_url(),
                                login_session.short_uuid())

    login_uid = login_session.uuid()

    message = "Success: Login via %s" % login_url

    return_value = create_return_value(status, message)

    if login_uid:
        return_value["session_uid"] = login_uid

    if login_url:
        return_value["login_url"] = login_url
    else:
        return_value["login_url"] = None

    if user_uid:
        return_value["user_uid"] = user_uid

    return return_value