Exemple #1
0
    def GET(self, *args, **kwargs):

        session = UserSession()
        user = session.get_user()
        transdata = self.trans.retrieve()
        self.stage = transdata['saml2_stage']

        if user.is_anonymous:
            self.debug("User is marked anonymous?!")
            message = transdata.get('message')
            if message is not None:
                data = {'message': None}
                self.trans.store(data)
            # TODO: Return to SP with auth failed error
            raise cherrypy.HTTPError(401, message)

        self.debug('Continue auth for %s' % user.name)

        if 'saml2_request' not in transdata:
            self.error("Couldn't find Request dump in transaction?!")
            # TODO: Return to SP with auth failed error
            raise cherrypy.HTTPError(400)
        dump = transdata['saml2_request']

        try:
            login = self.cfg.idp.get_login_handler(dump)
        except Exception, e:  # pylint: disable=broad-except
            self.error('Failed to load login status from dump: %r' % e)
Exemple #2
0
    def GET(self, *args, **kwargs):

        session = UserSession()
        user = session.get_user()
        transdata = self.trans.retrieve()
        self.stage = transdata['saml2_stage']

        if user.is_anonymous:
            self.debug("User is marked anonymous?!")
            message = transdata.get('message')
            if message is not None:
                data = {'message': None}
                self.trans.store(data)
            # TODO: Return to SP with auth failed error
            raise cherrypy.HTTPError(401, message)

        self.debug('Continue auth for %s' % user.name)

        if 'saml2_request' not in transdata:
            self.error("Couldn't find Request dump in transaction?!")
            # TODO: Return to SP with auth failed error
            raise cherrypy.HTTPError(400)
        dump = transdata['saml2_request']

        try:
            login = self.cfg.idp.get_login_handler(dump)
        except Exception as e:  # pylint: disable=broad-except
            self.error('Failed to load login status from dump: %r' % e)

        if not login:
            self.error("Empty login Request dump?!")
            # TODO: Return to SP with auth failed error
            raise cherrypy.HTTPError(400)

        return self.auth(login)
Exemple #3
0
    def root(self, *args, **kwargs):
        us = UserSession()
        user = us.get_user()
        consents = user.list_consents()

        for consent in consents:
            provname = consent['provider']
            provider = self._site['provider_config'].available.get(provname,
                                                                   None)

            if provider is not None:
                consent['providerdn'] = provider.get_display_name()
                consent['clientdn'] = provider.\
                    get_client_display_name(consent['client'])
                attrs = provider.consent_to_display(consent['attrs'])
            else:
                self.debug('Consent relates to unknown provider %s' % provname)
                attrs = []
            consent['attrs'] = attrs

        return self._template('user/index.html',
                              title='',
                              baseurl=self.url,
                              menu=self.menu,
                              consents=consents)
Exemple #4
0
    def _openid_checks(self, request, form, **kwargs):
        us = UserSession()
        user = us.get_user()
        immediate = False

        self.debug('Mode: %s Stage: %s User: %s' % (
            kwargs['openid.mode'], self.stage, user.name))
        if kwargs.get('openid.mode', None) == 'checkid_setup':
            if user.is_anonymous:
                if self.stage == 'init':
                    returl = '%s/openid/Continue?%s' % (
                        self.basepath, self.trans.get_GET_arg())
                    data = {'openid_stage': 'auth',
                            'openid_request': json.dumps(kwargs),
                            'login_return': returl,
                            'login_target': request.trust_root}
                    self.trans.store(data)
                    redirect = '%s/login?%s' % (self.basepath,
                                                self.trans.get_GET_arg())
                    self.debug('Redirecting: %s' % redirect)
                    raise cherrypy.HTTPRedirect(redirect)
                else:
                    raise UnauthorizedRequest("unknown user")

        elif kwargs.get('openid.mode', None) == 'checkid_immediate':
            # This is immediate, so we need to assert or fail
            if user.is_anonymous:
                return self._respond(request.answer(False))

            immediate = True

        else:
            return self._respond(self.cfg.server.handleRequest(request))

        # check if this is discovery or needs identity matching checks
        if not request.idSelect():
            idurl = self.cfg.identity_url_template % {'username': user.name}
            if request.identity != idurl:
                raise UnauthorizedRequest("User ID mismatch!")

        # check if the relying party is trusted
        if request.trust_root in self.cfg.untrusted_roots:
            raise UnauthorizedRequest("Untrusted Relying party")

        # if the party is explicitly whitelisted just respond
        if request.trust_root in self.cfg.trusted_roots:
            return self._respond(self._response(request, us))

        allowroot = 'allow-%s' % request.trust_root

        try:
            userdata = user.load_plugin_data(self.cfg.name)
            expiry = int(userdata[allowroot])
        except Exception, e:  # pylint: disable=broad-except
            self.debug(e)
            expiry = 0
Exemple #5
0
    def GET(self, *args, **kwargs):

        session = UserSession()
        session.logout(None)

        # return to the caller if any
        transdata = self.get_valid_transaction('login', **kwargs).retrieve()
        if 'login_return' not in transdata:
            raise cherrypy.HTTPError(401)
        raise cherrypy.HTTPRedirect(transdata['login_return'])
