"Fossies" - the Fresh Open Source Software Archive

Member "LinOTP-release-2.10.5.2/linotpd/src/linotp/controllers/openid.py" (13 May 2019, 19402 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 "openid.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 openid controller - This is the controller for the openid service
   28 """
   29 
   30 import logging
   31 import webob
   32 from urllib import urlencode
   33 
   34 import linotp.model
   35 
   36 from linotp.lib.base import BaseController
   37 from linotp.lib.auth.validate import ValidationHandler
   38 
   39 from pylons import tmpl_context as c
   40 from pylons import request, response
   41 from pylons.controllers.util import redirect
   42 from pylons import config
   43 from pylons import url as url
   44 
   45 from linotp.lib.error import ParameterError
   46 
   47 from linotp.lib.util import get_client
   48 from linotp.lib.util import is_valid_fqdn
   49 from linotp.lib.user import getUserFromParam
   50 from linotp.lib.realm import getDefaultRealm
   51 from linotp.lib.util import get_version
   52 from linotp.lib.util import get_copyright_info
   53 
   54 from linotp.lib.policy import PolicyException
   55 from pylons.templating import render_mako as render
   56 from webob.exc import HTTPBadRequest
   57 
   58 from linotp.lib.reply import sendError
   59 
   60 from linotp.lib.openid import IdResMessage
   61 from linotp.lib.openid import create_association, check_authentication
   62 
   63 from linotp.lib.openid import OPENID_2_0_TYPE
   64 from linotp.lib.openid import OPENID_1_0_TYPE
   65 
   66 from linotp.lib.context import request_context
   67 
   68 Session = linotp.model.Session
   69 
   70 ASSOC_EXPIRES_IN = 3600
   71 COOKIE_NAME = "linotp_openid"
   72 
   73 audit = config.get('audit')
   74 
   75 log = logging.getLogger(__name__)
   76 
   77 
   78 class OpenidController(BaseController):
   79 
   80     '''
   81     this is the controller for doing the openid stuff
   82 
   83         https://server/openid/<functionname>
   84 
   85     '''
   86     BASEURL = "https://linotpserver"
   87     COOKIE_EXPIRE = 3600
   88 
   89     def __before__(self, action, **params):
   90 
   91         valid_request = False
   92         try:
   93 
   94             c.audit = request_context[audit]
   95             c.audit['client'] = get_client(request)
   96             request_context['Audit'] = audit
   97 
   98             self.storage = config.get('openid_sql')
   99 
  100             getCookieExpire = int(config.get("linotpOpenID.CookieExpire", -1))
  101 
  102             self.COOKIE_EXPIRE = 3600
  103             if getCookieExpire >= 0:
  104                 self.COOKIE_EXPIRE = getCookieExpire
  105 
  106             c.logged_in = False
  107             c.login = ""
  108             c.version = get_version()
  109             c.licenseinfo = get_copyright_info()
  110 
  111             http_host = request.environ.get("HTTP_HOST")
  112             log.debug("[__before__] Doing openid request from host %s",
  113                        http_host)
  114             if not is_valid_fqdn(http_host, split_port=True):
  115                 err = "Bad hostname: %s" % http_host
  116                 audit.log(c.audit)
  117                 c.audit["action_detail"] = err
  118                 log.error(err)
  119                 raise HTTPBadRequest(err)
  120 
  121             self.BASEURL = request.environ.get("wsgi.url_scheme") + "://" + http_host
  122 
  123             # check if the browser is logged in
  124             login = request.cookies.get(COOKIE_NAME)
  125 
  126             if login:
  127                 c.logged_in = True
  128 
  129             # default return for the __before__ and __after__
  130             valid_request = True
  131 
  132             return response
  133 
  134         except PolicyException as pex:
  135             log.exception("[__before__::%r] policy exception %r" % (action, pex))
  136             return sendError(response, pex, context='before')
  137 
  138         except webob.exc.HTTPUnauthorized as acc:
  139             ## the exception, when an abort() is called if forwarded
  140             log.exception("[__before__::%r] webob.exception %r" % (action, acc))
  141             raise acc
  142 
  143         except Exception as exx:
  144             log.exception("[__before__::%r] exception %r" % (action, exx))
  145             return sendError(response, exx, context='before')
  146 
  147         finally:
  148             if valid_request is False:
  149                 self.storage.session.rollback()
  150                 self.storage.session.close()
  151 
  152 
  153     def __after__(self):
  154         try:
  155             audit.log(c.audit)
  156             self.storage.session.commit()
  157             ## default return for the __before__ and __after__
  158             return response
  159 
  160         except Exception as exx:
  161             log.exception("[__after__] exception %r" % (exx))
  162             self.storage.session.rollback()
  163             return sendError(response, exx, context='after')
  164 
  165         finally:
  166             self.storage.session.close()
  167 
  168 
  169     def id(self):
  170         '''
  171         This method is used by the consumer to authenticate like this:
  172         https://server/openid/id/<user>
  173 
  174         The URL has to return this one in the html head:
  175         <link rel="openid.server" href="http://FQDN/openidserver">
  176         <meta http-equiv="x-xrds-location" content="http://FQDN/yadis/someuser">
  177 
  178         The request flow is:
  179             -> GET /openid/id
  180             -> GET /openid/yadis
  181             -> POST /openid/openidserver -> assocication
  182             -> POST /openid/openidserver -> checkid setup
  183 
  184         '''
  185         user = request.environ['pylons.routes_dict'].get('id')
  186         log.debug("[id] requesting access for user %s" % user)
  187 
  188         baseurl = self.BASEURL
  189         baseyadis = baseurl + "/openid/yadis/"
  190         endpoint = baseurl + "/openid/openidserver"
  191 
  192         response.content_type = 'text/html'
  193         head = '''\
  194 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  195 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  196 <head>
  197   <meta http-equiv="Content-type" content="text/html;charset=UTF-8" />
  198   <link rel="openid.server" href="%s" />
  199   <meta http-equiv="x-xrds-location" content="%s%s" />
  200 </head>
  201 <body>This is used to issue the user names</body>
  202 </html>''' % (endpoint, baseyadis, user)
  203         return head
  204 
  205 
  206     def yadis(self):
  207         user = request.environ['pylons.routes_dict'].get('id')
  208         response.content_type = 'application/xrds+xml'
  209 
  210         endpoint_url = self.BASEURL + "/openid/openidserver"
  211         user_url = self.BASEURL + "/openid/id/%s" % user
  212 
  213         body = """\
  214 <?xml version="1.0" encoding="UTF-8"?>
  215 <xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)">
  216   <XRD>
  217     <Service priority="0">
  218       <Type>%s</Type>
  219       <Type>%s</Type>
  220       <URI>%s</URI>
  221       <LocalID>%s</LocalID>
  222     </Service>
  223   </XRD>
  224 </xrds:XRDS>
  225 """ % (OPENID_2_0_TYPE, OPENID_1_0_TYPE, endpoint_url, user_url)
  226 
  227         return body
  228 
  229 
  230     def openidserver(self):
  231         '''
  232         This is the so called server endpoint, that decides, if the user is authenticated or not.
  233         and returns to the given "openid." either directly or after authenticating the user
  234         openid.claimed_id.
  235         '''
  236         params = self.request_params
  237 
  238         # distpatching the request depending on the mode
  239         if 'openid.mode' not in params:
  240             raise HTTPBadRequest('Missing "openid.mode"')
  241 
  242         mode = params.get('openid.mode')
  243         log.debug("[openidserver] openid.mode=%s" % mode)
  244 
  245         if 'associate' == mode:
  246             return self.associate(params)
  247 
  248         # mandatory fields for other actions
  249         for field in ('openid.identity',):  #, 'openid.'):
  250             if field not in params:
  251                 raise HTTPBadRequest('Missing "%s"' % field)
  252 
  253         if 'check_authentication' == mode:
  254             return self.check_authentication(params)
  255 
  256         elif mode in ('checkid_setup', 'checkid_immediate'):
  257 
  258             authenticated = False
  259             c.login, token = self._split_cookie()
  260             if "" != c.login:
  261                 # what user wants to login?
  262                 rest, claimed_user = params.get("openid.claimed_id").rsplit("/", 1)
  263                 stored_token = self.storage.get_user_token(c.login)
  264 
  265                 log.debug("[openidserver] checking authenticated? %s=%s, %s=%s" %
  266                             (stored_token, token, c.login, claimed_user))
  267 
  268                 if stored_token == token and self._compare_users(c.login, claimed_user):
  269                     authenticated = True
  270 
  271             if not authenticated:
  272                 # Not logged in!
  273                 redirect_to = self.BASEURL + "/openid/openidserver"
  274                 openid_params = urlencode(params)
  275                 redirect("/openid/login?%s&%s" % (urlencode({ "redirect_to" : redirect_to }), openid_params))
  276             else:
  277                 return self.checkid_setup(params)
  278 
  279         # other modes are ignored
  280         raise HTTPBadRequest('"%s" mode not supported' % mode)
  281 
  282 
  283     def checkid_setup(self, param):
  284         '''
  285         This function is called, when the used needs to verify that he is willing to
  286         authenticate for a relying party
  287         '''
  288         params = {}
  289         params.update(param)
  290 
  291         HOST = self.BASEURL + "/openid/openidserver"
  292         message = IdResMessage(self.storage, HOST, 3600, **params)
  293         # signing it
  294         message.sign()
  295 
  296         # for checking trusted roots
  297         user, token = self._split_cookie()
  298         redirect_token = message.store_redirect()
  299         _url, site, handle = self.storage.get_redirect(redirect_token)
  300         trusted_roots = self.storage.get_trusted_roots(user)
  301 
  302         # was it called by the identity plugin ?
  303         if 'X-Identity' in request.headers:
  304             # storing the site in allowed sites
  305             message.store_site()
  306             # redirecting
  307             redirect(message.get_url())
  308 
  309         elif site in trusted_roots:
  310             login, token = self._split_cookie()
  311             user = self.storage.get_user_by_token(token)
  312             c.audit['user'], c.audit['realm'] = user.split('@', 2)
  313             c.audit['success'] = True
  314             c.audit['action_detail'] = site
  315             c.audit['info'] = "site found in trusted root"
  316             # automatic validate, i.e.
  317             # the user gets redirected to the relying party
  318             redirect_to = message.get_url()
  319             redirect(redirect_to)
  320 
  321         else:
  322             # if not, we store the redirect url and display
  323             # a manual screen the user needs to validate
  324             c.identity = message.identity
  325             c.redirect_token = redirect_token
  326             c.rely_party = message.site
  327             return render('/openid/check_setup.mako')
  328 
  329 
  330     def checkid_submit(self):
  331         '''
  332         This is called when the user accepts - hit the submit button - that he will login to the consumer
  333         '''
  334         log.debug("[checkid_submit] params: %s" % self.request_params)
  335 
  336         try:
  337             redirect_token = self.request_params["redirect_token"]
  338         except KeyError:
  339             raise ParameterError("Missing parameter: 'redirect_token'", id=905)
  340 
  341         verify_always = self.request_params.get("verify_always")
  342         r_url, site, handle = self.storage.get_redirect(redirect_token)
  343         self.storage.add_site(site, handle)
  344 
  345         # The user checked the box, that he wants not be bothered again in the future
  346         # the relying party will be added to the trusted root
  347         login, token = self._split_cookie()
  348         user = self.storage.get_user_by_token(token)
  349         c.audit['user'], c.audit['realm'] = user.split('@', 2)
  350         c.audit['success'] = True
  351         c.audit['action_detail'] = site
  352 
  353         if "always" == verify_always:
  354             log.debug("[checkid_submit] putting into trusted root: %s, %s" % (site, handle))
  355             if "" != user:
  356                 self.storage.add_trusted_root(user, site)
  357 
  358         log.debug("[checkid_submit] redirecting to %s" % r_url)
  359         redirect(r_url)
  360 
  361     def check_authentication(self, params):
  362         res = check_authentication(**params)
  363         response.status = 200
  364         response.content_type = "text/plain"
  365         return res
  366 
  367 
  368     def associate(self, params):
  369         '''
  370         This sets up a association (encryption key) bewtween the ID Provider and the consumer
  371         '''
  372         expires_in = ASSOC_EXPIRES_IN
  373         res = create_association(self.storage, expires_in, **params)
  374         response.status = 200
  375         response.content_type = "text/plain"
  376         return res
  377 
  378 
  379 ######### Auth stuff #################################################
  380     def logout(self):
  381         '''
  382         This action deletes the cookie and redirects to the
  383         /openid/status to show the login status
  384 
  385         If the logout is called in the context of an openid authentication,
  386         the user is already logged in as a different user. In this case we
  387         forward to the /openid/login page after the logout was made.
  388 
  389         Another option for the openid authentication context would be to
  390         redirect to the return_to url by setting
  391             redirect_to = params["openid.return_to"]
  392             p["openid.mode"] = "setup_needed"
  393         which advises the openid relying party to restart the login process.
  394         '''
  395 
  396         response.delete_cookie(COOKIE_NAME)
  397 
  398         ## are we are called during an openid auth request?
  399         if "openid.return_to" in self.request_params:
  400             redirect_to = "/openid/login"
  401             do_redirect = url(str("%s?%s" % (redirect_to, urlencode(self.request_params))))
  402 
  403         else:
  404             redirect_to = "/openid/status"
  405             do_redirect = url(redirect_to)
  406 
  407         redirect(do_redirect)
  408 
  409     def login(self):
  410         '''
  411         This is the redirect of the first template
  412         '''
  413         param = self.request_params
  414 
  415         c.defaultRealm = getDefaultRealm()
  416         c.p = {}
  417         c.user = ""
  418         c.title = "LinOTP OpenID Service"
  419 
  420         for k in param:
  421             c.p[k] = param[k]
  422             if "openid.claimed_id" == k:
  423                 c.user = param[k].rsplit("/", 1)[1]
  424 
  425         ## if we have already a cookie but
  426         ## a difference between login and cookie user
  427         ## we show (via  /status) that he is already logged in
  428         ## and that he first must log out
  429         cookie = request.cookies.get(COOKIE_NAME)
  430         if cookie is not None:
  431             cookie_user, token = cookie.split(":")
  432 
  433             if cookie_user != c.user:
  434                 c.login = cookie_user
  435                 c.message = ("Before logging in as >%s< you have to log out."
  436                              % (c.user))
  437 
  438                 return render("/openid/status.mako")
  439 
  440         return render('/openid/login.mako')
  441 
  442     def status(self):
  443         '''
  444         This shows the login status.
  445         '''
  446         cookie = request.cookies.get(COOKIE_NAME)
  447         if cookie is not None:
  448             c.login, token = cookie.split(":")
  449 
  450         if "message" in self.request_params:
  451             c.message = self.request_params.get("message")
  452 
  453         return render("/openid/status.mako")
  454 
  455 
  456     def check(self):
  457         '''
  458         This function is used to login
  459 
  460         method:
  461             openid/check
  462 
  463         arguments:
  464             user     - user to login
  465             realm    - in which realm the user should login
  466             pass     - password
  467 
  468         returns:
  469             JSON response
  470         '''
  471         ok = False
  472         param = self.request_params
  473         do_redirect = None
  474         message = None
  475 
  476         try:
  477             same_user = True
  478             passw = param.get("pass")
  479 
  480             ## getUserFromParam will return default realm if no realm is
  481             ## provided via @ append or extra parameter realm
  482             ## if the provided realm does not exist, the realm is left empty
  483             user = getUserFromParam(param)
  484 
  485             ## if the requested user has a realm specified (via @realm append)
  486             ## and this is not the same as the user from getUserFromParam
  487             ## the requested user is not a valid one!
  488             p_user = param.get('user', '')
  489             if "@" in p_user:
  490                 if p_user != "%s@%s" % (user.login, user.realm):
  491                     same_user = False
  492 
  493             c.audit['user'] = user.login
  494             c.audit['realm'] = user.realm or getDefaultRealm()
  495 
  496             vh = ValidationHandler()
  497             if same_user is True:
  498                 (ok, opt) = vh.checkUserPass(user, passw)
  499 
  500             c.audit['success'] = ok
  501 
  502             if ok:
  503                 ## if the user authenticated successfully we need to set the cookie aka
  504                 ## the ticket and we need to remember this ticket.
  505                 user = "%s@%s" % (user.login, c.audit['realm'])
  506                 log.debug("[check] user=%s" % user)
  507                 token = self.storage.set_user_token(user, expire=self.COOKIE_EXPIRE)
  508                 log.debug("[check] token=%s" % token)
  509                 cookie = "%s:%s" % (user, token)
  510                 log.debug("[check] cookie=%s" % cookie)
  511                 response.set_cookie(COOKIE_NAME, cookie, max_age=self.COOKIE_EXPIRE)
  512             else:
  513                 message = "Your login attempt was not successful!"
  514 
  515             Session.commit()
  516             # Only if we logged in successfully we redirect to the original
  517             # page (Servive Provider). Otherwise we will redirect to the
  518             # status page
  519 
  520             p = {}
  521             redirect_to = param.get("redirect_to")
  522             if redirect_to and ok:
  523                 for k in  [ 'openid.return_to', "openid.realm", "openid.ns", "openid.claimed_id", "openid.mode",
  524                             "openid.identity" ]:
  525                     p[k] = param[k]
  526             else:
  527                 if message is not None:
  528                     p["message"] = message
  529                 redirect_to = "/openid/status"
  530 
  531             do_redirect = url(str("%s?%s" % (redirect_to, urlencode(p))))
  532 
  533         except Exception as exx:
  534             log.exception("[check] openid/check failed: %r" % exx)
  535             Session.rollback()
  536             return sendError(response, "openid/check failed: %r" % exx, 0)
  537 
  538         finally:
  539             Session.close()
  540             log.debug('[check] done')
  541 
  542         if do_redirect:
  543             log.debug("[check] now redirecting to %s" % do_redirect)
  544             redirect(do_redirect)
  545 
  546     def _compare_users(self, u1, u2):
  547         realm = getDefaultRealm()
  548         if len(u1.split('@')) == 1:
  549             u1 = "%s@%s" % (u1, realm)
  550         if len(u2.split('@')) == 1:
  551             u2 = "%s@%s" % (u2, realm)
  552         log.debug("[compare_users] %s == %s?" % (u1, u2))
  553         return u1 == u2
  554 
  555     def _split_cookie(self):
  556         login = ""
  557         token = ""
  558         cookie = request.cookies.get(COOKIE_NAME)
  559         if cookie is not None:
  560             login, token = cookie.split(":")
  561         return login, token
  562 
  563     def custom_style(self):
  564         '''
  565         If this action was called, the user hasn't created a custom css yet. To avoid hitting
  566         the debug console over and over, we serve an empty file.
  567         '''
  568         response.headers['Content-type'] = 'text/css'
  569         return ''
  570 
  571 # eof #