def _sendEmail(self): """ Prepares the e-mail by gathering all relevant information and then sends it out. :return: A tuple of success and status_message :rtype: bool, string """ otp = self._getNextOtp() email_address = self._email_address if not email_address: raise Exception("No e-mail address was defined for this token.") owner = get_token_owner(self) message = self._getEmailMessage(user=owner) if "<otp>" not in message: message = message + "<otp>" message = message.replace("<otp>", otp) message = message.replace("<serial>", self.getSerial()) subject = self._getEmailSubject(user=owner) subject = subject.replace("<otp>", otp) subject = subject.replace("<serial>", self.getSerial()) email_provider = loadProviderFromPolicy(provider_type='email', user=owner) status, status_message = email_provider.submitMessage(email_address, subject=subject, message=message) return status, status_message
def checkYubikeyPass(self, passw): """ Checks the password of a yubikey in Yubico mode (44,48), where the first 12 or 16 characters are the tokenid :param passw: The password that consist of the static yubikey prefix and the otp :type passw: string :return: True/False and the User-Object of the token owner :rtype: dict """ audit = context['audit'] opt = None res = False tokenList = [] # strip the yubico OTP and the PIN modhex_serial = passw[:-32][-16:] try: serialnum = "UBAM" + modhex_decode(modhex_serial) except TypeError as exx: log.error("Failed to convert serialnumber: %r" % exx) return res, opt # build list of possible yubikey tokens serials = [serialnum] for i in range(1, 3): serials.append("%s_%s" % (serialnum, i)) for serial in serials: tokens = getTokens4UserOrSerial(serial=serial, read_for_update=True) tokenList.extend(tokens) if len(tokenList) == 0: audit['action_detail'] = ( 'The serial %s could not be found!' % serialnum) return res, opt # FIXME if the Token has set a PIN and the User does not want to enter # the PIN for authentication, we need to do something different here... # and avoid PIN checking in __checkToken. # We could pass an "option" to __checkToken. (res, opt) = self.checkTokenList(tokenList, passw) # Now we need to get the user if res is not False and 'serial' in audit: serial = audit.get('serial', None) if serial is not None: user = get_token_owner(tokenList[0]) audit['user'] = user.login audit['realm'] = user.realm opt = {'user': user.login, 'realm': user.realm} return res, opt
def createChallenge(self, transaction_id, options): """ """ _ = context['translate'] valid_states = ['pairing_response_received', 'pairing_complete'] self.ensure_state_is_in(valid_states) if self.current_state == 'pairing_response_received': content_type = CONTENT_TYPE_PAIRING reset_url = True else: content_type = options.get('content_type') reset_url = False message = options.get('data') # ---------------------------------------------------------------------- owner = get_token_owner(self) if owner and owner.login and owner.realm: realms = [owner.realm] else: realms = self.getRealms() callback_policies = ['qrtoken_challenge_callback_url', 'qrtoken_challenge_callback_sms'] callback_url = get_single_auth_policy(callback_policies[0], user=owner, realms=realms) callback_sms = get_single_auth_policy(callback_policies[1], user=owner, realms=realms) if not callback_url and not callback_sms: raise Exception(_('Policy %s must have a value') % _(" or ").join(callback_policies)) # TODO: get from policy/config compression = False # ---------------------------------------------------------------------- challenge_url, user_sig = self.create_challenge_url(transaction_id, content_type, message, callback_url, callback_sms, compression, reset_url) data = {'message': message, 'user_sig': user_sig} if self.current_state == 'pairing_response_received': self.change_state('pairing_challenge_sent') return (True, challenge_url, data, {})
def _sendEmail(self): """ Prepares the e-mail by gathering all relevant information and then sends it out. :return: A tuple of success and status_message :rtype: bool, string """ otp = self._getNextOtp() email_address = self._email_address if not email_address: raise Exception("No e-mail address was defined for this token.") owner = get_token_owner(self) message = self._getEmailMessage(user=owner) if "<otp>" not in message: message = message + "<otp>" message = message.replace("<otp>", otp) message = message.replace("<serial>", self.getSerial()) subject = self._getEmailSubject(user=owner) subject = subject.replace("<otp>", otp) subject = subject.replace("<serial>", self.getSerial()) try: email_provider_class = self._getEmailProviderClass() email_provider = email_provider_class() except Exception as exc: LOG.exception("[sendEmail] Failed to load EmailProvider: %r" % exc) raise exc ## now we need the config from the env LOG.debug("[sendEmail] loading e-mail configuration for class %s" % email_provider) config = self._getEmailProviderConfig() LOG.debug("[sendEmail] config: %r" % config) email_provider.loadConfig(config) status, status_message = email_provider.submitMessage(email_address, subject=subject, message=message) return status, status_message
def do_forward_failcounter(token): ''' this function checks the for the policy scope=authentication, action=forwardtoken:no_failcounter_forwarding defining if the target token failcounter should be incremented / reseted :param serial: the token serial number, which allows to derive the realm(s) and owner from :return: boolean ''' boolean = True owner = get_token_owner(token) if owner and owner.realm: realms = [owner.realm] else: realms = getTokenRealms(token.getSerial()) if not realms: realms = ['*'] for realm in realms: params = {'scope': 'authentication', 'realm': realm, 'action': "forwardtoken:no_failcounter_forwarding" } if owner and owner.login: params['user'] = owner.login pol = getPolicy(params) if pol: boolean = False break return boolean
def update(self, params): param_keys = set(params.keys()) init_rollout_state_keys = set(['type', 'hashlib', 'serial', '::scope::', 'key_size', 'user.login', 'description', 'user.realm', 'session', 'otplen', 'resConf', 'user', 'realm', 'qr', 'pin']) # ------------------------------------------------------------------- -- if not param_keys.issubset(init_rollout_state_keys): # make sure the call aborts, if request # type wasn't recognized raise Exception('Unknown request type for token type qr') # 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) # --------------------------------------------------------------- -- # we check if callback policies are set. this must be done here # because the token gets saved directly after the update method # in the TokenHandler _ = context['translate'] owner = get_token_owner(self) if owner and owner.login and owner.realm: realms = [owner.realm] else: realms = self.getRealms() pairing_policies = ['qrtoken_pairing_callback_url', 'qrtoken_pairing_callback_sms'] cb_url = get_single_auth_policy(pairing_policies[0], user=owner, realms=realms) cb_sms = get_single_auth_policy(pairing_policies[1], user=owner, realms=realms) if not cb_url and not cb_sms: raise Exception(_('Policy %s must have a value') % _(" or ").join(pairing_policies)) challenge_policies = ['qrtoken_challenge_callback_url', 'qrtoken_challenge_callback_sms'] cb_url = get_single_auth_policy(challenge_policies[0], user=owner, realms=realms) cb_sms = get_single_auth_policy(challenge_policies[1], user=owner, realms=realms) if not cb_url and not cb_sms: raise Exception(_('Policy %s must have a value') % _(" or ").join(challenge_policies)) partition = get_partition(realms, owner) self.addToTokenInfo('partition', partition) # --------------------------------------------------------------- -- # 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 # --------------------------------------------------------------- -- if 'otplen' not in params: params['otplen'] = getFromConfig("QRTokenOtpLen", 8) # -------------------------------------------------------------- -- TokenClass.update(self, params, reset_failcount=True)
def sendSMS(self, message=None, transactionid=None): ''' send sms :param message: the sms submit message - could contain placeholders like <otp> or <serial> :type message: string :return: submitted message :rtype: string ''' success = None if not message: message = "<otp>" if not SMSPROVIDER_IMPORTED: raise Exception("The SMSProvider could not be imported. Maybe you " "didn't install the package (Debian " "linotp-smsprovider or PyPI SMSProvider)") # we require the token owner to get the phone number and the provider owner = get_token_owner(self) phone = self.get_mobile_number(owner) otp = self.getNextOtp() serial = self.getSerial() if '<otp>' not in message: log.error('Message unconfigured: prepending <otp> to message') if isinstance(message, str): message = "<otp> %s" % message else: message = "<otp> %r" % message message = message.replace("<otp>", otp) message = message.replace("<serial>", serial) if transactionid: message = message.replace("<transactionid>", transactionid) log.debug("[sendSMS] sending SMS to phone number %s " % phone) realm = None realms = self.getRealms() if realms: realm = realms[0] # we require the token owner to get the phone number and the provider owner = get_token_owner(self) if not owner or not owner.login: log.warning("[sendSMS] Missing required token owner") # ------------------------------------------------------------------ -- # load providers for the user providers = get_provider_from_policy('sms', realm=realm, user=owner) # remember if at least one provider could be accessed available = False res_scheduler = ResourceScheduler(tries=1, uri_list=providers) for provider_name in next(res_scheduler): sms_provider = loadProvider('sms', provider_name=provider_name) if not sms_provider: raise Exception('unable to load provider') try: success = sms_provider.submitMessage(phone, message) available = True break except ProviderNotAvailable as exx: log.error('Provider not available %r', provider_name) res_scheduler.block(provider_name, delay=30) if not available: raise AllResourcesUnavailable("unable to connect to any " "SMSProvider %r" % providers) log.debug("[sendSMS] message submitted") # # after submit set validity time self.setValidUntil() return success, message
def submitChallenge(self, options=None): ''' submit the sms message - former method name was checkPin :param options: the request options context :return: tuple of success and message ''' _ = context['translate'] res = 0 fallback_realm = (self.getRealms() or [None])[0] login, realm = get_user_from_options( options, fallback_user=get_token_owner(self), fallback_realm=fallback_realm) message = options.get('challenge', "<otp>") result = _("sending sms failed") # it is configurable, if sms should be triggered by a valid pin send_by_PIN = getFromConfig("sms.sendByPin") or True # if the token is not active or there is no pin triggered sending, we leave if not (self.isActive() is True and send_by_PIN is True): return res, result counter = self.getOtpCount() log.debug("[submitChallenge] counter=%r" % counter) # At this point we MUST NOT bail out in case of an # Gateway error, since checkPIN is successful, as the bail # out would cancel the checking of the other tokens try: sms_ret = False new_message = None sms_ret, new_message = get_auth_smstext(user=login, realm=realm) if sms_ret: message = new_message # ---------------------------------------------------------- -- # if there is a data or message part in the request, it might # overrule the given smstext if 'data' in options or 'message' in options: # if there is an enforce policy # we do not allow the owerwrite enforce = enforce_smstext(user=login, realm=realm) if not enforce: message = options.get('data', options.get('message', '<otp>')) # ---------------------------------------------------------- -- # fallback if no message is defined if not message: message = "<otp>" # ---------------------------------------------------------- -- # submit the sms message transactionid = options.get('transactionid', None) res, result = self.sendSMS(message=message, transactionid=transactionid) self.info['info'] = "SMS sent: %r" % res log.debug('SMS sent: %s', result) return res, result except Exception as e: # The PIN was correct, but the SMS could not be sent. self.info['info'] = str(e) info = ("The SMS could not be sent: %r" % e) log.warning("[submitChallenge] %s", info) return False, info finally: # we increment the otp in any case, independend if sending # of the sms was sucsessful self.incOtpCounter(counter, reset=False)
def createChallenge(self, transaction_id, options): """ """ _ = context['translate'] valid_states = ['pairing_response_received', 'pairing_complete'] self.ensure_state_is_in(valid_states) # ------------------------------------------------------------------- -- if self.current_state == 'pairing_response_received': content_type = CONTENT_TYPE_PAIRING reset_url = True else: content_type_as_str = options.get('content_type') reset_url = False if content_type_as_str is None: content_type = None else: try: # pylons silently converts all ints in json # to unicode :( content_type = int(content_type_as_str) except: raise ValueError('Unrecognized content type: %s' % content_type_as_str) # ------------------------------------------------------------------- -- message = options.get('data') # ------------------------------------------------------------------- -- owner = get_token_owner(self) if owner and owner.login and owner.realm: realms = [owner.realm] else: realms = self.getRealms() callback_policies = [ 'qrtoken_challenge_callback_url', 'qrtoken_challenge_callback_sms' ] callback_url = get_single_auth_policy(callback_policies[0], user=owner, realms=realms) callback_sms = get_single_auth_policy(callback_policies[1], user=owner, realms=realms) if not callback_url and not callback_sms: raise Exception( _('Policy %s must have a value') % _(" or ").join(callback_policies)) # TODO: get from policy/config compression = False # ------------------------------------------------------------------- -- challenge_url, user_sig = self.create_challenge_url( transaction_id, content_type, message, callback_url, callback_sms, compression, reset_url) data = {'message': message, 'user_sig': user_sig} if self.current_state == 'pairing_response_received': self.change_state('pairing_challenge_sent') return (True, challenge_url, data, {})
def getInitDetail(self, params, user=None): _ = context['translate'] response_detail = {} param_keys = set(params.keys()) init_rollout_state_keys = set([ 'type', 'hashlib', 'serial', '::scope::', 'key_size', 'user.login', 'description', 'user.realm', 'session', 'otplen', 'pin', 'resConf', 'user', 'realm', 'qr' ]) # ------------------------------------------------------------------- -- if param_keys.issubset(init_rollout_state_keys): # collect data used for generating the pairing url serial = self.getSerial() # for qrtoken hashlib is ignored hash_algorithm = None otp_pin_length = int(self.getOtpLen()) owner = get_token_owner(self) if owner and owner.login and owner.realm: realms = [owner.realm] user = owner else: realms = self.getRealms() pairing_policies = [ 'qrtoken_pairing_callback_url', 'qrtoken_pairing_callback_sms' ] # it is guaranteed, that either cb_url or cb_sms has a value # because we checked it in the update method cb_url = get_single_auth_policy(pairing_policies[0], user=owner, realms=realms) cb_sms = get_single_auth_policy(pairing_policies[1], user=owner, realms=realms) # --------------------------------------------------------------- -- partition = self.getFromTokenInfo('partition') # FIXME: certificate usage pairing_url = generate_pairing_url(token_type='qr', partition=partition, serial=serial, callback_url=cb_url, callback_sms_number=cb_sms, otp_pin_length=otp_pin_length, hash_algorithm=hash_algorithm, use_cert=False) # --------------------------------------------------------------- -- self.addToInfo('pairing_url', pairing_url) response_detail['pairing_url'] = pairing_url # create response tabs response_detail['lse_qr_url'] = { 'description': _('QRToken Pairing Url'), 'img': create_img(pairing_url, width=250), 'order': 0, 'value': pairing_url } response_detail['lse_qr_cert'] = { 'description': _('QRToken Certificate'), 'img': create_img(pairing_url, width=250), 'order': 1, 'value': pairing_url } response_detail['serial'] = self.getSerial() # ------------------------------------------------------------------ -- else: # make sure the call aborts, if request # type wasn't recognized raise Exception('Unknown request type for token type qr') # ------------------------------------------------------------------- -- self.change_state('pairing_url_sent') return response_detail
def setPin(self): """ method: api/helpdesk/setPin description: This function sets the PIN of the token arguments: * serial - required * pin - optional - uses random pin instead returns: an array with the list of affected serial numbers """ res = {} try: params = self.request_params serial = params.get("serial") if not serial: raise ParameterError("Missing parameter: 'serial'") tokens = getTokens4UserOrSerial(serial=serial) result = [] for token in tokens: owner = get_token_owner(token) current_serial = token.getSerial() pin = params.get( 'pin', createRandomPin(owner, min_pin_length=6)) # as the parameter pin in the params is evaluated by # the checkPolicyPre and checkPolicyPost we need to put the # parameter pin and current_serial into the params params['pin'] = pin params['serial'] = current_serial # set pin is done by the admin/set with the parameter pin checkPolicyPre( 'admin', method='set', param=params, user=owner) token.setPin(pin) # while in the pre checks for method='set' the post checks # for 'setPin' which is used to determin if a new pin has to # be generated res = checkPolicyPost( 'admin', 'setPin', param=params, user=owner) pin = res.get('new_pin', pin) info = { 'message': ('A new pin ${Pin} has been set for your ' 'token: ${serial}'), 'Subject': 'new pin set for token ${serial}', 'Pin': pin, 'serial': current_serial, } notify_user(owner, 'setPin', info, required=True) result.append(serial) c.audit['success'] = True c.audit['info'] = result Session.commit() return sendResult(response, result) except PolicyException as pex: log.exception('[setPin] policy failed %r') Session.rollback() return sendError(response, pex, 1) except Exception as exx: log.exception('[setPin] error while setting pin') Session.rollback() return sendError(response, exx, 0) finally: Session.close()
def getInitDetail(self, params, user=None): _ = context['translate'] response_detail = {} param_keys = set(params.keys()) init_rollout_state_keys = {'type', 'hashlib', 'serial', '::scope::', 'key_size', 'user.login', 'description', 'user.realm', 'session', 'otplen', 'pin', 'resConf', 'user', 'realm', 'qr'} # ---------------------------------------------------------------------- if param_keys.issubset(init_rollout_state_keys): # collect data used for generating the pairing url serial = self.getSerial() # for qrtoken hashlib is ignored hash_algorithm = None pub_key = get_qrtoken_public_key() otp_pin_length = int(self.getOtpLen()) owner = get_token_owner(self) if owner and owner.login and owner.realm: realms = [owner.realm] user = owner else: realms = self.getRealms() pairing_policies = ['qrtoken_pairing_callback_url', 'qrtoken_pairing_callback_sms'] # it is guaranteed, that either cb_url or cb_sms has a value # because we checked it in the update method cb_url = get_single_auth_policy(pairing_policies[0], user=owner, realms=realms) cb_sms = get_single_auth_policy(pairing_policies[1], user=owner, realms=realms) cert_id = get_pairing_certificate_id(realms=realms, user=user) # ------------------------------------------------------------------ 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, cert_id=cert_id) # ------------------------------------------------------------------ self.addToInfo('pairing_url', pairing_url) response_detail['pairing_url'] = pairing_url # create response tabs response_detail['lse_qr_url'] = { 'description': _('QRToken Pairing Url'), 'img': create_img(pairing_url, width=250), 'order': 0, 'value': pairing_url} response_detail['lse_qr_cert'] = { 'description': _('QRToken Certificate'), 'img': create_img(pairing_url, width=250), 'order': 1, 'value': pairing_url} response_detail['serial'] = self.getSerial() # ------------------------------------------------------------------ -- else: # make sure the call aborts, if request # type wasn't recognized raise Exception('Unknown request type for token type qr') # ---------------------------------------------------------------------- self.change_state('pairing_url_sent') return response_detail
def createChallenge(self, transaction_id, options): """ create a challenge - either for pairing or challenges when the token is activated. we support re-activation by the means that if we are in the state 'pairing_challenge_sent' the activation challenge could be triggered again :param transaction_id: scope of the challenge, will become part of the challenge url code :param options: the request optional parameters :return: tuple with (True, challenge_url, data, {}) whereby the data is a dict with {'message': message, 'user_sig': user_sig} """ _ = context['translate'] valid_states = [ 'pairing_response_received', 'pairing_challenge_sent', 'pairing_complete' ] self.ensure_state_is_in(valid_states) # ------------------------------------------------------------------- -- if self.current_state in [ 'pairing_challenge_sent', 'pairing_response_received' ]: content_type = CONTENT_TYPE_PAIRING reset_url = True elif self.current_state == 'pairing_complete': content_type_as_str = options.get('content_type') reset_url = False if content_type_as_str is None: content_type = None else: try: # pylons silently converts all ints in json # to unicode :( content_type = int(content_type_as_str) except: raise ValueError('Unrecognized content type: %s' % content_type_as_str) # ------------------------------------------------------------------- -- message = options.get('data') # ------------------------------------------------------------------- -- owner = get_token_owner(self) if owner and owner.login and owner.realm: realms = [owner.realm] else: realms = self.getRealms() callback_policies = [ 'qrtoken_challenge_callback_url', 'qrtoken_challenge_callback_sms' ] callback_url = get_single_auth_policy(callback_policies[0], user=owner, realms=realms) callback_sms = get_single_auth_policy(callback_policies[1], user=owner, realms=realms) if not callback_url and not callback_sms: raise Exception( _('Policy %s must have a value') % _(" or ").join(callback_policies)) # TODO: get from policy/config compression = False # ------------------------------------------------------------------- -- challenge_url, user_sig = self.create_challenge_url( transaction_id, content_type, message, callback_url, callback_sms, compression, reset_url) data = {'message': message, 'user_sig': user_sig} if self.current_state == 'pairing_response_received': self.change_state('pairing_challenge_sent') return (True, challenge_url, data, {})
def janitor_to_remove_enrollment_token(valid_tokens): """ remove all enrollment only tokens """ # ------------------------------------------------------------------ -- # get all owners for the valid tokens all_owners = set() for token in valid_tokens: # if the authenticated token is a rollout token, we dont count him path = token.getFromTokenInfo('scope', {}).get('path', []) if len(path) == 1 and path[0] == 'userservice': continue # TODO: get owner sadly throws a genric exception in case of # no intersection beteen token realms and user realms :( try: owner = get_token_owner(token) except Exception: continue if owner: all_owners.add(owner) # ------------------------------------------------------------------ -- # get all rollout only tokens per owner to_be_removed_tokens = [] for owner in all_owners: # should be purge the tokens of the user? <- defined by policy if not purge_enrollment_token(user=owner): continue user_tokens = getTokens4UserOrSerial(user=owner) # if there is only one token either # - the user has still the rollout: do not delete # - or the user has a new one: we dont delete either if len(user_tokens) < 2: continue for token in user_tokens: path = token.getFromTokenInfo('scope', {}).get('path', []) if len(path) == 1 and path[0] == 'userservice': to_be_removed_tokens.append(token) # ------------------------------------------------------------------ -- # delete all rollout tokens serials = [] for token in to_be_removed_tokens: serials.append(token.getSerial()) remove_token(token) # ------------------------------------------------------------------ -- # add info about the purging audit = context['audit'] if serials: audit['info'] += 'purged rollout tokens:' + ', '.join(serials)
def sendSMS(self, message=None, transactionid=None): ''' send sms :param message: the sms submit message - could contain placeholders like <otp> or <serial> :type message: string :return: submitted message :rtype: string ''' ret = None if not message: message = "<otp>" if not SMSPROVIDER_IMPORTED: raise Exception("The SMSProvider could not be imported. Maybe you " "didn't install the package (Debian " "linotp-smsprovider or PyPI SMSProvider)") # we require the token owner to get the phone number and the provider owner = get_token_owner(self) phone = self.get_mobile_number(owner) otp = self.getNextOtp() serial = self.getSerial() if '<otp>' not in message: log.error('Message unconfigured: prepending <otp> to message') if isinstance(message, basestring): message = "<otp> %s" % message else: message = "<otp> %r" % message message = message.replace("<otp>", otp) message = message.replace("<serial>", serial) if transactionid: message = message.replace("<transactionid>", transactionid) log.debug("[sendSMS] sending SMS to phone number %s " % phone) realm = None realms = self.getRealms() if realms: realm = realms[0] # we require the token owner to get the phone number and the provider owner = get_token_owner(self) if not owner or not owner.login: log.warning("[sendSMS] Missing required token owner") sms_provider = loadProviderFromPolicy(provider_type='sms', realm=realm, user=owner) if not sms_provider: raise Exception('unable to load provider') ret = sms_provider.submitMessage(phone, message) if not ret: raise Exception("Failed to submit message") log.debug("[sendSMS] message submitted") # # after submit set validity time self.setValidUntil() # return OTP for selftest purposes return ret, message
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, read_for_update=True) 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: %s' % (unicode(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['token_type'] = token.getType() reply['failcount'] = token.getFailCount() reply['value'] = ok if ok: break return ok, reply
def sendSMS(self, message=None, transactionid=None): ''' send sms :param message: the sms submit message - could contain placeholders like <otp> or <serial> :type message: string :return: submitted message :rtype: string ''' log.debug("[sendSMS] begin. process the submitting of " "the sms message %r" % (message)) ret = None if not message: message = "<otp>" if not SMSPROVIDER_IMPORTED: raise Exception("The SMSProvider could not be imported. Maybe you " "didn't install the package (Debian " "linotp-smsprovider or PyPI SMSProvider)") phone = self.getPhone() otp = self.getNextOtp() serial = self.getSerial() if '<otp>' not in message: log.error('Message unconfigured: prepending <otp> to message') if isinstance(message, basestring): message = "<otp> %s" % message else: message = "<otp> %r" % message message = message.replace("<otp>", otp) message = message.replace("<serial>", serial) if transactionid: message = message.replace("<transactionid>", transactionid) log.debug("[sendSMS] sending SMS to phone number %s " % phone) owner = get_token_owner(self) sms_provider = loadProviderFromPolicy(provider_type='sms', user=owner) if not sms_provider: raise Exception('unable to load provider') log.debug("[sendSMS] submitMessage: %r, to phone %r", message, phone) ret = sms_provider.submitMessage(phone, message) if not ret: raise Exception("Failed to submit message") log.debug("[sendSMS] message submitted") # # after submit set validity time self.setValidUntil() # return OTP for selftest purposes log.debug("[sendSMS] end. sms message submitted: message %r" % (message)) return ret, message
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
def update(self, params): param_keys = set(params.keys()) init_rollout_state_keys = {'type', 'hashlib', 'serial', '::scope::', 'key_size', 'user.login', 'description', 'user.realm', 'session', 'otplen', 'resConf', 'user', 'realm', 'qr', 'pin'} # ---------------------------------------------------------------------- if not param_keys.issubset(init_rollout_state_keys): # make sure the call aborts, if request # type wasn't recognized raise Exception('Unknown request type for token type qr') # 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) # ------------------------------------------------------------------ # we check if callback policies are set. this must be done here # because the token gets saved directly after the update method # in the TokenHandler _ = context['translate'] owner = get_token_owner(self) if owner and owner.login and owner.realm: realms = [owner.realm] else: realms = self.getRealms() pairing_policies = ['qrtoken_pairing_callback_url', 'qrtoken_pairing_callback_sms'] cb_url = get_single_auth_policy(pairing_policies[0], user=owner, realms=realms) cb_sms = get_single_auth_policy(pairing_policies[1], user=owner, realms=realms) if not cb_url and not cb_sms: raise Exception(_('Policy %s must have a value') % _(" or ").join(pairing_policies)) challenge_policies = ['qrtoken_challenge_callback_url', 'qrtoken_challenge_callback_sms'] cb_url = get_single_auth_policy(challenge_policies[0], user=owner, realms=realms) cb_sms = get_single_auth_policy(challenge_policies[1], user=owner, realms=realms) if not cb_url and not cb_sms: raise Exception(_('Policy %s must have a value') % _(" or ").join(pairing_policies)) # ------------------------------------------------------------------ # 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 # ------------------------------------------------------------------ if 'otplen' not in params: params['otplen'] = getFromConfig("QRTokenOtpLen", 8) # -------------------------------------------------------------- -- TokenClass.update(self, params, reset_failcount=True)
def getInitDetail(self, params, user=None): """ returns initialization details in the enrollment process (gets called after update method). used here to pass the pairing url to the user :param params: parameters provided by the client :param user: (unused) :raises TokenStateError: If token state is not 'initialized' :returns: a dict consisting of a 'pairing_url' entry, containing the pairing url and a 'pushtoken_pairing_url' entry containing a data structure used in the manage frontend in the enrollment process """ _ = context['translate'] response_detail = {} self.ensure_state('initialized') # ------------------------------------------------------------------- -- # collect data used for generating the pairing url serial = self.getSerial() # ------------------------------------------------------------------- -- owner = get_token_owner(self) if owner and owner.login and owner.realm: realms = [owner.realm] else: realms = self.getRealms() # it is guaranteed, that cb_url has a value # because we checked it in the update method cb_url = get_single_auth_policy('pushtoken_pairing_callback_url', user=owner, realms=realms) # --------------------------------------------------------------- -- partition = self.getFromTokenInfo('partition') # FIXME: certificate usage pairing_url = generate_pairing_url(token_type='push', partition=partition, serial=serial, callback_url=cb_url, use_cert=False) # --------------------------------------------------------------- -- self.addToInfo('pairing_url', pairing_url) response_detail['pairing_url'] = pairing_url # --------------------------------------------------------------- -- # add response tabs (used in the manage view on enrollment) response_detail['lse_qr_url'] = { 'description': _('Pairing URL'), 'img': create_img(pairing_url, width=250), 'order': 0, 'value': pairing_url} response_detail['serial'] = self.getSerial() # ------------------------------------------------------------------ -- self.change_state('unpaired') return response_detail
def createChallenge(self, transaction_id, options): """ entry hook for the challenge logic. when this function is called a challenge with an transaction was created. :param transaction_id: A unique transaction id used to identity the challenge object :param options: additional options as a dictionary :raises TokenStateError: If token state is not 'active' or 'pairing_response_received' :returns: A tuple (success, message, data, attributes) with success being a boolean indicating if the call to this method was successful, message being a string that is passed to the user, attributes being additional output data (unused in here) """ _ = context['translate'] valid_states = ['active', 'pairing_response_received', # we support re activation # as long as the token is not active 'pairing_challenge_sent', ] self.ensure_state_is_in(valid_states) # ------------------------------------------------------------------- -- # inside the challenge url we sent a callback url for the client # which is defined by an authentication policy owner = get_token_owner(self) if owner and owner.login and owner.realm: realms = [owner.realm] else: realms = self.getRealms() callback_policy_name = 'pushtoken_challenge_callback_url' callback_url = get_single_auth_policy(callback_policy_name, user=owner, realms=realms) if not callback_url: raise Exception(_('Policy pushtoken_challenge_callback_url must ' 'have a value')) # ------------------------------------------------------------------- -- # load and configure provider # the realm logic was taken from the # provider loading in the smstoken class # TODO: refactor & centralize logic realm = None if realms: realm = realms[0] push_provider = loadProviderFromPolicy(provider_type='push', realm=realm, user=owner) # ------------------------------------------------------------------- -- if self.current_state in ['pairing_response_received', 'pairing_challenge_sent']: content_type = CONTENT_TYPE_PAIRING message = '' challenge_url, sig_base = self.create_challenge_url(transaction_id, content_type, callback_url) elif self.current_state in ['active']: content_type_as_str = options.get( 'content_type', CONTENT_TYPE_SIGNREQ) try: # pylons silently converts all ints in json # to unicode :( content_type = int(content_type_as_str) except: raise ValueError('Unrecognized content type: %s' % content_type_as_str) # --------------------------------------------------------------- -- if content_type == CONTENT_TYPE_SIGNREQ: message = options.get('data') challenge_url, sig_base = self.create_challenge_url( transaction_id, content_type, callback_url, message=message) # --------------------------------------------------------------- -- elif content_type == CONTENT_TYPE_LOGIN: message = options.get('data') login, __, host = message.partition('@') challenge_url, sig_base = self.create_challenge_url( transaction_id, content_type, callback_url, login=login, host=host) else: raise ValueError('Unrecognized content type: %s' % content_type) # ------------------------------------------------------------------- -- # send the challenge_url to the push notification proxy token_info = self.getTokenInfo() gda = token_info['gda'] log.debug("pushing notification: %r : %r", challenge_url, gda) success, response = push_provider.push_notification( challenge_url, gda, transaction_id) if not success: raise Exception('push mechanism failed. response was %r' % response) # ------------------------------------------------------------------- -- # we save sig_base in the challenge data, because we need it in # checkOtp to verify the signature b64_sig_base = b64encode(sig_base) data = {'sig_base': b64_sig_base} if self.current_state == 'pairing_response_received': self.change_state('pairing_challenge_sent') # ------------------------------------------------------------------- -- # don't pass the challenge_url as message to the user return (True, '', data, {})
def update(self, params): """ initialization entry hook for the enrollment process. :param params: parameters provided by the client :raises Exception: If the client supplied unrecognized configuration parameters for this token type :raises Exception: If the policy 'pushtoken_pairing_callback_url' was not set. :raises TokenStateError: If token state is not None (default pre-enrollment state) """ param_keys = set(params.keys()) init_rollout_state_keys = set(['type', 'serial', '::scope::', 'user.login', 'description', 'user.realm', 'session', 'key_size', 'resConf', 'user', 'realm', 'pin']) # ------------------------------------------------------------------- -- if not param_keys.issubset(init_rollout_state_keys): # make sure the call aborts, if request # type wasn't recognized raise Exception('Unknown request type for token type pushtoken') # if param keys are in above set, 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: the data for # the rollout state doesn't exist yet) self.ensure_state(None) # --------------------------------------------------------------- -- # we check if callback policies are set. this must be done here # because the token gets saved directly after the update method # in the TokenHandler _ = context['translate'] owner = get_token_owner(self) if owner and owner.login and owner.realm: realms = [owner.realm] else: realms = self.getRealms() cb_url = get_single_auth_policy('pushtoken_pairing_callback_url', user=owner, realms=realms) if not cb_url: raise Exception(_('Policy pushtoken_pairing_callback_url must ' 'have a value')) partition = get_partition(realms, owner) self.addToTokenInfo('partition', partition) # --------------------------------------------------------------- -- # 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 # -------------------------------------------------------------- -- TokenClass.update(self, params, reset_failcount=True) # -------------------------------------------------------------- -- self.change_state('initialized')
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: %s' % (unicode(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