Exemple #6
0
    def idp_initiated_logout(self):
        """
        Logout all SP sessions when the logout comes from the IdP.

        For the current user only.

        Only use HTTP-Redirect to start the logout. This is guaranteed
        to be supported in SAML 2.
        """
        self.debug("IdP-initiated SAML2 logout")
        us = UserSession()
        user = us.get_user()

        saml_sessions = self.sessionfactory
        # pylint: disable=unused-variable
        (mech, session) = saml_sessions.get_next_logout(
            logout_mechs=[lasso.SAML2_METADATA_BINDING_REDIRECT], user=us.user)
        if session is None:
            return

        logout = self.idp.get_logout_handler()
        logout.setSessionFromDump(session.login_session)
        try:
            logout.initRequest(session.provider_id)
        except lasso.ServerProviderNotFoundError:
            self.error('Service Provider %s not found. Trying next session' %
                       session.provider_id)
            return self.idp_initiated_logout()
        except lasso.ProfileUnsupportedProfileError:
            self.error('Service Provider %s does not support Single Logout. '
                       'Trying next session' % session.provider_id)
            return self.idp_initiated_logout()
        try:
            logout.buildRequestMsg()
        except lasso.Error as e:
            self.error('failure to build logout request msg: %s' % e)
            raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s ' % e)

        # Add a fake session to indicate where the user should
        # be redirected to when all SP's are logged out.
        idpurl = self._root.instance_base_url()
        session_id = "_" + uuid.uuid4().hex.upper()
        saml_sessions.add_session(session_id, idpurl, user.name, "", "",
                                  [lasso.SAML2_METADATA_BINDING_REDIRECT])
        init_session = saml_sessions.get_session_by_id(session_id)
        saml_sessions.start_logout(init_session, relaystate=idpurl)

        # Add the logout request id we just created to the session to be
        # logged out so that when it responds we can find the right
        # session.
        session.set_logoutstate(request_id=logout.request.id)
        saml_sessions.start_logout(session, initial=False)

        self.debug('Sending initial logout request to %s' % logout.msgUrl)
        raise cherrypy.HTTPRedirect(logout.msgUrl)
Exemple #7
0
    def root(self, *args, **kwargs):
        us = UserSession()

        if us.user is not None:
            for provider in self.handlers:
                self.debug("Calling logout for provider %s" % provider)
                obj = self.handlers[provider]
                obj()

        us.logout(self.user)
        return self._template('logout.html', title='Logout')
Exemple #8
0
    def revoke(self, provider, clientid):
        us = UserSession()
        user = us.get_user()

        provname = provider
        provmod = self._site['provider_config'].available.get(provname,
                                                              None)
        if provmod is not None:
            if not provmod.revoke_consent(user.name, clientid):
                raise Exception('Provider refused to revoke')
        user.revoke_consent(provider, clientid)
        raise cherrypy.HTTPRedirect(self._master.url)
Exemple #9
0
 def POST(self, *args, **kwargs):
     us = UserSession()
     us.remote_login()
     user = us.get_user()
     if not user.is_anonymous:
         return self.lm.auth_successful(self.trans, user.name, 'password')
     else:
         error = cherrypy.request.wsgi_environ.get(
             'EXTERNAL_AUTH_ERROR',
             'Unknown error using external authentication')
         error = PAM_AUTH_ERRORS.get(error, error)
         cherrypy.log.error("Error: %s" % error)
         return self.lm.auth_failed(self.trans, error)
Exemple #10
0
 def POST(self, *args, **kwargs):
     us = UserSession()
     us.remote_login()
     user = us.get_user()
     if not user.is_anonymous:
         return self.lm.auth_successful(self.trans, user.name, 'password')
     else:
         error = cherrypy.request.wsgi_environ.get(
             'EXTERNAL_AUTH_ERROR',
             'Unknown error using external authentication'
         )
         error = PAM_AUTH_ERRORS.get(error, error)
         cherrypy.log.error("Error: %s" % error)
         return self.lm.auth_failed(self.trans, error)
Exemple #11
0
    def __call__(self, *args, **kwargs):
        cherrypy.response.headers.update(self.default_headers)

        self.user = UserSession().get_user()

        if self.auth_protect and self.user.is_anonymous:
            raise cherrypy.HTTPError(401)

        self.debug("method: %s" % cherrypy.request.method)
        op = getattr(self, cherrypy.request.method, None)
        if callable(op):
            # Basic CSRF protection
            if cherrypy.request.method != 'GET':
                url = self.get_url()
                if 'referer' not in cherrypy.request.headers:
                    self.debug("Missing referer in %s request to %s"
                               % (cherrypy.request.method, url))
                    raise cherrypy.HTTPError(403)
                referer = cherrypy.request.headers['referer']
                if not self._check_referer(referer, url):
                    self.debug("Wrong referer %s in request to %s"
                               % (referer, url))
                    raise cherrypy.HTTPError(403)
            return op(*args, **kwargs)
        else:
            op = getattr(self, 'root', None)
            if callable(op):
                return op(*args, **kwargs)

        return self.default(*args, **kwargs)
 def root(self, *args, **kwargs):
     trans = self.get_valid_transaction("login", **kwargs)
     # If we can get here, we must be authenticated and remote_user
     # was set. Check the session has a user set already or error.
     us = UserSession()
     us.remote_login()
     self.user = us.get_user()
     if not self.user.is_anonymous:
         principal = cherrypy.request.wsgi_environ.get("GSS_NAME", None)
         if principal:
             userdata = {"gssapi_principal_name": principal}
         else:
             userdata = {"gssapi_principal_name": self.user.name}
         return self.lm.auth_successful(trans, self.user.name, "gssapi", userdata)
     else:
         return self.lm.auth_failed(trans)
Exemple #13
0
 def root(self, *args, **kwargs):
     trans = self.get_valid_transaction('login', **kwargs)
     # If we can get here, we must be authenticated and remote_user
     # was set. Check the session has a user set already or error.
     us = UserSession()
     us.remote_login(is_krb=True)
     self.user = us.get_user()
     if not self.user.is_anonymous:
         principal = cherrypy.request.wsgi_environ.get('GSS_NAME', None)
         if principal:
             userdata = {'gssapi_principal_name': principal}
         else:
             userdata = {'gssapi_principal_name': self.user.name}
         return self.lm.auth_successful(trans, self.user.name,
                                        'gssapi', userdata)
     else:
         return self.lm.auth_failed(trans)
