Ejemplo n.º 1
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)
Ejemplo n.º 2
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)
Ejemplo n.º 3
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)
Ejemplo n.º 4
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)