def test_ignore_pin(self, mocked_get_pin_policies): """ test that on otppin policy 3 the pin is ignored """ userObj = User(login="******", realm="mymixrealm") token = FakeToken() mocked_get_pin_policies.return_value = [3] res = check_pin(token, "QUATSCH", userObj, options={}) assert res res = check_pin(token, "", userObj, options={}) assert res token.type = "spass" res = check_pin(token, "bad", userObj, options={}) assert not res token.type = "spass" res = check_pin(token, "good", userObj, options={}) assert res return
def test_ignore_pin(self, mocked_get_pin_policies): """ test that on otppin policy 3 the pin is ignored """ userObj = User(login='******', realm='mymixrealm') token = FakeToken() mocked_get_pin_policies.return_value = [3] res = check_pin(token, 'QUATSCH', userObj, options={}) self.assertTrue(res) res = check_pin(token, '', userObj, options={}) self.assertTrue(res) token.type = 'spass' res = check_pin(token, 'bad', userObj, options={}) self.assertFalse(res) token.type = 'spass' res = check_pin(token, 'good', userObj, options={}) self.assertTrue(res) return
def authenticate(self, passw, user, options=None): """ This is the method that verifies single shot authentication like they are done with push button tokens. It is a high level interface to support as well other tokens, which do not have a pin and otp seperation - they could overwrite this method **remarks:** we have to call the global methods (check_pin,++) as they take the pin policies into account :param passw: the passw which could be pin+otp :type passw: string :param user: The authenticating user :type user: User object :param options: dictionary of additional request parameters :type options: (dict) :return: returns tuple true or false for the pin match, the otpcounter (int) and the reply (dict) that will be added as additional information in the JSON response of ``/validate/check``. """ pin_match = False otp_counter = -1 reply = None (res, pin, otpval) = split_pin_otp(self, passw, user, options=options) if res != -1: pin_policies = get_pin_policies(user) if 1 in pin_policies: otp_counter = check_otp(self, otpval, options=options) if otp_counter >= 0: pin_match = check_pin(self, pin, user=user, options=options) if not pin_match: otp_counter = -1 else: pin_match = check_pin(self, pin, user=user, options=options) if pin_match is True: otp_counter = check_otp(self, otpval, options=options) # for special token that have no otp like passwordtoken if not self.auth_info and pin_match is True and otp_counter == 0: self.auth_info = {"auth_info": [("pin_length", len(passw))]} return (pin_match, otp_counter, reply)
def authenticate(self, passw, user, options=None): ''' This is the method that verifies single shot authentication like they are done with push button tokens. It is a high level interface to support as well other tokens, which do not have a pin and otp seperation - they could overwrite this method **remarks:** we have to call the global methods (check_pin,++) as they take the pin policies into account :param passw: the passw which could be pin+otp :type passw: string :param user: The authenticating user :type user: User object :param options: dictionary of additional request parameters :type options: (dict) :return: returns tuple true or false for the pin match, the otpcounter (int) and the reply (dict) that will be added as additional information in the JSON response of ``/validate/check``. ''' pin_match = False otp_counter = -1 reply = None (res, pin, otpval) = split_pin_otp(self, passw, user, options=options) if res != -1: pin_policies = get_pin_policies(user) if 1 in pin_policies: otp_counter = check_otp(self, otpval, options=options) if otp_counter >= 0: pin_match = check_pin( self, pin, user=user, options=options) if not pin_match: otp_counter = -1 else: pin_match = check_pin(self, pin, user=user, options=options) if pin_match is True: otp_counter = check_otp(self, otpval, options=options) # for special token that have no otp like passwordtoken if not self.auth_info and pin_match is True and otp_counter == 0: self.auth_info = {'auth_info': [('pin_length', len(passw))]} return (pin_match, otp_counter, reply)
def authenticate(self, passw, user, options=None): """ do the authentication on base of password / otp and serial and options, the request parameters. :param passw: the password / otp :param user: the requesting user :param options: the additional request parameters :return: tupple of (success, otp_count - 0 or -1, reply) """ log.debug("authenticate") otp_count = -1 reply = None # we do a local pin check _res, pin, otpval = split_pin_otp(self, passw, user, options=options) res = check_pin(self, pin, user, options) if res is False: return res, otp_count, reply res, otp_count, reply = self.do_request(otpval, user=user) return res, otp_count, reply
def is_challenge_request(self, passw, user, options=None): """ This method checks, if this is a request, that triggers a challenge. The default behaviour to trigger a challenge is, if the ``passw`` parameter only contains the correct token pin *and* the request contains a ``data`` or a ``challenge`` key i.e. if the ``options`` parameter contains a key ``data`` or ``challenge``. Each token type can decide on its own under which condition a challenge is triggered by overwriting this method. **please note**: in case of pin policy == 2 (no pin is required) the ``check_pin`` would always return true! Thus each request containing a ``data`` or ``challenge`` would trigger a challenge! :param passw: password, which might be pin or pin+otp :type passw: string :param user: The user from the authentication request :type user: User object :param options: dictionary of additional request parameters :type options: dict :return: true or false """ request_is_valid = False pin_match = check_pin(self, passw, user=user, options=options) if pin_match is True: if "data" in options or "challenge" in options: request_is_valid = True return request_is_valid
def is_challenge_request(self, passw, user, options=None): ''' check, if the request would start a challenge - default: if the passw contains only the pin, this request would trigger a challenge - in this place as well the policy for a token is checked :param passw: password, which might be pin or pin+otp :param options: dictionary of additional request parameters :retrun: returns true or false ''' request_is_valid = False # do we need to call the # (res, pin, otpval) = split_pin_otp(self, passw, user, options=options) realms = self.token.getRealmNames() if trigger_sms(realms): if 'check_s' in options.get('scope', {}) and 'challenge' in options: request_is_valid = True return request_is_valid # if its a challenge, the passw contains only the pin pin_match = check_pin(self, passw, user=user, options=options) if pin_match is True: request_is_valid = True return request_is_valid
def authenticate(self, passw, user, options=None): """ do the authentication on base of password / otp and user and options, the request parameters. Here we contact the other LinOTP server to validate the OtpVal. :param passw: the password / otp :param user: the requesting user :param options: the additional request parameters :return: tupple of (success, otp_count - 0 or -1, reply) """ res = False otp_counter = -1 reply = None otpval = passw # should we check the pin localy?? if self.check_pin_local(): (res, pin, otpval) = split_pin_otp(self, passw, user, options=options) res = check_pin(self, pin, user=user, options=options) if res is False: return (res, otp_counter, reply) (res, otp_count, reply) = self.do_request(otpval, user=user) return (res, otp_count, reply)
def is_challenge_request(self, passw, user, options=None): """ check, if the request would start a challenge - default: if the passw contains only the pin, this request would trigger a challenge - in this place as well the policy for a token is checked :param passw: password, which might be pin or pin+otp :param options: dictionary of additional request parameters :return: returns true or false """ request_is_valid = False # do we need to call the # (res, pin, otpval) = split_pin_otp(self, passw, user, options=options) # if policy to send sms on emtpy pin is set, return true realms = self.token.getRealmNames() if trigger_sms(realms): if ( "check_s" in options.get("scope", {}) and "challenge" in options ): request_is_valid = True return request_is_valid # if its a challenge, the passw contains only the pin pin_match = check_pin(self, passw, user=user, options=options) if pin_match is True: request_is_valid = True return request_is_valid
def is_challenge_response(self, passw, user, options=None, challenges=None): ''' check, if the request contains the result of a challenge :param passw: password, which might be pin or pin+otp :param user: the requesting user :param options: dictionary of additional request parameters :return: returns true or false ''' if "state" in options or "transactionid" in options: return True # it as well might be a challenge response, # if the passw is longer than the pin (res, pin, otpval) = split_pin_otp(self, passw, user=user, options=options) if res >= 0: otp_counter = check_otp(self, otpval, options=options) if otp_counter >= 1: pin_match = check_pin(self, pin, user=user, options=options) if not pin_match: return False if otp_counter >= 0: return True return False
def is_challenge_response( self, passw, user, options=None, challenges=None ): """ check, if the request contains the result of a challenge :param passw: password, which might be pin or pin+otp :param user: the requesting user :param options: dictionary of additional request parameters :return: returns true or false """ if "state" in options or "transactionid" in options: return True # it as well might be a challenge response, # if the passw is longer than the pin (res, pin, otpval) = split_pin_otp( self, passw, user=user, options=options ) if res >= 0: otp_counter = check_otp(self, otpval, options=options) if otp_counter >= 1: pin_match = check_pin(self, pin, user=user, options=options) if not pin_match: return False if otp_counter >= 0: return True return False
def is_challenge_request(self, passw, user, options=None): ''' This method checks, if this is a request, that triggers a challenge. The default behaviour to trigger a challenge is, if the ``passw`` parameter only contains the correct token pin *and* the request contains a ``data`` or a ``challenge`` key i.e. if the ``options`` parameter contains a key ``data`` or ``challenge``. Each token type can decide on its own under which condition a challenge is triggered by overwriting this method. **please note**: in case of pin policy == 2 (no pin is required) the ``check_pin`` would always return true! Thus each request containing a ``data`` or ``challenge`` would trigger a challenge! :param passw: password, which might be pin or pin+otp :type passw: string :param user: The user from the authentication request :type user: User object :param options: dictionary of additional request parameters :type options: dict :return: true or false ''' request_is_valid = False pin_match = check_pin(self, passw, user=user, options=options) if pin_match is True: if "data" in options or "challenge" in options: request_is_valid = True return request_is_valid
def authenticate(self, passw, user, options=None): """ do the authentication on base of password / otp and serial and options, the request parameters. :param passw: the password / otp :param user: the requesting user :param options: the additional request parameters :return: tupple of (success, otp_count - 0 or -1, reply) """ otp_count = -1 reply = None # we do a local pin check _res, pin, otpval = split_pin_otp(self, passw, user, options=options) res = check_pin(self, pin, user, options) if res is False: return res, otp_count, reply res, otp_count, reply = self.do_request(otpval, user=user) return res, otp_count, reply
def is_challenge_request(self, passw, user, options=None): """ This method checks, if this is a request, that triggers a challenge. It depends on the way, the pin is checked - either locally or remote :param passw: password, which might be pin or pin+otp :type passw: string :param user: The user from the authentication request :type user: User object :param options: dictionary of additional request parameters :type options: dict :return: true or false """ request_is_valid = False if self.check_pin_local(): pin_match = check_pin(self, passw, user=user, options=options) if pin_match is True: request_is_valid = True elif self.isRemoteChallengeRequest: request_is_valid = True return request_is_valid
def authenticate(self, passw, user, options=None): """ do the authentication on base of password / otp and user and options, the request parameters. Here we contact the other LinOTP server to validate the OtpVal. :param passw: the password / otp :param user: the requesting user :param options: the additional request parameters :return: tupple of (success, otp_count - 0 or -1, reply) """ log.debug("authenticate") res = False otp_counter = -1 reply = None otpval = passw # should we check the pin localy?? if self.check_pin_local(): (res, pin, otpval) = split_pin_otp(self, passw, user, options=options) res = check_pin(self, pin, user=user, options=options) if res is False: return (res, otp_counter, reply) (res, otp_count, reply) = self.do_request(otpval, user=user) return (res, otp_count, reply)
def is_challenge_response(self, passw, user, options=None, challenges=None): """ check if the request contains the result of a challenge :param passw: password, which might be pin or pin+otp :param user: the requesting user :param options: dictionary of additional request parameters :param challenges: Not used in this method #TODO :return: returns true or false """ if "state" in options or "transactionid" in options: return True # LEGACY: some client applications can not process transaction ids. # to support them, we provide a workaround heuristic with pin+otp # during the verification that (policy_type, pin, otp_val) = split_pin_otp(self, passw, user=user, options=options) if policy_type >= 0 and len(otp_val) == self.getOtpLen(): return check_pin(self, pin, user=user, options=options) return False
def update(self, param, reset_failcount=False): self.setSyncWindow(0) self.setOtpLen(32) self.setCounterWindow(0) tdesc = param.get("description") if tdesc is not None: self.token.setDescription(tdesc) # requested_phase must be either "registration1" or "registration2" # current_phase is either "registration" or "authentication" requested_phase = param.get("phase") current_phase = self.getFromTokenInfo("phase", None) if requested_phase == "registration1" and current_phase is None: # This initial registration phase triggers a challenge # which is sent to the FIDO U2F compatible client device # Set the optional token pin in this first phase pin = param.get("pin") if pin is not None: TokenClass.setPin(self, pin) # preserve the registration state self.addToTokenInfo("phase", "registration") self.token.LinOtpIsactive = False elif ( requested_phase == "registration2" and current_phase == "registration" ): # Check the token pin pin = param.get("pin") if pin is None: pin = "" if check_pin(self, pin) is False: raise ValueError("Wrong token pin!") # check for set phases which are not "registration1" or "registration2" elif ( requested_phase != "registration2" and requested_phase is not None ): raise Exception("Wrong phase parameter!") # only allow empty phase parameters once the token is registered # successfully elif current_phase != "authentication" and requested_phase is None: raise Exception("Wrong phase parameter!") # only allow "registration2" if the token already completed # "registration1" elif ( current_phase != "registration" and requested_phase == "registration2" ): raise Exception( "Phase 'registration2' requested but we are not in the correct phase \ to process the request." ) else: raise Exception( 'Unknown "phase" and "current_phase" parameter combination!' )
def checkResponse4Challenge( self, user, passw, options=None, challenges=None ): """ verify the response of a previous challenge :param user: the requesting user :param passw: the to be checked pass (pin+otp) :param options: options an additional argument, which could be token specific :param challenges: the list of challenges, where each challenge is described as dict :return: tuple of (otpcounter and the list of matching challenges) do the standard check for the response of the challenge + change the tokeninfo data of the last challenge """ tok = super(SmsTokenClass, self) counter = self.getOtpCount() window = self.getOtpCountWindow() now = datetime.datetime.now() timeScope = self.loadLinOtpSMSValidTime() otp_val = passw # # fallback: do we have pin+otp ?? (res, pin, otp) = split_pin_otp( self, passw, user=user, options=options ) if res >= 0: res = check_pin(self, pin, user=user, options=options) if res is True: otp_val = otp if otp_val and not challenges: otp_count = self.checkOtp( otp_val, counter, window, options=options ) return otp_count, [] otp_count = -1 matching = [] for challenge in challenges: _otp_count = self.checkOtp( otp_val, counter, window, options=options ) if _otp_count > 0: matching.append(challenge) # ensure that a positive otp_counter is preserved otp_count = _otp_count return (otp_count, matching)
def checkResponse4Challenge(self, user, passw, options=None, challenges=None): """ This method verifies if the given ``passw`` matches any existing ``challenge`` of the token. It then returns the new otp_counter of the token and the list of the matching challenges. In case of success the otp_counter needs to be > 0. The matching_challenges is passed to the method :py:meth:`~linotp.tokens.base.TokenClass.challenge_janitor` to clean up challenges. :param user: the requesting user :type user: User object :param passw: the password (pin+otp) :type passw: string :param options: additional arguments from the request, which could be token specific :type options: dict :param challenges: A sorted list of valid challenges for this token. :type challenges: list :return: tuple of (otpcounter and the list of matching challenges) """ otp_counter = -1 transid = None matching = None matching_challenges = [] # fetch the transactionid if 'transactionid' in options: transid = options.get('transactionid', None) # check if the transactionid is in the list of challenges if transid is not None: for challenge in challenges: if Challenges.is_same_transaction(challenge, transid): matching = challenge break if matching is not None: # Split pin from otp and check the resulting pin and otpval (pin, otpval) = self.splitPinPass(passw) if not check_pin(self, pin, user=user, options=options): otpval = passw # The U2F checkOtp functions needs to know the saved challenge # to compare the received challenge value to the saved one, # thus we add the transactionid to the options options['transactionid'] = transid options['challenges'] = challenges otp_counter = check_otp(self, otpval, options=options) if otp_counter >= 0: matching_challenges.append(matching) return (otp_counter, matching_challenges)
def authenticate(self, passw, user, options=None): """ in case of a wrong passw, we return a bad matching pin, so the result will be an invalid token """ otp_count = -1 pin_match = check_pin(self, passw, user=user, options=options) if pin_match == True: otp_count = 0 return (pin_match, otp_count, None)
def authenticate(self, passw, user, options=None): ''' in case of a wrong passw, we return a bad matching pin, so the result will be an invalid token ''' otp_count = -1 pin_match = check_pin(self, passw, user=user, options=options) if pin_match == True: otp_count = 0 self.auth_info = {'auth_info': [('pin_length', len(passw))]} return (pin_match, otp_count, None)
def authenticate(self, passw, user, options=None): ''' in case of a wrong passw, we return a bad matching pin, so the result will be an invalid token ''' otp_count = -1 pin_match = check_pin(self, passw, user=user, options=options) if pin_match is True: otp_count = 0 self.auth_info = {'auth_info': [('pin_length', len(passw))]} return (pin_match, otp_count, None)
def update(self, param, reset_failcount=False): self.setSyncWindow(0) self.setOtpLen(32) self.setCounterWindow(0) tdesc = getParam(param, "description", optional) if tdesc is not None: self.token.setDescription(tdesc) # requested_phase must be either "registration1" or "registration2" # current_phase is either "registration" or "authentication" requested_phase = getParam(param, "phase", optional) current_phase = self.getFromTokenInfo('phase', None) if requested_phase == "registration1" and current_phase is None: # This initial registration phase triggers a challenge # which is sent to the FIDO U2F compatible client device # Set the optional token pin in this first phase pin = getParam(param, "pin", optional) if pin is not None: TokenClass.setPin(self, pin) # preserve the registration state self.addToTokenInfo('phase', 'registration') self.token.LinOtpIsactive = False elif requested_phase == "registration2" and current_phase == "registration": # Check the token pin pin = getParam(param, "pin", optional) if pin is None: pin = '' if check_pin(self, pin) is False: log.error("Wrong token pin!") raise ValueError("Wrong token pin!") # check for set phases which are not "registration1" or "registration2" elif requested_phase != "registration2" and requested_phase is not None: log.error('Wrong phase parameter!') raise Exception('Wrong phase parameter!') # only allow empty phase parameters once the token is registered successfully elif current_phase != "authentication" and requested_phase is None: log.error('Wrong phase parameter!') raise Exception('Wrong phase parameter!') # only allow "registration2" if the token already completed "registration1" elif current_phase != "registration" and requested_phase == "registration2": log.error( "Phase 'registration2' requested but we are not in the correct phase \ to process the request.") raise Exception( "Phase 'registration2' requested but we are not in the correct phase \ to process the request.") else: log.error('Unknown "phase" and "current_phase" parameter combination!') raise Exception('Unknown "phase" and "current_phase" parameter combination!')
def checkResponse4Challenge( self, user, passw, options=None, challenges=None ): """ This method verifies if the given ``passw`` matches any existing ``challenge`` of the token. It then returns the new otp_counter of the token and the list of the matching challenges. In case of success the otp_counter needs to be > 0. The matching_challenges is passed to the method :py:meth:`~linotp.tokens.base.TokenClass.challenge_janitor` to clean up challenges. :param user: the requesting user :type user: User object :param passw: the password (pin+otp) :type passw: string :param options: additional arguments from the request, which could be token specific :type options: dict :param challenges: A sorted list of valid challenges for this token. :type challenges: list :return: tuple of (otpcounter and the list of matching challenges) """ if not challenges: return -1, [] otp_counter = -1 matching_challenges = [] for challenge in challenges: # Split pin from otp and check the resulting pin and otpval (pin, otpval) = self.splitPinPass(passw) if not check_pin(self, pin, user=user, options=options): otpval = passw # The U2F checkOtp functions needs to know the saved challenge # to compare the received challenge value to the saved one, # thus we add the transactionid to the options options["transactionid"] = challenge.transid options["challenges"] = challenges _otp_counter = check_otp(self, otpval, options=options) if _otp_counter >= 0: matching_challenges.append(challenge) # ensure that a positive otp_counter is preserved otp_counter = _otp_counter return otp_counter, matching_challenges
def is_challenge_request(self, passw, user, options=None): """ check if the request would start a challenge - default: if the passw contains only the pin, this request would trigger a challenge - in this place as well the policy for a token is checked :param passw: password, which might be pin or pin+otp :param options: dictionary of additional request parameters :return: returns true or false """ return check_pin(self, passw, user=user, options=options)
def checkResponse4Challenge(self, user, passw, options=None, challenges=None): """ verify the response of a previous challenge :param user: the requesting user :param passw: the to be checked pass (pin+otp) :param options: options an additional argument, which could be token specific :param challenges: the list of challenges, where each challenge is described as dict :return: tuple of (otpcounter and the list of matching challenges) do the standard check for the response of the challenge + change the tokeninfo data of the last challenge """ otp_count = -1 matching = [] tok = super(SmsTokenClass, self) counter = self.getOtpCount() window = self.getOtpCountWindow() now = datetime.datetime.now() timeScope = self.loadLinOtpSMSValidTime() otp_val = passw # # fallback: do we have pin+otp ?? (res, pin, otp) = split_pin_otp(self, passw, user=user, options=options) if res >= 0: res = check_pin(self, pin, user=user, options=options) if res == True: otp_val = otp for challenge in challenges: otp_count = self.checkOtp(otp_val, counter, window, options=options) if otp_count > 0: matching.append(challenge) break return (otp_count, matching)
def is_challenge_request(self, passw, user, options=None): """ This method checks, if this is a request, that triggers a challenge. The pin is checked locally only :param passw: password, which might be pin or pin+otp :param user: The user from the authentication request :param options: dictionary of additional request parameters :return: true or false """ request_is_valid = False pin_match = check_pin(self, passw, user=user, options=options) if pin_match is True: request_is_valid = True return request_is_valid
def checkResponse4Challenge(self, user, passw, options=None, challenges=None): """ verify the response of a previous challenge :param user: requesting user :param passw: to be checked pass (pin+otp) :param options: an additional argument, which could be token specific :param challenges: list of challenges, where each challenge is described as dict :return: tuple containing a) otp counter and b) the list of matching challenges: (a,b) do the standard check for the response of the challenge + change the tokeninfo data of the last challenge """ otp_count = -1 matching = [] # in var passw might be only the otp, otherwise otp_val will be # overwritten later. otp_val = passw # # fallback: do we have pin+otp ?? (active_pin_policy, pin, otp) = split_pin_otp(self, passw, user=user, options=options) if active_pin_policy >= 0: res = check_pin(self, pin, user=user, options=options) if res is True: otp_val = otp for challenge in challenges: counter_from_challenge = challenge.get('data').get('counter') otp_count = self.check_otp(otp_value=otp_val, counter=int(counter_from_challenge)) if otp_count > 0: matching.append(challenge) break return otp_count, matching
def is_challenge_request(self, passw, user, options=None): ''' This method checks, if this is a request, that triggers a challenge. :param passw: password, which might be pin or pin+otp :type passw: string :param user: The user from the authentication request :type user: User object :param options: dictionary of additional request parameters :type options: dict :return: true or false ''' request_is_valid = False pin_match = check_pin(self, passw, user=user, options=options) if pin_match is True: request_is_valid = True return request_is_valid
def checkResponse4Challenge(self, user, passw, options=None, challenges=None): """ verify the response of a previous challenge There are two possible cases: 1) The 'transaction_id' (also know as 'state', which has the same value) is available in options 2) No 'transaction_id' In the first case we can safely assume that the passw only contains the OTP (no pin). In the second case passw will contain both and we split to get the OTP. :param user: the requesting user :param passw: the to be checked pass (pin+otp) :param options: options an additional argument, which could be token specific :param challenges: the list of challenges, where each challenge is described as dict :return: tuple of (otpcounter and the list of matching challenges) """ if not challenges: return -1, [] transaction_id = options and options.get("transactionid", options.get("state", None)) if transaction_id: otp = passw # if the transaction_id is set we can assume that we have only # received a single challenge with that transaction_id thanks to # linotp.lib.validate.ValidateToken.get_challenges() assert len(challenges) == 1 else: # If no transaction_id is set the request came through the WebUI # and we have to check all challenges split_status, pin, otp = split_pin_otp(self, passw, user, options) if split_status < 0: raise Exception("Could not split passw") if not check_pin(self, pin, user, options): return -1, [] window = self.getOtpCountWindow() otp_counter = -1 matching_challenges = [] for challenge in challenges: challenge_data = challenge.getData() stored_counter = int(challenge_data.get("counter_value", -1)) _otp_counter = self.checkOtp(otp, stored_counter, window, options) if _otp_counter > 0 and _otp_counter == stored_counter: matching_challenges.append(challenge) # ensure that a positive otp_counter is preserved otp_counter = _otp_counter return otp_counter, matching_challenges
def checkResponse4Challenge(self, user, passw, options=None, challenges=None): """ verify the response of a previous challenge There are two possible cases: 1) The 'transaction_id' (also know as 'state', which has the same value) is available in options 2) No 'transaction_id' In the first case we can safely assume that the passw only contains the OTP (no pin). In the second case passw will contain both and we split to get the OTP. :param user: the requesting user :param passw: the to be checked pass (pin+otp) :param options: options an additional argument, which could be token specific :param challenges: the list of challenges, where each challenge is described as dict :return: tuple of (otpcounter and the list of matching challenges) """ transaction_id = None otp_counter = -1 matching_challenges = [] if challenges is None or len(challenges) == 0: # There are no challenges for this token return -1, [] if options and ('transactionid' in options or 'state' in options): # fetch the transactionid transaction_id = options.get('transactionid', None) if transaction_id is None: transaction_id = options.get('state', None) if transaction_id: otp = passw # if the transaction_id is set we can assume that we have only # received a single challenge with that transaction_id thanks to # linotp.lib.validate.ValidateToken.get_challenges() assert(len(challenges) == 1) assert(Challenges.is_same_transaction(challenges[0], transaction_id)) else: # If no transaction_id is set the request came through the WebUI # and we have to check all challenges split_status, pin, otp = split_pin_otp(self, passw, user, options) if split_status < 0: raise Exception("Could not split passw") if not check_pin(self, pin, user, options): return -1, [] window = self.getOtpCountWindow() for challenge in challenges: challenge_data = challenge.getData() stored_counter = challenge_data.get("counter_value") temp_otp_counter = self.checkOtp(otp, int(stored_counter), window, options) if temp_otp_counter > 0: otp_counter = temp_otp_counter matching_challenges = [challenge] break # The matching_challenges list will either contain a single challenge # or will be empty. Returning multiple challenges is not useful in this # case because all older challenges arecleaned up anyway. return otp_counter, matching_challenges