Exemple #14
0
    def POST(self, *args, **kwargs):
        if 'email' not in kwargs or 'publicKey' not in kwargs \
                or 'certDuration' not in kwargs or '@' not in kwargs['email']:
            cherrypy.response.status = 400
            raise Exception('Invalid request: %s' % kwargs)

        us = UserSession()
        user = us.get_user()

        if user.is_anonymous:
            raise cherrypy.HTTPError(401, 'Not signed in')

        if not self._willing_to_sign(kwargs['email'], user.name):
            self.log('Not willing to sign for %s, logged in as %s' % (
                kwargs['email'], user.name))
            raise cherrypy.HTTPError(403, 'Incorrect user')

        return self._persona_sign(kwargs['email'], kwargs['publicKey'],
                                  kwargs['certDuration'])
Exemple #15
0
    def POST(self, *args, **kwargs):
        if 'email' not in kwargs or 'publicKey' not in kwargs \
                or 'certDuration' not in kwargs or '@' not in kwargs['email']:
            cherrypy.response.status = 400
            raise Exception('Invalid request: %s' % kwargs)

        us = UserSession()
        user = us.get_user()

        if user.is_anonymous:
            raise cherrypy.HTTPError(401, 'Not signed in')

        if not self._willing_to_sign(kwargs['email'], user.name):
            self.log('Not willing to sign for %s, logged in as %s' %
                     (kwargs['email'], user.name))
            raise cherrypy.HTTPError(403, 'Incorrect user')

        return self._persona_sign(kwargs['email'], kwargs['publicKey'],
                                  kwargs['certDuration'])
Exemple #16
0
    def idp_initiated_logout(self):
        """
        Logout all SP sessions when the logout comes from the IdP.

        For the current user only.

        Only use HTTP-Redirect to start the logout. This is guaranteed
        to be supported in SAML 2.
        """
        self.debug("IdP-initiated SAML2 logout")
        us = UserSession()
        user = us.get_user()

        saml_sessions = self.sessionfactory
        # pylint: disable=unused-variable
        (mech, session) = saml_sessions.get_next_logout(
            logout_mechs=[lasso.SAML2_METADATA_BINDING_REDIRECT])
        if session is None:
            return

        logout = self.idp.get_logout_handler()
        logout.setSessionFromDump(session.login_session)
        try:
            logout.initRequest(session.provider_id)
        except lasso.ServerProviderNotFoundError:
            self.error(
                'Service Provider %s not found. Trying next session' %
                session.provider_id
            )
            return self.idp_initiated_logout()
        except lasso.ProfileUnsupportedProfileError:
            self.error(
                'Service Provider %s does not support Single Logout. '
                'Trying next session' %
                session.provider_id
            )
            return self.idp_initiated_logout()
        try:
            logout.buildRequestMsg()
        except lasso.Error, e:
            self.error('failure to build logout request msg: %s' % e)
            raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
                                        % e)
Exemple #17
0
    def auth_failed(self, trans, message=None):
        # try with next module
        next_login = self.next_login()
        data = {'message': message}
        trans.store(data)
        if next_login:
            return self.redirect_to_path(next_login.path, trans)

        # return to the caller if any
        session = UserSession()

        transdata = trans.retrieve()

        # on direct login the UI (ie not redirected by a provider) we ned to
        # remove the transaction cookie as it won't be needed anymore
        if trans.provider == 'login':
            trans.wipe()

        # destroy session and return error
        if 'login_return' not in transdata:
            session.logout(None)
            raise cherrypy.HTTPError(401, message)

        raise cherrypy.HTTPRedirect(transdata['login_return'])
Exemple #18
0
    def check(*args, **kwargs):
        if UserSession().get_user().is_admin:
            return fn(*args, **kwargs)

        raise cherrypy.HTTPError(403)
Exemple #19
0
    def GET(self, *args, **kwargs):
        user = UserSession().get_user()

        return self._template('persona/signin_result.html',
                              loggedin=not user.is_anonymous)
Exemple #20
0
    def initialize_login_session(self, username, info=None,
                                 auth_type=None, userdata=None):
        """Establish a login session for a user.

        Builds a `UserSession` object and bind attributes associated
        with the user to the session.

        User attributes derive from two sources, the `Info` object
        passed as the info parameter and the userdata dict. The `Info`
        object encapsulates the info plugins run by Ipsilon. The
        userdata dict is additional information typically derived
        during authentication.

        The `Info` derived attributes are merged with the userdata
        attributes to form one set of user attributes. The user
        attributes are checked for consistenccy. Additional attrbutes
        may be synthesized and added to the user attributes. The final
        set of user attributes is then bound to the returned
        `UserSession` object.

        :param username:  The username bound to the identity principal
        :param info:      A `Info` object providing user attributes
        :param auth_type: Authenication method name
        :param userdata:  Dict of additional user attributes

        :return: `UserSession` object
        """

        session = UserSession()

        # merge attributes from login plugin and info plugin
        if info:
            infoattrs = info.get_user_attrs(username)
        else:
            infoattrs = dict()

        if userdata is None:
            userdata = dict()

        if '_username' not in userdata:
            userdata['_username'] = username

        if '_groups' in infoattrs:
            userdata['_groups'] = list(set(userdata.get('_groups', []) +
                                           infoattrs['_groups']))
            del infoattrs['_groups']

        if '_extras' in infoattrs:
            userdata['_extras'] = userdata.get('_extras', {})
            userdata['_extras'].update(infoattrs['_extras'])
            del infoattrs['_extras']

        userdata.update(infoattrs)

        self.debug("User %s attributes: %s" % (username, repr(userdata)))

        if auth_type:
            userdata.update({'_auth_type': auth_type})

        userdata.update({'_auth_time': int(time.time())})

        # create session login including all the userdata just gathered
        session.login(username, userdata)

        return session
