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)
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)
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)
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)