def test_change_password(self, remember): from webidentity.models import PasswordReset from webidentity.models import User remember.return_value = [('X-Login', 'john.doe')] user = User(u'john.doe', u'secret', u'*****@*****.**') reset = PasswordReset(1, datetime.now() + timedelta(days=7), u'uniquetoken') session = DBSession() session.add(reset) session.add(user) session.flush() self.assertEquals(1, user.id) self.assertEquals(1, session.query(PasswordReset)\ .filter(PasswordReset.user_id == 1)\ .filter(PasswordReset.token == u'uniquetoken')\ .filter(PasswordReset.expires >= datetime.now())\ .count()) view = self._makeView(post={ 'token': 'uniquetoken', 'password': '******', 'confirm_password': '******'}) response = view.change_password() self.assertEquals(dict(response.headers), { 'Content-Length': '0', 'X-Login': '******', 'Content-Type': 'text/html; charset=UTF-8', 'Location': 'http://example.com'}) # Check that the password was changed self.failUnless(session.query(User).get(1).check_password('abc123')) # Check that the reset request was deleted self.assertEquals(0, session.query(PasswordReset).count())
def test_login__form_submission__success_with_identity_wo_scheme(self, remember): from webidentity.views.login import login from webidentity.models import User session = DBSession() session.add(User(u'john.doe', u'secret', u'*****@*****.**')) self.assertEquals( session.query(User).filter_by(username=u'john.doe').first().email, u'*****@*****.**') remember.return_value = [('X-Login', 'john.doe')] request = testing.DummyRequest(environ={ 'wsgi.url_scheme': 'http', }) token = request.session.new_csrf_token() request.POST = { 'form.submitted': u'1', 'login': u'example.com/id/john.doe', 'password': u'secret', 'csrf_token': token, } response = login(request) self.assertEquals(dict(response.headers), { 'Content-Length': '0', 'Content-Type': 'text/html; charset=UTF-8', 'Location': 'http://example.com', 'X-Login': u'john.doe'}) self.assertEquals(request.session.pop_flash(), [u'You have successfully logged in.'])
def test_change_password__success(self): from webidentity.views.user import change_password self.config.testing_securitypolicy(userid=u"dokai") self.config.add_route("change_password", "/change-password") user = User("dokai", "secret", "*****@*****.**") session = DBSession() session.add(user) request = testing.DummyRequest( post={ "form.submitted": "1", "current_password": "******", "password": "******", "confirm_password": "******", } ) token = request.session.new_csrf_token() request.POST["csrf_token"] = token self.assertEquals( change_password(request), {"action_url": "http://example.com/change-password", "csrf_token": token} ) # Check that the password was changed correctly. self.failUnless(user.check_password("new_password"))
def test_login__form_submission__invalid_password(self): from webidentity.views.login import login from webidentity.models import User session = DBSession() session.add(User(u'john.doe', u'secret', u'*****@*****.**')) self.assertEquals( session.query(User).filter_by(username=u'john.doe').first().email, u'*****@*****.**') request = testing.DummyRequest(environ={ 'wsgi.url_scheme': 'http', }) token = request.session.new_csrf_token() request.POST = { 'form.submitted': u'1', 'login': u'john.doe', 'password': u'thisiswrong', 'csrf_token': token, } options = login(request) self.assertEquals(options, { 'title': u'Login', 'reset_url': 'http://example.com/reset-password', 'action_url': 'http://example.com/login', 'login': u'john.doe', 'csrf_token': token})
def test_home_page__authenticated(self): from webidentity.views.user import home_page self.config.testing_securitypolicy(userid=u"dokai") session = DBSession() session.add(User("dokai", "secret", "*****@*****.**")) request = testing.DummyRequest() self.assertEquals(home_page(request), {})
def test_visited_sites__empty(self): from webidentity.views.user import visited_sites self.config.testing_securitypolicy(userid=u"dokai") session = DBSession() session.add(User("dokai", "secret", "*****@*****.**")) request = testing.DummyRequest() self.assertEquals(visited_sites(request), {"action_url": "form_action", "sites": []})
def test_authenticated_user__authenticated(self): from webidentity.views.user import authenticated_user self.config.testing_securitypolicy(userid=u"dokai") session = DBSession() user = User("dokai", "secret", "*****@*****.**") session.add(user) request = testing.DummyRequest() self.assertEquals(user, authenticated_user(request))
def test_password_change_form__expired_token(self): from webidentity.models import PasswordReset view = self._makeView() reset = PasswordReset(1, datetime.now() - timedelta(days=7)) session = DBSession() session.add(reset) self.assertEquals(1, session.query(PasswordReset).filter_by(token=reset.token).count()) view.request.matchdict['token'] = reset.token self.assertRaises(NotFound, view.password_change_form)
def test_prune_expired(self): from webidentity.models import PasswordReset view = self._makeView() session = DBSession() session.add(PasswordReset(1, datetime.now() - timedelta(days=30))) session.add(PasswordReset(2, datetime.now() + timedelta(days=30))) self.assertEquals(2, session.query(PasswordReset).count()) view.prune_expired() self.assertEquals(1, session.query(PasswordReset).count()) self.assertEquals(2, session.query(PasswordReset).first().user_id)
def populate_accounts(): """Populates the database with production data.""" engine = engine_from_config(get_config(), 'sqlalchemy.') initialize_sql(engine, False) session = DBSession() if len(sys.argv) < 3: print "Usage: {0} <config> <data>".format(sys.argv[0]) sys.exit(1) for line in open(sys.argv[2]).readlines(): username, password = line.strip().split('|', 1) session.add(User(username.strip(), password.strip(), '*****@*****.**')) print username session.flush() transaction.commit()
def test_change_password__invalid_user(self): from webidentity.models import PasswordReset reset = PasswordReset(1, datetime.now() + timedelta(days=7), u'uniquetoken') session = DBSession() session.add(reset) self.assertEquals(1, session.query(PasswordReset)\ .filter(PasswordReset.token == u'uniquetoken')\ .filter(PasswordReset.expires >= datetime.now())\ .count()) view = self._makeView(post={ 'token': 'uniquetoken', 'password': '******', 'confirm_password': '******'}) self.assertRaises(NotFound, view.change_password)
def populate_demo(): """Populates the database with 50 demo users.""" engine = engine_from_config(get_config(), 'sqlalchemy.') initialize_sql(engine, False) session = DBSession() for count in range(1, 51): session.add(User('test.user{0:02}'.format(count), 'testi', '*****@*****.**')) session.add(User('test.pref01', 'testi', u'*****@*****.**')) session.add(User('test.pref02', 'testi', None)) session.add(User('test.pref03', 'testi', u'*****@*****.**')) session.add(User('test.pref04', 'testi', u'*****@*****.**')) session.add(User('test.pref05', 'testi', None)) session.flush() transaction.commit() print("Generated 50 demo users.")
def test_log_activity__with_url(self): from webidentity.views.user import log_activity self.config.testing_securitypolicy(userid=u"dokai") session = DBSession() self.assertEquals(0, session.query(Activity).count()) user = User("dokai", "secret", "*****@*****.**") session.add(user) request = testing.DummyRequest(environ={"REMOTE_ADDR": "1.2.3.4"}, cookies={"auth_tkt": "sessiontoken"}) log_activity(request, Activity.LOGIN, "http://www.rp.com") self.assertEquals(1, session.query(Activity).count()) log_entry = session.query(Activity).first() self.assertEquals(user.id, log_entry.user_id) self.assertEquals("1.2.3.4", log_entry.ipaddr) self.assertEquals("sessiontoken", log_entry.session) self.assertEquals(Activity.LOGIN, log_entry.action) self.assertEquals("http://www.rp.com", log_entry.url)
def test_send_confirmation_message(self, send_mail): from webidentity.models import PasswordReset from webidentity.models import User from email.message import Message self.config.add_settings(DUMMY_SETTINGS) session = DBSession() user = User(u'john.doe', u'secret', u'*****@*****.**') session.add(user) self.assertEquals(u'john.doe', session.query(User).get(1).username) view = self._makeView(post={'username': u'john.doe'}) response = view.send_confirmation_message() self.assertEquals(1, session.query(PasswordReset).filter_by(user_id=1).count()) self.assertEquals(response.location, 'http://example.com') self.assertEquals(view.request.session.pop_flash(), [u'Password retrieval instructions have been emailed to you.']) self.assertEquals(send_mail.call_args[0][0], u'*****@*****.**') self.assertEquals(send_mail.call_args[0][1], [u'*****@*****.**']) self.failUnless(isinstance(send_mail.call_args[0][2], Message))
def test_password_change_form(self): from webidentity.models import PasswordReset from webidentity.models import User view = self._makeView() session = DBSession() user = User(u'john.doe', u'secret', u'*****@*****.**') session.add(user) reset = PasswordReset(1, datetime.now() + timedelta(days=7), u'uniquetoken') session.add(reset) self.assertEquals(1, session.query(PasswordReset).filter_by(token=reset.token).count()) self.assertEquals(u'john.doe', session.query(User).get(user.id).username) view.request.matchdict['token'] = reset.token self.assertEquals(view.password_change_form(), { 'action_url': 'http://example.com/reset-password/process', 'token': u'uniquetoken', 'title': u'Change password'})
def DISABLED__test_cascading_delete(self): from webidentity.models import Persona from webidentity.models import User from webidentity.models import UserAttribute from webidentity.models import VisitedSite user = User(u'john.doe', u'secret', u'*****@*****.**') user.personas.append( Persona(u'Test persönä', attributes=[ UserAttribute(type_uri, value) for type_uri, value in DUMMY_USER_ATTRIBUTES.iteritems()])) user.personas.append( Persona(u'Reversed persönä', attributes=[ UserAttribute(type_uri, ''.join(reversed(value))) for type_uri, value in DUMMY_USER_ATTRIBUTES.iteritems()])) site1 = VisitedSite('http://www.rp.com', remember=False) site2 = VisitedSite('http://www.plone.org', remember=True) site2.persona = user.personas[0] user.visited_sites.append(site1) user.visited_sites.append(site2) session = DBSession() session.add(user) session.flush() self.assertEquals(2, len(session.query(User).get(1).personas)) self.assertEquals(2, len(session.query(User).get(1).visited_sites)) self.assertEquals(6, len(session.query(User).get(1).personas[0].attributes)) session.query(User).filter_by(username=u'john.doe').delete() session.flush() self.assertEquals(0, session.query(User).count()) self.assertEquals(0, session.query(Persona).count()) self.assertEquals(0, session.query(UserAttribute).count()) self.assertEquals(0, session.query(VisitedSite).count())
def test_change_password__csrf_mismatch(self): from pyramid.exceptions import Forbidden from webidentity.views.user import change_password self.config.testing_securitypolicy(userid=u"dokai") self.config.add_route("change_password", "/change-password") user = User("dokai", "secret", "*****@*****.**") session = DBSession() session.add(user) request = testing.DummyRequest( post={ "form.submitted": "1", "current_password": "******", "password": "******", "confirm_password": "******", } ) token = request.session.new_csrf_token() request.POST["csrf_token"] = "invalid" self.failIf(token == "invalid") self.assertRaises(Forbidden, lambda: change_password(request))
class PasswordResetView(object): """Password reset logic.""" def __init__(self, request): self.request = request self.session = DBSession() self.prune_expired() def prune_expired(self): """Prunes password reset requests that have expired.""" self.session.query(PasswordReset)\ .filter(PasswordReset.expires < datetime.now())\ .delete() def render_form(self): """Renders the password reset form.""" return { 'action_url': route_url('reset_password_initiate', self.request), 'title': _(u'Reset password'), } def password_change_form(self): """Renders the form for changing a password for a valid token.""" reset = self.session.query(PasswordReset)\ .filter(PasswordReset.token == self.request.matchdict['token'])\ .filter(PasswordReset.expires >= datetime.now())\ .first() if reset is None: # No matching password reset found raise NotFound() user = self.session.query(User).get(reset.user_id) if user is None: raise NotFound() return { 'action_url': route_url('reset_password_process', self.request), 'title': _(u'Change password'), 'token': reset.token, } def send_confirmation_message(self): """Sends an email confirmation message to the user.""" username = self.request.POST.get('username', '').strip() redirect_url = route_url('reset_password', self.request) if not username: self.request.session.flash(_(u'Please supply a username.')) else: user = self.session.query(User).filter(User.username == username).first() if user is None: self.request.session.flash(_(u'The given username does not match any account.')) else: # Create a password reset request that is valid for 24 hours reset = PasswordReset(user.id, datetime.now() + timedelta(hours=24)) self.session.add(reset) message = self.create_message(user, reset) from_address = self.request.registry.settings['webidentity_from_address'].strip() send_mail(from_address, [user.email], message) self.request.session.flash(_(u'Password retrieval instructions have been emailed to you.')) redirect_url = self.request.application_url return HTTPFound(location=redirect_url) def create_message(self, user, reset): """Returns an email.message.Message object representing the password reset message. """ from_address = self.request.registry.settings['webidentity_from_address'].strip() date_format = self.request.registry.settings['webidentity_date_format'].strip() locale = get_localizer(self.request) subject = locale.translate( _(u'Password reset for ${identity}', mapping={'identity': identity_url(self.request, user.username)})) message = Message() message['From'] = Header(from_address, 'utf-8') message['To'] = Header(u'{0} <{1}>'.format(user.username, user.email), 'utf-8') message['Subject'] = Header(subject, 'utf-8') message.set_payload(locale.translate(_( u'password-reset-email', default=textwrap.dedent(u''' Hi ${username} A password retrieval process has been initiated for your OpenID identity ${identity} If the process was initiated by you you can continue the process of resetting your password by opening the following link in your browser ${reset_url} The link will will expire at ${expiration}. If you did not initiate this request you can just ignore this email. Your password has not been changed. ''').lstrip(), mapping=dict( username=user.username, identity=identity_url(self.request, user.username), expiration=reset.expires.strftime(date_format), reset_url=route_url('reset_password_token', self.request, token=reset.token))))) return message def change_password(self): """Changes the password for a user.""" token = self.request.POST.get('token', '').strip() if token: password = self.request.POST.get('password', '') confirm_password = self.request.POST.get('confirm_password', '') if len(password.strip()) < 5: self.request.session.flash(_(u'Password must be at least five characters long')) return HTTPFound( location=route_url('reset_password_token', self.request, token=token)) elif password != confirm_password: self.request.session.flash(_(u'Given passwords do not match')) return HTTPFound( location=route_url('reset_password_token', self.request, token=token)) else: reset = self.session.query(PasswordReset)\ .filter(PasswordReset.token == token)\ .filter(PasswordReset.expires >= datetime.now())\ .first() if reset is None: raise NotFound() user = self.session.query(User).get(reset.user_id) if user is None: raise NotFound() # Update the user password user.password = password self.session.add(user) self.session.delete(reset) self.request.session.flash(_(u'Password changed.')) headers = remember(self.request, user.username) return HTTPFound(location=self.request.application_url, headers=headers) raise NotFound()
def auth_decision(request, openid_request): """Renders the appropriate page that allows the user to make a decision about the OpenID authentication request. The possible scenarios are: 1. The user is logged in to the OP and RP wants OP to select the id 2. The user is logged in to the OP and her identity matches the claimed id. 3. The user is logged in to the OP and her identity does not match the claimed id. 4. The user is not logged in and RP wants OP to select the id. 5. The user is not logged in and RP sent a claimed id. In cases 1) and 2) the user can simply make the decision on allowing the authentication to proceed (and AX related decisions.) In cases 3), 4) and 5) the user needs to log in to the OP first. """ user = authenticated_user(request) expected_user = extract_local_id(request, openid_request.identity) session = DBSession() log = logging.getLogger('webidentity') # Persist the OpenID request so we can continue processing it after the # user has authenticated overriding any previous request. cidrequest = session.query(CheckIdRequest).get(request.environ['repoze.browserid']) if cidrequest is None: cidrequest = CheckIdRequest(request.environ['repoze.browserid'], openid_request) else: # Unconditionally overwrite any existing checkid request. cidrequest.request = openid_request cidrequest.issued = int(time.time()) session.add(cidrequest) # Clean up dangling requests for roughly 10% of the time. # We do not do this on every call to reduce the possibility of a database # deadlock as rows need to be locked. if random.randint(0, 100) < 10: threshold = int(time.time()) - 3600 session.query(CheckIdRequest).filter(CheckIdRequest.issued < threshold).delete() log.info('Cleaned up dangling OpenID authentication requests.') options = { 'title': _(u'Approve OpenID request?'), 'action_url': route_url('openid_confirm', request), 'trust_root': openid_request.trust_root, #'reset_url': route_url('reset_password', request), } if user is not None: ax_request = ax.FetchRequest.fromOpenIDRequest(openid_request) sreg_request = sreg.SRegRequest.fromOpenIDRequest(openid_request) if ax_request is not None or sreg_request is not None: # Prepare the user attributes for an SReg/AX request. # Mapping of attributes the RP has requested from the user. requested_attributes = {} if sreg_request.wereFieldsRequested(): requested_attributes.update(dict( (SREG_TO_AX.get(field), field in sreg_request.required) for field in sreg_request.allRequestedFields())) # The AX request takes precedence over SReg if both are requested. if ax_request is not None: requested_attributes.update(dict( (type_uri, attrinfo.required) for type_uri, attrinfo in ax_request.requested_attributes.iteritems())) # Set of attributes that the RP has marked mandatory required_attributes = set( type_uri for type_uri, required in requested_attributes.iteritems() if required) for persona in user.personas: available_attributes = set(a.type_uri for a in persona.attributes) options.setdefault('personas', []).append({ 'id': persona.id, 'name': persona.name, 'attributes': [ dict(type_uri=a.type_uri, value=a.value) for a in persona.attributes if a.type_uri in requested_attributes], 'missing': required_attributes - available_attributes, }) if openid_request.idSelect(): # Case 1. # The Relying Party has requested us to select an identifier. options.update({ 'identity': identity_url(request, user.username), 'csrf_token': request.session.get_csrf_token(), }) request.add_response_callback(disable_caching) return render_to_response('templates/openid_confirm.pt', options, request) elif user.username == expected_user: # Case 2. # A logged-in user that matches the user in the claimed identifier. options.update({ 'identity': openid_request.identity, 'csrf_token': request.session.get_csrf_token(), }) request.add_response_callback(disable_caching) return render_to_response('templates/openid_confirm.pt', options, request) else: # Case 3 # TODO: Implement case 3 raise NotImplementedError else: # Cases 4 and 5. options.update({ 'action_url': route_url('openid_confirm_login', request), 'login_name': expected_user if not openid_request.idSelect() else None, 'csrf_token': request.session.get_csrf_token(), }) return render_to_response('templates/openid_confirm_login.pt', options, request)
def confirm(request): """Confirmation for an OpenID authentication request.""" user = authenticated_user(request) if user is not None and 'form.submitted' in request.params: if request.POST.get('csrf_token') != request.session.get_csrf_token(): raise Forbidden session = DBSession() result = session.query(CheckIdRequest)\ .filter_by(key=request.environ.get('repoze.browserid', ''))\ .first() if result is not None: openid_request = result.request session.delete(result) else: # TODO: Return an error message return HTTPBadRequest('Invalid confirmation request') if 'accept' in request.params: ax_attributes = get_ax_attributes(request) visit = session.query(VisitedSite)\ .join(User)\ .filter(User.id == user.id)\ .filter(VisitedSite.trust_root == openid_request.trust_root)\ .first() if visit is None: # This is the first time the user is visiting this RP visit = VisitedSite(openid_request.trust_root) user.visited_sites.append(visit) visit.remember = 'remember' in request.params try: persona_id = int(request.params.get('persona')) # Make sure that the referenced persona actually belongs to the user persona = session.query(Persona)\ .join(User)\ .filter(User.id == user.id)\ .filter(Persona.id == persona_id)\ .first() if persona is not None: visit.persona = persona except (TypeError, ValueError): pass session.add(visit) identity = identity_url(request, user.username) openid_response = openid_request.answer(True, identity=identity) add_ax_response(openid_request, openid_response, ax_attributes) add_sreg_response(openid_request, openid_response, ax_attributes) if visit.remember: log_activity(request, Activity.AUTHORIZE, openid_request.trust_root) else: log_activity(request, Activity.AUTHORIZE_ONCE, openid_request.trust_root) else: log_activity(request, Activity.DENY, openid_request.trust_root) openid_response = openid_request.answer(False) store = AlchemyStore(session) openid_server = server.Server(store, route_url('openid_endpoint', request)) response = webob_response(openid_server.encodeResponse(openid_response), request) response.headerlist.extend(forget(request)) return response return HTTPBadRequest('Invalid confirmation request')
def test_visited_sites__not_empty(self): from webidentity.views.user import visited_sites self.config.testing_securitypolicy(userid=u"dokai") session = DBSession() user = User("dokai", "secret", "*****@*****.**") user.personas.append( Persona( u"Test persönä", attributes=[UserAttribute(type_uri, value) for type_uri, value in DUMMY_USER_ATTRIBUTES.iteritems()], ) ) site1 = VisitedSite("http://www.rp.com", remember=False) site2 = VisitedSite("http://www.plone.org", remember=True) site2.persona = user.personas[0] user.visited_sites.append(site1) user.visited_sites.append(site2) user.activity.append( Activity( ipaddr="1.2.3.4", session="session1", action=Activity.AUTHORIZE_ONCE, url="http://www.rp.com", timestamp=datetime(2010, 1, 15, 12, 0), ) ) user.activity.append( Activity( ipaddr="2.3.4.5", session="session2", action=Activity.AUTHORIZE_ONCE, url="http://www.plone.org", timestamp=datetime(2010, 3, 24, 15, 23), ) ) user.activity.append( Activity( ipaddr="2.3.4.5", session="session3", action=Activity.AUTHORIZE, url="http://www.plone.org", timestamp=datetime(2010, 11, 15, 17, 9), ) ) session.add(user) request = testing.DummyRequest() self.assertEquals( visited_sites(request), { "action_url": "form_action", "sites": [ { "url": u"http://www.plone.org", "timestamp": "15.11.2010 17:09", "persona": {"id": 1, "edit_url": "http://fo.bar/", "name": u"Test persönä"}, "id": 2, "remember": "checked", }, { "url": u"http://www.rp.com", "timestamp": "15.01.2010 12:00", "persona": None, "id": 1, "remember": None, }, ], }, )