Exemple #21
0
    def saml2checks(self, login):

        us = UserSession()
        user = us.get_user()
        if user.is_anonymous:
            if self.stage == 'init':
                returl = '%s/saml2/SSO/Continue?%s' % (
                    self.basepath, self.trans.get_GET_arg())
                data = {
                    'saml2_stage': 'auth',
                    'saml2_request': login.dump(),
                    'login_return': returl,
                    'login_target': login.remoteProviderId
                }
                self.trans.store(data)
                redirect = '%s/login?%s' % (self.basepath,
                                            self.trans.get_GET_arg())
                raise cherrypy.HTTPRedirect(redirect)
            else:
                raise AuthenticationError("Unknown user",
                                          lasso.SAML2_STATUS_CODE_AUTHN_FAILED)

        self._audit("Logged in user: %s [%s]" % (user.name, user.fullname))

        # We can wipe the transaction now, as this is the last step
        self.trans.wipe()

        # TODO: check if this is the first time this user access this SP
        # If required by user prefs, ask user for consent once and then
        # record it
        consent = True

        # TODO: check destination

        try:
            provider = ServiceProvider(self.cfg, login.remoteProviderId)
            nameidfmt = provider.get_valid_nameid(login.request.nameIdPolicy)
        except NameIdNotAllowed as e:
            raise AuthenticationError(
                str(e), lasso.SAML2_STATUS_CODE_INVALID_NAME_ID_POLICY)
        except InvalidProviderId as e:
            raise AuthenticationError(str(e),
                                      lasso.SAML2_STATUS_CODE_AUTHN_FAILED)

        # TODO: check login.request.forceAuthn

        login.validateRequestMsg(not user.is_anonymous, consent)

        authtime = datetime.datetime.utcnow()
        skew = datetime.timedelta(0, 60)
        authtime_notbefore = authtime - skew
        authtime_notafter = authtime + skew

        # Let's first do the attribute mapping, so we could map the username
        # Check attribute policy and perform mapping and filtering.
        # If the SP has its own mapping or filtering policy use that
        # instead of the global policy.
        if (provider.attribute_mappings is not None
                and len(provider.attribute_mappings) > 0):
            attribute_mappings = provider.attribute_mappings
        else:
            attribute_mappings = self.cfg.default_attribute_mapping
        if (provider.allowed_attributes is not None
                and len(provider.allowed_attributes) > 0):
            allowed_attributes = provider.allowed_attributes
        else:
            allowed_attributes = self.cfg.default_allowed_attributes
        self.debug("Allowed attrs: %s" % allowed_attributes)
        self.debug("Mapping: %s" % attribute_mappings)
        policy = Policy(attribute_mappings, allowed_attributes)
        userattrs = us.get_user_attrs()
        mappedattrs, _ = policy.map_attributes(userattrs)
        attributes = policy.filter_attributes(mappedattrs)

        if '_groups' in attributes and 'groups' not in attributes:
            attributes['groups'] = attributes['_groups']

        self.debug("%s's attributes: %s" % (user.name, attributes))

        # Perform authorization check.
        # We use the raw userattrs here so that we can make decisions based
        # on attributes we don't want to send to the SP
        provinfo = {
            'name': provider.name,
            'url': provider.splink,
            'owner': provider.owner
        }
        if not self._site['authz'].authorize_user('saml2', provinfo, user.name,
                                                  userattrs):
            self.trans.wipe()
            self.error('Authorization denied by authorization provider')
            raise AuthenticationError("Authorization denied",
                                      lasso.SAML2_STATUS_CODE_AUTHN_FAILED)

        # TODO: get authentication type fnd name format from session
        # need to save which login manager authenticated and map it to a
        # saml2 authentication context
        authn_context = lasso.SAML2_AUTHN_CONTEXT_UNSPECIFIED

        timeformat = '%Y-%m-%dT%H:%M:%SZ'
        login.buildAssertion(authn_context, authtime.strftime(timeformat),
                             None, authtime_notbefore.strftime(timeformat),
                             authtime_notafter.strftime(timeformat))

        nameid = None
        if nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT:
            idpsalt = self.cfg.idp_nameid_salt
            if idpsalt is None:
                raise AuthenticationError(
                    "idp nameid salt is not set in configuration")
            value = hashlib.sha512()
            value.update(idpsalt)
            value.update(login.remoteProviderId)
            value.update(mappedattrs.get('_username'))
            nameid = '_' + value.hexdigest()
        elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT:
            nameid = '_' + uuid.uuid4().hex
        elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS:
            nameid = userattrs.get('gssapi_principal_name')
        elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL:
            nameid = mappedattrs.get('email')
            if not nameid:
                nameid = '%s@%s' % (user.name, self.cfg.default_email_domain)
        elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED:
            nameid = provider.normalize_username(mappedattrs.get('_username'))

        if nameid:
            login.assertion.subject.nameId.format = nameidfmt
            login.assertion.subject.nameId.content = nameid
        else:
            self.trans.wipe()
            self.error('Authentication succeeded but it was not ' +
                       'provided by NameID %s' % nameidfmt)
            raise AuthenticationError("Unavailable Name ID type",
                                      lasso.SAML2_STATUS_CODE_AUTHN_FAILED)

        # The saml-core-2.0-os specification section 2.7.3 requires
        # the AttributeStatement element to be non-empty.
        if attributes:
            if not login.assertion.attributeStatement:
                attrstat = lasso.Saml2AttributeStatement()
                login.assertion.attributeStatement = [attrstat]
            else:
                attrstat = login.assertion.attributeStatement[0]
            if not attrstat.attribute:
                attrstat.attribute = ()

        for key in attributes:
            # skip internal info
            if key[0] == '_':
                continue
            values = attributes[key]
            if isinstance(values, dict):
                continue
            if not isinstance(values, list):
                values = [values]
            attr = lasso.Saml2Attribute()
            attr.name = key
            attr.nameFormat = lasso.SAML2_ATTRIBUTE_NAME_FORMAT_BASIC
            attr.attributeValue = []
            vals = []
            for value in values:
                if value is None:
                    self.log('Ignoring None value for attribute %s' % key)
                    continue
                self.debug('value %s' % value)
                node = lasso.MiscTextNode.newWithString(value)
                node.textChild = True
                attrvalue = lasso.Saml2AttributeValue()
                attrvalue.any = [node]
                vals.append(attrvalue)

            attr.attributeValue = vals
            attrstat.attribute = attrstat.attribute + (attr, )

        self.debug('Assertion: %s' % login.assertion.dump())

        saml_sessions = self.cfg.idp.sessionfactory

        lasso_session = lasso.Session()
        lasso_session.addAssertion(login.remoteProviderId, login.assertion)
        provider = ServiceProvider(self.cfg, login.remoteProviderId)
        saml_sessions.add_session(login.assertion.id, login.remoteProviderId,
                                  user.name, lasso_session.dump(), None,
                                  provider.logout_mechs)
