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. Args: args (dict): containing login data such as username, password etc Returns: dict: containing status of login attempt """ username = args["username"] public_key = PublicKey.from_data(args["public_key"]) public_cert = PublicKey.from_data(args["public_certificate"]) try: scope = args["scope"] except: scope = None try: permissions = args["permissions"] except: permissions = None try: hostname = args["hostname"] except: hostname = None try: ipaddr = args["ipaddr"] except: ipaddr = None try: login_message = args["login_message"] except: login_message = None # Generate a login session for this request login_session = LoginSession(username=username, public_key=public_key, public_cert=public_cert, ipaddr=ipaddr, hostname=hostname, login_message=login_message, scope=scope, permissions=permissions) return_value = {} return_value["login_url"] = login_session.login_url() return_value["short_uid"] = login_session.short_uid() return_value["session_uid"] = login_session.uid() return return_value
def run(args): """This function will allow anyone to query the current login status of the session with passed UID""" status = 0 message = None session_status = None session_uid = args["session_uid"] username = args["username"] # generate a sanitised version of the username user_account = UserAccount(username) # now log into the central identity account to query # the current status of this login session bucket = login_to_service_account() user_session_key = "sessions/%s/%s" % \ (user_account.sanitised_name(), session_uid) try: login_session = LoginSession.from_data( ObjectStore.get_object_from_json(bucket, user_session_key)) except: login_session = None if login_session is None: user_session_key = "expired_sessions/%s/%s" % \ (user_account.sanitised_name(), session_uid) login_session = LoginSession.from_data( ObjectStore.get_object_from_json(bucket, user_session_key)) if login_session is None: raise InvalidSessionError("Cannot find the session '%s'" % session_uid) status = 0 message = "Success: Status = %s" % login_session.status() session_status = login_session.status() return_value = create_return_value(status, message) if session_status: return_value["session_status"] = session_status return return_value
def run(args): """This function will allow the current user to authorise a logout from the current session - this will be authorised by signing the request to logout Args: args (dict): contains identifying information about the session Returns: dict: contains data on the success of the logout request """ session_uid = args["session_uid"] try: authorisation = Authorisation.from_data(args["authorisation"]) except: authorisation = None try: signature = string_to_bytes(args["signature"]) except: signature = None login_session = LoginSession.load(uid=session_uid) login_session.set_logged_out(authorisation=authorisation, signature=signature)
def test_credentials(username, password): identity_uid = create_uuid() session_uid = create_uuid() if random.randint(0, 1): device_uid = create_uuid() else: device_uid = None short_uid = LoginSession.to_short_uid(session_uid) otpcode = "%06d" % random.randint(1, 999999) data = Credentials.package(identity_uid=identity_uid, short_uid=short_uid, device_uid=device_uid, username=username, password=password, otpcode=otpcode) creds = Credentials.unpackage(data=data, username=username, short_uid=short_uid) assert (creds["username"] == username) assert (creds["short_uid"] == short_uid) if device_uid is None: assert (creds["device_uid"] != device_uid) else: assert (creds["device_uid"] == device_uid) encoded_password = Credentials.encode_password(identity_uid=identity_uid, device_uid=device_uid, password=password) assert (creds["password"] == encoded_password) assert (creds["otpcode"] == otpcode)
def run(args): """This function will allow the current user to authorise a logout from the current session - this will be authorised by signing the request to logout""" status = 0 message = None session_uid = args["session_uid"] username = args["username"] permission = args["permission"] signature = string_to_bytes(args["signature"]) # generate a sanitised version of the username user_account = UserAccount(username) # now log into the central identity account to query # the current status of this login session bucket = login_to_service_account() user_session_key = "sessions/%s/%s" % \ (user_account.sanitised_name(), session_uid) request_session_key = "requests/%s/%s" % (session_uid[:8], session_uid) login_session = LoginSession.from_data( ObjectStore.get_object_from_json(bucket, user_session_key)) if login_session: # get the signing certificate from the login session and # validate that the permission object has been signed by # the user requesting the logout cert = login_session.public_certificate() cert.verify(signature, permission) # the signature was correct, so log the user out. For record # keeping purposes we change the loginsession to a logout state # and move it to another part of the object store if login_session.is_approved(): login_session.logout() # only save sessions that were successfully approved if login_session: if login_session.is_logged_out(): expired_session_key = "expired_sessions/%s/%s" % \ (user_account.sanitised_name(), session_uid) ObjectStore.set_object_from_json(bucket, expired_session_key, login_session.to_data()) try: ObjectStore.delete_object(bucket, user_session_key) except: pass try: ObjectStore.delete_object(bucket, request_session_key) except: pass status = 0 message = "Successfully logged out" return_value = create_return_value(status, message) return return_value
def request_login(self, login_message=None): """Request to authenticate as this user. This returns a login URL that you must connect to to supply your login credentials If 'login_message' is supplied, then this is passed to the identity service so that it can be displayed when the user accesses the login page. This helps the user validate that they have accessed the correct login page. Note that if the message is None, then a random message will be generated. """ self._check_for_error() from Acquire.Client import LoginError if not self.is_empty(): raise LoginError("You cannot try to log in twice using the same " "User object. Create another object if you want " "to try to log in again.") if self._username is None or len(self._username) == 0: raise LoginError("Please supply a valid username!") # first, create a private key that will be used # to sign all requests and identify this login from Acquire.Client import PrivateKey as _PrivateKey session_key = _PrivateKey(name="user_session_key %s" % self._username) signing_key = _PrivateKey(name="user_session_cert %s" % self._username) args = {"username": self._username, "public_key": session_key.public_key().to_data(), "public_certificate": signing_key.public_key().to_data(), "scope": self._scope, "permissions": self._permissions } # get information from the local machine to help # the user validate that the login details are correct try: hostname = _socket.gethostname() ipaddr = _socket.gethostbyname(hostname) args["hostname"] = hostname args["ipaddr"] = ipaddr except: pass if login_message is None: try: login_message = _get_random_sentence() except: pass if login_message is not None: args["login_message"] = login_message identity_service = self.identity_service() result = identity_service.call_function( function="request_login", args=args) try: login_url = result["login_url"] except: login_url = None if login_url is None: error = "Failed to login. Could not extract the login URL! " \ "Result is %s" % (str(result)) self._set_error_state(error) raise LoginError(error) try: session_uid = result["session_uid"] except: session_uid = None if session_uid is None: error = "Failed to login. Could not extract the login " \ "session UID! Result is %s" % (str(result)) self._set_error_state(error) raise LoginError(error) # now save all of the needed data self._login_url = result["login_url"] self._session_key = session_key self._signing_key = signing_key self._session_uid = session_uid self._status = _LoginStatus.LOGGING_IN self._user_uid = None _output("Login by visiting: %s" % self._login_url) if login_message is not None: _output("(please check that this page displays the message '%s')" % login_message) from Acquire.Identity import LoginSession as _LoginSession return {"login_url": self._login_url, "session_uid": session_uid, "short_uid": _LoginSession.to_short_uid(session_uid)}
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
def run(args): """This function will allow anyone to obtain the public keys for the passed login session """ try: session_uid = args["session_uid"] except: session_uid = None try: short_uid = args["short_uid"] except: short_uid = None try: scope = args["scope"] except: scope = None try: permissions = args["permissions"] except: permissions = None if session_uid: login_session = LoginSession.load(uid=session_uid, scope=scope, permissions=permissions) else: if short_uid is None: raise PermissionError( "You must specify either the session_uid or the short_uid " "of the login session") try: status = args["status"] except: raise PermissionError( "You must specify the status of the short_uid session you " "wish to query...") login_session = LoginSession.load(short_uid=short_uid, status=status, scope=scope, permissions=permissions) return_value = {} # only send information if the user had logged in! should_return_data = False if login_session.is_approved(): should_return_data = True return_value["public_key"] = login_session.public_key().to_data() elif login_session.is_logged_out(): should_return_data = True return_value["logout_datetime"] = \ datetime_to_string(login_session.logout_time()) if should_return_data: return_value["public_cert"] = \ login_session.public_certificate().to_data() return_value["scope"] = login_session.scope() return_value["permissions"] = login_session.permissions() return_value["user_uid"] = login_session.user_uid() return_value["session_status"] = login_session.status() return_value["login_message"] = login_session.login_message() return return_value
def run(args): """This function is called by the user to log in and validate that a session is authorised to connect Args: args (dict): contains identifying information about the user, short_UID, username, password and OTP code Returns: dict: contains a URI and a UID for this login """ short_uid = args["short_uid"] packed_credentials = args["credentials"] try: user_uid = args["user_uid"] except: user_uid = None try: remember_device = args["remember_device"] if remember_device: remember_device = True else: remember_device = False except: remember_device = False # get the session referred to by the short_uid sessions = LoginSession.load(short_uid=short_uid, status="pending") if isinstance(sessions, LoginSession): # we have many sessions to test... sessions = [sessions] result = None login_session = None last_error = None credentials = None for session in sessions: try: if credentials is None: credentials = Credentials.from_data( data=packed_credentials, username=session.username(), short_uid=short_uid) else: credentials.assert_matching_username(session.username()) result = UserAccount.login(credentials=credentials, user_uid=user_uid, remember_device=remember_device) login_session = session # success! break except Exception as e: last_error = e if result is None or login_session is None: # no valid logins raise last_error # we've successfully logged in login_session.set_approved(user_uid=result["user"].uid(), device_uid=result["device_uid"]) return_value = {} return_value["user_uid"] = login_session.user_uid() if remember_device: try: service = get_this_service(need_private_access=False) hostname = service.hostname() if hostname is None: hostname = "acquire" issuer = "%s@%s" % (service.service_type(), hostname) username = result["user"].name() device_uid = result["device_uid"] otp = result["otp"] provisioning_uri = otp.provisioning_uri(username=username, issuer=issuer) return_value["provisioning_uri"] = provisioning_uri return_value["otpsecret"] = otp.secret() return_value["device_uid"] = device_uid except: pass return return_value
def run(args): """This function will allow anyone to obtain the public keys for the passed login session of a user with a specified login UID""" public_key = None public_cert = None login_status = None logout_timestamp = None session_uid = args["session_uid"] username = args["username"] # generate a sanitised version of the username user_account = UserAccount(username) # now log into the central identity account to query # the current status of this login session bucket = login_to_service_account() user_session_key = "sessions/%s/%s" % \ (user_account.sanitised_name(), session_uid) try: login_session = LoginSession.from_data( ObjectStore.get_object_from_json(bucket, user_session_key)) except: login_session = None if login_session is None: user_session_key = "expired_sessions/%s/%s" % \ (user_account.sanitised_name(), session_uid) login_session = LoginSession.from_data( ObjectStore.get_object_from_json(bucket, user_session_key)) if login_session is None: raise InvalidSessionError("Cannot find the session '%s'" % session_uid) # only send valid keys if the user had logged in! if login_session.is_approved(): public_key = login_session.public_key() public_cert = login_session.public_certificate() elif login_session.is_logged_out(): public_cert = login_session.public_certificate() logout_timestamp = login_session.logout_time().timestamp() else: raise InvalidSessionError("You cannot get the keys for a session " "for which the user has not logged in!") login_status = login_session.status() status = 0 message = "Success: Status = %s" % login_session.status() return_value = create_return_value(status, message) if public_key: return_value["public_key"] = public_key.to_data() if public_cert: return_value["public_cert"] = public_cert.to_data() if login_status: return_value["login_status"] = str(login_status) if logout_timestamp: return_value["logout_timestamp"] = logout_timestamp return return_value
def run(args): """This function will allow anyone to query who matches the passed UID or username (map from one to the other)""" status = 0 message = None user_uid = None username = None public_key = None public_cert = None logout_timestamp = None login_status = None try: user_uid = args["user_uid"] except: pass try: username = args["username"] except: pass try: session_uid = args["session_uid"] except: session_uid = None bucket = None user_account = None if user_uid is None and username is None: raise WhoisLookupError( "You must supply either a username or user_uid to look up...") elif user_uid is None: # look up the user_uid from the username user_account = UserAccount(username) bucket = login_to_service_account() user_key = "accounts/%s" % user_account.sanitised_name() try: user_account = UserAccount.from_data( ObjectStore.get_object_from_json(bucket, user_key)) except: raise WhoisLookupError("Cannot find an account for name '%s'" % username) user_uid = user_account.uid() elif username is None: # look up the username from the uuid bucket = login_to_service_account() uid_key = "whois/%s" % user_uid try: username = ObjectStore.get_string_object(bucket, uid_key) except: raise WhoisLookupError("Cannot find an account for user_uid '%s'" % user_uid) else: raise WhoisLookupError("You must only supply one of the username " "or user_uid to look up - not both!") if session_uid: # now look up the public signing key for this session, if it is # a valid login session if user_account is None: user_account = UserAccount(username) user_session_key = "sessions/%s/%s" % \ (user_account.sanitised_name(), session_uid) try: login_session = LoginSession.from_data( ObjectStore.get_object_from_json(bucket, user_session_key)) except: login_session = None if login_session is None: user_session_key = "expired_sessions/%s/%s" % \ (user_account.sanitised_name(), session_uid) login_session = LoginSession.from_data( ObjectStore.get_object_from_json(bucket, user_session_key)) if login_session is None: raise InvalidSessionError("Cannot find the session '%s'" % session_uid) if login_session.is_approved(): public_key = login_session.public_key() public_cert = login_session.public_certificate() elif login_session.is_logged_out(): public_cert = login_session.public_certificate() logout_timestamp = login_session.logout_time().timestamp() else: raise InvalidSessionError("You cannot get the keys for a session " "for which the user has not logged in!") login_status = login_session.status() status = 0 message = "Success" return_value = create_return_value(status, message) if user_uid: return_value["user_uid"] = str(user_uid) if username: return_value["username"] = str(username) if public_key: return_value["public_key"] = public_key.to_data() if public_cert: return_value["public_cert"] = public_cert.to_data() if logout_timestamp: return_value["logout_timestamp"] = logout_timestamp if login_status: return_value["login_status"] = str(login_status) return return_value
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
def prune_expired_sessions(bucket, user_account, root, sessions, log=[]): """This function will scan through all open requests and login sessions and will prune away old, expired or otherwise weird sessions. It will also use the ipaddress of the source to rate limit or blacklist sources""" for name in sessions: key = "%s/%s" % (root, name) request_key = "requests/%s/%s" % (name[:8], name) try: session = ObjectStore.get_object_from_json(bucket, key) except: log.append("Session %s does not exist!" % name) session = None if session: should_delete = False should_logout = False try: session = LoginSession.from_data(session) if session.is_approved() or session.is_suspicious(): if session.hours_since_creation() > user_account \ .login_timeout(): should_logout = True should_delete = True else: if session.hours_since_creation() > user_account \ .login_request_timeout(): log.append("Expired login request: %s > %s" % (session.hours_since_creation(), user_account.login_request_timeout())) should_delete = True except Exception as e: # this is corrupt - delete it log.append("Deleting session as corrupt? %s" % str(e)) should_delete = True if should_logout: # auto-logout expired sessions log.append("Auto-logging out expired session '%s'" % key) session.logout() expire_session_key = "expired_sessions/%s/%s" % \ (user_account.sanitised_name(), session.uuid()) ObjectStore.set_object_from_json(bucket, expire_session_key, session.to_data()) # now delete any expired sessions if should_delete: log.append("Deleting expired session '%s'" % key) try: ObjectStore.delete_object(bucket, key) except: pass try: ObjectStore.delete_object(bucket, request_key) except: pass