def __after__(response): ''' __after__ is called after every action :param response: the previously created response - for modification :return: return the response ''' if request_context.get('reponse_redirect', False): # FIXME: does this really do a redirect??? return response param = request.params action = request_context['action'] try: if c.audit['action'] in ['selfservice/index']: if isSelfTest(): log.debug("[__after__] Doing selftest!") if "selftest_user" in param: (c.user, _foo, c.realm) = param["selftest_user"].rpartition('@') else: c.realm = "" c.user = "******" env = request.environ uuser = env.get('REMOTE_USER') if uuser is not None: (c.user, _foo, c.realm) = uuser.rpartition('@') log.debug("[__after__] authenticating as %s in realm %s!" % (c.user, c.realm)) c.audit['user'] = c.user c.audit['realm'] = c.realm c.audit['success'] = True if 'serial' in param: c.audit['serial'] = param['serial'] c.audit['token_type'] = getTokenType(param['serial']) audit = config.get('audit') audit.log(c.audit) return response except flap.HTTPUnauthorized as acc: # the exception, when an abort() is called if forwarded log.exception("[__after__::%r] webob.exception %r" % (action, acc)) Session.rollback() Session.close() # FIXME: verify that this really works raise acc except Exception as e: log.exception("[__after__] failed with error: %r" % e) Session.rollback() Session.close() return sendError(response, e, context='after')
def __before__(self, **params): """ __before__ is called before every action :param params: list of named arguments :return: -nothing- or in case of an error a Response created by sendError with the context info 'before' """ action = request_context["action"] try: g.audit["success"] = False g.audit["client"] = get_client(request) # Session handling check_session(request) audit = config.get("audit") request_context["Audit"] = audit checkAuthorisation(scope="monitoring", method=action) return except Exception as exception: log.error(exception) db.session.rollback() return sendError(response, exception, context="before")
def __before__(self, **params): """ __before__ is called before every action :param params: list of named arguments :return: -nothing- or in case of an error a Response created by sendError with the context info 'before' """ action = request_context['action'] try: c.audit = request_context['audit'] c.audit['success'] = False c.audit['client'] = get_client(request) # Session handling check_session(request) audit = config.get('audit') request_context['Audit'] = audit checkAuthorisation(scope='monitoring', method=action) return except Exception as exception: log.exception(exception) Session.rollback() Session.close() return sendError(response, exception, context='before')
def tokenview(self): ''' This is the template for the token TAB ''' c.title = "LinOTP Management" c.tokenArray = [] c.getotp_active = config.get("linotpGetotp.active", "False") == "True" return render('/manage/tokenview.mako')
def define_sql_resolver(self, name, params=None, user_mapping=None): """ create sql useridresolver """ engine = create_engine(config.get('DATABASE_URI')) db_url = engine.url server = db_url.host if db_url.port: server = "%s:%s" % (db_url.host, db_url.port) if not user_mapping: user_mapping = {} usermap = { "userid": "id", "username": "******", "phone": "telephoneNumber", "mobile": "mobile", "email": "mail", "surname": "sn", "givenname": "givenName", "password": "******", "salt": "salt" } user_mapping.update(usermap) if not params: params = {} resolver_def = { 'name': name, 'Server': server, 'Database': db_url.database, 'Driver': db_url.drivername, 'User': db_url.username, # 'Password': db_url.password, 'Map': json.dumps(usermap), 'Where': '', 'Encoding': '', 'Limit': '40', 'Table': 'usertable', 'type': 'sqlresolver', 'Port': '3306', 'conParams': ''} resolver_def.update(params) resolver_def['name'] = name response = self.make_system_request('setResolver', params=resolver_def) return response, resolver_def
def help(self, id=None): """ This downloads the Manual The filename will be the 3. part,ID https://172.16.200.6/manage/help/somehelp.pdf The file is downloaded through Flask! """ try: directory = config.get( "linotpManual.Directory", "/usr/share/doc/linotp" ) default_filename = config.get( "linotpManual.File", "LinOTP_Manual-en.pdf" ) mimetype = "application/pdf" headers = [] # FIXME: Compression is better done using # `Content-Encoding` (ideally farther up the WSGI stack). # if not id: # id = default_filename + ".gz" # mimetype = "application/x-gzip" # iffy id = id or default_filename r = flask.send_file( "%s/%s" % (directory, id), mimetype=mimetype, as_attachment=True, attachment_filename=default_filename, ) db.session.commit() return r except Exception as exx: log.error("[help] Error loading helpfile: %r", exx) db.session.rollback() return sendError(response, exx)
def __after__(response): ''' __after__ is called after every action :param response: the previously created response - for modification :return: return the response ''' audit = config.get('audit') audit.log(c.audit) return response
def check_session(request, scope='admin'): ''' This function checks the session cookie and compares it to the session parameter :param request: the request object :param scope: by default the admin scope, but used to as well for the scope helpdesk with the helpdesk_session cookie name :return: boolean ''' if isSelfTest(): return # check if the client is in the allowed IP range no_session_clients = [] for no_session_client in config.get("linotpNoSessionCheck", "").split(","): no_session_clients.append(no_session_client.strip()) client = request.environ.get('REMOTE_ADDR', None) log.debug("[check_session] checking %s in %s" % (client, no_session_clients)) for network in no_session_clients: if not network: continue try: if netaddr.IPAddress(client) in netaddr.IPNetwork(network): log.debug("skipping session check since client" " %s in allowed: %s" % (client, no_session_clients)) return except Exception as ex: log.warning("misconfiguration in linotpNoSessionCheck: " "%r - %r" % (network, ex)) cookie = request.cookies.get(scope + '_session') session = get_request_param(request, 'session') # doing any other request, we need to check the session! log.debug("[check_session]: session: %s" % session) log.debug("[check_session]: cookie: %s" % cookie) if session is None or session == "" or session != cookie: log.error("The request did not pass a valid session!") abort(401, "You have no valid session!") cookie = request.cookies.get(scope + '_session') session = get_request_param(request, 'session') # doing any other request, we need to check the session! log.debug("[check_session]: session: %s" % session) log.debug("[check_session]: cookie: %s" % cookie) if session is None or session == "" or session != cookie: log.error("The request did not pass a valid session!") abort(401, "You have no valid session!")
def define_sql_resolver(self, name, params=None, user_mapping=None): """ create sql useridresolver """ engine = create_engine(config.get("DATABASE_URI")) db_url = engine.url server = db_url.host if db_url.port: server = "%s:%s" % (db_url.host, db_url.port) if not user_mapping: user_mapping = {} usermap = { "userid": "id", "username": "******", "phone": "telephoneNumber", "mobile": "mobile", "email": "mail", "surname": "sn", "givenname": "givenName", "password": "******", "salt": "salt", } user_mapping.update(usermap) if not params: params = {} resolver_def = { "name": name, "Server": server, "Database": db_url.database, "Driver": db_url.drivername, "User": db_url.username, # 'Password': db_url.password, "Map": json.dumps(usermap), "Where": "", "Encoding": "", "Limit": "40", "Table": "usertable", "type": "sqlresolver", "Port": "3306", "conParams": "", } resolver_def.update(params) resolver_def["name"] = name response = self.make_system_request("setResolver", params=resolver_def) return response, resolver_def
def test_license(self): old_lic = None old_sig = None try: old_lic, old_sig = self.getCurrentLicense() except InvalidLicenseException as exx: if (str(exx) != "Support not available, your product is " "unlicensed"): raise exx try: # Load the license file... licfile = config.get('monitoringTests.licfile', '') if not licfile: self.skipTest('Path to test license file is not configured, ' 'check your configuration (test.ini)!') lic_dict, lic_sig = readLicenseInfo(licfile) self.installLicense(licfile) self.create_token(serial='0031') self.create_token(serial='0032', user='******') self.create_token(serial='0033', realm='mydefrealm') self.create_token(serial='0034', realm='myotherrealm') self.create_token(serial='0035', realm='myotherrealm', active=False) self.create_token(serial='0036', realm='myotherrealm', user='******', active=False) response = self.make_authenticated_request(controller='monitoring', action='license', params={}) resp = json.loads(response.body) value = resp.get('result').get('value') assert value.get('token-num') == \ int(lic_dict.get('token-num')), \ response token_left = int(lic_dict.get('token-num')) - 4 assert value.get('token-left') == token_left, response finally: # restore previous license... if old_lic and old_sig: self.setCurrentLicense(old_lic, old_sig) return
def test_update_critical_data_sql(self): """ test: it's not possible to define a resolver w. critical changes """ # # define resolver SqlX w. the required Password if config.get('SQLALCHEMY_DATABASE_URI').startswith('sqlite://'): # We cannot define a sqlite databse with a password, so skip # this test pytest.skip("not possible with sqlite") params = { "Password": "******", } response, params = self.define_sql_resolver('SqlX', params=params) assert '"status": true,' in response, response # # rename resolver SqlX to SqlZ with critical changes # w.o. password will fail params = { 'previous_name': 'SqlX', 'User': '******', } response, params = self.define_sql_resolver('SqlZ', params=params) assert '"status": false,' in response, response response = self.make_system_request('getResolvers') assert 'SqlZ' not in response, response assert 'SqlX' in response, response # # rename resolver SqlX to SqlZ with critical changes # w. password will have success params = { 'previous_name': 'SqlX', 'User': '******', 'Password': '******' } response, params = self.define_sql_resolver('SqlZ', params=params) assert '"status": true,' in response, response response = self.make_system_request('getResolvers') assert 'SqlX' not in response, response assert 'SqlZ' in response, response
def __after__(response): ''' __after__ is called after every action :param response: the previously created response - for modification :return: return the response ''' audit = config.get('audit') c.audit['administrator'] = getUserFromRequest(request).get("login") audit.log(c.audit) return response
def uencode(value): """ unicode escape the value - required to support non-unicode databases :param value: string to be escaped :return: unicode encoded value """ ret = value if (env.get("linotp.uencode_data", "").lower() == 'true'): try: ret = json.dumps(value)[1:-1] except Exception as exx: log.exception("Failed to encode value %r. Exception was %r" % (value, exx)) return ret
def __after__(response): ''' __after__ is called after every action :param response: the previously created response - for modification :return: return the response ''' c.audit['administrator'] = getUserFromRequest(request).get("login") if 'serial' in request.params: serial = request.params['serial'] c.audit['serial'] = serial c.audit['token_type'] = getTokenType(serial) audit = config.get('audit') audit.log(c.audit) return response
def get_imprint(realm): ''' This function returns the imprint for a certain realm. This is just the contents of the file <realm>.imprint in the directory <imprint_directory> ''' res = "" realm = realm.lower() directory = config.get("linotp.imprint_directory", "/etc/linotp/imprint") filename = "%s/%s.imprint" % (directory, realm) try: pass f = open(filename) res = f.read() f.close() except Exception as e: log.info("[get_imprint] can not read imprint file: %s. (%r)" % (filename, e)) return res
def __before__(self, **params): """ __before__ is called before every action Here we see, what action is to be called and check the authorization :param params: list of named arguments :return: -nothing- or in case of an error a Response created by sendError with the context info 'before' """ action = request_context['action'] try: c.audit = request_context['audit'] c.audit['success'] = False c.audit['client'] = get_client(request) if action != "check_t": check_session(request) audit = config.get('audit') request_context['Audit'] = audit return except flap.HTTPUnauthorized as acc: # the exception, when an abort() is called if forwarded log.exception("[__before__::%r] webob.exception %r" % (action, acc)) Session.rollback() Session.close() raise acc except Exception as exx: log.exception("[__before__::%r] exception %r" % (action, exx)) Session.rollback() Session.close() return sendError(response, exx, context='before')
def __after__(response): ''' __after__ is called after every action :param response: the previously created response - for modification :return: return the response ''' if c.audit['action'] in ['manage/tokenview_flexi', 'manage/userview_flexi' ]: c.audit['administrator'] = getUserFromRequest(request).get("login") if 'serial' in request.params: serial = request.params['serial'] c.audit['serial'] = serial c.audit['token_type'] = getTokenType(serial) c.audit['action_detail'] += linotp.lib.audit.base.get_token_num_info() audit = config.get('audit') audit.log(c.audit) return response
def __before__(self, **params): """ __before__ is called before every action we check if the client cert was valid by looking for the existance of a CGI environment variable. For apache this is SSL_CLIENT_S_DN_CN. To support other servers we read the name of the variable from the config :param params: list of named arguments :return: -nothing- or in case of an error a Response created by sendError with the context info 'before' """ env_var = config.get('MAINTENANCE_VERIFY_CLIENT_ENV_VAR', False) if env_var: client_cert = request.environ.get(env_var) if client_cert is None: abort(401)
def __before__(self, **params): """ __before__ is called before every action :param params: list of named arguments :return: -nothing- or in case of an error a Response created by sendError """ action = request_context['action'] try: c.audit = request_context['audit'] c.audit['client'] = get_client(request) audit = config.get('audit') request_context['Audit'] = audit return except Exception as exx: log.exception("[__before__::%r] exception %r" % (action, exx)) Session.rollback() Session.close() return sendError(response, exx, context='before')
def __after__(response): ''' __after__ is called after every action :param response: the previously created response - for modification :return: return the response ''' try: c.audit['administrator'] = getUserFromRequest(request).get('login') audit = config.get('audit') audit.log(c.audit) Session.commit() return response except Exception as exception: log.exception(exception) Session.rollback() return sendError(response, exception, context='after') finally: Session.close()
def check_session(request): """ This function checks if the client is in the allowed IP range. The session cookie is no longer checked here because flask-jwt-extended does this now in BaseController::jwt_check. :param request: the request object :return: boolean """ # check if the client is in the allowed IP range no_session_clients = [] for no_session_client in config.get("linotpNoSessionCheck", "").split(","): no_session_clients.append(no_session_client.strip()) client = request.environ.get("REMOTE_ADDR", None) log.debug("[check_session] checking %s in %s", client, no_session_clients) for network in no_session_clients: if not network: continue try: if netaddr.IPAddress(client) in netaddr.IPNetwork(network): log.debug( "skipping session check since client %s in allowed: %s", client, no_session_clients, ) return except Exception as ex: log.warning( "misconfiguration in linotpNoSessionCheck: %r - %r", network, ex, )
def init_vasco(): """ Vasco library initialiser """ global vasco_dll global vasco_libs # get the Vacman Controller lib fallbacks = ["/opt/vasco/Vacman_Controller/lib/libaal2sdk.so"] vasco_lib = config.get("linotpImport.vasco_dll") if not vasco_lib: log.info("Missing linotpImport.vasco_dll in config file") else: vasco_libs.append(vasco_lib) vasco_libs.extend(fallbacks) for vasco_lib in vasco_libs: try: log.debug("loading vasco lib %r", vasco_lib) vasco_dll = CDLL(vasco_lib) break except Exception as exx: log.info("cannot load vasco library: %r", exx)
def index(self): ''' This is the main function of the management web UI ''' try: c.debug = bool(config.get('debug', False)) c.title = "LinOTP Management" admin_user = getUserFromRequest(request) if 'login' in admin_user: c.admin = admin_user['login'] log.debug("[index] importers: %s" % IMPORT_TEXT) c.importers = IMPORT_TEXT help_version = c.version[:c.version.find('.')] c.help_url = config.get('HELP_URL').format(help_version) # -------------------------------------------------------------- -- # check for support of setting admin password c.admin_can_change_password = False if ('linotpadmin.user' in config and 'linotpadmin.password' in config): c.admin_can_change_password = True # -------------------------------------------------------------- -- # add render info for token type config confs = _getTokenTypeConfig('config') token_config_tab = {} token_config_div = {} for conf in confs: tab = '' div = '' try: # loc = conf +'_token_settings' tab = confs.get(conf).get('title') # tab = '<li ><a href=#'+loc+'>'+tab+'</a></li>' div = confs.get(conf).get('html') # div = +div+'</div>' except Exception as e: log.debug( '[index] no config info for token type %s (%r)' % (conf, e)) if tab is not None and div is not None and len( tab) > 0 and len(div) > 0: token_config_tab[conf] = tab token_config_div[conf] = div c.token_config_tab = token_config_tab c.token_config_div = token_config_div # add the enrollment fragments from the token definition # tab: <option value="ocra">${_("OCRA - challenge/response Token")}</option> # div: "<div id='"+ tt + "'>"+enroll+"</div>" enrolls = _getTokenTypeConfig('init') token_enroll_tab = {} token_enroll_div = {} for conf in enrolls: tab = '' div = '' try: tab = enrolls.get(conf).get('title') div = enrolls.get(conf).get('html') except Exception as e: log.debug( '[index] no enrollment info for token type %s (%r)' % (conf, e)) if tab is not None and div is not None and len( tab) > 0 and len(div) > 0: token_enroll_tab[conf] = tab token_enroll_div[conf] = div c.token_enroll_tab = token_enroll_tab c.token_enroll_div = token_enroll_div c.tokentypes = _getTokenTypes() # Use HTTP_X_FORWARDED_HOST in preference to HTTP_HOST # in case we're running behind a reverse proxy http_host = request.environ.get("HTTP_X_FORWARDED_HOST", '') if not http_host: http_host = request.environ.get("HTTP_HOST") url_scheme = request.environ.get("wsgi.url_scheme") c.logout_url = "%s://log-me-out:fake@%s/manage/logout" % ( url_scheme, http_host) Session.commit() ren = render('/manage/manage-base.mako') return ren except PolicyException as pe: log.exception("[index] Error during checking policies: %r" % pe) Session.rollback() return sendError(response, str(pe), 1) except Exception as ex: log.exception("[index] failed! %r" % ex) Session.rollback() raise finally: Session.close()
def getotp(self): ''' This function is used to retrieve the current otp value for a given user or a given serial. If the user has more than one token, the list of the tokens is returend. method: gettoken/getotp arguments: user - username / loginname realm - additional realm to match the user to a useridresolver serial - the serial number of the token curTime - used ONLY for internal testing: datetime.datetime object returns: JSON response ''' getotp_active = config.get("GETOTP_ENABLED") if not getotp_active: return sendError(response, "getotp is not activated.", 0) param = self.request_params ret = {} res = -1 otpval = "" passw = "" serials = [] try: serial = getParam(param, "serial", optional) user = getUserFromParam(param) curTime = getParam(param, "curTime", optional) g.audit['user'] = user.login if "" != user.login: g.audit['realm'] = user.realm or getDefaultRealm() if serial: log.debug("[getotp] retrieving OTP value for token %s", serial) elif user.login: log.debug( "[getotp] retrieving OTP value for token for user " "%s@%s", user.login, user.realm) toks = getTokens4UserOrSerial(user, serial) tokennum = len(toks) if tokennum > 1: log.debug("[getotp] The user has more than one token." "Returning the list of serials") res = -3 for token in toks: serials.append(token.getSerial()) elif 1 == tokennum: serial = toks[0].getSerial() log.debug( "[getotp] retrieving OTP for token %s for user" " %s@%s", serial, user.login, user.realm) else: log.debug("[getotp] no token found for user %s@%s", user.login, user.realm) res = -4 else: res = -5 # if a serial was given or a unique serial could be # received from the given user. if serial: max_count = checkPolicyPre('gettoken', 'max_count', param) log.debug("[getmultiotp] max_count policy: %s", max_count) if max_count <= 0: return sendError( response, "The policy forbids receiving" " OTP values for the token %s in " "this realm" % serial, 1) (res, pin, otpval, passw) = getOtp(serial, curTime=curTime) g.audit['success'] = True if int(res) < 0: ret['result'] = False if -1 == otpval: ret['description'] = "No Token with this serial number" if -2 == otpval: ret['description'] = ("This Token does not support the" " getOtp function") if -3 == otpval: ret['description'] = "The user has more than one token" ret['serials'] = serials if -4 == otpval: ret['description'] = "No Token found for this user" if -5 == otpval: ret['description'] = ("you need to provide a user or " "a serial") else: ret['result'] = True ret['otpval'] = otpval ret['pin'] = pin ret['pass'] = passw db.session.commit() return sendResult(response, ret, 0) except PolicyException as pe: log.exception("[getotp] gettoken/getotp policy failed: %r", pe) db.session.rollback() return sendError(response, str(pe), 1) except Exception as exx: log.exception("[getotp] gettoken/getotp failed: %r", exx) db.session.rollback() return sendError(response, "gettoken/getotp failed: %s" % exx, 0)
def getmultiotp(self): ''' This function is used to retrieve multiple otp values for a given user or a given serial. If the user has more than one token, the list of the tokens is returend. method: gettoken/getmultiotp arguments: serial - the serial number of the token count - number of otp values to return curTime - used ONLY for internal testing: datetime.datetime object returns: JSON response ''' getotp_active = config.get("GETOTP_ENABLED") if not getotp_active: return sendError(response, "getotp is not activated.", 0) param = self.request_params ret = {} try: serial = getParam(param, "serial", required) count = int(getParam(param, "count", required)) curTime = getParam(param, "curTime", optional) view = getParam(param, "view", optional) r1 = checkPolicyPre('admin', 'getotp', param) log.debug("[getmultiotp] admin-getotp policy: %s", r1) max_count = checkPolicyPre('gettoken', 'max_count', param) log.debug("[getmultiotp] maxcount policy: %s", max_count) if count > max_count: count = max_count log.debug("[getmultiotp] retrieving OTP value for token %s", serial) ret = get_multi_otp(serial, count=int(count), curTime=curTime) ret["serial"] = serial g.audit['success'] = True db.session.commit() if view: c.ret = ret return render('/manage/multiotp_view.mako').decode('utf-8') else: return sendResult(response, ret, 0) except PolicyException as pe: log.exception("[getotp] gettoken/getotp policy failed: %r", pe) db.session.rollback() return sendError(response, str(pe), 1) except Exception as exx: log.exception("[getmultiotp] gettoken/getmultiotp failed: %r", exx) db.session.rollback() return sendError(response, "gettoken/getmultiotp failed: %r" % exx, 0)
def __init__(self): self.engine = create_engine(config.get("DATABASE_URI"))
def __before__(self, **params): """ __before__ is called before every action This is the authentication to self service. If you want to do ANYTHING with the selfservice, you need to be authenticated. The _before_ is executed before any other function in this controller. :param params: list of named arguments :return: -nothing- or in case of an error a Response created by sendError with the context info 'before' """ action = request_context['action'] self.redirect = None try: c.version = get_version() c.licenseinfo = get_copyright_info() c.audit = request_context['audit'] c.audit['success'] = False self.client = get_client(request) c.audit['client'] = self.client audit = config.get('audit') request_context['Audit'] = audit # -------------------------------------------------------------- -- # handle requests which dont require authetication if action in ['logout', 'custom_style']: return # -------------------------------------------------------------- -- # get the authenticated user auth_type, auth_user, auth_state = get_auth_user(request) # -------------------------------------------------------------- -- # handle not authenticated requests if not auth_user or auth_type not in ['user_selfservice']: if action in ['login']: return if action in ['index']: self.redirect = True return redirect( url(controller='selfservice', action='login')) else: Unauthorized('No valid session') # -------------------------------------------------------------- -- # handle authenticated requests # there is only one special case, which is the login that # could be forwarded to the index page if action in ['login']: if auth_state != 'authenticated': return self.redirect = True return redirect(url(controller='selfservice', action='index')) # -------------------------------------------------------------- -- # in case of user_selfservice, an unauthenticated request should always go to login if auth_user and auth_type is 'user_selfservice' \ and auth_state is not 'authenticated': self.redirect = True return redirect(url(controller='selfservice', action='login')) # futher processing with the authenticated user if auth_state != 'authenticated': Unauthorized('No valid session') c.user = auth_user.login c.realm = auth_user.realm self.authUser = auth_user # -------------------------------------------------------------- -- # authenticated session verification if auth_type == 'user_selfservice': # checking the session only for not_form_access actions if action not in self.form_access_methods: valid_session = check_session(request, auth_user, self.client) if not valid_session: c.audit['action'] = request.path[1:] c.audit['info'] = "session expired" audit.log(c.audit) Unauthorized('No valid session') # -------------------------------------------------------------- -- c.imprint = get_imprint(c.realm) c.tokenArray = [] c.user = self.authUser.login c.realm = self.authUser.realm # only the defined actions should be displayed # - remark: the generic actions like enrollTT are allready approved # to have a rendering section and included actions = getSelfserviceActions(self.authUser) c.actions = actions for policy in actions: if policy: if "=" not in policy: c.__setattr__(policy, -1) else: (name, val) = policy.split('=') val = val.strip() # try if val is a simple numeric - # w.r.t. javascript evaluation try: nval = int(val) except ValueError: nval = val c.__setattr__(name.strip(), nval) c.dynamic_actions = add_dynamic_selfservice_enrollment( config, c.actions) # we require to establish all token local defined # policies to be initialiezd additional_policies = add_dynamic_selfservice_policies( config, actions) for policy in additional_policies: c.__setattr__(policy, -1) c.otplen = -1 c.totp_len = -1 c.pin_policy = _get_auth_PinPolicy(user=self.authUser) except (flap.HTTPUnauthorized, flap.HTTPForbidden) as acc: # the exception, when an abort() is called if forwarded log.info("[__before__::%r] webob.exception %r" % (action, acc)) Session.rollback() Session.close() raise acc except Exception as e: log.exception("[__before__] failed with error: %r" % e) Session.rollback() Session.close() return sendError(response, e, context='before')
def __init__(self): self.engine = create_engine(config.get('SQLALCHEMY_DATABASE_URI'))
def create_challenge_url(self, transaction_id, content_type, callback_url='', message=None, login=None, host=None): """ creates a challenge url (looking like lseqr://push/<base64string>), returns the url and the unencrypted challenge data :param transaction_id: The transaction id generated by LinOTP :param content_type: One of the types CONTENT_TYPE_SIGNREQ, CONTENT_TYPE_PAIRING, CONTENT_TYPE_LOGIN :param callback_url: callback url (optional), default is empty string :param message: the transaction message, that should be signed by the client. Only for content type CONTENT_TYPE_SIGNREQ :param login: the login name of the user. Only for content type CONTENT_TYPE_LOGIN :param host: hostname of the user. Only for content type CONTENT_TYPE_LOGIN :returns: tuple (challenge_url, sig_base), with challenge_url being the push url and sig_base the message, that is used for the client signature """ serial = self.getSerial() # ------------------------------------------------------------------- -- # sanity/format checks if content_type not in [ CONTENT_TYPE_SIGNREQ, CONTENT_TYPE_PAIRING, CONTENT_TYPE_LOGIN ]: raise InvalidFunctionParameter( 'content_type', 'content_type must ' 'be CONTENT_TYPE_SIGNREQ, ' 'CONTENT_TYPE_PAIRING or ' 'CONTENT_TYPE_LOGIN.') # ------------------------------------------------------------------- -- # after the lseqr://push/ prefix the following data is encoded # in urlsafe base64: # --------------------------------------------------- # fields | version | user token id | R | ciphertext | sign | # --------------------------------------------------- # | header | body | # --------------------------------------------------- # size | 1 | 4 | 32 | ? | 64 | # --------------------------------------------------- # # create header user_token_id = self.getFromTokenInfo('user_token_id') data_header = struct.pack('<bI', CHALLENGE_URL_VERSION, user_token_id) # ------------------------------------------------------------------- -- # create body r = secrets.token_bytes(32) R = calc_dh_base(r) b64_user_dsa_public_key = self.getFromTokenInfo('user_dsa_public_key') user_dsa_public_key = b64decode(b64_user_dsa_public_key) user_dh_public_key = dsa_to_dh_public(user_dsa_public_key) ss = calc_dh(r, user_dh_public_key) U = SHA256.new(ss).digest() zerome(ss) sk = U[0:16] nonce = U[16:32] zerome(U) # ------------------------------------------------------------------- -- # create plaintext section # ------------------------------------------------------------------- -- # generate plaintext header # ------------------------------------------------ # fields | content_type | transaction_id | timestamp | .. # ------------------------------------------------ # size | 1 | 8 | 8 | ? # ------------------------------------------------- transaction_id = transaction_id_to_u64(transaction_id) plaintext = struct.pack('<bQQ', content_type, transaction_id, int(time.time())) # ------------------------------------------------------------------- -- utf8_callback_url = callback_url.encode('utf8') # enforce max url length as specified in protocol if len(utf8_callback_url) > 511: raise InvalidFunctionParameter( 'callback_url', 'max string ' 'length (encoded as utf8) is ' '511') # ------------------------------------------------------------------- -- # create data package depending on content type # ------------------------------------------------------------------- -- if content_type == CONTENT_TYPE_PAIRING: # ----------------------------------------- # fields | header | serial | NUL | callback | NUL | # ----------------------------------------- # size | 9 | ? | 1 | ? | 1 | # ----------------------------------------- utf8_serial = serial.encode('utf8') if len(utf8_serial) > 63: raise ValueError('serial (encoded as utf8) can only be 63 ' 'characters long') plaintext += utf8_serial + b'\00' + utf8_callback_url + b'\00' # ------------------------------------------------------------------- -- if content_type == CONTENT_TYPE_SIGNREQ: if message is None: raise InvalidFunctionParameter( 'message', 'message must be ' 'supplied for content type ' 'SIGNREQ') # ------------------------------------------ # fields | header | message | NUL | callback | NUL | # ------------------------------------------ # size | 9 | ? | 1 | ? | 1 | # ------------------------------------------ utf8_message = message.encode('utf8') # enforce max sizes specified by protocol if len(utf8_message) > 511: raise InvalidFunctionParameter( 'message', 'max string ' 'length (encoded as utf8) is ' '511') plaintext += utf8_message + b'\00' + utf8_callback_url + b'\00' # ------------------------------------------------------------------- -- if content_type == CONTENT_TYPE_LOGIN: if login is None: raise InvalidFunctionParameter( 'login', 'login must be ' 'supplied for content type ' 'LOGIN') if host is None: raise InvalidFunctionParameter( 'host', 'host must be ' 'supplied for content type ' 'LOGIN') # ----------------------------------------------------- # fields | header | login | NUL | host | NUL | callback | NUL | # ----------------------------------------------------- # size | 9 | ? | 1 | ? | 1 | ? | 1 | # ----------------------------------------------------- utf8_login = login.encode('utf8') utf8_host = host.encode('utf8') # enforce max sizes specified by protocol if len(utf8_login) > 127: raise InvalidFunctionParameter( 'login', 'max string ' 'length (encoded as utf8) is ' '127') if len(utf8_host) > 255: raise InvalidFunctionParameter( 'host', 'max string ' 'length (encoded as utf8) is ' '255') plaintext += utf8_login + b'\00' plaintext += utf8_host + b'\00' plaintext += utf8_callback_url + b'\00' # ------------------------------------------------------------------- -- # encrypt inner layer nonce_as_int = int_from_bytes(nonce, byteorder='big') ctr = Counter.new(128, initial_value=nonce_as_int) cipher = AES.new(sk, AES.MODE_CTR, counter=ctr) ciphertext = cipher.encrypt(plaintext) unsigned_raw_data = data_header + R + ciphertext # ------------------------------------------------------------------- -- # create signature partition = self.getFromTokenInfo('partition') secret_key = get_secret_key(partition) signature = crypto_sign_detached(unsigned_raw_data, secret_key) raw_data = unsigned_raw_data + signature protocol_id = config.get('mobile_app_protocol_id', 'lseqr') url = protocol_id + '://push/' + encode_base64_urlsafe(raw_data) return url, (signature + plaintext)
def generate_pairing_url( token_type, partition=None, serial=None, callback_url=None, callback_sms_number=None, otp_pin_length=None, hash_algorithm=None, use_cert=False, ): """ Generates a pairing url that should be sent to the client. Mandatory parameters: :param: token_type The token type for which this url is generated as a string (currently supported is only 'qr') Optional parameters: :param partition: A partition id that should be used during pairing. Partitions identitify a subspace of tokens, that share a common key pair. This currently defaults to the enum id of the token type when set to None and is reserved for future use. :param serial: When a token for the client was already enrolled (e.g. manually in the manage interface) its serial has to be sent to the client. When serial is not specified the client will receive a so-called 'anonymous pairing url' with no token data inside it. The token will then be created after the server received a pairing response from the client. :param callback_url: A callback URL that should be used by the client to sent back the pairing reponse. Please note, that this url will be cached by the client and used in the challenge step, if the challenge doesn't provide a custom url :param callback_sms_number: A sms number that can be used by the client to send back the pairing response. Typically this is used as a fallback for offline pairing. As with the callback url please note, that the number will be cached by the client. If you want a different number in the challenge step you have to send it inside the challenge as specified in the challenge protocol :param otp_pin_length: The number of digits the otp has to consist of. :param hash_algorithm: A string value that signifies the hash algorithm used in calculating the hmac. Currently the values 'sha1', 'sha256', 'sha512' are supported. If the parameter is left out the default depends on the token type. qrtoken uses sha256 as default, while hotp/totp uses sha1. :param use_cert: A boolean, if a server certificate should be used in the pairing url The function can raise several exceptions: :raises InvalidFunctionParameter: If the string given in token_type doesn't match a supported token type :raises InvalidFunctionParameter: If the string given in hash_algorithm doesn't match a supported hash algorithm :raises InvalidFunctionParameter: If public key has a different size than 32 bytes :raises InvalidFunctionParameter: If otp_pin_length value is not between 1 and 127 :return: Pairing URL string """ # ---------------------------------------------------------------------- -- # check the token type try: TOKEN_TYPE = TOKEN_TYPES[token_type] except KeyError: allowed_types = ", ".join(list(TOKEN_TYPES.keys())) raise InvalidFunctionParameter( "token_type", "Unsupported token type %s. Supported " "types for pairing are: %s" % (token_type, allowed_types), ) # ---------------------------------------------------------------------- -- # initialize the flag bitmap flags = 0 if not use_cert: flags |= FLAG_PAIR_PK if serial is not None: flags |= FLAG_PAIR_SERIAL if callback_url is not None: flags |= FLAG_PAIR_CBURL if callback_sms_number is not None: flags |= FLAG_PAIR_CBSMS if hash_algorithm is not None: flags |= FLAG_PAIR_HMAC if otp_pin_length is not None: flags |= FLAG_PAIR_DIGITS # ---------------------------------------------------------------------- -- # ---------------------------- -- # fields | version | type | flags | ... | # ---------------------------- -- # size | 1 | 1 | 4 | ? | # ---------------------------- -- data = struct.pack("<bbI", PAIR_URL_VERSION, TOKEN_TYPE, flags) # ---------------------------------------------------------------------- -- # --------------------- -- # fields | ... | partition | ... | # --------------------- -- # size | 6 | 4 | ? | # --------------------- -- data += struct.pack("<I", partition) # ---------------------------------------------------------------------- -- # ------------------------------ -- # fields | .... | server public key | ... | # ------------------------------ -- # size | 10 | 32 | ? | # ------------------------------ -- if flags & FLAG_PAIR_PK: server_public_key = get_public_key(partition) if len(server_public_key) != 32: raise InvalidFunctionParameter( "server_public_key", "Public key must be 32 bytes long" ) data += server_public_key # ---------------------------------------------------------------------- -- # Depending on flags additional data may be sent. If serial was provided # serial will be sent back. If callback url or callback sms was provided # the corresponding data will be added, too # ------------------------------------------------------- -- # fields | .... | serial | NUL | cb url | NUL | cb sms | NUL | ... | # ------------------------------------------------------- -- # size | 42 | ? | 1 | ? | 1 | ? | 1 | ? | # ------------------------------------------------------- -- if flags & FLAG_PAIR_SERIAL: data += serial.encode("utf8") + b"\x00" if flags & FLAG_PAIR_CBURL: data += callback_url.encode("utf8") + b"\x00" if flags & FLAG_PAIR_CBSMS: data += callback_sms_number.encode("utf8") + b"\x00" # ---------------------------------------------------------------------- -- # Other optional values: allowed pin length of otp (number of digits) # and custom hash algorithm # ------------------------------------- -- # fields | ... | otp pin length | hash_algorithm | # ------------------------------------- -- # size | ? | 1 | 1 | # ------------------------------------- -- if flags & FLAG_PAIR_DIGITS: if not (6 <= otp_pin_length <= 12): raise InvalidFunctionParameter( "otp_pin_length", "Pin length must be in the range 6..12" ) data += struct.pack("<b", otp_pin_length) if flags & FLAG_PAIR_HMAC: try: HASH_ALGO = hash_algorithms[hash_algorithm] except KeyError: allowed_values = ", ".join(list(hash_algorithms.keys())) raise InvalidFunctionParameter( "hash_algorithm", "Unsupported hash algorithm %s, " "allowed values are %s" % (hash_algorithm, allowed_values), ) data += struct.pack("<b", HASH_ALGO) # ---------------------------------------------------------------------- -- # TODO missing token details for other protocols (hotp, hmac, etc) # * counter (u64le) # * tstart (u64le) # * tstep (u32le) if not (flags & FLAG_PAIR_PK): secret_key = get_secret_key(partition) server_sig = crypto_sign_detached(data, secret_key) data += server_sig protocol_id = config.get("mobile_app_protocol_id", "lseqr") return protocol_id + "://pair/" + encode_base64_urlsafe(data)