def finish_challenge_token(self): """ processing of the challenge tokens """ challenge_tokens = self.challenge_tokens options = self.options if not options: options = {} action_detail = 'challenge created' if len(challenge_tokens) == 1: challenge_token = challenge_tokens[0] _res, reply = Challenges.create_challenge( challenge_token, options=options) return (False, reply, action_detail) # processing of multiple challenges else: # for each token, who can submit a challenge, we have to # create the challenge. To mark the challenges as depending # the transaction id will have an id that all sub transaction share # and a postfix with their enumaration. Finally the result is # composed by the top level transaction id and the message # and below in a dict for each token a challenge description - # the key is the token type combined with its token serial number all_reply = {'challenges': {}} challenge_count = 0 transactionid = '' challenge_id = "" for challenge_token in challenge_tokens: challenge_count += 1 id_postfix = ".%02d" % challenge_count if transactionid: challenge_id = "%s%s" % (transactionid, id_postfix) (_res, reply) = Challenges.create_challenge( challenge_token, options=options, challenge_id=challenge_id, id_postfix=id_postfix ) transactionid = reply.get('transactionid').rsplit('.')[0] # add token type and serial to ease the type specific processing reply['linotp_tokentype'] = challenge_token.type reply['linotp_tokenserial'] = challenge_token.getSerial() key = challenge_token.getSerial() all_reply['challenges'][key] = reply # finally add the root challenge response with top transaction id # and message, that indicates that 'multiple challenges have been # submitted all_reply['transactionid'] = transactionid all_reply['message'] = "Multiple challenges submitted." log.debug("Multiple challenges submitted: %d", len(challenge_tokens)) return (False, all_reply, action_detail)
def finish_challenge_token(self): """ processing of the challenge tokens """ challenge_tokens = self.challenge_tokens options = self.options if not options: options = {} action_detail = 'challenge created' if len(challenge_tokens) == 1: challenge_token = challenge_tokens[0] _res, reply = Challenges.create_challenge(challenge_token, options=options) return (False, reply, action_detail) # processing of multiple challenges else: # for each token, who can submit a challenge, we have to # create the challenge. To mark the challenges as depending # the transaction id will have an id that all sub transaction share # and a postfix with their enumeration. Finally the result is # composed by the top level transaction id and the message # and below in a dict for each token a challenge description - # the key is the token type combined with its token serial number all_reply = {'challenges': {}} challenge_count = 0 transactionid = '' challenge_id = "" for challenge_token in challenge_tokens: challenge_count += 1 id_postfix = ".%02d" % challenge_count if transactionid: challenge_id = "%s%s" % (transactionid, id_postfix) (_res, reply) = Challenges.create_challenge( challenge_token, options=options, challenge_id=challenge_id, id_postfix=id_postfix) transactionid = reply.get('transactionid').rsplit('.')[0] key = challenge_token.getSerial() all_reply['challenges'][key] = reply # finally add the root challenge response with top transaction id # and message, that indicates that 'multiple challenges have been # submitted all_reply['transactionid'] = transactionid all_reply['message'] = "Multiple challenges submitted." log.debug("Multiple challenges submitted: %d", len(challenge_tokens)) return (False, all_reply, action_detail)
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() 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_pin_matching_tokens(self): """ check, if there have been some tokens where the pin matched (but OTP failed and increment only these """ pin_matching_tokens = self.pin_matching_tokens action_detail = "wrong otp value" for tok in pin_matching_tokens: tok.statusValidationFail() tok.inc_count_auth() Challenges.finish_challenges(tok, success=False) return (False, None, action_detail)
def get_transaction_detail(transactionid): """Provide the information about a transaction. :param transactionid: the transaction id :return: dict with detail about challenge status """ _exp, challenges = Challenges.get_challenges(transid=transactionid) if not challenges: return {} challenge = challenges[0] challenge_session = challenge.getSession() if challenge_session: challenge_session = json.loads(challenge_session) else: challenge_session = {} details = { 'received_count': challenge.received_count, 'received_tan': challenge.received_tan, 'valid_tan': challenge.valid_tan, 'message': challenge.getChallenge(), 'status': challenge.getStatus(), 'accept': challenge_session.get('accept', False), 'reject': challenge_session.get('reject', False), } return details
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) ''' otp_counter = -1 transid = None matching = None matchin_challenges = [] if 'transactionid' in options or 'state' in options: ## fetch the transactionid transid = options.get('transactionid', options.get('state', 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: otp_counter = check_otp(self, passw, options=options) if otp_counter >= 0: matchin_challenges.append(matching) return (otp_counter, matchin_challenges)
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 check_by_transactionid(self, transid, passw, options=None): """ check the passw against the open transaction :param transid: the transaction id :param passw: the pass parameter :param options: the additional optional parameters :return: tuple of boolean and detail dict """ reply = {} serials = [] challenges = Challenges.lookup_challenges(transid=transid) for challenge in challenges: serials.append(challenge.tokenserial) if not serials: reply['value'] = False reply['failure'] = ('No challenge for transaction %r found' % transid) return False, reply reply['failcount'] = 0 reply['value'] = False reply['token_type'] = '' for serial in serials: tokens = getTokens4UserOrSerial(serial=serial) if not tokens: raise Exception('tokenmismatch for token serial: %s' % (unicode(serial))) # there could be only one token = tokens[0] owner = linotp.lib.token.get_token_owner(token) (ok, opt) = self.checkTokenList(tokens, passw, user=owner, options=options) if opt: reply.update(opt) reply['token_type'] = token.getType() reply['failcount'] = token.getFailCount() reply['value'] = ok if ok: break return ok, reply
def finish_invalid_tokens(self): """ """ invalid_tokens = self.invalid_tokens user = self.user for tok in invalid_tokens: tok.statusValidationFail() Challenges.finish_challenges(tok, success=False) import linotp.lib.policy pin_policies = linotp.lib.policy.get_pin_policies(user) or [] if 1 in pin_policies: action_detail = "wrong user password -1" else: action_detail = "wrong otp pin -1" return (False, None, action_detail)
def test_transactionid_length(self): with patch('linotp.lib.challenges.context') as mock_context: mock_context.get.return_value = {} transid_length = Challenges.get_tranactionid_length() self.assertAlmostEqual( transid_length, Challenges.DefaultTransactionIdLength) with self.assertRaises(Exception) as wrong_range: too_short_length = 7 wrong_range_message = \ "TransactionIdLength must be between 12 and 17, " \ "was %d" % too_short_length mock_context.get.return_value = { 'TransactionIdLength': too_short_length } Challenges.get_tranactionid_length() self.assertEqual(wrong_range.exception.message, wrong_range_message)
def test_transactionid_length(self): with patch('linotp.lib.challenges.context') as mock_context: mock_context.get.return_value = {} transid_length = Challenges.get_tranactionid_length() assert round( abs(transid_length - Challenges.DefaultTransactionIdLength), 7) == 0 too_short_length = 7 wrong_range_message = \ "TransactionIdLength must be between 12 and 17, " \ "was %d" % too_short_length mock_context.get.return_value = { 'TransactionIdLength': too_short_length } with pytest.raises(Exception) as wrong_range: Challenges.get_tranactionid_length() assert str(wrong_range.value) == wrong_range_message
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 = [] if 'transactionid' in options or 'state' in options: # fetch the transactionid transid = options.get('transactionid', None) if transid is None: transid = options.get('state', 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: otp_counter = check_otp(self, passw, options=options) if otp_counter >= 0: matching_challenges.append(matching) return (otp_counter, matching_challenges)
def checkSerialPass(self, serial, passw, options=None, user=None): """ This function checks the otp for a given serial :attention: the parameter user must be set, as the pin policy==1 will verify the user pin """ token_type = options.get("token_type", None) tokenList = getTokens4UserOrSerial(None, serial, token_type=token_type, read_for_update=True) if passw is None: # other than zero or one token should not happen, as serial is # unique if len(tokenList) == 1: theToken = tokenList[0] tok = theToken.token realms = tok.getRealmNames() if realms is None or len(realms) == 0: realm = getDefaultRealm() elif len(realms) > 0: realm = realms[0] userInfo = getUserInfo( tok.LinOtpUserid, tok.LinOtpIdResolver, tok.LinOtpIdResClass, ) user = User(login=userInfo.get("username"), realm=realm) user.info = userInfo if theToken.is_challenge_request(passw, user, options=options): (res, opt) = Challenges.create_challenge(theToken, options) res = False else: raise ParameterError("Missing parameter: pass", id=905) else: raise Exception("No token found: " "unable to create challenge for %s" % serial) else: (res, opt) = self.checkTokenList(tokenList, passw, user=user, options=options) return (res, opt)
def finish_invalid_tokens(self): """""" invalid_tokens = self.invalid_tokens user = self.user for tok in invalid_tokens: # count all token accesses if tok.count_auth_max > 0: tok.inc_count_auth() tok.statusValidationFail() Challenges.finish_challenges(tok, success=False) pin_policies = get_pin_policies(user) or [] if 1 in pin_policies: action_detail = "wrong user password -1" else: action_detail = "wrong otp pin -1" return (False, None, action_detail)
def finish_invalid_tokens(self): """ """ invalid_tokens = self.invalid_tokens user = self.user for tok in invalid_tokens: # count all token accesses if tok.count_auth_max > 0: tok.inc_count_auth() tok.statusValidationFail() Challenges.finish_challenges(tok, success=False) pin_policies = get_pin_policies(user) or [] if 1 in pin_policies: action_detail = "wrong user password -1" else: action_detail = "wrong otp pin -1" return (False, None, action_detail)
def checkSerialPass(self, serial, passw, options=None, user=None): """ This function checks the otp for a given serial :attention: the parameter user must be set, as the pin policy==1 will verify the user pin """ log.debug('checking for serial %r' % serial) tokenList = linotp.lib.token.getTokens4UserOrSerial( None, serial) if passw is None: # other than zero or one token should not happen, as serial is # unique if len(tokenList) == 1: theToken = tokenList[0] tok = theToken.token realms = tok.getRealmNames() if realms is None or len(realms) == 0: realm = getDefaultRealm() elif len(realms) > 0: realm = realms[0] userInfo = getUserInfo(tok.LinOtpUserid, tok.LinOtpIdResolver, tok.LinOtpIdResClass) user = User(login=userInfo.get('username'), realm=realm) user.info = userInfo if theToken.is_challenge_request(passw, user, options=options): (res, opt) = Challenges.create_challenge( theToken, options) res = False else: raise ParameterError('Missing parameter: pass', id=905) else: raise Exception('No token found: ' 'unable to create challenge for %s' % serial) else: log.debug('checking len(pass)=%r for serial %r' % (len(passw), serial)) (res, opt) = self.checkTokenList( tokenList, passw, user=user, options=options) return (res, opt)
def checkSerialPass(self, serial, passw, options=None, user=None): """ This function checks the otp for a given serial :attention: the parameter user must be set, as the pin policy==1 will verify the user pin """ log.debug('checking for serial %r' % serial) tokenList = linotp.lib.token.getTokens4UserOrSerial(None, serial) if passw is None: # other than zero or one token should not happen, as serial is # unique if len(tokenList) == 1: theToken = tokenList[0] tok = theToken.token realms = tok.getRealmNames() if realms is None or len(realms) == 0: realm = getDefaultRealm() elif len(realms) > 0: realm = realms[0] userInfo = getUserInfo(tok.LinOtpUserid, tok.LinOtpIdResolver, tok.LinOtpIdResClass) user = User(login=userInfo.get('username'), realm=realm) user.info = userInfo if theToken.is_challenge_request(passw, user, options=options): (res, opt) = Challenges.create_challenge(theToken, options) res = False else: raise ParameterError('Missing parameter: pass', id=905) else: raise Exception('No token found: ' 'unable to create challenge for %s' % serial) else: log.debug('checking len(pass)=%r for serial %r' % (len(passw), serial)) (res, opt) = self.checkTokenList(tokenList, passw, user=user, options=options) return (res, opt)
def statusValidationFail(self): ''' statusValidationFail - callback to enable a status change, will be called if the token verification has failed :return - nothing ''' log.debug('[statusValidationFail]') challenge = None if self.transId == 0 : return try: challenges = Challenges.lookup_challenges(self.context, self.getSerial(), transid=self.transId) if len(challenges) == 1: challenge = challenges[0] challenge.setTanStatus(received=True, valid=False) ## still in rollout state?? rolloutState = self.getFromTokenInfo('rollout', '0') if rolloutState == '1': log.info('rollout state 1 for token %r not completed' % (self.getSerial())) elif rolloutState == '2': if challenge.received_count >= int(getFromConfig("OcraMaxChallengeRequests", '3')): ## after 3 fails in rollout state 2 - reset to rescan self.addToTokenInfo('rollout', '1') log.info('rollout for token %r reset to phase 1:' % (self.getSerial())) log.info('rollout for token %r not completed' % (self.getSerial())) except Exception as ex: log.exception('[Ocra2TokenClass:statusValidationFail] Error during validation finalisation for token %r :%r' % (self.getSerial(), ex)) raise Exception(ex) finally: if challenge is not None: challenge.save() log.debug('[statusValidationFail]') return
def check_challenge_response(self, challenges, user, passw, options=None): """ This function checks, if the given response (passw) matches any of the open challenges to prevent the token author to deal with the database layer, the token.checkResponse4Challenge will recieve only the dictionary of the challenge data :param challenges: the list of database challenges :param user: the requesting use :param passw: the to password of the request, which must be pin+otp :param options: the addtional request parameters :return: tuple of otpcount (as result of an internal token.checkOtp) and additional optional reply """ # challenge reply will stay None as we are in the challenge response # mode reply = None if options is None: options = {} otp = passw self.transId = options.get('transactionid', options.get('state', None)) # only check those challenges, which currently have not been verified check_challenges = [] for ch in challenges: if Challenges.verify_checksum(ch) and ch.is_open(): check_challenges.append(ch) (otpcount, matching_challenges) = self.checkResponse4Challenge( user, otp, options=options, challenges=check_challenges) if otpcount >= 0: self.matching_challenges = matching_challenges self.valid_token.append(self) if len(self.invalid_token) > 0: del self.invalid_token[0] else: self.invalid_token.append(self) return (otpcount, reply)
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) ''' otp_counter = -1 transid = None matching = None matching_challenges = [] if 'transactionid' in options or 'state' in options: ## fetch the transactionid transid = options.get('transactionid', options.get('state', None)) if not transid and self.authenticated is not None: pin_match, otp_counter, reply = self.authenticated return otp_counter, matching_challenges # 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: otp_counter = check_otp(self, passw, options=options) if otp_counter >= 0: matching_challenges.append(matching) return (otp_counter, matching_challenges)
def statusValidationSuccess(self): ''' statusValidationSuccess - callback to enable a status change, remark: will be called if the token has been succesfull verified :return: - nothing ''' log.debug('[statusValidationSuccess]') if self.transId == 0 : return challenges = Challenges.lookup_challenges(self.context, self.getSerial(), transid=self.transId) if len(challenges) == 1: challenge = challenges[0] challenge.setTanStatus(True, True) challenge.save() ## still in rollout state?? rolloutState = self.getFromTokenInfo('rollout', '0') if rolloutState == '2': t_info = self.getTokenInfo() if t_info.has_key('rollout'): del t_info['rollout'] if t_info.has_key('sharedSecret'): del t_info['sharedSecret'] if t_info.has_key('nonce'): del t_info['nonce'] self.setTokenInfo(t_info) log.info('rollout for token %r completed' % (self.getSerial())) elif rolloutState == '1': raise Exception('unable to complete the rollout ') log.debug('[statusValidationSuccess]:') return
def getStatus(self, transactionId): ''' getStatus - assembles the status of a transaction / challenge in a dict { "serial": SERIENNUMMER1, "transactionid": TRANSACTIONID1, "received_tan": true, "valid_tan": true, "failcount": 0 } :param transactionId: the transaction / challenge id :type transactionId: string :return: status dict :rtype: dict ''' log.debug('[getStatus] %r' % (transactionId)) statusDict = {} challenge = Challenges.lookup_challenges(self.context, self.getSerial(), transid=transactionId) if challenge is not None: statusDict['serial'] = challenge.tokenserial statusDict['transactionid'] = challenge.transid statusDict['received_tan'] = challenge.received_tan statusDict['valid_tan'] = challenge.valid_tan statusDict['failcount'] = self.getFailCount() statusDict['id'] = challenge.id statusDict['timestamp'] = unicode(challenge.timestamp) statusDict['active'] = unicode(self.isActive()) log.debug('[getStatus]: %r' % (statusDict)) return statusDict
def challenge(self, data, session='', typ='raw', challenge=None): ''' the challenge method is for creating an transaction / challenge object remark: the transaction has a maximum lifetime and a reference to the OcraSuite token (serial) :param data: data, which is the base for the challenge or None :type data: string or None :param session: session support for ocratokens :type session: string :type typ: define, which kind of challenge base should be used could be raw - take the data input as is (extract chars accordind challenge definition Q) or random - will generate a random input or hased - will take the hash of the input data :return: challenge response containing the transcation id and the challenge for the ocrasuite :rtype : tuple of (transId(string), challenge(string)) ''' log.debug('[challenge] %r: %r: %r' % (data, session, challenge)) secretHOtp = self.token.getHOtpKey() ocraSuite = OcraSuite(self.getOcraSuiteSuite(), secretHOtp) if data is None or len(data) == 0: typ = 'random' if challenge is None: if typ == 'raw': challenge = ocraSuite.data2rawChallenge(data) elif typ == 'random': challenge = ocraSuite.data2randomChallenge(data) elif typ == 'hash': challenge = ocraSuite.data2hashChallenge(data) log.debug('[Ocra2TokenClass] challenge: %r ' % (challenge)) counter = self.getOtpCount() ## set the pin onyl in the compliant hashed mode pin = '' if ocraSuite.P is not None: pinObj = self.token.getUserPin() pin = pinObj.getKey() try: param = {} param['C'] = counter param['Q'] = challenge param['P'] = pin param['S'] = session if ocraSuite.T is not None: now = datetime.datetime.now() stime = now.strftime("%s") itime = int(stime) param['T'] = itime ''' verify that the data is compliant with the OcraSuitesuite and the client is able to calc the otp ''' c_data = ocraSuite.combineData(**param) ocraSuite.compute(c_data) except Exception as ex: raise Exception('[Ocra2TokenClass] Failed to create ocrasuite ' 'challenge: %r' % (ex)) ## create a non exisiting challenge try: (res, opt) = Challenges.create_challenge(self, self.context, options={'messgae': data}) transid = opt.get('transactionid') challenge = opt.get('challenge') except Exception as ex: ## this might happen if we have a db problem or ## the uniqnes constrain does not fit log.exception("[Ocra2TokenClass] %r" % ex) raise Exception('[Ocra2TokenClass] Failed to create ' 'challenge object: %s' % (ex)) realm = None realms = self.token.getRealms() if len(realms) > 0: realm = realms[0] url = '' if realm is not None: url = get_qrtan_url(realm.name, context=self.context) log.debug('[challenge]: %r: %r: %r' % (transid, challenge, url)) return (transid, challenge, True, url)
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
def checkOtp(self, passw , counter, window, options=None): ''' checkOtp - standard callback of linotp to verify the token :param passw: the passw / otp, which has to be checked :type passw: string :param counter: the start counter :type counter: int :param window: the window, in which the token is valid :type window: int :param options: options contains the transaction id, eg. if check_t checks one transaction this will support assynchreonous otp checks (when check_t is used) :type options: dict :return: verification counter or -1 :rtype: int (-1) ''' log.debug('[checkOtp] %r: %r: %r' % (passw, counter, window)) ret = -1 challenges = [] serial = self.getSerial() if options is None: options = {} maxRequests = int(getFromConfig("Ocra2MaxChallengeRequests", '3')) if 'transactionid' in options: transid = options.get('transactionid', None) challs = Challenges.lookup_challenges(self.context, serial=serial, transid=transid) for chall in challs: (rec_tan, rec_valid) = chall.getTanStatus() if rec_tan == False: challenges.append(chall) elif rec_valid == False: ## add all touched but failed challenges if chall.getTanCount() <= maxRequests: challenges.append(chall) if 'challenge' in options: ## direct challenge - there might be addtionalget info like ## session data in the options challenges.append(options) if len(challenges) == 0: challs = Challenges.lookup_challenges(self.context, serial=serial) for chall in challs: (rec_tan, rec_valid) = chall.getTanStatus() if rec_tan == False: ## add all untouched challenges challenges.append(chall) elif rec_valid == False: ## add all touched but failed challenges if chall.getTanCount() <= maxRequests: challenges.append(chall) if len(challenges) == 0: err = 'No open transaction found for token %s' % serial log.error(err) ##TODO should log and fail!! raise Exception(err) ## prepare the challenge check - do the ocra setup secretHOtp = self.token.getHOtpKey() ocraSuite = OcraSuite(self.getOcraSuiteSuite(), secretHOtp) ## set the ocra token pin ocraPin = '' if ocraSuite.P is not None: ocraPinObj = self.token.getUserPin() ocraPin = ocraPinObj.getKey() if ocraPin is None or len(ocraPin) == 0: ocraPin = '' timeShift = 0 if ocraSuite.T is not None: defTimeWindow = int(getFromConfig("ocra.timeWindow", 180)) window = int(self.getFromTokenInfo('timeWindow', defTimeWindow)) / ocraSuite.T defTimeShift = int(getFromConfig("ocra.timeShift", 0)) timeShift = int(self.getFromTokenInfo("timeShift", defTimeShift)) default_retry_window = int(getFromConfig("ocra2.max_check_challenge_retry", 0)) retry_window = int(self.getFromTokenInfo("max_check_challenge_retry", default_retry_window)) ## now check the otp for each challenge for ch in challenges: challenge = {} ## preserve transaction context, so we could use this in the status callback self.transId = ch.get('transid', None) challenge['transid'] = self.transId challenge['session'] = ch.get('session', None) ## we saved the 'real' challenge in the data data = ch.get('data', None) if data is not None: challenge['challenge'] = data.get('challenge') elif 'challenge' in ch: ## handle explicit challenge requests challenge['challenge'] = ch.get('challenge') if challenge.get('challenge') is None: raise Exception('could not checkOtp due to missing challenge' ' in request: %r' % ch) ret = ocraSuite.checkOtp(passw, counter, window, challenge, pin=ocraPin , options=options, timeshift=timeShift) log.debug('[checkOtp]: %r' % (ret)) ## due to the assynchronous challenge verification of the checkOtp ## it might happen, that the found counter is lower than the given ## one. Thus we fix this here to deny assynchronous verification # we do not support retry checks anymore: # which means, that ret might be smaller than the actual counter if ocraSuite.T is None: if ret + retry_window < counter: ret = -1 if ret != -1: break if -1 == ret: ## autosync: test if two consecutive challenges + it's counter match ret = self.autosync(ocraSuite, passw, challenge) return ret
def checkOtp(self, passwd, counter, window, options=None): valid_states = ['pairing_challenge_sent', 'pairing_complete'] self.ensure_state_is_in(valid_states) # ---------------------------------------------------------------------- filtered_challenges = [] serial = self.getSerial() if options is None: options = {} max_fail = int(getFromConfig('QRMaxChallenges', '3')) # ---------------------------------------------------------------------- # TODO: from which point is checkOtp called, when there # is no challenge response in the request? if 'transactionid' in options: # ------------------------------------------------------------------ # fetch all challenges that match the transaction id or serial transaction_id = options.get('transaction_id') challenges = Challenges.lookup_challenges(serial, transaction_id) # ------------------------------------------------------------------ # filter into filtered_challenges for challenge in challenges: (received_tan, tan_is_valid) = challenge.getTanStatus() fail_counter = challenge.getTanCount() # if we iterate over matching challenges (that is: challenges # with the correct transaction id) we either find a fresh # challenge, that didn't receive a TAN at all (first case) # or a challenge, that already received a number of wrong # TANs but still has tries left (second case). if not received_tan: filtered_challenges.append(challenge) elif not tan_is_valid and fail_counter <= max_fail: filtered_challenges.append(challenge) # ------------------------------------------------------------------ if not filtered_challenges: return -1 for challenge in filtered_challenges: data = challenge.getData() correct_passwd = data['user_sig'] # compare values with python's native constant # time comparison if compare_digest(correct_passwd, passwd): return 1 else: # maybe we got a tan instead of a signature correct_passwd_as_bytes = decode_base64_urlsafe(correct_passwd) tan_length = 8 # TODO fetch from policy correct_tan = extract_tan(correct_passwd_as_bytes, tan_length) # TODO PYLONS-HACK pylons silently converts integers in # incoming json to unicode. since extract_tan returns # an integer, we have to convert it here correct_tan = unicode(correct_tan) if compare_digest(correct_tan, passwd): return 1 return -1 # TODO: ??? semantics of this ret val?
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 checkOtp(self, passwd, counter, window, options=None): """ checks if the supplied challenge response is correct. :param passwd: The challenge response :param options: A dictionary of parameters passed by the upper layer (used for transaction_id in this context) :param counter: legacy API (unused) :param window: legacy API (unused) :raises TokenStateError: If token state is not 'active' or 'pairing_challenge_sent' :returns: -1 for failure, 1 for success """ valid_states = ['pairing_challenge_sent', 'active'] self.ensure_state_is_in(valid_states) # ------------------------------------------------------------------ -- # new pushtoken protocoll supports the keyword based accept or deny. # the old 'passwd' argument is not supported anymore try: signature_accept = passwd.get('accept', None) signature_reject = passwd.get('reject', None) except AttributeError: # will be raised with a get() on a str object raise Exception('Pushtoken version %r requires "accept" or' ' "reject" as parameter' % CHALLENGE_URL_VERSION) if signature_accept is not None and signature_reject is not None: raise Exception('Pushtoken version %r requires "accept" or' ' "reject" as parameter' % CHALLENGE_URL_VERSION) # ------------------------------------------------------------------ -- filtered_challenges = [] serial = self.getSerial() if options is None: options = {} max_fail = int(getFromConfig('PushMaxChallenges', '3')) # ------------------------------------------------------------------ -- if 'transactionid' in options: # -------------------------------------------------------------- -- # fetch all challenges that match the transaction id or serial transaction_id = options.get('transactionid') challenges = Challenges.lookup_challenges(serial=serial, transid=transaction_id, filter_open=True) # -------------------------------------------------------------- -- # filter into filtered_challenges for challenge in challenges: (received_tan, tan_is_valid) = challenge.getTanStatus() fail_counter = challenge.getTanCount() # if we iterate over matching challenges (that is: challenges # with the correct transaction id) we either find a fresh # challenge, that didn't receive a TAN at all (first case) # or a challenge, that already received a number of wrong # TANs but still has tries left (second case). if not received_tan: filtered_challenges.append(challenge) elif not tan_is_valid and fail_counter <= max_fail: filtered_challenges.append(challenge) # ------------------------------------------------------------------ -- if not filtered_challenges: return -1 if len(filtered_challenges) > 1: log.error('multiple challenges for one transaction and for one' ' token found!') return -1 # for the serial and the transaction id there could always be only # at max one challenge matching. This is even true for sub transactions challenge = filtered_challenges[0] # client verifies the challenge by signing the challenge # plaintext. we retrieve the original plaintext (saved # in createChallenge) and check for a match data = challenge.getData() data_to_verify = b64decode(data['sig_base']) b64_dsa_public_key = self.getFromTokenInfo('user_dsa_public_key') user_dsa_public_key = b64decode(b64_dsa_public_key) # -------------------------------------------------------------- -- # handle the accept case if signature_accept is not None: accept_signature_as_bytes = decode_base64_urlsafe( signature_accept) accept_data_to_verify_as_bytes = ( struct.pack('<b', CHALLENGE_URL_VERSION) + b'ACCEPT\0' + data_to_verify) try: verify_sig(accept_signature_as_bytes, accept_data_to_verify_as_bytes, user_dsa_public_key) challenge.add_session_info({'accept': True}) return 1 except ValueError: challenge.add_session_info({'accept': False}) log.error("accept signature mismatch!") return -1 # -------------------------------------------------------------- -- # handle the reject case elif signature_reject is not None: reject_signature_as_bytes = decode_base64_urlsafe( signature_reject) reject_data_to_verify_as_bytes = ( struct.pack('<b', CHALLENGE_URL_VERSION) + b'DENY\0' + data_to_verify) try: verify_sig(reject_signature_as_bytes, reject_data_to_verify_as_bytes, user_dsa_public_key) challenge.add_session_info({'reject': True}) return 1 except ValueError: challenge.add_session_info({'reject': False}) log.error("reject signature mismatch!") return -1 return -1
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) 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 checkTokenList(self, tokenList, passw, user=User(), options=None): """ identify a matching token and test, if the token is valid, locked .. This function is called by checkSerialPass and checkUserPass to :param tokenList: list of identified tokens :param passw: the provided passw (mostly pin+otp) :param user: the identified use - as class object :param options: additional parameters, which are passed to the token :return: tuple of boolean and optional response """ reply = None # add the user to the options, so that every token could see the user if not options: options = {} options["user"] = user # if there has been one token in challenge mode, we only handle # challenges # if we got a validation against a sub_challenge, we extend this to # be a validation to all challenges of the transaction id import copy check_options = copy.deepcopy(options) state = check_options.get("state", check_options.get("transactionid", "")) if state and "." in state: transid = state.split(".")[0] if "state" in check_options: check_options["state"] = transid if "transactionid" in check_options: check_options["transactionid"] = transid # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # transaction id optimization - part 1: # # if we have a transaction id, we check only those tokens # that belong to this transaction id: challenges = [] transaction_serials = [] transid = check_options.get("state", check_options.get("transactionid", "")) # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- audit_entry = {} audit_entry["action_detail"] = "no token found!" challenge_tokens = [] pin_matching_tokens = [] invalid_tokens = [] valid_tokens = [] related_challenges = [] # we have to preserve the result / reponse for token counters validation_results = {} for token in tokenList: audit_entry["serial"] = token.getSerial() audit_entry["token_type"] = token.getType() # preselect: the token must be in the same realm as the user if user is not None: t_realms = token.token.getRealmNames() u_realm = user.realm if (len(t_realms) > 0 and len(u_realm) > 0 and u_realm.lower() not in t_realms): audit_entry[ "action_detail"] = "Realm mismatch for token and user" continue # check if the token is the list of supported tokens # if not skip to the next token in list typ = token.getType() if typ.lower() not in tokenclass_registry: log.error( "token typ %r not found in tokenclasses: %r", typ, list(tokenclass_registry.keys()), ) audit_entry["action_detail"] = "Unknown Token type" continue if not token.isActive(): audit_entry["action_detail"] = "Token inactive" continue if token.getFailCount() >= token.getMaxFailCount(): audit_entry["action_detail"] = "Failcounter exceeded" token.incOtpFailCounter() continue # ---------------------------------------------------------------------- -- # check for restricted path usage path = context["Path"].strip("/").partition("/")[0] token_path = token.getFromTokenInfo("scope", {}).get("path", []) if token_path and path not in token_path: continue # -------------------------------------------------------------- -- # token validity handling if token.is_not_yet_valid(): msg = "Authentication validity period mismatch!" audit_entry["action_detail"] = msg token.incOtpFailCounter() continue if not token.is_valid(): if token.has_exceeded_usage(): msg = "Authentication counter exceeded" elif token.has_exceeded_success(): msg = "Authentication sucess counter exceeded" elif token.is_expired(): msg = "Authentication validity period exceeded" else: raise Exception("Validity check failed without reason") audit_entry["action_detail"] = msg token.incOtpFailCounter() # what should happen with exceeding tokens t_realms = None if not user.login and not user.realm: t_realms = token.token.getRealmNames() if disable_on_authentication_exceed(user, realms=t_realms): token.enable(False) if delete_on_authentication_exceed(user, realms=t_realms): token.deleteToken() continue # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # gather all open challenges for this token if transid: _expired, challenges = Challenges.get_challenges( token=token, transid=transid, filter_open=True) else: # if there is no transaction id given we check all challenges # related to the given token _expired, challenges = Challenges.get_challenges( token=token, filter_open=True, options=check_options) # -------------------------------------------------------------- -- # finally we check the token try: (ret, reply) = token.check_token(passw, user, options=check_options, challenges=challenges) except Exception as exx: # in case of a failure during checking token, we log the error # and continue with the next one log.error("checking token %r failed: %r", token, exx) ret = -1 reply = "%r" % exx audit_entry[ "action_detail"] = "checking token %r failed: %r" % (token, exx) audit_entry["info"] = audit_entry.get("info", "") + "%r" % exx continue finally: validation_results[token.getSerial()] = (ret, reply) (cToken, pToken, iToken, vToken) = token.get_verification_result() related_challenges.extend(token.related_challenges) challenge_tokens.extend(cToken) pin_matching_tokens.extend(pToken) invalid_tokens.extend(iToken) valid_tokens.extend(vToken) valid_tokens = list(set(valid_tokens)) invalid_tokens = list(set(invalid_tokens)) pin_matching_tokens = list(set(pin_matching_tokens)) challenge_tokens = list(set(challenge_tokens)) # end of token verification loop matching_challenges = [] for token in valid_tokens: matching_challenges.extend(token.matching_challenges) matching_challenges = list(set(matching_challenges)) # if there are related / sub challenges, we have to call their janitor Challenges.handle_related_challenge(matching_challenges) # now we finalize the token validation result fh = FinishTokens( valid_tokens, challenge_tokens, pin_matching_tokens, invalid_tokens, validation_results, user, options, audit_entry=audit_entry, ) (res, reply) = fh.finish_checked_tokens() # ------------------------------------------------------------------ -- # add to all tokens the last accessed time stamp add_last_accessed_info( set(valid_tokens + pin_matching_tokens + challenge_tokens + invalid_tokens)) # add time stamp to all valid tokens add_last_verified_info(valid_tokens) # ------------------------------------------------------------------ -- # now we care for all involved tokens and their challenges for token in set(valid_tokens + pin_matching_tokens + challenge_tokens + invalid_tokens): expired, _valid = Challenges.get_challenges(token) if expired: Challenges.delete_challenges(None, expired) log.debug( "Number of valid tokens found (validTokenNum): %d", len(valid_tokens), ) return (res, reply)
def checkTokenList(self, tokenList, passw, user=User(), options=None): """ identify a matching token and test, if the token is valid, locked .. This function is called by checkSerialPass and checkUserPass to :param tokenList: list of identified tokens :param passw: the provided passw (mostly pin+otp) :param user: the identified use - as class object :param options: additonal parameters, which are passed to the token :return: tuple of boolean and optional response """ log.debug("[__checkTokenList] checking tokenlist: %r" % tokenList) reply = None tokenclasses = config['tokenclasses'] # add the user to the options, so that every token could see the user if not options: options = {} options['user'] = user # if there has been one token in challenge mode, we only handle # challenges # if we got a validation against a sub_challenge, we extend this to # be a validation to all challenges of the transaction id import copy check_options = copy.deepcopy(options) state = check_options.get('state', check_options.get('transactionid', '')) if state and '.' in state: transid = state.split('.')[0] if 'state' in check_options: check_options['state'] = transid if 'transactionid' in check_options: check_options['transactionid'] = transid # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # transaction id optimization - part 1: # # if we have a transaction id, we check only those tokens # that belong to this transaction id: challenges = [] transaction_serials = [] transid = check_options.get('state', check_options.get('transactionid', '')) if transid: expired, challenges = Challenges.get_challenges(transid=transid, filter_open=True) for challenge in challenges: serial = challenge.tokenserial transaction_serials.append(serial) # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- audit_entry = {} audit_entry['action_detail'] = "no token found!" challenge_tokens = [] pin_matching_tokens = [] invalid_tokens = [] valid_tokens = [] related_challenges = [] # we have to preserve the result / reponse for token counters validation_results = {} for token in tokenList: log.debug('Found user with loginId %r: %r:\n', token.getUserId(), token.getSerial()) # transaction id optimization - part 2: if transid: if token.getSerial() not in transaction_serials: continue audit_entry['serial'] = token.getSerial() audit_entry['token_type'] = token.getType() # preselect: the token must be in the same realm as the user if user is not None: t_realms = token.token.getRealmNames() u_realm = user.getRealm() if (len(t_realms) > 0 and len(u_realm) > 0 and u_realm.lower() not in t_realms): audit_entry['action_detail'] = ("Realm mismatch for " "token and user") continue # check if the token is the list of supported tokens # if not skip to the next token in list typ = token.getType() if typ.lower() not in tokenclasses: log.error('token typ %r not found in tokenclasses: %r' % (typ, tokenclasses)) audit_entry['action_detail'] = "Unknown Token type" continue if not token.isActive(): audit_entry['action_detail'] = "Token inactive" continue if token.getFailCount() >= token.getMaxFailCount(): audit_entry['action_detail'] = "Failcounter exceeded" token.incOtpFailCounter() continue if not token.check_auth_counter(): audit_entry[ 'action_detail'] = "Authentication counter exceeded" token.set_count_auth(token.get_count_auth() + 1) continue if not token.check_validity_period(): audit_entry['action_detail'] = "validity period mismatch" token.incOtpFailCounter() continue # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # start the token validation if not transid: # if there is no transaction id given we check all token # related challenges (_ex_challenges, challenges) = Challenges.get_challenges(token, options=check_options, filter_open=True) try: (ret, reply) = token.check_token(passw, user, options=check_options, challenges=challenges) except Exception as exx: # in case of a failure during checking token, we log the error # and continue with the next one log.exception("checking token %r failed: %r" % (token, exx)) ret = -1 reply = "%r" % exx audit_entry['action_detail'] = ("checking token %r " "failed: %r" % (token, exx)) continue finally: validation_results[token.getSerial()] = (ret, reply) (cToken, pToken, iToken, vToken) = token.get_verification_result() related_challenges.extend(token.related_challenges) challenge_tokens.extend(cToken) pin_matching_tokens.extend(pToken) invalid_tokens.extend(iToken) valid_tokens.extend(vToken) # end of token verification loop matching_challenges = [] for token in valid_tokens: matching_challenges.extend(token.matching_challenges) # if there are related / sub challenges, we have to call their janitor Challenges.handle_related_challenge(matching_challenges) # now we finalize the token validation result fh = FinishTokens(valid_tokens, challenge_tokens, pin_matching_tokens, invalid_tokens, validation_results, user, options, audit_entry=audit_entry) (res, reply) = fh.finish_checked_tokens() # add to all tokens the last accessd time stamp linotp.lib.token.add_last_accessed_info([ valid_tokens, pin_matching_tokens, challenge_tokens, valid_tokens ]) # now we care for all involved tokens and their challenges for token in (valid_tokens + pin_matching_tokens + challenge_tokens + invalid_tokens): expired, _valid = Challenges.get_challenges(token) if expired: Challenges.delete_challenges(None, expired) log.debug("Number of valid tokens found " "(validTokenNum): %d" % len(valid_tokens)) return (res, 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.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 resync(self, otp1, otp2, options=None): ''' - for the resync to work, we take the last two transactions and their challenges - for each challenge, we search forward the sync window length ''' log.debug('[resync] %r : %r' % (otp1, otp2)) ret = False challenges = [] ## the challenges are orderd, the first one is the newest challenges = Challenges.lookup_challenges(self.context, self.getSerial()) ## check if there are enough challenges around if len(challenges) < 2: return False challenge1 = {} challenge2 = {} if options is None: ## the newer one ch1 = challenges[0] challenge1['challenge'] = ch1.get('data').get('challenge') challenge1['transid'] = ch1.get('transid') challenge1['session'] = ch1.get('session') challenge1['id'] = ch1.get('id') ## the elder one ch2 = challenges[0] challenge2['challenge'] = ch2.get('data').get('challenge') challenge2['transid'] = ch2.get('transid') challenge2['session'] = ch2.get('session') challenge2['id'] = ch2.get('id') else: if options.has_key('challenge1'): challenge1['challenge'] = options.get('challenge1') if options.has_key('challenge2'): challenge2['challenge'] = options.get('challenge2') if len(challenge1) == 0 or len(challenge2) == 0: error = "No challeges found!" log.error('[Ocra2TokenClass:resync] %s' % (error)) raise Exception('[Ocra2TokenClass:resync] %s' % (error)) secretHOtp = self.token.getHOtpKey() ocraSuite = OcraSuite(self.getOcraSuiteSuite(), secretHOtp) syncWindow = self.token.getSyncWindow() if ocraSuite.T is not None: syncWindow = syncWindow / 10 counter = self.token.getOtpCounter() ## set the ocra token pin ocraPin = '' if ocraSuite.P is not None: ocraPinObj = self.token.getUserPin() ocraPin = ocraPinObj.getKey() if ocraPin is None or len(ocraPin) == 0: ocraPin = '' timeShift = 0 if ocraSuite.T is not None: timeShift = int(self.getFromTokenInfo("timeShift", 0)) try: count_1 = ocraSuite.checkOtp(otp1, counter, syncWindow, challenge1, pin=ocraPin, timeshift=timeShift) if count_1 == -1: log.info('[resync] lookup for first otp value failed!') ret = False else: count_2 = ocraSuite.checkOtp(otp2, counter, syncWindow, challenge2, pin=ocraPin, timeshift=timeShift) if count_2 == -1: log.info('[resync] lookup for second otp value failed!') ret = False else: if ocraSuite.C is not None: if count_1 + 1 == count_2: self.setOtpCount(count_2) ret = True if ocraSuite.T is not None: if count_1 - count_2 <= ocraSuite.T * 2: ## callculate the timeshift date = datetime.datetime.fromtimestamp(count_2) log.info('[resync] syncing token to new timestamp: %r' % (date)) now = datetime.datetime.now() stime = now.strftime("%s") timeShift = count_2 - int(stime) self.addToTokenInfo('timeShift', timeShift) ret = True except Exception as ex: log.exception('[Ocra2TokenClass:resync] unknown error: %r' % (ex)) raise Exception('[Ocra2TokenClass:resync] unknown error: %s' % (ex)) log.debug('[resync]: %r ' % (ret)) return ret
def checkTokenList(self, tokenList, passw, user=User(), options=None): """ identify a matching token and test, if the token is valid, locked .. This function is called by checkSerialPass and checkUserPass to :param tokenList: list of identified tokens :param passw: the provided passw (mostly pin+otp) :param user: the identified use - as class object :param options: additonal parameters, which are passed to the token :return: tuple of boolean and optional response """ log.debug("[__checkTokenList] checking tokenlist: %r" % tokenList) reply = None tokenclasses = config['tokenclasses'] # add the user to the options, so that every token could see the user if not options: options = {} options['user'] = user # if there has been one token in challenge mode, we only handle challenges # if we got a validation against a sub_challenge, we extend this to # be a validation to all challenges of the transaction id import copy check_options = copy.deepcopy(options) state = check_options.get('state', check_options.get('transactionid', '')) if state and '.' in state: transid = state.split('.')[0] if 'state' in check_options: check_options['state'] = transid if 'transactionid' in check_options: check_options['transactionid'] = transid audit_entry = {} audit_entry['action_detail'] = "no token found!" challenge_tokens = [] pin_matching_tokens = [] invalid_tokens = [] valid_tokens = [] related_challenges = [] # we have to preserve the result / reponse for token counters validation_results = {} for token in tokenList: log.debug('Found user with loginId %r: %r:\n', token.getUserId(), token.getSerial()) audit_entry['serial'] = token.getSerial() audit_entry['token_type'] = token.getType() # preselect: the token must be in the same realm as the user if user is not None: t_realms = token.token.getRealmNames() u_realm = user.getRealm() if (len(t_realms) > 0 and len(u_realm) > 0 and u_realm.lower() not in t_realms): audit_entry['action_detail'] = ("Realm mismatch for " "token and user") continue # check if the token is the list of supported tokens # if not skip to the next token in list typ = token.getType() if typ.lower() not in tokenclasses: log.error('token typ %r not found in tokenclasses: %r' % (typ, tokenclasses)) audit_entry['action_detail'] = "Unknown Token type" continue if not token.isActive(): audit_entry['action_detail'] = "Token inactive" continue if token.getFailCount() >= token.getMaxFailCount(): audit_entry['action_detail'] = "Failcounter exceeded" continue if not token.check_auth_counter(): audit_entry['action_detail'] = "Authentication counter exceeded" continue if not token.check_validity_period(): audit_entry['action_detail'] = "validity period mismatch" continue # start the token validation try: # are there outstanding challenges (_ex_challenges, challenges) = Challenges.get_challenges(token, options=check_options) (ret, reply) = token.check_token( passw, user, options=check_options, challenges=challenges) except Exception as exx: # in case of a failure during checking token, we log the error # and continue with the next one log.exception("checking token %r failed: %r" % (token, exx)) ret = -1 reply = "%r" % exx audit_entry['action_detail'] = ("checking token %r " "failed: %r" % (token, exx)) continue finally: validation_results[token.getSerial()] = (ret, reply) (cToken, pToken, iToken, vToken) = token.get_verification_result() related_challenges.extend(token.related_challenges) challenge_tokens.extend(cToken) pin_matching_tokens.extend(pToken) invalid_tokens.extend(iToken) valid_tokens.extend(vToken) # end of token verification loop # if there are related / sub challenges, we have to call their janitor Challenges.handle_related_challenge(related_challenges) # now we finalize the token validation result fh = FinishTokens(valid_tokens, challenge_tokens, pin_matching_tokens, invalid_tokens, validation_results, user, options, audit_entry=audit_entry) (res, reply) = fh.finish_checked_tokens() # add to all tokens the last accessd time stamp linotp.lib.token.add_last_accessed_info( [valid_tokens, pin_matching_tokens, challenge_tokens, valid_tokens]) # now we care for all involved tokens and their challenges for token in (valid_tokens + pin_matching_tokens + challenge_tokens + valid_tokens): expired, _valid = Challenges.get_challenges(token) if expired: Challenges.delete_challenges(None, expired) log.debug("Number of valid tokens found " "(validTokenNum): %d" % len(valid_tokens)) return (res, reply)
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, _, otp = split_pin_otp(self, passw, user, options) if split_status < 0: raise Exception("Could not split passw") 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
def update(self, params): param_keys = set(params.keys()) init_rollout_state_keys = set(['type', 'hashlib', 'serial', 'key_size', 'user.login', 'user.realm', 'session']) # ---------------------------------------------------------------------- if param_keys.issubset(init_rollout_state_keys): # if param keys are in {'type', 'hashlib'} the token is # initialized for the first time. this is e.g. done on the # manage web ui. since the token doesn't exist in the database # yet, its rollout state must be None (that is: they data for # the rollout state doesn't exist yet) self.ensure_state(None) # ------------------------------------------------------------------ # collect data used for generating the pairing url serial = params.get('serial') hash_algorithm = params.get('hashlib') pub_key = get_qrtoken_public_key() cb_url = get_single_auth_policy('qrtoken_pairing_callback_url') cb_sms = get_single_auth_policy('qrtoken_pairing_callback_sms') # TODO: read from config otp_pin_length = None # ------------------------------------------------------------------ pairing_url = generate_pairing_url('qrtoken', server_public_key=pub_key, serial=serial, callback_url=cb_url, callback_sms_number=cb_sms, otp_pin_length=otp_pin_length, hash_algorithm=hash_algorithm) # ------------------------------------------------------------------ self.addToInfo('pairing_url', pairing_url) # we set the the active state of the token to False, because # it should not be allowed to use it for validation before the # pairing process is done self.token.LinOtpIsactive = False # ------------------------------------------------------------------ self.change_state('pairing_url_sent') # ---------------------------------------------------------------------- elif 'pairing_response' in params: # if a pairing response is in the parameters, we guess, # that the request refers to a token in the state # 'pairing_url_sent' self.ensure_state('pairing_url_sent') # ------------------------------------------------------------------ # adding the user's public key to the token info # as well as the user_token_id, which is used to # identify the token on the user's side self.addToTokenInfo('user_token_id', params['user_token_id']) # user public key arrives in the bytes format. # we must convert to a string in order to be # able to dump it as json in the db b64_user_public_key = b64encode(params['user_public_key']) self.addToTokenInfo('user_public_key', b64_user_public_key) # ------------------------------------------------------------------ # create challenge through the challenge factory # add the content type and the challenge data to the params # (needed in the createChallenge method) params['content_type'] = CONTENT_TYPE_PAIRING params['data'] = self.getSerial() self.change_state('pairing_response_received') success, challenge_dict = Challenges.create_challenge(self, params) if not success: raise Exception('Unable to create challenge from ' 'pairing response %s' % params['pairing_response']) challenge_url = challenge_dict['message'] # ------------------------------------------------------------------ self.addToInfo('pairing_challenge_url', challenge_url) # ------------------------------------------------------------------ self.change_state('pairing_challenge_sent')
def _rollout_2(self, params): ''' 2. https://linotpserver/admin/init? type=ocra& genkey=1& activationcode=AKTIVIERUNGSCODE& user=BENUTZERNAME& message=MESSAGE& session=SESSIONKEY =>> "serial" : SERIENNUMMER, "nonce" : DATAOBJECT, "transactionid" : "TRANSAKTIONSID, "app_import" : IMPORTURL - nonce - von HSM oder random ? - pkcs5 - kdf2 - es darf zur einer Zeit nur eine QR Token inaktiv (== im Ausrollzustand) sein !!!!! der Token wird über den User gefunden - seed = pdkdf2(nonce + activcode + shared secret) - challenge generiern - von urandom oder HSM ''' log.debug('[_rollout_2] %r ' % (params)) activationcode = params.get('activationcode', None) if activationcode is not None: ## genkey might have created a new key, so we have to rely on encSharedSecret = self.getFromTokenInfo('sharedSecret', None) if encSharedSecret is None: raise Exception ('missing shared secret of initialition for token %r' % (self.getSerial())) sharedSecret = decryptPin(encSharedSecret) ## we generate a nonce, which in the end is a challenge nonce = createNonce() self.addToTokenInfo('nonce', nonce) ## create a new key from the ocrasuite key_len = 20 if self.ocraSuite.find('-SHA256'): key_len = 32 elif self.ocraSuite.find('-SHA512'): key_len = 64 newkey = kdf2(sharedSecret, nonce, activationcode, key_len) self.setOtpKey(binascii.hexlify(newkey)) ## generate challenge, which is part of the app_import message = params.get('message', None) #(transid, challenge, _ret, url) = self.challenge(message) #self.createChallenge() (res, opt) = Challenges.create_challenge(self, self.context, options=params) challenge = opt.get('challenge') url = opt.get('url') transid = opt.get('transactionid') ## generate response info = {} uInfo = {} info['serial'] = self.getSerial() uInfo['se'] = self.getSerial() info['nonce'] = nonce uInfo['no'] = nonce info['transactionid'] = transid uInfo['tr'] = transid info['challenge'] = challenge uInfo['ch'] = challenge if message is not None: uInfo['me'] = str(message.encode("utf-8")) ustr = urllib.urlencode({'u':str(url.encode("utf-8"))}) uInfo['u'] = ustr[2:] info['url'] = str(url.encode("utf-8")) app_import = 'lseqr://nonce?%s' % (urllib.urlencode(uInfo)) ## add a signature of the url signature = {'si': self.signData(app_import) } info['signature'] = signature.get('si') info['app_import'] = "%s&%s" % (app_import, urllib.urlencode(signature)) self.info = info ## setup new state self.addToTokenInfo('rollout', '2') self.enable(True) log.debug('[_rollout_2]:') return
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 checkTokenList(self, tokenList, passw, user=User(), options=None): """ identify a matching token and test, if the token is valid, locked .. This function is called by checkSerialPass and checkUserPass to :param tokenList: list of identified tokens :param passw: the provided passw (mostly pin+otp) :param user: the identified use - as class object :param options: additional parameters, which are passed to the token :return: tuple of boolean and optional response """ reply = None # add the user to the options, so that every token could see the user if not options: options = {} options['user'] = user # if there has been one token in challenge mode, we only handle # challenges # if we got a validation against a sub_challenge, we extend this to # be a validation to all challenges of the transaction id import copy check_options = copy.deepcopy(options) state = check_options.get( 'state', check_options.get('transactionid', '')) if state and '.' in state: transid = state.split('.')[0] if 'state' in check_options: check_options['state'] = transid if 'transactionid' in check_options: check_options['transactionid'] = transid # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # transaction id optimization - part 1: # # if we have a transaction id, we check only those tokens # that belong to this transaction id: challenges = [] transaction_serials = [] transid = check_options.get('state', check_options.get('transactionid', '')) if transid: expired, challenges = Challenges.get_challenges(transid=transid, filter_open=True) for challenge in challenges: serial = challenge.tokenserial transaction_serials.append(serial) # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- audit_entry = {} audit_entry['action_detail'] = "no token found!" challenge_tokens = [] pin_matching_tokens = [] invalid_tokens = [] valid_tokens = [] related_challenges = [] # we have to preserve the result / reponse for token counters validation_results = {} for token in tokenList: # transaction id optimization - part 2: if transid: if token.getSerial() not in transaction_serials: continue audit_entry['serial'] = token.getSerial() audit_entry['token_type'] = token.getType() # preselect: the token must be in the same realm as the user if user is not None: t_realms = token.token.getRealmNames() u_realm = user.realm if (len(t_realms) > 0 and len(u_realm) > 0 and u_realm.lower() not in t_realms): audit_entry['action_detail'] = ("Realm mismatch for " "token and user") continue # check if the token is the list of supported tokens # if not skip to the next token in list typ = token.getType() if typ.lower() not in tokenclass_registry: log.error('token typ %r not found in tokenclasses: %r' % (typ, tokenclass_registry.keys())) audit_entry['action_detail'] = "Unknown Token type" continue if not token.isActive(): audit_entry['action_detail'] = "Token inactive" continue if token.getFailCount() >= token.getMaxFailCount(): audit_entry['action_detail'] = "Failcounter exceeded" token.incOtpFailCounter() continue # ---------------------------------------------------------------------- -- # check for restricted path usage path = context['Path'].strip('/').partition('/')[0] token_path = token.getFromTokenInfo('scope', {}).get('path', []) if token_path and path not in token_path: continue # -------------------------------------------------------------- -- # token validity handling now = datetime.now() if (token.validity_period_start and now < token.validity_period_start): audit_entry['action_detail'] = ("Authentication validity " "period mismatch!") token.incOtpFailCounter() continue token_success_excceed = ( token.count_auth_success_max > 0 and token.count_auth_success >= token.count_auth_success_max) token_access_exceed = ( token.count_auth_max > 0 and token.count_auth >= token.count_auth_max) token_expiry = ( token.validity_period_end and now >= token.validity_period_end) if token_success_excceed or token_access_exceed or token_expiry: if token_access_exceed: msg = "Authentication counter exceeded" if token_success_excceed: msg = "Authentication sucess counter exceeded" if token_expiry: msg = "Authentication validity period exceeded!" audit_entry['action_detail'] = msg token.incOtpFailCounter() # what should happen with exceeding tokens t_realms = None if not user.login and not user.realm: t_realms = token.token.getRealmNames() if disable_on_authentication_exceed(user, realms=t_realms): token.enable(False) if delete_on_authentication_exceed(user, realms=t_realms): token.deleteToken() continue # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # start the token validation if not transid: # if there is no transaction id given we check all token # related challenges (_ex_challenges, challenges) = Challenges.get_challenges(token, options=check_options, filter_open=True) try: (ret, reply) = token.check_token( passw, user, options=check_options, challenges=challenges) except Exception as exx: # in case of a failure during checking token, we log the error # and continue with the next one log.exception("checking token %r failed: %r" % (token, exx)) ret = -1 reply = "%r" % exx audit_entry['action_detail'] = ("checking token %r " "failed: %r" % (token, exx)) audit_entry['info'] = audit_entry.get('info','') + "%r" % exx continue finally: validation_results[token.getSerial()] = (ret, reply) (cToken, pToken, iToken, vToken) = token.get_verification_result() related_challenges.extend(token.related_challenges) challenge_tokens.extend(cToken) pin_matching_tokens.extend(pToken) invalid_tokens.extend(iToken) valid_tokens.extend(vToken) # end of token verification loop matching_challenges = [] for token in valid_tokens: matching_challenges.extend(token.matching_challenges) # if there are related / sub challenges, we have to call their janitor Challenges.handle_related_challenge(matching_challenges) # now we finalize the token validation result fh = FinishTokens(valid_tokens, challenge_tokens, pin_matching_tokens, invalid_tokens, validation_results, user, options, audit_entry=audit_entry) (res, reply) = fh.finish_checked_tokens() # add to all tokens the last accessd time stamp add_last_accessed_info( [valid_tokens, pin_matching_tokens, challenge_tokens, valid_tokens]) # now we care for all involved tokens and their challenges for token in (valid_tokens + pin_matching_tokens + challenge_tokens + invalid_tokens): expired, _valid = Challenges.get_challenges(token) if expired: Challenges.delete_challenges(None, expired) log.debug("Number of valid tokens found " "(validTokenNum): %d" % len(valid_tokens)) return (res, 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(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 checkOtp(self, passwd, counter, window, options=None): """ checks if the supplied challenge response is correct. :param passwd: The challenge response :param options: A dictionary of parameters passed by the upper layer (used for transaction_id in this context) :param counter: legacy API (unused) :param window: legacy API (unused) :raises TokenStateError: If token state is not 'active' or 'pairing_challenge_sent' :returns: -1 for failure, 1 for success """ valid_states = ['pairing_challenge_sent', 'active'] self.ensure_state_is_in(valid_states) # ------------------------------------------------------------------ -- # new pushtoken protocoll supports the keyword based accept or deny. # the old 'passwd' argument is not supported anymore try: signature_accept = passwd.get('accept', None) signature_reject = passwd.get('reject', None) except AttributeError: # will be raised with a get() on a str object raise Exception('Pushtoken version %r requires "accept" or' ' "reject" as parameter' % CHALLENGE_URL_VERSION) if signature_accept is not None and signature_reject is not None: raise Exception('Pushtoken version %r requires "accept" or' ' "reject" as parameter' % CHALLENGE_URL_VERSION) # ------------------------------------------------------------------ -- filtered_challenges = [] serial = self.getSerial() if options is None: options = {} max_fail = int(getFromConfig('PushMaxChallenges', '3')) # ------------------------------------------------------------------ -- if 'transactionid' in options: # -------------------------------------------------------------- -- # fetch all challenges that match the transaction id or serial transaction_id = options.get('transactionid') challenges = Challenges.lookup_challenges(serial=serial, transid=transaction_id, filter_open=True) # -------------------------------------------------------------- -- # filter into filtered_challenges for challenge in challenges: (received_tan, tan_is_valid) = challenge.getTanStatus() fail_counter = challenge.getTanCount() # if we iterate over matching challenges (that is: challenges # with the correct transaction id) we either find a fresh # challenge, that didn't receive a TAN at all (first case) # or a challenge, that already received a number of wrong # TANs but still has tries left (second case). if not received_tan: filtered_challenges.append(challenge) elif not tan_is_valid and fail_counter <= max_fail: filtered_challenges.append(challenge) # ------------------------------------------------------------------ -- if not filtered_challenges: return -1 if len(filtered_challenges) > 1: log.error('multiple challenges for one transaction and for one' ' token found!') return -1 # for the serial and the transaction id there could always be only # at max one challenge matching. This is even true for sub transactions challenge = filtered_challenges[0] # client verifies the challenge by signing the challenge # plaintext. we retrieve the original plaintext (saved # in createChallenge) and check for a match data = challenge.getData() data_to_verify = b64decode(data['sig_base']) b64_dsa_public_key = self.getFromTokenInfo('user_dsa_public_key') user_dsa_public_key = b64decode(b64_dsa_public_key) # -------------------------------------------------------------- -- # handle the accept case if signature_accept is not None: accept_signature_as_bytes = decode_base64_urlsafe(signature_accept) accept_data_to_verify_as_bytes = ( struct.pack('<b', CHALLENGE_URL_VERSION) + b'ACCEPT\0' + data_to_verify) try: verify_sig(accept_signature_as_bytes, accept_data_to_verify_as_bytes, user_dsa_public_key) challenge.add_session_info({'accept': True}) return 1 except ValueError: challenge.add_session_info({'accept': False}) log.error("accept signature mismatch!") return -1 # -------------------------------------------------------------- -- # handle the reject case elif signature_reject is not None: reject_signature_as_bytes = decode_base64_urlsafe(signature_reject) reject_data_to_verify_as_bytes = ( struct.pack('<b', CHALLENGE_URL_VERSION) + b'DENY\0' + data_to_verify) try: verify_sig(reject_signature_as_bytes, reject_data_to_verify_as_bytes, user_dsa_public_key) challenge.add_session_info({'reject': True}) return 1 except ValueError: challenge.add_session_info({'reject': False}) log.error("reject signature mismatch!") return -1 return -1
def check_status(self): """ check the status of a transaction - for polling support """ try: param = self.request_params # # we require either state or transactionid as parameter transid = param.get("state", param.get("transactionid", None)) if not transid: raise ParameterError( _( 'Missing required parameter "state" or ' '"transactionid"!' ) ) # # serial is an optional parameter serial = param.get("serial", None) # user is an optional parameter: # if no 'user' in the parameters, the User object will be empty user = getUserFromParam(param) passw = param.get("pass") if passw is None: raise ParameterError(_('Missing required parameter "pass"!')) use_offline = param.get("use_offline", False) va = ValidationHandler() ok, opt = va.check_status( transid=transid, user=user, serial=serial, password=passw, use_offline=use_offline, ) serials = [] types = [] owner = None challenges = Challenges.lookup_challenges(transid=transid) for ch in challenges: tokens = getTokens4UserOrSerial(serial=ch.getTokenSerial()) if not tokens: continue for token in tokens: serials.append(token.getSerial()) types.append(token.getType()) if not owner: owner = get_token_owner(token) if owner: g.audit["user"] = g.audit["user"] or owner.login g.audit["realm"] = g.audit["realm"] or owner.realm g.audit["serial"] = " ".join(serials) g.audit["token_type"] = " ".join(types) g.audit["success"] = ok g.audit["info"] = str(opt) db.session.commit() return sendResult(response, ok, 0, opt=opt) except Exception as exx: log.error("check_status failed: %r", exx) g.audit["info"] = str(exx) db.session.rollback() return sendResult(response, False, 0)
def check_status(self, transid=None, user=None, serial=None, password=None): """ 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 :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) if 1 not in pin_policies: pin_match = check_pin(tokens[0], password, user=user, options=None) if not pin_match: ret = False continue ret = True trans_dict = {} trans_dict['transactionid'] = ch.transid trans_dict['received_count'] = ch.received_count trans_dict['received_tan'] = ch.received_tan trans_dict['valid_tan'] = ch.valid_tan trans_dict['linotp_tokenserial'] = serial trans_dict['linotp_tokentype'] = tokens[0].type trans_dict['message'] = ch.challenge transactions[serial] = trans_dict if transactions: reply['transactions'] = transactions return ret, reply
def checkOtp(self, passwd, counter, window, options=None): valid_states = ['pairing_challenge_sent', 'pairing_complete'] self.ensure_state_is_in(valid_states) # ------------------------------------------------------------------- -- filtered_challenges = [] serial = self.getSerial() if options is None: options = {} max_fail = int(getFromConfig('QRMaxChallenges', '3')) # ------------------------------------------------------------------- -- # TODO: from which point is checkOtp called, when there # is no challenge response in the request? if 'transactionid' in options: # --------------------------------------------------------------- -- # fetch all challenges that match the transaction id or serial transaction_id = options.get('transaction_id') challenges = Challenges.lookup_challenges(serial, transaction_id) # --------------------------------------------------------------- -- # filter into filtered_challenges for challenge in challenges: (received_tan, tan_is_valid) = challenge.getTanStatus() fail_counter = challenge.getTanCount() # if we iterate over matching challenges (that is: challenges # with the correct transaction id) we either find a fresh # challenge, that didn't receive a TAN at all (first case) # or a challenge, that already received a number of wrong # TANs but still has tries left (second case). if not received_tan: filtered_challenges.append(challenge) elif not tan_is_valid and fail_counter <= max_fail: filtered_challenges.append(challenge) # --------------------------------------------------------------- -- if not filtered_challenges: return -1 for challenge in filtered_challenges: data = challenge.getData() correct_passwd = data['user_sig'] # compare values with python's native constant # time comparison if compare_digest(correct_passwd, passwd): return 1 else: # maybe we got a tan instead of a signature correct_passwd_as_bytes = decode_base64_urlsafe(correct_passwd) tan_length = 8 # TODO fetch from policy correct_tan = extract_tan(correct_passwd_as_bytes, tan_length) # TODO PYLONS-HACK pylons silently converts integers in # incoming json to unicode. since extract_tan returns # an integer, we have to convert it here correct_tan = unicode(correct_tan) if compare_digest(correct_tan, passwd): return 1 return -1 # TODO: ??? semantics of this ret val?
def checkOtp(self, passwd, counter, window, options=None): """ checks if the supplied challenge response is correct. :param passwd: The challenge response :param options: A dictionary of parameters passed by the upper layer (used for transaction_id in this context) :param counter: legacy API (unused) :param window: legacy API (unused) :raises TokenStateError: If token state is not 'active' or 'pairing_challenge_sent' :returns: -1 for failure, 1 for success """ valid_states = ['pairing_challenge_sent', 'active'] self.ensure_state_is_in(valid_states) # ------------------------------------------------------------------- -- filtered_challenges = [] serial = self.getSerial() if options is None: options = {} max_fail = int(getFromConfig('PushMaxChallenges', '3')) # ------------------------------------------------------------------- -- if 'transactionid' in options: # --------------------------------------------------------------- -- # fetch all challenges that match the transaction id or serial transaction_id = options.get('transaction_id') challenges = Challenges.lookup_challenges(serial, transaction_id) # --------------------------------------------------------------- -- # filter into filtered_challenges for challenge in challenges: (received_tan, tan_is_valid) = challenge.getTanStatus() fail_counter = challenge.getTanCount() # if we iterate over matching challenges (that is: challenges # with the correct transaction id) we either find a fresh # challenge, that didn't receive a TAN at all (first case) # or a challenge, that already received a number of wrong # TANs but still has tries left (second case). if not received_tan: filtered_challenges.append(challenge) elif not tan_is_valid and fail_counter <= max_fail: filtered_challenges.append(challenge) # ------------------------------------------------------------------- -- if not filtered_challenges: return -1 for challenge in filtered_challenges: # client verifies the challenge by signing the challenge # plaintext. we retrieve the original plaintext (saved # in createChallenge) and check for a match b64_dsa_public_key = self.getFromTokenInfo('user_dsa_public_key') user_dsa_public_key = b64decode(b64_dsa_public_key) data = challenge.getData() sig_base = data['sig_base'] passwd_as_bytes = decode_base64_urlsafe(passwd) sig_base_as_bytes = b64decode(sig_base) try: verify_sig(passwd_as_bytes, sig_base_as_bytes, user_dsa_public_key) return 1 except ValueError: # signature mismatch return -1 return -1
def check_by_transactionid(self, transid, passw, options=None): """ check the passw against the open transaction :param transid: the transaction id :param passw: the pass parameter :param options: the additional optional parameters :return: tuple of boolean and detail dict """ reply = {} serials = [] challenges = Challenges.lookup_challenges(transid=transid) for challenge in challenges: serials.append(challenge.tokenserial) if not serials: reply["value"] = False reply["failure"] = ("No challenge for transaction %r found" % transid) return False, reply ok = False reply["failcount"] = 0 reply["value"] = False reply["token_type"] = "" token_type = options.get("token_type", None) for serial in serials: tokens = getTokens4UserOrSerial(serial=serial, token_type=token_type, read_for_update=True) if not tokens and token_type: continue if not tokens and not token_type: raise Exception("tokenmismatch for token serial: %r" % serial) # there could be only one token = tokens[0] owner = get_token_owner(token) (ok, opt) = self.checkTokenList(tokens, passw, user=owner, options=options) if opt: reply.update(opt) reply["value"] = ok reply["token_type"] = token.getType() reply["failcount"] = token.getFailCount() reply["serial"] = token.getSerial() if ok: break return ok, reply
def checkTokenList(self, tokenList, passw, user=User(), options=None): """ identify a matching token and test, if the token is valid, locked .. This function is called by checkSerialPass and checkUserPass to :param tokenList: list of identified tokens :param passw: the provided passw (mostly pin+otp) :param user: the identified use - as class object :param options: additional parameters, which are passed to the token :return: tuple of boolean and optional response """ reply = None # add the user to the options, so that every token could see the user if not options: options = {} options['user'] = user # if there has been one token in challenge mode, we only handle # challenges # if we got a validation against a sub_challenge, we extend this to # be a validation to all challenges of the transaction id import copy check_options = copy.deepcopy(options) state = check_options.get( 'state', check_options.get('transactionid', '')) if state and '.' in state: transid = state.split('.')[0] if 'state' in check_options: check_options['state'] = transid if 'transactionid' in check_options: check_options['transactionid'] = transid # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # transaction id optimization - part 1: # # if we have a transaction id, we check only those tokens # that belong to this transaction id: challenges = [] transaction_serials = [] transid = check_options.get('state', check_options.get('transactionid', '')) if transid: expired, challenges = Challenges.get_challenges(transid=transid, filter_open=True) for challenge in challenges: serial = challenge.tokenserial transaction_serials.append(serial) # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- audit_entry = {} audit_entry['action_detail'] = "no token found!" challenge_tokens = [] pin_matching_tokens = [] invalid_tokens = [] valid_tokens = [] related_challenges = [] # we have to preserve the result / reponse for token counters validation_results = {} for token in tokenList: # transaction id optimization - part 2: if transid: if token.getSerial() not in transaction_serials: continue audit_entry['serial'] = token.getSerial() audit_entry['token_type'] = token.getType() # preselect: the token must be in the same realm as the user if user is not None: t_realms = token.token.getRealmNames() u_realm = user.realm if (len(t_realms) > 0 and len(u_realm) > 0 and u_realm.lower() not in t_realms): audit_entry['action_detail'] = ("Realm mismatch for " "token and user") continue # check if the token is the list of supported tokens # if not skip to the next token in list typ = token.getType() if typ.lower() not in tokenclass_registry: log.error('token typ %r not found in tokenclasses: %r' % (typ, list(tokenclass_registry.keys()))) audit_entry['action_detail'] = "Unknown Token type" continue if not token.isActive(): audit_entry['action_detail'] = "Token inactive" continue if token.getFailCount() >= token.getMaxFailCount(): audit_entry['action_detail'] = "Failcounter exceeded" token.incOtpFailCounter() continue # ---------------------------------------------------------------------- -- # check for restricted path usage path = context['Path'].strip('/').partition('/')[0] token_path = token.getFromTokenInfo('scope', {}).get('path', []) if token_path and path not in token_path: continue # -------------------------------------------------------------- -- # token validity handling now = datetime.now() if (token.validity_period_start and now < token.validity_period_start): audit_entry['action_detail'] = ("Authentication validity " "period mismatch!") token.incOtpFailCounter() continue token_success_excceed = ( token.count_auth_success_max > 0 and token.count_auth_success >= token.count_auth_success_max) token_access_exceed = ( token.count_auth_max > 0 and token.count_auth >= token.count_auth_max) token_expiry = ( token.validity_period_end and now >= token.validity_period_end) if token_success_excceed or token_access_exceed or token_expiry: if token_access_exceed: msg = "Authentication counter exceeded" if token_success_excceed: msg = "Authentication sucess counter exceeded" if token_expiry: msg = "Authentication validity period exceeded!" audit_entry['action_detail'] = msg token.incOtpFailCounter() # what should happen with exceeding tokens t_realms = None if not user.login and not user.realm: t_realms = token.token.getRealmNames() if disable_on_authentication_exceed(user, realms=t_realms): token.enable(False) if delete_on_authentication_exceed(user, realms=t_realms): token.deleteToken() continue # -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # start the token validation if not transid: # if there is no transaction id given we check all token # related challenges (_ex_challenges, challenges) = Challenges.get_challenges(token, options=check_options, filter_open=True) try: (ret, reply) = token.check_token( passw, user, options=check_options, challenges=challenges) except Exception as exx: # in case of a failure during checking token, we log the error # and continue with the next one log.exception("checking token %r failed: %r" % (token, exx)) ret = -1 reply = "%r" % exx audit_entry['action_detail'] = ("checking token %r " "failed: %r" % (token, exx)) audit_entry['info'] = audit_entry.get('info','') + "%r" % exx continue finally: validation_results[token.getSerial()] = (ret, reply) (cToken, pToken, iToken, vToken) = token.get_verification_result() related_challenges.extend(token.related_challenges) challenge_tokens.extend(cToken) pin_matching_tokens.extend(pToken) invalid_tokens.extend(iToken) valid_tokens.extend(vToken) valid_tokens = list(set(valid_tokens)) invalid_tokens = list(set(invalid_tokens)) pin_matching_tokens = list(set(pin_matching_tokens)) challenge_tokens = list(set(challenge_tokens)) # end of token verification loop matching_challenges = [] for token in valid_tokens: matching_challenges.extend(token.matching_challenges) matching_challenges = list(set(matching_challenges)) # if there are related / sub challenges, we have to call their janitor Challenges.handle_related_challenge(matching_challenges) # now we finalize the token validation result fh = FinishTokens(valid_tokens, challenge_tokens, pin_matching_tokens, invalid_tokens, validation_results, user, options, audit_entry=audit_entry) (res, reply) = fh.finish_checked_tokens() # ------------------------------------------------------------------ -- # add to all tokens the last accessed time stamp add_last_accessed_info(valid_tokens + pin_matching_tokens + challenge_tokens + invalid_tokens) # add time stamp to all valid tokens add_last_verified_info(valid_tokens) # ------------------------------------------------------------------ -- # now we care for all involved tokens and their challenges for token in (valid_tokens + pin_matching_tokens + challenge_tokens + invalid_tokens): expired, _valid = Challenges.get_challenges(token) if expired: Challenges.delete_challenges(None, expired) log.debug("Number of valid tokens found " "(validTokenNum): %d" % len(valid_tokens)) return (res, 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(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