Exemple #22
0
    def _perform_continue(self, *args, **kwargs):
        us = UserSession()
        user = us.get_user()

        if user.is_anonymous:
            raise InvalidRequest('User not authenticated at continue')

        transdata = self.trans.retrieve()
        stage = transdata.get('openidc_stage', None)
        request_data = transdata.get('openidc_request', None)
        if stage not in ['continue', 'consent'] or request_data is None:
            raise InvalidRequest('Invalid stage or no request')

        # Since we have openidc_stage continue or consent, request is sane
        try:
            request_data = json.loads(request_data)
        except:
            raise InvalidRequest('Unable to re-parse stored request')

        client = self.cfg.datastore.getClient(request_data['client_id'])
        if not client:
            return self._respond_error(request_data, 'unauthorized_client',
                                       'Unknown client ID')

        # Return error if authz check fails
        authz_check_res = self._authz_stack_check(request_data,
                                                  client, user.name,
                                                  us.get_user_attrs())
        if authz_check_res:
            return authz_check_res

        userattrs = self._source_attributes(us)
        if client['ipsilon_internal']['trusted']:
            # No consent needed, approve
            self.debug('Client trusted, no consent needed')
            return self._respond_success(request_data, client, user, userattrs)

        consentdata = user.get_consent('openidc', request_data['client_id'])
        if consentdata is not None:
            # Consent has already been granted
            self.debug('Consent already granted')

            consclaimset = set(consentdata['claims'])
            claimset = set(
                self._valid_claims(request_data['claims'], userattrs))
            consscopeset = set(consentdata['scopes'])
            scopeset = set(self._valid_scopes(request_data['scope']))

            if claimset.issubset(consclaimset) and \
                    scopeset.issubset(consscopeset):
                return self._respond_success(request_data, client, user,
                                             userattrs)
            else:
                self.debug('Client is asking for new claims or scopes, user '
                           'must give consent again')

        if 'none' in request_data['prompt']:
            # We were asked to not show any UI
            return self._respond_error(request_data, 'consent_required',
                                       'user interface required')

        # Now ask consent
        if 'form_filled' in kwargs and stage == 'consent':
            # The user has been shown the form, let's process his choice
            if 'decided_allow' in kwargs:
                # User allowed the request

                # Record the consent for the future, including the list of
                # claims that the user was informed of at the time.
                consentdata = {
                    'claims': self._valid_claims(request_data['claims'],
                                                 userattrs),
                    'scopes': self._valid_scopes(request_data['scope'])
                }
                user.grant_consent('openidc', request_data['client_id'],
                                   consentdata)

                return self._respond_success(request_data, client, user,
                                             userattrs)

            else:
                # User denied consent
                self.debug('User denied consent')
                return self._respond_error(request_data, 'access_denied',
                                           'user denied consent')
        else:
            # The user was not shown the form yet, let's
            data = {
                'openidc_stage': 'consent',
                'openidc_request': json.dumps(request_data)
            }
            self.trans.store(data)

            claim_requests = {}
            for claimtype in request_data['claims']:
                for claim in request_data['claims'][claimtype]:
                    if claim in userattrs:
                        essential = False
                        if isinstance(request_data['claims'][claimtype][claim],
                                      dict):
                            essential = \
                                request_data['claims'][claimtype][claim].get(
                                    'essential', False)

                        claim_requests[claim] = {
                            'display_name':
                            self.cfg.mapping.display_name(claim),
                            'value': userattrs[claim],
                            'essential': essential
                        }

            scopes = {}
            # Add extension data
            for n, e in self.cfg.extensions.available().items():
                data = e.get_display_data(request_data['scope'])
                self.debug('%s returned %s' % (n, repr(data)))
                if len(data) > 0:
                    scopes[e.get_display_name()] = data

            client_params = {
                'name': client.get('client_name', None),
                'logo': client.get('logo_uri', None),
                'homepage': client.get('client_uri', None),
                'policy': client.get('policy_uri', None),
                'tos': client.get('tos_uri', None)
            }

            if not client_params['name']:
                client_params['name'] = get_url_hostpart(
                    request_data['redirect_uri'])

            context = {
                "title": 'Consent',
                "action": '%s/%s/Continue' % (self.basepath, URLROOT),
                "client": client_params,
                "claim_requests": claim_requests,
                "scopes": scopes,
                "username": user.name,
            }
            context.update(dict((self.trans.get_POST_tuple(), )))
            return self._template(URLROOT + '/consent_form.html', **context)
