5.3. Tokens

If you decide to implement new token classes, you need to change the parameter linotpTokenModules in /etc/linotp2/linotp.ini in the DEFAULT section. This parameter containes a list of token modules LinOTP should use.

Note

The module linotp.lib.tokenclass will always be used.

Starting with LinOTP 2.5.2 it is even simpler to add new token types to LinOTP. The complete token type handling is now done dynamically. This means you can

  • add a new token module
  • new policies that are necessary for this token
  • new management GUI (enrollment and token settings) and
  • new selfservice enrollment GUI

for this new token without having to adapt anything in the LinOTP core.

This is done by creating two files

new_token.py

This file containes all the program logic, the calculation and verification of the OTP values and the handling of the policies.

new_token.mako

This file contains the GUI parts for management and selfservice.

The existing tokens are located in the module linotp.lib.tokens.

Both files describe the new token and need to be located in the same directory i.e. the same python module.

5.3.1. The GUI file

The new_token.mako file is written in the MAKO templating language. More information on how MAKO works can be found on the project website [1].

[1]http://www.makotemplates.org/

Note

Please take a look at the example file dyntoken.mako.

To make the file handling easier there is only this single file providing the GUI for

  • selfservice
  • token configurations
  • token enrollment.

Therefore there is a top level switch in this file that evaluates the variable c.scope.

5.3.1.1. The scope config

config

The scope config is used for the generic token configuration information. E.g. for a time based HMAC token (TOTP) you can use this scope to configure the default time window and the default OTP length. The scope config contains the complete HTML code of this config tab.

config.title

This is the header text of the config tab.

To read and write the token configuration to the LinOTP configuration database you need to define two javascript functions:

function dyn_get_config_val(){
       var ret = {};
   ret['DynTokenTimeOut']                    = 'dyn_challenge_timeout';
   ret['DynTokenMaxChallemges']      = 'dyn_max_challenge';
   return ret;
}

function dyn_get_config_params(){
   var ret ={}
   ret['DynTokenTimeOut']             = $('#dyn_challenge_timeout').val();
   ret['DynTokenMaxChallemges'] = $('#dyn_max_challenge').val();
   return ret
}

The name of the function starts with the token type name of the token. You already know tokens like hmac, totp, spass. So the function names concatenate like <token type>_get_config_val and <token type>_get_config_params(). The example above are the functions of the token type dyn.

In the config tab for the token, you might have several HTML object. The function maps the identifier of the HTML objects to the configuration keys stored in the LinOTP configuration database.

The function _get_config_val is called, when the config tab is opened. It tells LinOTP to get the config entry with the key DynTokenTimeOut from the LinOTP configuration and put it into the HTML object with the id=dyn_challenge_timeout.

The function _get_config_params is called, when the config tab is saved. The values of the HTML objects are written to the corresponding LinOTP config database entries.

Warning

All config entries are stored in one config table. So you need to review third party token modules and assure, that a foreign token modules does not interfere with your other tokens or break your LinOTP. Best practice is to let your token config key start with the token type as prefix.

5.3.1.2. The scope enroll

enroll

This is the HTML GUI for the enrollment of a token by the administrator. Here all necessary data input is displayed.

enroll.title

This is the header text for the enrollment.

In the enrollment process an administrator needs to add additional information, like the RADIUS server in case of a RADIUS token, the secret key or whatever. These data need to be passed to LinOTP, therefore you need to define a javascript function <token type>_get_enroll_params. The function has to return a hash of all the parameters, that will be passed to the admin/init function:

function dyn_get_enroll_params(){
   var url = {}

   url['type'] = 'dyn';
   url['otppin'] = $('#dyn_pin1').val();
   url['hashlib'] = $('#dyn_algorith').val();
   url['otplen'] = $('#dyn_otplen').val();
   url['serial'] = create_serial('DYN');

   if  ( $('#dyn_key_cb').attr('checked') ) {
      url['genkey']='1';}
   else {
      url['otpkey'] = $('#dyn_key').val();}

  return url;
}

5.3.1.3. The scope selfservice

selfservice.enroll

This is the HTML GUI for the user self enrollment in the selfservice portal.

selfservice.title.enroll

This is the header text of the selfservice tab.

In the self enrollment process several information needs to be entered by the user and needs to be passed to LinOTP. Three javascript functions are used to read the data from the HTML GUI and pass it to LinOTP. The first function self_<token type>_get_param function is used to retrieve the hashed parameters that needs to be passed to the selfservice/userinit function:

function self_dyn_get_param()
{
   var urlparam = {};

   urlparam['type']          = 'dyn';
   urlparam['description'] = "self enrolled" ;
   urlparam['otpkey']                = $('#motp_secret').val();
   urlparam['otppin']                = $('#motp_s_pin1').val();
   urlparam['serial']                = generate_serial("LSMO");

   return urlparam;
}

Note

The generate_serial() function generates a new serial number with the passed prefix.

The function self_<token type>_clear is used to clear the input fields:

function self_dyn_clear()
{
   $('#motp_s_pin1').val('');
   $('#motp_secret').val('');
   $('#motp_s_pin2').val('');
}

