"Fossies" - the Fresh Open Source Software Archive

Member "LinOTP-release-2.10.5.2/linotpd/src/linotp/tokens/smstoken.py" (13 May 2019, 32603 Bytes) of package /linux/misc/LinOTP-release-2.10.5.2.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "smstoken.py" see the Fossies "Dox" file reference documentation.

    1 # -*- coding: utf-8 -*-
    2 #
    3 #    LinOTP - the open source solution for two factor authentication
    4 #    Copyright (C) 2010 - 2019 KeyIdentity GmbH
    5 #
    6 #    This file is part of LinOTP server.
    7 #
    8 #    This program is free software: you can redistribute it and/or
    9 #    modify it under the terms of the GNU Affero General Public
   10 #    License, version 3, as published by the Free Software Foundation.
   11 #
   12 #    This program is distributed in the hope that it will be useful,
   13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
   14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   15 #    GNU Affero General Public License for more details.
   16 #
   17 #    You should have received a copy of the
   18 #               GNU Affero General Public License
   19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
   20 #
   21 #
   22 #    E-mail: linotp@keyidentity.com
   23 #    Contact: www.linotp.org
   24 #    Support: www.keyidentity.com
   25 #
   26 
   27 """This file containes the dynamic sms token implementation:
   28               - SMSTokenClass (sms)
   29 
   30     the SMS Token is an challenge - response token, by the means, that there is
   31     a first request, which triggers the challenge (=sending of sms message)
   32     and a second request, which refers to the initial request by the
   33     transactionid and verifies the otp value:
   34 
   35     /validate/check_s
   36 
   37     with params
   38      :param serial: the token serialnumber [required]
   39      :param pass: the token pin or
   40                   if the token belongs to an user, the user pin/password
   41                   (s. otppin policy) [required]
   42     :param data: the message, that will contain the otp value [optional]
   43                  * In the message, the strings <otp>, <serial> and
   44                    <transactionid> will be replaced
   45                  * if no data is provided, the smstext - policy value will be
   46                    evaluated. Fallback is the message "<otp>"
   47     :param message: alternative name for the data parameter
   48 
   49     :return: json result wit tansaction an message, that will be displayed to
   50              the user
   51 
   52                 {
   53                     "detail": {
   54                         "transactionid": "172682842808",
   55                         "message": "sms submitted",
   56                         "state": "172682842808"
   57                     },
   58                     "version": "LinOTP 2.7.2.1",
   59                     "jsonrpc": "2.0",
   60                     "result": {
   61                         "status": true,
   62                         "value": false
   63                     },
   64                     "id": 0
   65 
   66                 }
   67 
   68     for the validation of the sms request now the controller method
   69 
   70     /validate/check_t
   71 
   72     could be used with the parameters
   73 
   74     :param pass: received otp value
   75     :param transactionid: the transactionid, which referes, that the pin
   76                           has been verified and checked
   77 
   78     alternativly the controller method
   79 
   80     /validate/check_s
   81 
   82     could be used as well, by providing the combination of the pin+otp
   83     in the pass parameter:
   84 
   85     :param serial: serial number of the token
   86     :param pass: the password consisting of fixed part and the otp part
   87 
   88     :return: json response
   89 
   90     {
   91         "version": "LinOTP 2.7.2.1",
   92         "jsonrpc": "2.0",
   93         "result": {
   94             "status": true,
   95             "value": true
   96         },
   97         "id": 0
   98     }
   99 
  100 
  101 """
  102 
  103 import time
  104 import datetime
  105 
  106 from linotp.lib.HMAC import HmacOtp
  107 
  108 from linotp.lib.user import getUserDetail
  109 from linotp.lib.user import getUserFromParam
  110 from linotp.lib.user import get_user_from_options
  111 from linotp.lib.user import User
  112 
  113 from linotp.lib.auth.validate import check_pin
  114 from linotp.lib.auth.validate import check_otp
  115 from linotp.lib.auth.validate import split_pin_otp
  116 
  117 from linotp.lib.config import getFromConfig
  118 from linotp.lib.token import get_token_owner
  119 
  120 from linotp.lib.policy import getPolicyActionValue
  121 from linotp.lib.policy import getPolicy, get_client_policy
  122 from linotp.lib.policy import get_auth_AutoSMSPolicy
  123 from linotp.lib.policy import trigger_sms
  124 
  125 from linotp.lib.context import request_context as context
  126 
  127 from linotp.provider import get_provider_from_policy
  128 from linotp.provider import loadProvider
  129 from linotp.provider import loadProviderFromPolicy
  130 from linotp.provider import ProviderNotAvailable
  131 
  132 from linotp.lib.resources import ResourceScheduler
  133 from linotp.lib.resources import AllResourcesUnavailable
  134 
  135 from linotp.lib.error import ParameterError
  136 
  137 from linotp.tokens.hmactoken import HmacTokenClass
  138 from linotp.tokens import tokenclass_registry
  139 
  140 import logging
  141 log = logging.getLogger(__name__)
  142 
  143 try:
  144     from linotp.provider.smsprovider import getSMSProviderClass
  145     SMSPROVIDER_IMPORTED = True
  146 except ImportError as exx:
  147     log.warning("Failed to import SMSProvider %s" % exx)
  148     SMSPROVIDER_IMPORTED = False
  149 
  150 keylen = {'sha1': 20,
  151           'sha256': 32,
  152           'sha512': 64,
  153           }
  154 
  155 
  156 ##################################################################
  157 def get_auth_smstext(user="", realm=""):
  158     '''
  159     this function checks the policy scope=authentication, action=smstext
  160     This is a string policy
  161     The function returns the tuple (bool, string),
  162         bool: If a policy is defined
  163         string: the string to use
  164     '''
  165     # the default string is the OTP value
  166     ret = False
  167     smstext = "<otp>"
  168 
  169     pol = get_client_policy(context['Client'], scope="authentication",
  170                             realm=realm, user=user, action="smstext")
  171 
  172     if len(pol) > 0:
  173         smstext = getPolicyActionValue(pol, "smstext", is_string=True)
  174         log.debug("[get_auth_smstext] got the smstext = %s" % smstext)
  175         ret = True
  176 
  177     return ret, smstext
  178 
  179 
  180 def enforce_smstext(user="", realm=""):
  181     '''
  182     this function checks the boolean policy
  183                             scope=authentication,
  184                             action=enforce_smstext
  185 
  186     The function returns true if the smstext should be used instead of the
  187     challenge data
  188     :return: bool
  189     '''
  190     pol = get_client_policy(context['Client'], scope="authentication",
  191                             realm=realm, user=user, action="enforce_smstext")
  192 
  193     if len(pol) > 0:
  194         enforce_smstext = getPolicyActionValue(pol, "enforce_smstext")
  195         log.debug("got enforce_smstext = %r" % enforce_smstext)
  196         return enforce_smstext or False
  197 
  198     return False
  199 
  200 
  201 def is_phone_editable(user=""):
  202     '''
  203     this function checks the policy scope=selfservice, action=edit_sms
  204     This is a int policy, while the '0' is a deny
  205     '''
  206     # the default string is the OTP value
  207     ret = True
  208     realm = user.realm
  209     login = user.login
  210 
  211     policies = getPolicy({'scope': 'selfservice',
  212                           'realm': realm,
  213                           "action": "edit_sms",
  214                           "user": login})
  215     if policies:
  216         edit_sms = getPolicyActionValue(policies, "edit_sms")
  217         if edit_sms == 0:
  218             ret = False
  219 
  220     return ret
  221 
  222 @tokenclass_registry.class_entry('sms')
  223 @tokenclass_registry.class_entry('linotp.tokens.smstoken.SmsTokenClass')
  224 class SmsTokenClass(HmacTokenClass):
  225     '''
  226     implementation of the sms token class
  227     '''
  228     def __init__(self, aToken):
  229         HmacTokenClass.__init__(self, aToken)
  230         self.setType(u"sms")
  231         self.hKeyRequired = False
  232 
  233         # we support various hashlib methods, but only on create
  234         # which is effectively set in the update
  235         self.hashlibStr = getFromConfig("hotp.hashlib", "sha1")
  236         self.mode = ['challenge']
  237 
  238     @classmethod
  239     def getClassType(cls):
  240         '''
  241         return the generic token class identifier
  242         '''
  243         return "sms"
  244 
  245     @classmethod
  246     def getClassPrefix(cls):
  247         return "LSSM"
  248 
  249     def get_challenge_validity(self):
  250         '''
  251         This method returns the token specific challenge validity
  252 
  253         :return: int - validity in seconds
  254         '''
  255 
  256         validity = 120
  257 
  258         try:
  259             validity = int(getFromConfig('DefaultChallengeValidityTime', 120))
  260             lookup_for = 'SMSProviderTimeout'
  261             validity = int(getFromConfig(lookup_for, validity))
  262 
  263             # instance specific timeout
  264             validity = int(self.getFromTokenInfo('challenge_validity_time',
  265                                                  validity))
  266 
  267         except ValueError:
  268             validity = 120
  269 
  270         return validity
  271 
  272     @classmethod
  273     def getClassInfo(cls, key=None, ret='all'):
  274         '''
  275         getClassInfo - returns all or a subtree of the token definition
  276 
  277         :param key: subsection identifier
  278         :type key: string
  279 
  280         :param ret: default return value, if nothing is found
  281         :type ret: user defined
  282 
  283         :return: subsection if key exists or user defined
  284         :rtype : s.o.
  285 
  286         '''
  287 
  288         _ = context['translate']
  289 
  290         res = {
  291                'type' : 'sms',
  292                'title' : _('SMS Token'),
  293                'description' :
  294                     _('sms challenge-response token - hmac event based'),
  295                'init'         : { 'title'  : {'html'      : 'smstoken.mako',
  296                                              'scope'     : 'enroll.title', },
  297                                   'page' : {'html'      : 'smstoken.mako',
  298                                             'scope'      : 'enroll', },
  299                                    },
  300 
  301                'config'         : {'title'  : {'html'      : 'smstoken.mako',
  302                                              'scope'     : 'config.title', },
  303                                    'page' : {'html'      : 'smstoken.mako',
  304                                             'scope'      : 'config', },
  305                                    },
  306 
  307                'selfservice'   :  { 'enroll' :
  308                                    {'title'  :
  309                                     { 'html'      : 'smstoken.mako',
  310                                       'scope'     : 'selfservice.title.enroll',
  311                                       },
  312                                     'page' :
  313                                     {'html'       : 'smstoken.mako',
  314                                      'scope'      : 'selfservice.enroll',
  315                                      },
  316                                     },
  317                                   },
  318             'policy': {'selfservice':
  319                        {'edit_sms':
  320                         {'type': 'int',
  321                          'value': [0, 1],
  322                          'desc': _('define if the user should be allowed'
  323                                     ' to define the sms')
  324                          }},
  325                        'authentication':{
  326                            'sms_dynamic_mobile_number':{
  327                                'type': 'bool',
  328                                'desc': _('if set, a new mobile number will be '
  329                                        'retrieved from the user info instead '
  330                                        'of the token')},
  331 
  332                            }
  333                        }
  334         }
  335 
  336         if key and key in res:
  337             ret = res.get(key)
  338         else:
  339             if ret == 'all':
  340                 ret = res
  341 
  342         return ret
  343 
  344     def update(self, param, reset_failcount=True):
  345         '''
  346         update - process initialization parameters
  347 
  348         :param param: dict of initialization parameters
  349         :type param: dict
  350 
  351         :return: nothing
  352 
  353         '''
  354         _ = context['translate']
  355 
  356         # specific - phone
  357         try:
  358             phone = param['phone']
  359         except KeyError:
  360             raise ParameterError("Missing parameter: 'phone'")
  361 
  362         # in scope selfservice - check if edit_sms is allowed
  363         # if not allowed to edit, check if the phone is the same
  364         # as from the user data
  365         if param.get('::scope::', {}).get('selfservice', False):
  366             user = param['::scope::']['user']
  367             if not is_phone_editable(user):
  368                 u_info = getUserDetail(user)
  369                 u_phone = u_info.get('mobile', u_info.get('phone', None))
  370                 if u_phone != phone:
  371                     raise Exception(_('User is not allowed to '
  372                                       'set phone number'))
  373 
  374         self.setPhone(phone)
  375 
  376         # in case of the sms token, only the server must know the otpkey
  377         # thus if none is provided, we let create one (in the TokenClass)
  378         if 'genkey' not in param and 'otpkey' not in param:
  379             param['genkey'] = 1
  380 
  381         HmacTokenClass.update(self, param, reset_failcount)
  382 
  383         return
  384 
  385     def is_challenge_response(self, passw, user, options=None,
  386                               challenges=None):
  387         '''
  388         check, if the request contains the result of a challenge
  389 
  390         :param passw: password, which might be pin or pin+otp
  391         :param user: the requesting user
  392         :param options: dictionary of additional request parameters
  393 
  394         :return: returns true or false
  395         '''
  396 
  397         if "state" in options or "transactionid" in options:
  398             return True
  399 
  400         # it as well might be a challenge response,
  401         # if the passw is longer than the pin
  402         (res, pin, otpval) = split_pin_otp(self, passw, user=user,
  403                                            options=options)
  404         if res >= 0:
  405             otp_counter = check_otp(self, otpval, options=options)
  406             if otp_counter >= 1:
  407                 pin_match = check_pin(self, pin, user=user, options=options)
  408                 if not pin_match:
  409                     return False
  410             if otp_counter >= 0:
  411                 return True
  412 
  413         return False
  414 
  415 # ## challenge interfaces starts here
  416     def is_challenge_request(self, passw, user, options=None):
  417         '''
  418         check, if the request would start a challenge
  419 
  420         - default: if the passw contains only the pin, this request would
  421         trigger a challenge
  422 
  423         - in this place as well the policy for a token is checked
  424 
  425         :param passw: password, which might be pin or pin+otp
  426         :param options: dictionary of additional request parameters
  427 
  428         :return: returns true or false
  429         '''
  430 
  431         request_is_valid = False
  432         # do we need to call the
  433         # (res, pin, otpval) = split_pin_otp(self, passw, user, options=options)
  434         # if policy to send sms on emtpy pin is set, return true
  435         realms = self.token.getRealmNames()
  436         if trigger_sms(realms):
  437             if 'check_s' in options.get('scope', {}) and 'challenge' in options:
  438                 request_is_valid = True
  439                 return request_is_valid
  440 
  441         # if its a challenge, the passw contains only the pin
  442         pin_match = check_pin(self, passw, user=user, options=options)
  443         if pin_match is True:
  444             request_is_valid = True
  445 
  446         return request_is_valid
  447 
  448     #
  449     # !!! this function is to be called in the sms controller !!!
  450     #
  451     def submitChallenge(self, options=None):
  452         '''
  453         submit the sms message - former method name was checkPin
  454 
  455         :param options: the request options context
  456 
  457         :return: tuple of success and message
  458         '''
  459 
  460         _ = context['translate']
  461 
  462         res = 0
  463 
  464         fallback_realm = (self.getRealms() or [None])[0]
  465         login, realm = get_user_from_options(options,
  466                                      fallback_user=get_token_owner(self),
  467                                      fallback_realm=fallback_realm)
  468 
  469         message = options.get('challenge', "<otp>")
  470         result = _("sending sms failed")
  471 
  472         # it is configurable, if sms should be triggered by a valid pin
  473         send_by_PIN = getFromConfig("sms.sendByPin") or True
  474 
  475         # if the token is not active or there is no pin triggered sending, we leave
  476         if not(self.isActive() is True and send_by_PIN is True):
  477             return res, result
  478 
  479         counter = self.getOtpCount()
  480         log.debug("[submitChallenge] counter=%r" % counter)
  481 
  482         # At this point we MUST NOT bail out in case of an
  483         # Gateway error, since checkPIN is successful, as the bail
  484         # out would cancel the checking of the other tokens
  485         try:
  486             sms_ret = False
  487             new_message = None
  488 
  489             sms_ret, new_message = get_auth_smstext(user=login, realm=realm)
  490             if sms_ret:
  491                 message = new_message
  492 
  493             # ---------------------------------------------------------- --
  494 
  495             # if there is a data or message part in the request, it might
  496             # overrule the given smstext
  497 
  498             if 'data' in options or 'message' in options:
  499 
  500                 # if there is an enforce policy
  501                 # we do not allow the owerwrite
  502 
  503                 enforce = enforce_smstext(
  504                     user=login, realm=realm)
  505 
  506                 if not enforce:
  507                     message = options.get(
  508                                 'data', options.get('message', '<otp>'))
  509 
  510             # ---------------------------------------------------------- --
  511 
  512             # fallback if no message is defined
  513 
  514             if not message:
  515                 message = "<otp>"
  516 
  517             # ---------------------------------------------------------- --
  518 
  519             # submit the sms message
  520 
  521             transactionid = options.get('transactionid', None)
  522             res, result = self.sendSMS(message=message,
  523                                        transactionid=transactionid)
  524 
  525             self.info['info'] = "SMS sent: %r" % res
  526             log.debug('SMS sent: %s', result)
  527 
  528             return res, result
  529 
  530         except Exception as e:
  531             # The PIN was correct, but the SMS could not be sent.
  532             self.info['info'] = unicode(e)
  533             info = ("The SMS could not be sent: %r" % e)
  534             log.warning("[submitChallenge] %s", info)
  535             return False, info
  536 
  537         finally:
  538             # we increment the otp in any case, independend if sending
  539             # of the sms was sucsessful
  540             self.incOtpCounter(counter, reset=False)
  541 
  542     def initChallenge(self, transactionid, challenges=None, options=None):
  543         """
  544         initialize the challenge -
  545         in the linotp server a challenge object has been allocated and
  546         this method is called to confirm the need of a new challenge
  547         or if for the challenge request, there is an already outstanding
  548         challenge to which then could be referred (s. ticket #2986)
  549 
  550         :param transactionid: the id of the new challenge
  551         :param options: the request parameters
  552 
  553         :return: tuple of
  554                 success - bool
  555                 transid - the best transaction id for this request context
  556                 message - which is shown to the user
  557                 attributes - further info (dict) shown to the user
  558         """
  559 
  560         success = True
  561         transid = transactionid
  562         message = 'challenge init ok'
  563         attributes = {}
  564 
  565         now = datetime.datetime.now()
  566         blocking_time = int(getFromConfig('SMSBlockingTimeout', 60))
  567 
  568         for challenge in challenges:
  569             if not challenge.is_open():
  570                 continue
  571             start = challenge.get('timestamp')
  572             expiry = start + datetime.timedelta(seconds=blocking_time)
  573             # # check if there is already a challenge underway
  574             if now <= expiry:
  575                 transid = challenge.getTransactionId()
  576                 message = 'sms with otp already submitted'
  577                 success = False
  578                 attributes = {'info': 'challenge already submitted',
  579                               'state': transid}
  580                 break
  581 
  582         return (success, transid, message, attributes)
  583 
  584     def createChallenge(self, transactionid, options=None):
  585         """
  586         create a challenge, which is submitted to the user
  587 
  588         :param transactionid: the id of this challenge
  589         :param options: the request context parameters / data
  590         :return: tuple of (bool, message and data)
  591                  bool, if submit was successful
  592                  message is submitted to the user
  593                  data is preserved in the challenge
  594                  attributes - additional attributes, which are displayed in the
  595                     output
  596         """
  597         if options is None:
  598             options = {}
  599 
  600         message = getFromConfig(
  601                             self.type.upper() + "_CHALLENGE_PROMPT",
  602                             'sms submitted')
  603 
  604         attributes = {'state': transactionid}
  605 
  606         options['state'] = transactionid
  607         success, sms = self.submitChallenge(options=options)
  608 
  609         if success is True:
  610             self.setValidUntil()
  611         else:
  612             attributes = {'state': ''}
  613             message = 'sending sms failed'
  614 
  615             if sms:
  616                 message = sms
  617 
  618         # after submit set validity time in readable
  619         # datetime format in the storeing data
  620         timeScope = self.loadLinOtpSMSValidTime()
  621         expiryDate = datetime.datetime.now() + \
  622                                     datetime.timedelta(seconds=timeScope)
  623         data = {'valid_until': "%s" % expiryDate}
  624 
  625         return (success, message, data, attributes)
  626 
  627     def checkResponse4Challenge(self, user, passw, options=None, challenges=None):
  628         """
  629         verify the response of a previous challenge
  630 
  631         :param user:     the requesting user
  632         :param passw:    the to be checked pass (pin+otp)
  633         :param options:  options an additional argument, which could be token
  634                           specific
  635         :param challenges: the list of challenges, where each challenge is
  636                             described as dict
  637         :return: tuple of (otpcounter and the list of matching challenges)
  638 
  639         do the standard check for the response of the challenge +
  640         change the tokeninfo data of the last challenge
  641         """
  642         otp_count = -1
  643         matching = []
  644 
  645         tok = super(SmsTokenClass, self)
  646         counter = self.getOtpCount()
  647         window = self.getOtpCountWindow()
  648 
  649         now = datetime.datetime.now()
  650         timeScope = self.loadLinOtpSMSValidTime()
  651 
  652         otp_val = passw
  653 
  654         # # fallback: do we have pin+otp ??
  655         (res, pin, otp) = split_pin_otp(self, passw, user=user,
  656                                                             options=options)
  657 
  658         if res >= 0:
  659             res = check_pin(self, pin, user=user, options=options)
  660             if res is True:
  661                 otp_val = otp
  662 
  663         for challenge in challenges:
  664             otp_count = self.checkOtp(otp_val, counter, window,
  665                                                             options=options)
  666             if otp_count > 0:
  667                 matching.append(challenge)
  668                 break
  669 
  670         return (otp_count, matching)
  671 
  672     def checkOtp(self, anOtpVal, counter, window, options=None):
  673         '''
  674         checkOtp - check the otpval of a token against a given counter
  675         in the + window range
  676 
  677         :param passw: the to be verified passw/pin
  678         :type passw: string
  679 
  680         :return: counter if found, -1 if not found
  681         :rtype: int
  682         '''
  683 
  684         if not options:
  685             options = {}
  686 
  687         ret = HmacTokenClass.checkOtp(self, anOtpVal, counter, window)
  688         if ret != -1:
  689             if self.isValid() is False:
  690                 ret = -1
  691 
  692         if ret >= 0:
  693             if get_auth_AutoSMSPolicy():
  694                 user = None
  695                 message = "<otp>"
  696                 realms = self.getRealms()
  697                 if realms:
  698                     _sms_ret, message = get_auth_smstext(realm=realms[0])
  699 
  700                 if 'user' in options:
  701                     user = options.get('user', None)
  702                     if user:
  703                         _sms_ret, message = get_auth_smstext(realm=user.realm)
  704                 realms = self.getRealms()
  705 
  706                 if 'data' in options or 'message' in options:
  707                     message = options.get('data',
  708                                           options.get('message', '<otp>'))
  709 
  710                 try:
  711                     _success, message = self.sendSMS(message=message)
  712                 except Exception as exx:
  713                     log.exception(exx)
  714                 finally:
  715                     self.incOtpCounter(ret, reset=False)
  716         if ret >= 0:
  717             msg = "otp verification was successful!"
  718         else:
  719             msg = "otp verification failed!"
  720         log.debug(msg)
  721         return ret
  722 
  723     def getNextOtp(self):
  724         '''
  725         access the nex validf otp
  726 
  727         :return: otpval
  728         :rtype: string
  729         '''
  730 
  731         try:
  732             # ## TODO - replace tokenLen
  733             otplen = int(self.token.LinOtpOtpLen)
  734         except ValueError as ex:
  735             log.exception("[getNextOtp] ValueError %r" % ex)
  736             raise ex
  737 
  738         secObj = self._get_secret_object()
  739         counter = self.token.getOtpCounter()
  740 
  741         hmac2otp = HmacOtp(secObj, counter, otplen)
  742         nextotp = hmac2otp.generate(counter + 1)
  743 
  744         return nextotp
  745 
  746     # in the SMS token we use the generic TokenInfo
  747     # to store the phone number
  748     def setPhone(self, phone):
  749         '''
  750         setter for the phone number
  751 
  752         :param phone: phone number
  753         :type phone:  string
  754 
  755         :return: nothing
  756         '''
  757         self.setSMSInfo("phone", phone)
  758         return
  759 
  760     def setUntil(self, until):
  761         '''
  762         This is the time the sent OTP value is valid/can be used.
  763                                                         (internal function)
  764 
  765         :param until: until time in unix time sec
  766         :type until:  int
  767 
  768         :return: nothing
  769         '''
  770 
  771         self.setSMSInfo("until", until)
  772         return
  773 
  774     def _getPhone(self):
  775         '''
  776         getter for the phone number
  777 
  778         :return:  phone number
  779         :rtype:  string
  780         '''
  781 
  782         (phone, _till) = self.getSMSInfo()
  783         return phone
  784 
  785     def get_mobile_number(self, user=None):
  786         '''
  787         get the mobile number
  788             - from the token info or
  789             - if the policy allowes it, from the user info
  790         '''
  791 
  792         if not user:
  793             return self._getPhone()
  794 
  795         pol = get_client_policy(context['Client'],
  796                                 scope="authentication",
  797                                 user=user,
  798                                 action="sms_dynamic_mobile_number")
  799 
  800         if not pol:
  801             return self._getPhone()
  802 
  803         get_dynamic = getPolicyActionValue(pol, "sms_dynamic_mobile_number",
  804                                             is_string=True)
  805 
  806         if not get_dynamic:
  807             return self._getPhone()
  808 
  809         user_detail = getUserDetail(user)
  810         return user_detail.get('mobile', self._getPhone())
  811 
  812     def getUntil(self):
  813         '''
  814         getter for the until time definition
  815 
  816         :return:  until time definition of unix time sec
  817         :rtype:  int
  818         '''
  819 
  820         (_phone, until) = self.getSMSInfo()
  821 
  822         # # suport for direct verification
  823         if until == 0:
  824             timeScope = self.loadLinOtpSMSValidTime()
  825             until = int(time.time()) + timeScope
  826         return until
  827 
  828     def setSMSInfo(self, key, value):
  829         '''
  830         generic method to set the sms infos like phone or validity in the
  831         tokeninfo (json) entry
  832 
  833         :param key: name of the hash key
  834         :type key:  string
  835         :param value: value of the entry
  836         :type value: any
  837 
  838         :return: nothing
  839         '''
  840         self.addToTokenInfo(key, value)
  841         return
  842 
  843     def getSMSInfo(self):
  844         '''
  845         retrieve the phone number and the validity scope
  846 
  847         :return: tuple of phone number and validity time in unix lifetime sec
  848         '''
  849 
  850         info = self.getTokenInfo()
  851         phone = info.get("phone", '')
  852         until = info.get("until", 0)
  853 
  854         return (phone, until)
  855 
  856     # we take the countWindow.column to store the time
  857     #  in int format (cut off the .2 from the time() )
  858 
  859     def setValidUntil(self):
  860         '''
  861         adjust the timeframe of validity
  862 
  863         :return: nothing
  864         '''
  865         timeScope = self.loadLinOtpSMSValidTime()
  866         dueDate = int(time.time()) + timeScope
  867         self.setUntil(dueDate)
  868         # self.token.setCountWindow(dueDate)
  869 
  870         return dueDate
  871 
  872     def isValid(self):
  873         '''
  874         check if sms challenge is still valid
  875 
  876         :return: True or False
  877         :rtype: boolean
  878         '''
  879         ret = False
  880         dueDate = self.getUntil()
  881         now = int(time.time())
  882         if dueDate >= now:
  883             ret = True
  884         if ret is True:
  885             msg = "the sms challenge is still valid"
  886         else:
  887             msg = "the sms challenge is no more valid"
  888         return ret
  889 
  890     def sendSMS(self, message=None, transactionid=None):
  891         '''
  892         send sms
  893 
  894         :param message: the sms submit message - could contain placeholders
  895          like <otp> or <serial>
  896         :type message: string
  897 
  898         :return: submitted message
  899         :rtype: string
  900 
  901         '''
  902 
  903         success = None
  904 
  905         if not message:
  906             message = "<otp>"
  907 
  908         if not SMSPROVIDER_IMPORTED:
  909             raise Exception("The SMSProvider could not be imported. Maybe you "
  910                             "didn't install the package (Debian "
  911                             "linotp-smsprovider or PyPI SMSProvider)")
  912 
  913         # we require the token owner to get the phone number and the provider
  914         owner = get_token_owner(self)
  915 
  916         phone = self.get_mobile_number(owner)
  917 
  918         otp = self.getNextOtp()
  919         serial = self.getSerial()
  920 
  921         if '<otp>' not in message:
  922             log.error('Message unconfigured: prepending <otp> to message')
  923             if isinstance(message, basestring):
  924                 message = "<otp> %s" % message
  925             else:
  926                 message = "<otp> %r" % message
  927 
  928         message = message.replace("<otp>", otp)
  929         message = message.replace("<serial>", serial)
  930 
  931         if transactionid:
  932             message = message.replace("<transactionid>", transactionid)
  933 
  934         log.debug("[sendSMS] sending SMS to phone number %s " % phone)
  935 
  936         realm = None
  937         realms = self.getRealms()
  938         if realms:
  939             realm = realms[0]
  940 
  941         # we require the token owner to get the phone number and the provider
  942         owner = get_token_owner(self)
  943         if not owner or not owner.login:
  944             log.warning("[sendSMS] Missing required token owner")
  945 
  946         # ------------------------------------------------------------------ --
  947 
  948         # load providers for the user
  949 
  950         providers = get_provider_from_policy('sms', realm=realm, user=owner)
  951 
  952         # remember if at least one provider could be accessed
  953         available = False
  954 
  955         res_scheduler = ResourceScheduler(tries=1, uri_list=providers)
  956         for provider_name in res_scheduler.next():
  957 
  958             sms_provider = loadProvider('sms', provider_name=provider_name)
  959 
  960             if not sms_provider:
  961                 raise Exception('unable to load provider')
  962 
  963             try:
  964 
  965                 success = sms_provider.submitMessage(phone, message)
  966 
  967                 available = True
  968                 break
  969 
  970             except ProviderNotAvailable as exx:
  971                 log.error('Provider not available %r', provider_name)
  972                 res_scheduler.block(provider_name, delay=30)
  973 
  974         if not available:
  975             raise AllResourcesUnavailable("unable to connect to any "
  976                                           "SMSProvider %r" % providers)
  977 
  978         log.debug("[sendSMS] message submitted")
  979 
  980         # # after submit set validity time
  981         self.setValidUntil()
  982 
  983         # return OTP for selftest purposes
  984         return success, message
  985 
  986     def loadLinOtpSMSValidTime(self):
  987         '''
  988         get the challenge time is in the specified range
  989 
  990         :return: the defined validation timeout in seconds
  991         :rtype:  int
  992         '''
  993         try:
  994             timeout = int(getFromConfig("SMSProviderTimeout", 5 * 60))
  995         except Exception as ex:
  996             log.warning("SMSProviderTimeout: value error %r - reset "
  997                         "to 5*60", ex)
  998             timeout = 5 * 60
  999 
 1000         return timeout
 1001 
 1002     def getInitDetail(self, params, user=None):
 1003         '''
 1004         to complete the token normalisation, the response of the initialiastion
 1005         should be build by the token specific method, the getInitDetails
 1006         '''
 1007         response_detail = {}
 1008 
 1009         response_detail['serial'] = self.getSerial()
 1010 
 1011         return response_detail
 1012 
 1013 # eof #