Exemple #23
0
    def start_authz(self, arguments):
        request_data = {
            'scope': [],
            'response_type': [],
            'client_id': None,
            'redirect_uri': None,
            'state': None,
            'response_mode': None,
            'nonce': None,
            'display': None,
            'prompt': [],
            'max_age': None,
            'ui_locales': None,
            'id_token_hint': None,
            'login_hint': None,
            'acr_values': None,
            'claims': '{}'
        }

        # Get the request
        # Step 1: get the get query arguments
        for data in request_data.keys():
            if arguments.get(data, None):
                request_data[data] = arguments[data]

        # This is a workaround for python not understanding the splits we
        # do later
        if request_data['prompt'] == []:
            request_data['prompt'] = None

        for required_arg in ['scope', 'response_type', 'client_id']:
            if request_data[required_arg] is None or \
                    len(request_data[required_arg]) == 0:
                return self._respond_error(
                    request_data, 'invalid_request',
                    'missing required argument %s' % required_arg)

        client = self.cfg.datastore.getClient(request_data['client_id'])
        if not client:
            return self._respond_error(request_data, 'unauthorized_client',
                                       'Unknown client ID')

        request_data['response_type'] = request_data.get('response_type',
                                                         '').split(' ')
        for rtype in request_data['response_type']:
            if rtype not in ['id_token', 'token', 'code']:
                return self._respond_error(
                    request_data, 'unsupported_response_type',
                    'response type %s is not supported' % rtype)

        if request_data['response_type'] != ['code'] and \
                not request_data['nonce']:
            return self._respond_error(request_data, 'invalid_request',
                                       'nonce missing in non-code flow')

        # Step 2: get any provided request or request_uri
        if 'request' in arguments or 'request_uri' in arguments:
            # This is a JWT-encoded request
            if 'request' in arguments and 'request_uri' in arguments:
                return self._respond_error(
                    request_data, 'invalid_request',
                    'both request and request_uri ' + 'provided')

            if 'request' in arguments:
                jwt_object = arguments['request']
            else:
                try:
                    # FIXME: MAY cache this at client registration time and
                    # cache permanently until client registration is changed.
                    jwt_object = requests.get(arguments['request_uri']).text
                except Exception as ex:  # pylint: disable=broad-except
                    self.debug('Unable to get request: %s' % ex)
                    return self._respond_error(request_data, 'invalid_request',
                                               'unable to parse request_uri')

            jwt_request = None
            try:
                # FIXME: Implement decryption
                decoded = JWT(jwt=jwt_object)
                if client['request_object_signing_alg'] != 'none':
                    # Client told us we need to check signature
                    if decoded.token.jose_header['alg'] != \
                            client['request_object_signing_alg']:
                        raise Exception('Invalid algorithm used: %s' %
                                        decoded.token.jose_header['alg'])

                if client['request_object_signing_alg'] == 'none':
                    jwt_request = json.loads(decoded.token.objects['payload'])
                else:
                    keyset = None
                    if client['jwks']:
                        keys = json.loads(client['jkws'])
                    else:
                        keys = requests.get(client['jwks_uri']).json()
                    keyset = JWKSet()
                    for key in keys['keys']:
                        keyset.add(JWK(**key))
                    key = keyset.get_key(decoded.token.jose_header['kid'])
                    decoded = JWT(jwt=jwt_object, key=key)
                    jwt_request = json.loads(decoded.claims)

            except Exception as ex:  # pylint: disable=broad-except
                self.debug('Unable to parse request: %s' % ex)
                return self._respond_error(request_data, 'invalid_request',
                                           'unable to parse request')

            if 'response_type' in jwt_request:
                jwt_request['response_type'] = \
                    jwt_request['response_type'].split(' ')
                if jwt_request['response_type'] != \
                        request_data['response_type']:
                    return self._respond_error(request_data, 'invalid_request',
                                               'response_type does not match')

            if 'client_id' in jwt_request:
                if jwt_request['client_id'] != request_data['client_id']:
                    return self._respond_error(request_data, 'invalid_request',
                                               'client_id does not match')

            for data in request_data.keys():
                if data in jwt_request:
                    request_data[data] = jwt_request[data]

        # Split these options since they are space-separated lists
        for to_split in ['prompt', 'ui_locales', 'acr_values', 'scope']:
            if request_data[to_split] is not None:
                # We know better than pylint in this regard
                # pylint: disable=no-member
                request_data[to_split] = request_data[to_split].split(' ')
            else:
                request_data[to_split] = []

        # Start checking the request
        if request_data['redirect_uri'] is None:
            if len(client['redirect_uris']) != 1:
                return self._respond_error(request_data, 'invalid_request',
                                           'missing redirect_uri')
            else:
                request_data['redirect_uri'] = client['redirect_uris'][0]

        for scope in request_data['scope']:
            if scope not in self.cfg.supported_scopes:
                return self._respond_error(
                    request_data, 'invalid_scope',
                    'unknown scope %s requested' % scope)

        for response_type in request_data['response_type']:
            if response_type not in ['code', 'id_token', 'token']:
                return self._respond_error(
                    request_data, 'unsupported_response_type',
                    'response_type %s is unknown' % response_type)

        if request_data['redirect_uri'] not in client['redirect_uris']:
            raise InvalidRequest('Invalid redirect_uri')

        # Build the "claims" values from scopes
        try:
            request_data['claims'] = json.loads(request_data['claims'])
        except Exception as ex:  # pylint: disable=broad-except
            return self._respond_error(request_data, 'invalid_request',
                                       'claims malformed: %s' % ex)
        if 'userinfo' not in request_data['claims']:
            request_data['claims']['userinfo'] = {}
        if 'id_token' not in request_data['claims']:
            request_data['claims']['id_token'] = {}

        scopes_to_claim = {
            'profile': [
                'name', 'family_name', 'given_name', 'middle_name', 'nickname',
                'preferred_username', 'profile', 'picture', 'website',
                'gender', 'birthdate', 'zoneinfo', 'locale', 'updated_at'
            ],
            'email': ['email', 'email_verified'],
            'address': ['address'],
            'phone': ['phone_number', 'phone_number_verified']
        }
        for scope in scopes_to_claim:
            if scope in request_data['scope']:
                for claim in scopes_to_claim[scope]:
                    if claim not in request_data['claims']:
                        # pylint: disable=invalid-sequence-index
                        request_data['claims']['userinfo'][claim] = None

        # Add claims from extensions
        for n, e in self.cfg.extensions.available().items():
            data = e.get_claims(request_data['scope'])
            self.debug('%s returned %s' % (n, repr(data)))
            for claim in data:
                # pylint: disable=invalid-sequence-index
                request_data['claims']['userinfo'][claim] = None

        # Store data so we can continue with the request
        us = UserSession()
        user = us.get_user()

        returl = '%s/%s/Continue?%s' % (self.basepath, URLROOT,
                                        self.trans.get_GET_arg())
        data = {
            'login_target': client.get('client_name', None),
            'login_return': returl,
            'openidc_stage': 'continue',
            'openidc_request': json.dumps(request_data)
        }

        if request_data['login_hint']:
            data['login_username'] = request_data['login_hint']

        if not data['login_target']:
            data['login_target'] = get_url_hostpart(
                request_data['redirect_uri'])

        # Decide what to do with the request
        if request_data['max_age'] is None:
            request_data['max_age'] = client.get('default_max_age', None)

        needs_auth = True
        if not user.is_anonymous:
            if request_data['max_age'] in [None, 0]:
                needs_auth = False
            else:
                auth_time = us.get_user_attrs()['_auth_time']
                needs_auth = ((int(auth_time) + int(request_data['max_age']))
                              <= int(time.time()))

        if needs_auth or 'login' in request_data['prompt']:
            if 'none' in request_data['prompt']:
                # We were asked not to provide a UI. Answer with false.
                return self._respond_error(request_data, 'login_required',
                                           'user interface required')

            # Either the user wasn't logged in, or we were explicitly
            # asked to re-auth them. Let's do so!
            us.logout(user)

            # Let the user go to auth
            self.trans.store(data)
            redirect = '%s/login?%s' % (self.basepath,
                                        self.trans.get_GET_arg())
            self.debug('Redirecting: %s' % redirect)
            raise cherrypy.HTTPRedirect(redirect)

        # Return error if authz check fails
        authz_check_res = self._authz_stack_check(request_data,
                                                  client, user.name,
                                                  us.get_user_attrs())
        if authz_check_res:
            return authz_check_res

        self.trans.store(data)
        # The user was already signed on, and no request to re-assert its
        # identity. Let's forward directly to /Continue/
        self.debug('Redirecting: %s' % returl)
        raise cherrypy.HTTPRedirect(returl)