Finally there is a function self_<token type>_submit that is called when the token is enrolled. This function can also be used to do a input validation:

function self_dyn_submit(){
   var ret = false

   if ($('#form_registermotp').valid()) {
          var params =  self_motp_get_param();
              enroll_token( params );
              //self_motp_clear();
              ret = true;
   } else {
          alert("Form data not valid.");
   }
   return ret;
}

Note

When doing input validation the corresponding jQuery.validator needs to be defined.

5.3.2. The token file

The token file dyntoken.py itself contains the token definition as a python class that inherits from TokenClass which is defined in linotp.lib.tokenclass.

The token class needs to provide a class method getClassInfo, that tells LinOTP, which scopes to use in which situation. Therefore it returns a dictionary of the following kind:

{
    'type'           : 'dyn',
    'title'          : 'my dyn Token',
    'description'    : ('my own dynamic otp token'),

    'init'         : {'page' : {'html'      : 'dyntoken.mako',
                                'scope'     : 'enroll',},
                    'title'  : {'html'      : 'dyntoken.mako',
                                'scope'     : 'enroll.tab',},
                        },

    'config'        : { 'page' : {'html'      : 'dyntoken.mako',
                                 'scope'      : 'config',},
                        'title'  : {'html'    : 'dyntoken.mako',
                                  'scope'     : 'config.tab',},
                      },

    'selfservice'   :  { 'enroll' : {'page' : {'html'  : 'dyntoken.mako',
                                               'scope' : 'selfservice.enroll',},
                                    'title' : { 'html' : 'dyntoken.mako',
                                              'scope'  : 'selfservice.tab.enroll',},
                                       },
                       },


    }

5.3.2.1. Challenge Response

Starting with LinOTP 2.6 you can easily create tokens capable of doing challenge response.

Usually a challenge will be triggered when sending an authentication request to /validate/check with the additional parameter data or challenge. The method is_challenge_request() determines, if the given request is a challenge or not.

The response will also be sent to the /validate/check interface. Usually the response has to contain the parameter state or transactionid so that the request will be identified as a response to a previous challenge. The method is_challenge_response() determines, if the given request is a response for a previous challenge.

Each token type can define it’s own challenge handling by overwriting the corresponding base class method.

So the methods is_challenge_request and is_challenge_response determine in wich of the following three branches the request will be verified:

authenticate

This is the normal single shot authentication e.g. for push button OTP tokens. No challenge and no response is involved. is_challenge_request and is_challenge_response both return false. The authentication workflow will call the authenticate() method.

challenge

The method is_challenge_request returns true and is_challenge_response returns false. A new challenge will be created. The authentication workflow will call the method initChallenge() and createChallenge().

response

The method is_challenge_request returns false and is_challenge_response returns true. The authentication workflow will call the method checkResponse4Challenge().

Please see the base class definition in Base TokenClass.

5.3.2.2. Base TokenClass

class linotp.lib.tokenclass.TokenClass(token)[source]
authenticate(passw, user, options=None)[source]

This is the method that verifies single shot authentication like they are done with push button tokens.

It is a high level interface to support as well other tokens, which do not have a pin and otp seperation - they could overwrite this method

remarks: we have to call the global methods (check_pin,++) as they take the pin policies into account

Parameters:
  • passw (string) – the passw which could be pin+otp
  • user (User object) – The authenticating user
  • options ((dict)) – dictionary of additional request parameters
Returns:

returns tuple true or false for the pin match, the otpcounter (int) and the reply (dict) that will be added as additional information in the JSON response of /validate/check.

challenge_janitor(matching_challenges, challenges)[source]

This is the default janitor for the challenges of a token.

The idea is to delete all challenges, which have an id lower than the matching one. Other janitors could be implemented on a token base and overwrite this behaviour.

Remarks: In later versions this will be the place to hook a dynamically loaded default token specific janitor.

Parameters:
  • matching_challenges (list) – the last matching challenge
  • challenges (list) – all current challenges
Returns:

list of all challenges, which should be deleted

checkOtp(anOtpVal1, counter, window, options=None)[source]

This checks the OTP value, AFTER the upper level did the checkPIN

return:
counter of the matching OTP value.
checkPin(pin, options=None)[source]

checkPin - test is the pin is matching

Parameters:
  • pin – the pin
  • options – additional optional parameters, which could be token specific
Returns:

boolean

checkResponse4Challenge(user, passw, options=None, challenges=None)[source]

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 challenge_janitor() to clean up challenges.

Parameters:
  • user (User object) – the requesting user
  • passw (string) – the password (pin+otp)
  • options (dict) – additional arguments from the request, which could be token specific
  • challenges (list) – A sorted list of valid challenges for this token.
Returns:

tuple of (otpcounter and the list of matching challenges)

check_auth_counter()[source]

This function checks the count_auth and the count_auth_success

check_otp_exist(otp, window=None)[source]

checks if the given OTP value is/are values of this very token. This is used to autoassign and to determine the serial number of a token.

check_validity_period()[source]

This checks if the datetime.datetime.now() is within the validity period of the token.

