def test_extract_username(self): self.assertEquals(extract_username('username'), 'username') self.assertEquals(extract_username('*****@*****.**'), 'u2wqblarhim5su7pxemcbwdyryrghmuk') # test for non A-Za-z0-9._- with no @ self.assertRaises(ValueError, extract_username, 'user\r\nname') self.assertRaises(ValueError, extract_username, '%3Cscript%3E') self.assertRaises(ValueError, extract_username, '') # test unicode/punycode (straight UTF8 and urlencoded) self.assertEquals(extract_username('*****@*****.**'), 'ym3nccfhvptfrhn7nkhhyvzgf2yl7r5y') # proper char self.assertRaises(UnicodeError, extract_username, '*****@*****.**') # bad utf-8 char self.assertRaises(UnicodeError, extract_username, '*****@*****.**') # invalid BIDI
def setUp(self): # loading the app self.appdir, self.config, self.auth = initenv() self.app = TestApp(make_app(self.config)) # adding a user if needed self.email = '*****@*****.**' % random.randint(1, 1000) self.user_name = extract_username(self.email) self.user = User(self.user_name) self.user_id = self.auth.get_user_id(self.user) self.password = '******' * 9 if self.user_id is None: self.auth.create_user(self.user_name, self.password, self.email) self.user_id = self.auth.get_user_id(self.user) # for the ldap backend, filling available_nodes if self.auth.__class__.__name__ == 'LDAPAuth': query = ('insert into available_nodes (node, ct, actives) values ' ' ("weave:localhost", 10, 10)') self.auth._engine.execute(query) try: self.nodes = load_and_configure(self.config, 'nodes') except KeyError: logger.debug(traceback.format_exc()) logger.debug("No node library in place") self.nodes = None try: self.reset = load_and_configure(self.config, 'reset_codes') except Exception: logger.debug(traceback.format_exc()) logger.debug("No reset code library in place") self.reset = None
def test_reset_email(self): app = get_app(self.app) # let's ask via the web form now # the Python web form does not support captcha old = app.config['captcha.use'] app.config['captcha.use'] = False try: # let's try the reset process with an email user_name = extract_username('*****@*****.**') self.auth.create_user(user_name, self.password, '*****@*****.**') res = self.app.get('/weave-password-reset') res.form['username'].value = '*****@*****.**' res = res.form.submit() self.assertTrue('next 6 hours' in res) self.assertEquals(len(FakeSMTP.msgs), 1) # let's visit the link in the email msg = message_from_string(FakeSMTP.msgs[0][2]).get_payload() msg = base64.decodestring(msg) link = msg.split('\n')[2].strip() # let's call the real link, it's a form we can fill res = self.app.get(link) res.form['password'].value = 'mynewpassword' res.form['confirm'].value = 'mynewpassword' res = res.form.submit() self.assertTrue('Password successfully changed' in res) finally: app.config['captcha.use'] = old
def test_delete_user(self): # creating another user email = '*****@*****.**' user = extract_username(email) root = '/user/1.0/%s' % user res = self.app.get(root) if not json.loads(res.body): payload = {'email': '*****@*****.**', 'password': '******' * 9, 'captcha-challenge': 'xxx', 'captcha-response': 'xxx'} payload = json.dumps(payload) self.app.put(root, params=payload) # trying to suppress the old user with the new user # this should generate a 401 environ = {'HTTP_AUTHORIZATION': 'Basic %s' % \ base64.encodestring('%s:xxxxxxxxx' % user)} self.app.extra_environ = environ self.app.delete(self.root, status=401) # now using the right credentials token = base64.encodestring('%s:%s' % (self.user_name, self.password)) environ = {'HTTP_AUTHORIZATION': 'Basic %s' % token} self.app.extra_environ = environ res = self.app.delete(self.root) self.assertTrue(json.loads(res.body)) # tarek should be gone res = self.app.get(self.root) self.assertFalse(json.loads(res.body))
def test_create_user(self): # creating a user email = '*****@*****.**' % (time.time(), random.randint(1, 100)) name = extract_username(email) user_url = '/user/1.0/%s' % name try: # the user already exists payload = {'email': email, 'password': '******' * 9} payload = json.dumps(payload) res = self.app.put(self.root, params=payload, status=400) self.assertEquals(res.json, ERROR_INVALID_WRITE) # missing the password payload = {'email': email} payload = json.dumps(payload) res = self.app.put(user_url, params=payload, status=400) self.assertEquals(res.json, ERROR_MISSING_PASSWORD) # malformed e-mail payload = {'email': 'bademailhere', 'password': '******' * 9} payload = json.dumps(payload) res = self.app.put(user_url, params=payload, status=400) self.assertEquals(res.json, ERROR_NO_EMAIL_ADDRESS) # weak password payload = {'email': email, 'password': '******'} payload = json.dumps(payload) res = self.app.put(user_url, params=payload, status=400) self.assertEquals(res.json, ERROR_WEAK_PASSWORD) # weak password #2 payload = {'email': email, 'password': '******'} payload = json.dumps(payload) res = self.app.put(user_url, params=payload, status=400) self.assertEquals(res.json, ERROR_WEAK_PASSWORD) # the user name does not match the email payload = {'email': '*****@*****.**', 'password': '******' * 9} payload = json.dumps(payload) res = self.app.put(user_url, params=payload, status=400) self.assertEquals(res.json, ERROR_USERNAME_EMAIL_MISMATCH) # everything is there res = self.app.get(user_url) self.assertFalse(json.loads(res.body)) payload = {'email': email, 'password': '******' * 9, 'captcha-challenge': 'xxx', 'captcha-response': 'xxx'} payload = json.dumps(payload) res = self.app.put(user_url, params=payload) self.assertEquals(res.body, name) res = self.app.get(user_url) self.assertTrue(json.loads(res.body)) finally: self.auth.delete_user(User(name), 'x' * 9)
def change_email(request, **args): """The form and process for changing your email address. Not supported yet""" auth = request.registry["auth"] data = {} data['trail'] = [[None, _('Change Account Name')]] data['email'] = request.session.get('email') new_email = request.params.get('new_email') if not new_email or not check_crumb(request): data['crumb'] = generate_crumb(request) return data data['crumb'] = generate_crumb(request) if not valid_email(new_email): request.errors.append(_('The email address appears invalid.')) return data #because changing the email now changes the username, we need to check #for collisions - NOT YET new_username = extract_username(new_email) if auth.get_user_id(new_username): request.errors.append( _('The email address you specified is already in use.')) return data if auth.update_field(request.user, request.session.get('password'), 'mail', new_email): #auth.update_field(request.user, # request.session.get('password'), # 'uid', extract_username(new_email)): data['success'] = \ _('Your account name was succesfully changed to %s.' % new_email) #make sure to change our session to have the new email request.session['email'] = new_email #request.session['username'] = extract_username(new_email) request.session.save() data['email'] = new_email #data['username'] = new_email else: request.errors.append( _('An unknown problem ocurred. Please try again later.')) return data
def forgot_step_4(request, **args): """Final step. reset their password, clear their reset code, and let them know that we're done. """ data = {} username = extract_username(request.params.get('key_username')) request.user['username'] = username #verify that password and confirm match password = request.params.get('new_password') confirm = request.params.get('confirm_password') if password != confirm: request.errors.append(_('The new password and confirmation do ' 'not match. Please try again.')) return render_to_response('console/password_reset3.mako', forgot_step_3(request), request) if not valid_password(username, password): request.errors.append(_('The new password is not valid. ' 'Please try again.')) return render_to_response('console/password_reset3.mako', forgot_step_3(request), request) try: auth = request.registry["auth"] reset = request.registry.settings.get('app.console.reset') if not auth.admin_update_password(request.user, password, request.params['key']): request.errors.append(_('Changing the password failed. ' 'Please ask for a new key and try again later')) return data except InvalidCodeError: request.errors.append(_('The reset code you submitted was ' 'invalid. Please request a new one.')) return render_to_response('console/password_reset2.mako', forgot_step_1(request), request) log_cef('Password Changed', 5, request.environ, request.registry.settings.get('config').get_map(), username, signature="PasswordReset") reset.clear_reset_code(request.user) return data
def test_non_ascii_password(self): # creating a user email = '*****@*****.**' % (time.time(), random.randint(1, 100)) user = extract_username(email) password = u'\xe9' * 9 user_url = '/user/1.0/%s' % user payload = {'email': email, 'password': password, 'captcha-challenge': 'xxx', 'captcha-response': 'xxx'} payload = json.dumps(payload) res = self.app.put(user_url, params=payload) self.assertEquals(res.body, user) token = base64.encodestring('%s:%s' % (user, password.encode('utf8'))) environ = {'HTTP_AUTHORIZATION': 'Basic %s' % token} self.app.extra_environ = environ self.app.delete(user_url) res = self.app.get(user_url) self.assertFalse(json.loads(res.body))
def do_password_reset(self, request): """Do a password reset.""" if self.reset is None: logger.debug('reset attempted, but no resetcode library installed') raise HTTPServiceUnavailable() user_name = request.POST.get('username') if user_name is not None: user_name = extract_username(user_name) if request.POST.keys() == ['username']: # setting up a password reset # XXX add support for captcha here via **data request.user = User(user_name) try: self.password_reset(request) except (HTTPServiceUnavailable, HTTPJsonBadRequest), e: return render_mako('password_failure.mako', error=e.detail) else: return render_mako('password_key_sent.mako') raise HTTPJsonBadRequest()
def forgot_step_3(request, **args): """Accessed by the direct email link, we check their key and give them a chance to change their password. """ data = {} auth = request.registry["auth"] username = extract_username(request.params.get('username')) request.user['username'] = username data['key'] = request.params['key'] data['usernm'] = username #needs to be different from the session username #validate the access code #NOTE: sreg automatically returns true here, and defers code validation #until later user_id = auth.get_user_id(request.user) reset = request.registry.settings.get('app.console.reset') if not reset.verify_reset_code(request.user, data['key']): request.errors.append(_('The reset code you submitted was ' 'invalid. Please request a new one.')) return render_to_response('console/password_reset1.mako', forgot_step_1(request), request) return data
def create_user(self, request): """Creates a user.""" if self.auth.get_user_id(request.user): raise HTTPJsonBadRequest(ERROR_INVALID_WRITE) username = request.user['username'] try: data = json.loads(request.body) except ValueError: raise HTTPJsonBadRequest(ERROR_MALFORMED_JSON) email = data.get('email') if email and not valid_email(email): raise HTTPJsonBadRequest(ERROR_NO_EMAIL_ADDRESS) # checking that the e-mail matches the username munged_email = extract_username(email) if munged_email != username and self.strict_usernames: raise HTTPJsonBadRequest(ERROR_USERNAME_EMAIL_MISMATCH) password = data.get('password') if not password: raise HTTPJsonBadRequest(ERROR_MISSING_PASSWORD) if not valid_password(username, password): raise HTTPJsonBadRequest(ERROR_WEAK_PASSWORD) # check if captcha info are provided or if we bypass it if (self.shared_secret is None or request.headers.get('X-Weave-Secret') != self.shared_secret): self._check_captcha(request, data) # all looks good, let's create the user if not self.auth.create_user(request.user['username'], password, email): raise HTTPInternalServerError('User creation failed.') return request.user['username']
def authenticate(self, environ, identity): # Normalize the username for our backend. # Some repoze.who plugins use "login" instead of "username". username = identity.get("username") if username is None: username = identity.get("login") if username is None: return None orig_username = username identity["username"] = username = extract_username(username) # Normalize the password, if any, to be unicode. # It it's not valid utf8 then authentication fails. password = identity.get("password") if password is not None and not isinstance(password, unicode): try: identity["password"] = password.decode("utf8") except UnicodeDecodeError: return None # Decide whether it's a new-style or old-style auth backend. if hasattr(self.backend, 'generate_reset_code'): user = self._authenticate_oldstyle(environ, username, identity) else: user = self._authenticate_newstyle(environ, username, identity) # Log the error if that failed. if user is None: err_username = username if username != orig_username: err_username += ' (%s)' % (orig_username, ) self.logger.cef('User Authentication Failed', 5, environ, self.config, err_username, AUTH_FAILURE) return None # Success! Store any loaded attributes into the identity dict. identity.update(user) return user["username"]
def test_shared_secret(self): # creating a user email = '*****@*****.**' % (time.time(), random.randint(1, 100)) name = extract_username(email) user_url = '/user/1.0/%s' % name # we want the captcha to fail app = get_app(self.app) app.config['captcha.use'] = True def _failed(self, *args, **kw): return FakeCaptchaResponse(False) captcha.submit = _failed extra = {'X-Weave-Secret': 'xxx'} try: # everything is there, but bad secret. This should # fallback to the captcha test and eventually fail res = self.app.get(user_url) self.assertFalse(json.loads(res.body)) payload = {'email': email, 'password': '******' * 9} payload = json.dumps(payload) res = self.app.put(user_url, params=payload, headers=extra, status=400) self.assertEquals(res.json, ERROR_INVALID_CAPTCHA) # let's use the real secret extra['X-Weave-Secret'] = 'CHANGEME' res = self.app.put(user_url, params=payload, headers=extra) self.assertEquals(res.body, name) res = self.app.get(user_url) self.assertTrue(json.loads(res.body)) finally: self.auth.delete_user(User(name), 'x' * 9)
def authenticate(self, environ, identity): # Normalize the username for our backend. # Some repoze.who plugins use "login" instead of "username". username = identity.get("username") if username is None: username = identity.get("login") if username is None: return None orig_username = username identity["username"] = username = extract_username(username) # Normalize the password, if any, to be unicode. if "password" in identity: try: identity["password"] = identity["password"].decode("utf8") except UnicodeDecodeError: return None # Decide whether it's a new-style or old-style auth backend. if hasattr(self.backend, 'generate_reset_code'): user = self._authenticate_oldstyle(environ, username, identity) else: user = self._authenticate_newstyle(environ, username, identity) # Log the error if that failed. if user is None: err_username = username if username != orig_username: err_username += ' (%s)' % (orig_username,) log_cef('User Authentication Failed', 5, environ, self.config, err_username, AUTH_FAILURE) return None # Success! Store any loaded attributes into the identity dict. identity.update(user) return user["username"]
def authenticate_user(self, request, config, username=None): """Authenticates a user and returns his id. "request" is the request received. The function makes sure that the user name found in the headers is compatible with the username if provided. It returns the user id from the database, if the password is the right one. """ environ = request.environ if 'REMOTE_USER' in environ: # already authenticated return environ['REMOTE_USER'] auth = environ.get('HTTP_AUTHORIZATION') if auth is not None: # for now, only supporting basic authentication # let's decipher the base64 encoded value if not auth.startswith('Basic '): raise HTTPUnauthorized('Invalid token') auth = auth[len('Basic '):].strip() try: # Split in such a way as to preserve # passwords that contain ':'. user_name, password = base64.decodestring(auth).split(':', 1) except (binascii.Error, ValueError): raise HTTPUnauthorized('Invalid token') # let's reject the call if the url is not owned by the user if (username is not None and user_name != username): log_cef('Username Does Not Match URL', 7, environ, config, user_name, AUTH_FAILURE) raise HTTPUnauthorized() # if this is an email, hash it. Save the original for logging and # debugging. remote_user_original = user_name try: user_name = extract_username(user_name) except UnicodeError: raise HTTPBadRequest('Invalid characters specified in ' + 'username', {}, 'Username must be BIDI ' + 'compliant UTF-8') # let's try an authentication # the authenticate_user API takes a unicode UTF-8 for the password try: password = password.decode('utf8') except UnicodeDecodeError: raise HTTPUnauthorized() #first we need to figure out if this is old-style or new-style auth if hasattr(self.backend, 'generate_reset_code'): # XXX to be removed once we get the proper fix see bug #662859 if (hasattr(self.backend, 'check_node') and self.backend.check_node): user_id = self.backend.authenticate_user(user_name, password, environ.get('HTTP_HOST')) else: user_id = self.backend.authenticate_user(user_name, password) request.user = User(user_name, user_id) else: user = User(user_name) credentials = {"username": user_name, "password": password} user_id = self.backend.authenticate_user(user, credentials, ['syncNode']) if not user_id: user_id = None user = None else: if (self.config.get('auth.check_node') and user.get('syncNode') != environ.get('HTTP_HOST')): user_id = None user = None request.user = user if user_id is None: err_user = user_name if remote_user_original is not None and \ user_name != remote_user_original: err_user += ' (%s)' % (remote_user_original) log_cef('User Authentication Failed', 5, environ, config, err_user, AUTH_FAILURE) raise HTTPUnauthorized() # we're all clear ! setting up REMOTE_USER request.remote_user = environ['REMOTE_USER'] = user_name # we also want to keep the password in clear text to reuse it # and remove it from the environ request.user_password = password request._authorization = environ['HTTP_AUTHORIZATION'] del environ['HTTP_AUTHORIZATION'] return user_id
def forgot_step_2(request, **args): """Tries to send the email with a reset code, then lets the user know we've done that """ data = {} auth = request.registry["auth"] username = extract_username(request.params['username']) request.user['username'] = username user_id = auth.get_user_id(request.user) if not user_id: request.errors.append(_('Unable to locate your account. ' 'Please check your username.')) return render_to_response('console/password_reset1.mako', forgot_step_1(request), request) if not request.registry.settings['app.captcha'].check(request): log_cef('Captcha failed on forgot password', 3, request.environ, request.registry.settings.get('config').get_map(), username, signature=CAPTCHA_FAILURE) request.errors.append(_('The captcha did not match. ' 'Please try again')) return render_to_response('console/password_reset1.mako', forgot_step_1(request), request) try: reset = request.registry.settings.get('app.console.reset') reset_code = reset.generate_reset_code(request.user, True) if not reset_code: request.errors.append(_('Getting a reset code failed ' 'unexpectedly. Please try again later.')) logger.error("Could not generate a reset code") return render_to_response('console/password_reset1.mako', forgot_step_1(request), request) auth.get_user_info(request.user, ['mail']) if not valid_email(request.user['mail']): raise NoEmailError() maildata = {'forgot_url': '%s/forgot' % request.host_url, 'username': username, 'code': reset_code} template_path = get_template_lookup('console') template = \ template_path.get_template('password_reset_mail.mako') body = template.render(**maildata) subject = _('Resetting your Mozilla Services password') smtp = request.registry.settings.get('config').get_map('smtp') #sender has a required position, so we can't pass it in in the #dict sender = smtp['sender'] del smtp['sender'] send_email(sender, request.user['mail'], subject, body, **smtp) except AlreadySentError: #backend handled the reset code email. Keep going pass except NoEmailError: request.errors.append(_('We do not have an email on file for this ' 'account and cannot send you a reset code.')) return render_to_response('console/password_reset1.mako', forgot_step_1(request), request) return data
def authenticate_user(self, request, config, username=None): """Authenticates a user and returns his id. "request" is the request received. The function makes sure that the user name found in the headers is compatible with the username if provided. It returns the user id from the database, if the password is the right one. """ environ = request.environ # If something further up the stack has already dealt with auth, # then things are highly unlikely to work properly. if 'REMOTE_USER' in environ: msg = 'The webserver appears to have handled authentication '\ 'internally, which is not compatible with this product.' raise HTTPServerError(msg) auth = environ.get('HTTP_AUTHORIZATION') if auth is not None: # for now, only supporting basic authentication # let's decipher the base64 encoded value if not auth.startswith('Basic '): self._log_cef('Authorization header contains unknown protocol', 7, environ) raise HTTPUnauthorized('Invalid token') auth = auth[len('Basic '):].strip() try: # Split in such a way as to preserve # passwords that contain ':'. user_name, password = base64.decodestring(auth).split(':', 1) except (binascii.Error, ValueError): self._log_cef('Authorization header is badly encoded', 7, environ) raise HTTPUnauthorized('Invalid token') # let's reject the call if the url is not owned by the user if (username is not None and user_name != username): self._log_cef('Username Does Not Match URL', 7, environ, config, user_name, AUTH_FAILURE) raise HTTPUnauthorized() # if this is an email, hash it. Save the original for logging and # debugging. remote_user_original = user_name try: user_name = extract_username(user_name) except UnicodeError: self._log_cef('Username contains invalid characters', 7, environ) raise HTTPBadRequest( 'Invalid characters specified in ' + 'username', {}, 'Username must be BIDI ' + 'compliant UTF-8') # let's try an authentication # the authenticate_user API takes a unicode UTF-8 for the password try: password = password.decode('utf8') except UnicodeDecodeError: self._log_cef('Password is not utf-8 encoded', 7, environ) raise HTTPUnauthorized() #first we need to figure out if this is old-style or new-style auth if hasattr(self.backend, 'generate_reset_code'): # XXX to be removed once we get the proper fix see bug #662859 if (hasattr(self.backend, 'check_node') and self.backend.check_node): user_id = self.backend.authenticate_user( user_name, password, environ.get('HTTP_HOST')) else: user_id = self.backend.authenticate_user( user_name, password) request.user = User(user_name, user_id) else: user = User(user_name) credentials = {"username": user_name, "password": password} attrs = [] check_node = self.config.get('auth.check_node') if check_node: attrs.append('syncNode') user_id = self.backend.authenticate_user( user, credentials, attrs) if not user_id: user_id = None user = None else: if (check_node and user.get('syncNode') != environ.get('HTTP_HOST')): self._log_cef('User authenticated to wrong node', 7, environ) user_id = None user = None request.user = user if user_id is None: err_user = user_name if remote_user_original is not None and \ user_name != remote_user_original: err_user += ' (%s)' % (remote_user_original) self._log_cef('User Authentication Failed', 5, environ, config, err_user, AUTH_FAILURE) raise HTTPUnauthorized() # we're all clear ! setting up REMOTE_USER request.remote_user = environ['REMOTE_USER'] = user_name # we also want to keep the password in clear text to reuse it # and remove it from the environ request.user_password = password request._authorization = environ['HTTP_AUTHORIZATION'] del environ['HTTP_AUTHORIZATION'] return user_id