Exemple #24
0
    def _openid_checks(self, request, form, **kwargs):
        us = UserSession()
        user = us.get_user()
        immediate = False

        self.debug('Mode: %s Stage: %s User: %s' %
                   (kwargs['openid.mode'], self.stage, user.name))
        if kwargs.get('openid.mode', None) == 'checkid_setup':
            if user.is_anonymous:
                if self.stage == 'init':
                    returl = '%s/openid/Continue?%s' % (
                        self.basepath, self.trans.get_GET_arg())
                    data = {
                        'openid_stage': 'auth',
                        'openid_request': json.dumps(kwargs),
                        'login_return': returl,
                        'login_target': request.trust_root
                    }
                    self.trans.store(data)
                    redirect = '%s/login?%s' % (self.basepath,
                                                self.trans.get_GET_arg())
                    self.debug('Redirecting: %s' % redirect)
                    raise cherrypy.HTTPRedirect(redirect)
                else:
                    raise UnauthorizedRequest("unknown user")

        elif kwargs.get('openid.mode', None) == 'checkid_immediate':
            # This is immediate, so we need to assert or fail
            if user.is_anonymous:
                return self._respond(request.answer(False))

            immediate = True

        else:
            return self._respond(self.cfg.server.handleRequest(request))

        # check if this is discovery or needs identity matching checks
        if not request.idSelect():
            idurl = self.cfg.identity_url_template % {'username': user.name}
            if request.identity != idurl:
                raise UnauthorizedRequest("User ID mismatch!")

        # check if the relying party is trusted
        if request.trust_root in self.cfg.untrusted_roots:
            raise UnauthorizedRequest("Untrusted Relying party")

        # Perform authorization check.
        provinfo = {'url': kwargs.get('openid.realm', None)}
        if not self._site['authz'].authorize_user(
                'openid', provinfo, user.name, us.get_user_attrs()):
            self.error('Authorization denied by authorization provider')
            raise UnauthorizedRequest('Authorization denied')

        # if the party is explicitly whitelisted just respond
        if request.trust_root in self.cfg.trusted_roots:
            return self._respond(self._response(request, us))

        # Add extension data to this dictionary
        ad = {
            "Trust Root": request.trust_root,
        }
        userattrs = self._source_attributes(us)
        for n, e in self.cfg.extensions.available().items():
            data = e.get_display_data(request, userattrs)
            self.debug('%s returned %s' % (n, repr(data)))
            for key, value in data.items():
                ad[self.cfg.mapping.display_name(key)] = value

        # We base64 encode the trust_root when looking up consent data to
        # ensure the client ID is safe for the cherrypy url routing
        consentdata = user.get_consent('openid', b64encode(request.trust_root))
        if consentdata is not None:
            # Consent has already been granted
            self.debug('Consent already granted')

            attrlist = set(ad.keys())
            consattrs = set(consentdata['attributes'])

            if attrlist.issubset(consattrs):
                return self._respond(self._response(request, us))

        if immediate:
            raise UnauthorizedRequest("No consent for immediate")

        if self.stage == 'consent':
            if form is None:
                raise UnauthorizedRequest("Unintelligible consent")
            allow = form.get('decided_allow', False)
            if not allow:
                raise UnauthorizedRequest("User declined")

            # Store new consent
            consentdata = {'attributes': ad.keys()}
            user.grant_consent('openid', b64encode(request.trust_root),
                               consentdata)

            # all done we consent!
            return self._respond(self._response(request, us))

        else:
            data = {
                'openid_stage': 'consent',
                'openid_request': json.dumps(kwargs)
            }
            self.trans.store(data)

            context = {
                "title": 'Consent',
                "action": '%s/openid/Consent' % (self.basepath),
                "trustroot": request.trust_root,
                "username": user.name,
                "authz_details": ad,
            }
            context.update(dict((self.trans.get_POST_tuple(), )))
            return self._template('openid/consent_form.html', **context)