Returns either True/False

createChallenge(transactionid, options=None)[source]

This method creates a challenge, which is submitted to the user. The submitted challenge will be preserved in the challenge database.

This method is called after the method initChallenge().

Parameters:
  • transactionid – the id of this challenge
  • options (dict) – the request context parameters / data
Returns:

tuple of (bool, message, data, attributes)

The return tuple builds up like this:

bool if submit was successfull; message which is displayed in the JSON response; data is preserved in the challenge; additional attributes, which are displayed in the JSON response.

getInfo()[source]

getInfo - return the status of the token rollout

Returns:return the status dict.
Return type:dict
getInitDetail(params, user=None)[source]

to complete the token normalisation, the response of the initialiastion should be build by the token specific method, the getInitDetails

getOtp(curtTime='')[source]

The default token does not support getting the otp value will return something like:

1, pin, otpval, combined

a negative value is a failure.

getQRImageData(response_detail)[source]
get_multi_otp(count=0, epoch_start=0, epoch_end=0, curTime=None)[source]

This returns a dictionary of multiple future OTP values of a token.

parameter
count - how many otp values should be returned epoch_start - time based tokens: start when epoch_end - time based tokens: stop when
return
True/False error text OTP dictionary
get_validity_period_end()[source]

returns the end of validity period (if set)

get_validity_period_start()[source]

returns the start of validity period (if set)

get_vars(save=False)[source]

return the token state as dicts :return: token as dict

incOtpCounter(counter, reset=True)[source]
method
incOtpCounter(aToken, counter)
parameters:
token - a token object counter - the new counter reset - optional -
exception:
in case of an transaction fail an exception is thrown
side effects:
default of reset will reset the failCounter
initChallenge(transactionid, challenges=None, options=None)[source]

This method initializes the challenge.

This is a hook that is called before the method createChallenge(), which will only be called if this method returns success==true.

Thus this method can be used, to verify if there is an outstanding challenge or if a new challenge needs to be created. E.g. this hook can be used, to implement a blocking mechanism to allow the creation of a new challenge only after a certain timeout. If there is an already outstanding challenge the return value can refer to this. (s. ticket #2986)

Parameters:
  • transactionid (string) – the id of the new challenge
  • options (dict) – the request parameters
  • challenges (list) – a list of all valid challenges for this token.
Returns:

tuple of ( success, transid, message, additional attributes )

The transid (the best transaction id for this request context), message, and additional attributes (dictionar) are displayed as results in the JSON response of the /validate/check request.

Only in case of success == true the next method createChallenge will be called.

is_challenge_request(passw, user, options=None)[source]

This method checks, if this is a request, that triggers a challenge.

The default behaviour to trigger a challenge is, if the passw parameter only contains the correct token pin and the request contains a data or a challenge key i.e. if the options parameter contains a key data or challenge.

Each token type can decide on its own under which condition a challenge is triggered by overwriting this method.

please note: in case of pin policy == 2 (no pin is required) the check_pin would always return true! Thus each request containing a data or challenge would trigger a challenge!

Parameters:
  • passw (string) – password, which might be pin or pin+otp
  • user (User object) – The user from the authentication request
  • options (dict) – dictionary of additional request parameters
Returns:

true or false

is_challenge_response(passw, user, options=None, challenges=None)[source]

This method checks, if this is a request, that is the response to a previously sent challenge.

The default behaviour to check if this is the response to a previous challenge is simply by checking if the request contains a parameter state or transactionid i.e. checking if the options parameter contains a key state or transactionid.

This method does not try to verify the response itself! It only determines, if this is a response for a challenge or not.

Parameters:
  • passw (string) – password, which might be pin or pin+otp
  • user (User object) – the requesting user
  • options ((dict)) – dictionary of additional request parameters
  • challenges – A list of challenges for this token. These challenges may be used, to identify if this request is a response for a challenge.
Returns:

true or false

is_challenge_valid(challenge=None)[source]

This method verifies if the given challenge is still valid.

The default implementation checks, if the challenge start is in the default validity time window.

Please note: This method does not check the response for the challenge itself. This is done by the method checkResponse4Challenge(). E.g. this very method is_challenge_valid is used by the method challenge_janitor() to clean up old challenges.

Parameters:challenge (challenge object) – The challenge to be checked
Returns:true or false
setPin(pin, param={})[source]

set the PIN. The optional parameter “param” can hold the information, if the PIN is encrypted or hashed.

setUid(uid, uidResolver, uidResClass)[source]

sets the UID values in the database

setUser(user, report)[source]
Parameters:
  • user – a User() object, consisting of loginname and realm
  • report – tbdf.
set_count_auth(count)[source]

Sets the counter for the occurred login attepms

set_count_auth_max(count)[source]

Sets the counter for the maximum allowed login attemps

set_count_auth_success(count)[source]

Sets the counter for the occurred successful logins

set_count_auth_success_max(count)[source]

Sets the counter for the maximum allowed successful logins

set_validity_period_end(end_date)[source]

sets the end date of the validity period for a token

set_validity_period_start(start_date)[source]

sets the start date of the validity period for a token