def test_supports_offline( self, mocked__get_client, mocked__get_policies, mocked_get_policy_definitions, ): """verify that client in the policy is honored for supports_offline""" m_policy = { "general": { "realm": "*", "active": "True", "client": "*", "user": "******", "time": "* * * * * *;", "action": "support_offline=qr", "scope": "authentication", } } mocked_get_policy_definitions.return_value = { "authentication": { "support_offline": { "type": "set", "value": ["qr", "u2f"], # TODO: currently hardcoded "desc": "The token types that should support offline " "authentication", }, } } mocked__get_policies.return_value = m_policy qr_token = Token("qr") mocked__get_client.return_value = "127.0.0.1" assert supports_offline(realms=["defaultrealm"], token=qr_token) mocked__get_client.return_value = "128.0.0.1" assert supports_offline(realms=["defaultrealm"], token=qr_token) m_policy["general"]["client"] = "127.0.0.1/24" mocked__get_client.return_value = "127.0.0.1" assert supports_offline(realms=["defaultrealm"], token=qr_token) mocked__get_client.return_value = "128.0.0.1" assert not supports_offline(realms=["defaultrealm"], token=qr_token)
def test_supports_offline(self, mocked__get_client, mocked__get_policies, mocked_get_policy_definitions): """verify that client in the policy is honored for supports_offline""" m_policy = { 'general': { 'realm': '*', 'active': 'True', 'client': '*', 'user': '******', 'time': '* * * * * *;', 'action': 'support_offline=qr', 'scope': 'authentication', } } mocked_get_policy_definitions.return_value = { 'authentication': { 'support_offline': { 'type': 'set', 'value': ['qr', 'u2f'], # TODO: currently hardcoded 'desc': 'The token types that should support offline ' 'authentication' }, } } mocked__get_policies.return_value = m_policy qr_token = Token('qr') mocked__get_client.return_value = '127.0.0.1' assert supports_offline(realms=['defaultrealm'], token=qr_token) mocked__get_client.return_value = '128.0.0.1' assert supports_offline(realms=['defaultrealm'], token=qr_token) m_policy['general']['client'] = '127.0.0.1/24' mocked__get_client.return_value = '127.0.0.1' assert supports_offline(realms=['defaultrealm'], token=qr_token) mocked__get_client.return_value = '128.0.0.1' assert not supports_offline(realms=['defaultrealm'], token=qr_token)
def check_status(self, transid=None, user=None, serial=None, password=None, use_offline=False): """ check for open transactions - for polling support :param transid: the transaction id where we request the status from :param user: the token owner user :param serial: or the serial we are searching for :param password: the pin/password for authorization the request :param use_offline: on success the offline info is returned :return: tuple of success and detail dict """ expired, challenges = Challenges.get_challenges(token=None, transid=transid) # remove all expired challenges if expired: Challenges.delete_challenges(None, expired) if not challenges: return False, None # there is only one challenge per transaction id # if not multiple challenges, where transaction id is the parent one reply = {} involved_tokens = [] transactions = {} for ch in challenges: # only look for challenges that are not compromised if not Challenges.verify_checksum(ch): continue # is the requester authorized challenge_serial = ch.getTokenSerial() if serial and challenge_serial != serial: continue tokens = getTokens4UserOrSerial(serial=challenge_serial) if not tokens: continue involved_tokens.extend(tokens) # as one challenge belongs exactly to only one token, # we take this one as the token token = tokens[0] owner = get_token_owner(token) if user and user != owner: continue involved_tokens.extend(tokens) # we only check the user password / token pin if the user # paranmeter is given if user and owner: pin_match = check_pin(token, password, user=owner, options=None) else: pin_match = token.checkPin(password) if not pin_match: continue trans_dict = {} trans_dict['received_count'] = ch.received_count trans_dict['received_tan'] = ch.received_tan trans_dict['valid_tan'] = ch.valid_tan trans_dict['message'] = ch.getChallenge() trans_dict['status'] = ch.getStatus() # -------------------------------------------------------------- -- # extend the check status with the accept or deny of a transaction challenge_session = ch.getSession() if challenge_session: challenge_session_dict = json.loads(challenge_session) if 'accept' in challenge_session_dict: trans_dict['accept'] = challenge_session_dict['accept'] if 'reject' in challenge_session_dict: trans_dict['reject'] = challenge_session_dict['reject'] # -------------------------------------------------------------- -- token_dict = {'serial': token.getSerial(), 'type': token.type} # 1. check if token supports offline at all supports_offline_at_all = token.supports_offline_mode # 2. check if policy allows to use offline authentication if user is not None and user.login and user.realm: realms = [user.realm] else: realms = token.getRealms() offline_is_allowed = supports_offline(realms, token) if not ch.is_open() and ch.valid_tan and \ supports_offline_at_all and \ offline_is_allowed and \ use_offline: token_dict['offline_info'] = token.getOfflineInfo() trans_dict['token'] = token_dict transactions[ch.transid] = trans_dict if transactions: reply['transactions'] = transactions return len(reply) > 0, reply
def check_status(self, transid=None, user=None, serial=None, password=None, use_offline=False): """ check for open transactions - for polling support :param transid: the transaction id where we request the status from :param user: the token owner user :param serial: or the serial we are searching for :param password: the pin/password for authorization the request :param use_offline: on success the offline info is returned :return: tuple of success and detail dict """ expired, challenges = Challenges.get_challenges(None, transid=transid) # remove all expired challenges if expired: Challenges.delete_challenges(None, expired) if not challenges: return False, None # there is only one challenge per transaction id # if not multiple challenges, where transaction id is the parent one reply = {} pin_policies = get_pin_policies(user) if 1 in pin_policies: pin_match = check_pin(None, password, user=user, options=None) if not pin_match: return False, None involved_tokens = [] transactions = {} for ch in challenges: # only look for challenges that are not compromised if not Challenges.verify_checksum(ch): continue # is the requester authorized serial = ch.getTokenSerial() tokens = getTokens4UserOrSerial(serial=serial) if not tokens: continue involved_tokens.extend(tokens) # as one challenge belongs exactly to only one token, # we take this one as the token token = tokens[0] if 1 not in pin_policies: pin_match = check_pin(token, password, user=user, options=None) if not pin_match: ret = False continue ret = True trans_dict = {} trans_dict['received_count'] = ch.received_count trans_dict['received_tan'] = ch.received_tan trans_dict['valid_tan'] = ch.valid_tan trans_dict['message'] = ch.challenge trans_dict['status'] = ch.getStatus() token_dict = {'serial': serial, 'type': token.type} # 1. check if token supports offline at all supports_offline_at_all = token.supports_offline_mode # 2. check if policy allows to use offline authentication if user is not None and user.login and user.realm: realms = [user.realm] else: realms = token.getRealms() offline_is_allowed = supports_offline(realms, token) if not ch.is_open() and ch.valid_tan and \ supports_offline_at_all and \ offline_is_allowed and \ use_offline: token_dict['offline_info'] = token.getOfflineInfo() trans_dict['token'] = token_dict transactions[ch.transid] = trans_dict if transactions: reply['transactions'] = transactions return ret, reply
def check_status(self, transid=None, user=None, serial=None, password=None, use_offline=False): """ check for open transactions - for polling support :param transid: the transaction id where we request the status from :param user: the token owner user :param serial: or the serial we are searching for :param password: the pin/password for authorization the request :param use_offline: on success the offline info is returned :return: tuple of success and detail dict """ expired, challenges = Challenges.get_challenges(None, transid=transid) # remove all expired challenges if expired: Challenges.delete_challenges(None, expired) if not challenges: return False, None # there is only one challenge per transaction id # if not multiple challenges, where transaction id is the parent one reply = {} pin_policies = linotp.lib.policy.get_pin_policies(user) if 1 in pin_policies: pin_match = check_pin(None, password, user=user, options=None) if not pin_match: return False, None involved_tokens = [] transactions = {} for ch in challenges: # only look for challenges that are not compromised if not Challenges.verify_checksum(ch): continue # is the requester authorized serial = ch.getTokenSerial() tokens = getTokens4UserOrSerial(serial=serial) if not tokens: continue involved_tokens.extend(tokens) # as one challenge belongs exactly to only one token, # we take this one as the token token = tokens[0] if 1 not in pin_policies: pin_match = check_pin(token, password, user=user, options=None) if not pin_match: ret = False continue ret = True trans_dict = {} trans_dict['received_count'] = ch.received_count trans_dict['received_tan'] = ch.received_tan trans_dict['valid_tan'] = ch.valid_tan trans_dict['message'] = ch.challenge trans_dict['status'] = ch.getStatus() token_dict = {'serial': serial, 'type': token.type} # 1. check if token supports offline at all supports_offline_at_all = token.supports_offline_mode # 2. check if policy allows to use offline authentication if user is not None and user.login and user.realm: realms = [user.realm] else: realms = token.getRealms() offline_is_allowed = supports_offline(realms, token) if not ch.is_open() and ch.valid_tan and \ supports_offline_at_all and \ offline_is_allowed and \ use_offline: token_dict['offline_info'] = token.getOfflineInfo() trans_dict['token'] = token_dict transactions[ch.transid] = trans_dict if transactions: reply['transactions'] = transactions return ret, reply
def finish_valid_tokens(self): """ processing of the valid tokens """ valid_tokens = self.valid_tokens validation_results = self.validation_results user = self.user if len(valid_tokens) == 1: token = valid_tokens[0] if user: action_detail = ("user %r@%r successfully authenticated." % (user.login, user.realm)) else: action_detail = ("serial %r successfully authenticated." % token.getSerial()) log.info(action_detail) # there could be a match in the window ahead, # so we need the last valid counter here (counter, _reply) = validation_results[token.getSerial()] token.setOtpCount(counter + 1) token.statusValidationSuccess() # finish as well related open challenges Challenges.finish_challenges(token, success=True) if token.getFromTokenInfo('count_auth_success_max', default=None): auth_count = token.get_count_auth_success() token.set_count_auth_success(auth_count + 1) detail = None auth_info = self.options.get('auth_info', 'False') if auth_info.lower() == "true": detail = token.getAuthDetail() # 1. check if token supports offline at all supports_offline_at_all = token.supports_offline_mode # 2. check if policy allows to use offline authentication if user is not None and user.login and user.realm: realms = [user.realm] else: realms = token.getRealms() offline_is_allowed = supports_offline(realms, token) # 3. check if parameter 'use_offline' is provided use_offline_param = self.options.get('use_offline', 'False') use_offline = use_offline_param.lower() == 'true' if supports_offline_at_all and \ offline_is_allowed and \ use_offline: offline_info = token.getOfflineInfo() if detail is None: detail = {} offline = {'serial': token.getSerial(), 'type': token.type} offline['offline_info'] = offline_info detail.update({'offline': offline}) return (True, detail, action_detail) else: # we have to set the matching counter to prevent replay one one # single token for token in valid_tokens: (res, _reply) = validation_results[token.getSerial()] token.setOtpCount(res) context['audit']['action_detail'] = "Multiple valid tokens found!" if user: log.error( "[__checkTokenList] multiple token match error: " "Several Tokens matching with the same OTP PIN " "and OTP for user %r. Not sure how to auth", user.login) raise UserError("multiple token match error", id=-33)
def finish_valid_tokens(self): """ processing of the valid tokens """ valid_tokens = self.valid_tokens validation_results = self.validation_results user = self.user if len(valid_tokens) == 1: token = valid_tokens[0] if user: action_detail = ("user %r@%r successfully authenticated." % (user.login, user.realm)) else: action_detail = ("serial %r successfully authenticated." % token.getSerial()) log.info(action_detail) # there could be a match in the window ahead, # so we need the last valid counter here (counter, _reply) = validation_results[token.getSerial()] token.setOtpCount(counter + 1) token.statusValidationSuccess() # finish as well related open challenges Challenges.finish_challenges(token, success=True) if token.count_auth_success_max > 0: token.inc_count_auth_success() if token.count_auth_max > 0: token.inc_count_auth() detail = None auth_info = self.options.get('auth_info', 'False') if auth_info.lower() == "true": detail = token.getAuthDetail() # 1. check if token supports offline at all supports_offline_at_all = token.supports_offline_mode # 2. check if policy allows to use offline authentication if user is not None and user.login and user.realm: realms = [user.realm] else: realms = token.getRealms() offline_is_allowed = supports_offline(realms, token) # 3. check if parameter 'use_offline' is provided use_offline_param = self.options.get('use_offline', 'False') use_offline = use_offline_param.lower() == 'true' if supports_offline_at_all and \ offline_is_allowed and \ use_offline: offline_info = token.getOfflineInfo() if detail is None: detail = {} offline = {'serial': token.getSerial(), 'type': token.type} offline['offline_info'] = offline_info detail.update({'offline': offline}) janitor_to_remove_enrollment_token(valid_tokens=[token]) return (True, detail, action_detail) else: # we have to set the matching counter to prevent replay one one # single token for token in valid_tokens: (res, _reply) = validation_results[token.getSerial()] token.setOtpCount(res) # in case of multiple matches the tokens were accessed # so we count them as well if token.count_auth_max > 0: token.inc_count_auth() janitor_to_remove_enrollment_token(valid_tokens=valid_tokens) context['audit']['action_detail'] = "Multiple valid tokens found!" if user: log.error("multiple token match error: " "Several Tokens matching with the same OTP PIN " "and OTP for user %r. Not sure how to auth", user.login) raise UserError("multiple token match error", id=-33)
def finish_valid_tokens(self): """ processing of the valid tokens """ valid_tokens = self.valid_tokens validation_results = self.validation_results user = self.user if len(valid_tokens) == 1: token = valid_tokens[0] if user: action_detail = "user %r@%r successfully authenticated." % ( user.login, user.realm, ) else: action_detail = ("serial %r successfully authenticated." % token.getSerial()) log.info(action_detail) # there could be a match in the window ahead, # so we need the last valid counter here (counter, _reply) = validation_results[token.getSerial()] token.setOtpCount(counter + 1) token.statusValidationSuccess() # finish as well related open challenges Challenges.finish_challenges(token, success=True) if token.count_auth_success_max > 0: token.inc_count_auth_success() if token.count_auth_max > 0: token.inc_count_auth() detail = None auth_info = self.options.get("auth_info", "False") if auth_info.lower() == "true": detail = token.getAuthDetail() # 1. check if token supports offline at all supports_offline_at_all = token.supports_offline_mode # 2. check if policy allows to use offline authentication if user is not None and user.login and user.realm: realms = [user.realm] else: realms = token.getRealms() offline_is_allowed = supports_offline(realms, token) # 3. check if parameter 'use_offline' is provided use_offline_param = self.options.get("use_offline", "False") use_offline = use_offline_param.lower() == "true" if supports_offline_at_all and offline_is_allowed and use_offline: offline_info = token.getOfflineInfo() if detail is None: detail = {} offline = {"serial": token.getSerial(), "type": token.type} offline["offline_info"] = offline_info detail.update({"offline": offline}) janitor_to_remove_enrollment_token(valid_tokens=[token]) return (True, detail, action_detail) else: # we have to set the matching counter to prevent replay one one # single token for token in valid_tokens: (res, _reply) = validation_results[token.getSerial()] token.setOtpCount(res) # in case of multiple matches the tokens were accessed # so we count them as well if token.count_auth_max > 0: token.inc_count_auth() janitor_to_remove_enrollment_token(valid_tokens=valid_tokens) g.audit["action_detail"] = "Multiple valid tokens found!" if user: log.error( "multiple token match error: " "Several Tokens matching with the same OTP PIN " "and OTP for user %r. Not sure how to auth", user.login, ) raise UserError("multiple token match error", id=-33)
def check_status(self, transid=None, user=None, serial=None, password=None, use_offline=False): """ check for open transactions - for polling support :param transid: the transaction id where we request the status from :param user: the token owner user :param serial: or the serial we are searching for :param password: the pin/password for authorization the request :param use_offline: on success the offline info is returned :return: tuple of success and detail dict """ expired, challenges = Challenges.get_challenges(token=None, transid=transid) # remove all expired challenges if expired: Challenges.delete_challenges(None, expired) if not challenges: return False, None # there is only one challenge per transaction id # if not multiple challenges, where transaction id is the parent one reply = {} involved_tokens = [] transactions = {} for ch in challenges: # only look for challenges that are not compromised if not Challenges.verify_checksum(ch): continue # is the requester authorized challenge_serial = ch.getTokenSerial() if serial and challenge_serial != serial: continue tokens = getTokens4UserOrSerial(serial=challenge_serial) if not tokens: continue involved_tokens.extend(tokens) # as one challenge belongs exactly to only one token, # we take this one as the token token = tokens[0] owner = get_token_owner(token) if user and user != owner: continue involved_tokens.extend(tokens) # we only check the user password / token pin if the user # paranmeter is given if user and owner: pin_match = check_pin(token, password, user=owner, options=None) else: pin_match = token.checkPin(password) if not pin_match: continue trans_dict = {} trans_dict['received_count'] = ch.received_count trans_dict['received_tan'] = ch.received_tan trans_dict['valid_tan'] = ch.valid_tan trans_dict['message'] = ch.challenge trans_dict['status'] = ch.getStatus() # -------------------------------------------------------------- -- # extend the check status with the accept or deny of a transaction challenge_session = ch.getSession() if challenge_session: challenge_session_dict = json.loads(challenge_session) if 'accept' in challenge_session_dict: trans_dict['accept'] = challenge_session_dict['accept'] if 'reject' in challenge_session_dict: trans_dict['reject'] = challenge_session_dict['reject'] # -------------------------------------------------------------- -- token_dict = {'serial': token.getSerial(), 'type': token.type} # 1. check if token supports offline at all supports_offline_at_all = token.supports_offline_mode # 2. check if policy allows to use offline authentication if user is not None and user.login and user.realm: realms = [user.realm] else: realms = token.getRealms() offline_is_allowed = supports_offline(realms, token) if not ch.is_open() and ch.valid_tan and \ supports_offline_at_all and \ offline_is_allowed and \ use_offline: token_dict['offline_info'] = token.getOfflineInfo() trans_dict['token'] = token_dict transactions[ch.transid] = trans_dict if transactions: reply['transactions'] = transactions return len(reply) > 0, reply