Exemple #25
0
    def logout(self, message, relaystate=None, samlresponse=None):
        """
        Handle HTTP logout. The supported logout methods are stored
        in each session. First all the SOAP sessions are logged out
        then the HTTP Redirect method is used for any remaining
        sessions.

        The basic process is this:
         1. A logout request is received. It is processed and the response
            cached.
         2. If any other SP's have also logged in as this user then the
            first such session is popped off and a logout request is
            generated and forwarded to the SP.
         3. If a logout response is received then the user is marked
            as logged out from that SP.
         Repeat steps 2-3 until only the initial logout request is
         left unhandled, at which time the pre-generated response is sent
         back to the SP that originated the logout request.

        The final logout response is always a redirect.
        """
        logout = self.cfg.idp.get_logout_handler()

        us = UserSession()

        saml_sessions = self.cfg.idp.sessionfactory

        logout_type = None
        try:
            if lasso.SAML2_FIELD_REQUEST in message:
                logout_type = "request"
                self._handle_logout_request(us, logout, saml_sessions, message)
            elif samlresponse:
                logout_type = "response"
                self._handle_logout_response(us, logout, saml_sessions,
                                             message, samlresponse)
            else:
                raise cherrypy.HTTPError(
                    400, 'Bad Request. Not a ' + 'logout request or response.')
        except InvalidRequest as e:
            raise cherrypy.HTTPError(400, 'Bad Request. %s' % e)
        except UnknownProvider as e:
            raise cherrypy.HTTPError(
                400, 'Invalid logout %s: %s' % (logout_type, e))

        # Fall through to handle any remaining sessions.

        # Find the next SP to logout and send a LogoutRequest
        logout_order = [
            lasso.SAML2_METADATA_BINDING_SOAP,
            lasso.SAML2_METADATA_BINDING_REDIRECT,
        ]
        (logout_mech,
         session) = saml_sessions.get_next_logout(logout_mechs=logout_order,
                                                  user=us.user)
        while session:
            self.debug('Going to log out %s' % session.provider_id)

            try:
                logout.setSessionFromDump(session.login_session)
            except lasso.ProfileBadSessionDumpError as e:
                self.error('Failed to load session: %s' % e)
                raise cherrypy.HTTPRedirect(400,
                                            'Failed to log out user: %s ' % e)
            try:
                if logout_mech == lasso.SAML2_METADATA_BINDING_REDIRECT:
                    logout.initRequest(session.provider_id,
                                       lasso.HTTP_METHOD_REDIRECT)
                else:
                    logout.initRequest(session.provider_id,
                                       lasso.HTTP_METHOD_SOAP)
            except lasso.ServerProviderNotFoundError:
                self.error(
                    'Service Provider %s not found. Trying next session' %
                    session.provider_id)
                saml_sessions.remove_session(session)
                (logout_mech, session) = saml_sessions.get_next_logout(
                    logout_mechs=logout_order, user=us.user)
                continue

            try:
                logout.buildRequestMsg()
            except lasso.Error as e:
                self.error('failure to build logout request msg: %s' % e)
                raise cherrypy.HTTPRedirect(400,
                                            'Failed to log out user: %s ' % e)

            # Set the full list of session indexes for this provider to
            # log out
            self.debug('logging out provider id %s' % session.provider_id)
            indexes = saml_sessions.get_session_id_by_provider_id(
                session.provider_id, us.user)
            self.debug('Requesting logout for sessions %s' % (indexes, ))
            req = logout.get_request()
            req.setSessionIndexes(indexes)

            session.set_logoutstate(relaystate=logout.msgUrl,
                                    request_id=logout.request.id)
            saml_sessions.start_logout(session, initial=False)

            self.debug('Request logout ID %s for session ID %s' %
                       (logout.request.id, session.session_id))

            if logout_mech == lasso.SAML2_METADATA_BINDING_REDIRECT:
                self.debug('Redirecting to another SP to logout on %s at %s' %
                           (logout.remoteProviderId, logout.msgUrl))
                raise cherrypy.HTTPRedirect(logout.msgUrl)
            else:
                self.debug('SOAP request to another SP to logout on %s at %s' %
                           (logout.remoteProviderId, logout.msgUrl))
                if logout.msgBody:
                    message = self._soap_logout(logout)
                    try:
                        self._handle_logout_response(us, logout, saml_sessions,
                                                     message, samlresponse)
                    except Exception as e:  # pylint: disable=broad-except
                        self.error('SOAP SLO failed %s' % e)
                else:
                    self.error('Provider does not support SOAP')

            (logout_mech, session) = saml_sessions.get_next_logout(
                logout_mechs=logout_order, user=us.user)

        # done while

        # All sessions should be logged out now. Respond to the
        # original request using the response we cached earlier.

        try:
            session = saml_sessions.get_initial_logout(us.user)
        except ValueError:
            self.debug('SLO get_last_session() unable to find last session')
            raise cherrypy.HTTPError(400, 'Unable to determine logout state')

        redirect = session.relaystate
        if not redirect:
            redirect = self.basepath

        saml_sessions.remove_session(session)

        # Log out of cherrypy session
        user = us.get_user()
        self._audit('Logged out user: %s [%s] from %s' %
                    (user.name, user.fullname, session.provider_id))
        us.logout(user)

        self.debug('SLO redirect to %s' % redirect)

        raise cherrypy.HTTPRedirect(redirect)